This repository has no description
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>