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 { 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>