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.1 kB 153 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 previewClass?: string; 32 submitLabel: string; 33 submitIcon: Component<SvelteHTMLElements["svg"]>; 34 autofocus?: boolean; 35 onsubmitted?: (submitted: ThreadInput) => void; 36 oncancel?: () => void; 37 } 38 39 let { 40 subjectUri, 41 subjectCid, 42 replyToUri, 43 replyToCid, 44 authorDid, 45 authorHandle, 46 rkey, 47 createdAt, 48 body: initialBody = "", 49 markup, 50 rows = 6, 51 placeholder, 52 previewClass = "min-h-24", 53 submitLabel, 54 submitIcon, 55 autofocus = false, 56 onsubmitted, 57 oncancel 58 }: Props = $props(); 59 60 const auth = getAuth(); 61 62 let body = $state(untrack(() => initialBody)); 63 let tab = $state<"write" | "preview">("write"); 64 let isPublishing = $state(false); 65 let error = $state<string | null>(null); 66 67 const canSubmit = $derived(body.trim() !== "" && !isPublishing); 68 69 const handleSubmit = async (e: SubmitEvent) => { 70 e.preventDefault(); 71 const agent = auth.agent; 72 if (!agent || !canSubmit) return; 73 if (!subjectCid) { 74 error = "Cannot comment: the subject record is missing a cid."; 75 return; 76 } 77 isPublishing = true; 78 error = null; 79 try { 80 const createdAtValue = createdAt ?? new Date().toISOString(); 81 const targetRkey = rkey ?? tidNow(); 82 const record: CommentRecord = { 83 $type: "sh.tangled.feed.comment", 84 subject: { uri: subjectUri, cid: subjectCid } as CommentRecord["subject"], 85 body: { $type: "sh.tangled.markup.markdown", text: body }, 86 createdAt: createdAtValue 87 }; 88 if (replyToUri && replyToCid) { 89 record.replyTo = { uri: replyToUri, cid: replyToCid } as CommentRecord["replyTo"]; 90 } 91 const saved = await putComment(agent, targetRkey, record); 92 // render markdown client-side so the optimistic comment matches a real one 93 const bodyHtml = await renderMarkup(body, markup).catch(() => null); 94 onsubmitted?.({ 95 comment: { 96 uri: saved.uri, 97 cid: saved.cid, 98 rkey: targetRkey, 99 authorDid, 100 authorHandle, 101 createdAt: createdAtValue, 102 body, 103 bodyHtml 104 }, 105 replyTo: replyToUri ?? null 106 }); 107 // keep the prefilled body when editing; reset when composing a fresh comment 108 if (!rkey) { 109 body = ""; 110 tab = "write"; 111 } 112 } catch (err) { 113 error = err instanceof Error ? err.message : "Failed to post comment"; 114 } finally { 115 isPublishing = false; 116 } 117 }; 118</script> 119 120<form onsubmit={handleSubmit} class="flex flex-col gap-2"> 121 <MarkdownEditor 122 name="body" 123 {rows} 124 {previewClass} 125 {placeholder} 126 {markup} 127 {autofocus} 128 transparent 129 bind:value={body} 130 bind:tab 131 disabled={isPublishing} 132 /> 133 134 {#if error} 135 <ErrorAlert label={error} /> 136 {/if} 137 138 <div class="flex items-center gap-2"> 139 <Button 140 type="submit" 141 variant="primary" 142 icon={isPublishing ? Spinner : submitIcon} 143 disabled={!canSubmit} 144 > 145 {submitLabel} 146 </Button> 147 {#if oncancel} 148 <Button type="button" variant="default" icon={X} disabled={isPublishing} onclick={oncancel}> 149 Cancel 150 </Button> 151 {/if} 152 </div> 153</form>