This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / web / src / lib / components / comment / CommentEditor.svelte
4.0 kB 150 lines
1<script lang="ts"> 2 import { untrack, type Component } from "svelte"; 3 import type { SvelteHTMLElements } from "svelte/elements"; 4 import { now as tidNow } from "@atcute/tid"; 5 import X from "$icon/x"; 6 import { putComment } from "$lib/api/comment"; 7 import { getAuth } from "$lib/auth.svelte"; 8 import Button from "$lib/components/ui/Button.svelte"; 9 import ErrorAlert from "$lib/components/ui/Error.svelte"; 10 import MarkdownEditor from "$lib/components/ui/MarkdownEditor.svelte"; 11 import Spinner from "$lib/components/ui/Spinner.svelte"; 12 import { renderMarkup, type MarkupContext } from "$lib/markup"; 13 import type { CommentRecord } from "$lib/api/records"; 14 import type { ThreadInput } from "./comments"; 15 16 interface Props { 17 subjectUri: string; 18 subjectCid?: string; 19 // when set, the comment strongRefs this parent comment (threaded reply) 20 replyToUri?: string; 21 replyToCid?: string; 22 authorDid: string; 23 authorHandle: string; 24 // reused on edit so the record keeps its identity; omitted when composing 25 rkey?: string; 26 createdAt?: string; 27 body?: string; 28 markup: MarkupContext; 29 rows?: number; 30 placeholder?: string; 31 submitLabel: string; 32 submitIcon: Component<SvelteHTMLElements["svg"]>; 33 autofocus?: boolean; 34 onsubmitted?: (submitted: ThreadInput) => void; 35 oncancel?: () => void; 36 } 37 38 let { 39 subjectUri, 40 subjectCid, 41 replyToUri, 42 replyToCid, 43 authorDid, 44 authorHandle, 45 rkey, 46 createdAt, 47 body: initialBody = "", 48 markup, 49 rows = 6, 50 placeholder, 51 submitLabel, 52 submitIcon, 53 autofocus = false, 54 onsubmitted, 55 oncancel 56 }: Props = $props(); 57 58 const auth = getAuth(); 59 60 let body = $state(untrack(() => initialBody)); 61 let tab = $state<"write" | "preview">("write"); 62 let isPublishing = $state(false); 63 let error = $state<string | null>(null); 64 65 const canSubmit = $derived(body.trim() !== "" && !isPublishing); 66 67 const handleSubmit = async (e: SubmitEvent) => { 68 e.preventDefault(); 69 const agent = auth.agent; 70 if (!agent || !canSubmit) return; 71 if (!subjectCid) { 72 error = "Cannot comment: the subject record is missing a cid."; 73 return; 74 } 75 isPublishing = true; 76 error = null; 77 try { 78 const createdAtValue = createdAt ?? new Date().toISOString(); 79 const targetRkey = rkey ?? tidNow(); 80 const record: CommentRecord = { 81 $type: "sh.tangled.feed.comment", 82 subject: { uri: subjectUri, cid: subjectCid } as CommentRecord["subject"], 83 body: { $type: "sh.tangled.markup.markdown", text: body }, 84 createdAt: createdAtValue 85 }; 86 if (replyToUri && replyToCid) { 87 record.replyTo = { uri: replyToUri, cid: replyToCid } as CommentRecord["replyTo"]; 88 } 89 const saved = await putComment(agent, targetRkey, record); 90 // render markdown client-side so the optimistic comment matches a real one 91 const bodyHtml = await renderMarkup(body, markup).catch(() => null); 92 onsubmitted?.({ 93 comment: { 94 uri: saved.uri, 95 cid: saved.cid, 96 rkey: targetRkey, 97 authorDid, 98 authorHandle, 99 createdAt: createdAtValue, 100 body, 101 bodyHtml 102 }, 103 replyTo: replyToUri ?? null 104 }); 105 // keep the prefilled body when editing; reset when composing a fresh comment 106 if (!rkey) { 107 body = ""; 108 tab = "write"; 109 } 110 } catch (err) { 111 error = err instanceof Error ? err.message : "Failed to post comment"; 112 } finally { 113 isPublishing = false; 114 } 115 }; 116</script> 117 118<form onsubmit={handleSubmit} class="flex flex-col gap-2"> 119 <MarkdownEditor 120 name="body" 121 {rows} 122 {placeholder} 123 {markup} 124 {autofocus} 125 transparent 126 bind:value={body} 127 bind:tab 128 disabled={isPublishing} 129 /> 130 131 {#if error} 132 <ErrorAlert label={error} /> 133 {/if} 134 135 <div class="flex items-center gap-2"> 136 <Button 137 type="submit" 138 variant="primary" 139 icon={isPublishing ? Spinner : submitIcon} 140 disabled={!canSubmit} 141 > 142 {submitLabel} 143 </Button> 144 {#if oncancel} 145 <Button type="button" variant="default" icon={X} disabled={isPublishing} onclick={oncancel}> 146 Cancel 147 </Button> 148 {/if} 149 </div> 150</form>