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 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
149 type="button"
150 variant="ghost"
151 icon={X}
152 disabled={isPublishing}
153 onclick={oncancel}
154 class="text-foreground-danger hover:text-foreground-danger"
155 >
156 Cancel
157 </Button>
158 {/if}
159 </div>
160</form>