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
3.8 kB 141 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 { createAction } from "$lib/action.svelte"; 15 import type { ThreadInput } from "./comments"; 16 17 interface Props { 18 subjectUri: string; 19 subjectCid?: string; 20 // when set, the comment strongRefs this parent comment (threaded reply) 21 replyToUri?: string; 22 replyToCid?: string; 23 authorDid: string; 24 authorHandle: string; 25 // reused on edit so the record keeps its identity; omitted when composing 26 rkey?: string; 27 createdAt?: string; 28 body?: string; 29 markup: MarkupContext; 30 rows?: number; 31 placeholder?: 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 submitLabel, 53 submitIcon, 54 autofocus = false, 55 onsubmitted, 56 oncancel 57 }: Props = $props(); 58 59 const auth = getAuth(); 60 61 let body = $state(untrack(() => initialBody)); 62 let tab = $state<"write" | "preview">("write"); 63 64 const publish = createAction(async (event: SubmitEvent) => { 65 event.preventDefault(); 66 const agent = auth.agent; 67 if (!agent || body.trim() === "") return; 68 if (!subjectCid) throw new Error("Cannot comment: the subject record is missing a cid."); 69 const createdAtValue = createdAt ?? new Date().toISOString(); 70 const targetRkey = rkey ?? tidNow(); 71 const record: CommentRecord = { 72 $type: "sh.tangled.feed.comment", 73 subject: { uri: subjectUri, cid: subjectCid } as CommentRecord["subject"], 74 body: { $type: "sh.tangled.markup.markdown", text: body }, 75 createdAt: createdAtValue 76 }; 77 if (replyToUri && replyToCid) { 78 record.replyTo = { uri: replyToUri, cid: replyToCid } as CommentRecord["replyTo"]; 79 } 80 const saved = await putComment(agent, targetRkey, record); 81 // render markdown client-side so the optimistic comment matches a real one 82 const bodyHtml = await renderMarkup(body, markup).catch(() => null); 83 onsubmitted?.({ 84 comment: { 85 uri: saved.uri, 86 cid: saved.cid, 87 rkey: targetRkey, 88 authorDid, 89 authorHandle, 90 createdAt: createdAtValue, 91 body, 92 bodyHtml 93 }, 94 replyTo: replyToUri ?? null 95 }); 96 // keep the prefilled body when editing; reset when composing a fresh comment 97 if (!rkey) { 98 body = ""; 99 tab = "write"; 100 } 101 }); 102 const canSubmit = $derived(body.trim() !== "" && !publish.loading); 103</script> 104 105<form onsubmit={publish.run} class="flex flex-col gap-2"> 106 <MarkdownEditor 107 name="body" 108 {rows} 109 {placeholder} 110 {markup} 111 {autofocus} 112 transparent 113 bind:value={body} 114 bind:tab 115 disabled={publish.loading} 116 /> 117 118 <ErrorAlert label={publish.error} /> 119 120 <div class="flex items-center gap-2"> 121 <Button 122 type="submit" 123 variant="primary" 124 icon={publish.loading ? Spinner : submitIcon} 125 disabled={!canSubmit} 126 > 127 {submitLabel} 128 </Button> 129 {#if oncancel} 130 <Button 131 type="button" 132 variant="default" 133 icon={X} 134 disabled={publish.loading} 135 onclick={oncancel} 136 > 137 Cancel 138 </Button> 139 {/if} 140 </div> 141</form>