diff --git a/web/src/lib/components/comment/Comment.svelte b/web/src/lib/components/comment/Comment.svelte
index af1c5a09..2d8a8005 100644
--- a/web/src/lib/components/comment/Comment.svelte
+++ b/web/src/lib/components/comment/Comment.svelte
@@ -10,6 +10,7 @@
body: string;
bodyHtml: string | null;
variant?: "top" | "reply";
+ deleted?: boolean;
// when provided, replaces the body region (e.g. an inline edit form), leaving
// the avatar/handle header untouched so it doesn't shift
editor?: Snippet;
@@ -22,30 +23,39 @@
body,
bodyHtml,
variant = "top",
+ deleted = false,
editor
}: Props = $props();
-
-
-
+{#if deleted}
+
+ This comment was deleted.
-
-
-
{authorHandle}
-
-
-
+{:else}
+
+
+
+
+ {authorHandle}
+
+
+
+
+ {#if editor}
+
{@render editor()}
+ {:else if bodyHtml}
+
+
{@html bodyHtml}
+ {:else if body}
+
{body}
+ {/if}
- {#if editor}
-
{@render editor()}
- {:else if bodyHtml}
-
-
{@html bodyHtml}
- {:else if body}
-
{body}
- {/if}
-
+{/if}
diff --git a/web/src/lib/components/comment/CommentCard.svelte b/web/src/lib/components/comment/CommentCard.svelte
index 7768e088..a977ae47 100644
--- a/web/src/lib/components/comment/CommentCard.svelte
+++ b/web/src/lib/components/comment/CommentCard.svelte
@@ -118,9 +118,10 @@
body={thread.self.body}
bodyHtml={thread.self.bodyHtml}
variant="top"
+ deleted={thread.self.deleted}
editor={editingUri === thread.self.uri ? selfEditor : undefined}
/>
- {#if editingUri !== thread.self.uri}
+ {#if editingUri !== thread.self.uri && !thread.self.deleted}
{@render commentActions(thread.self)}
{/if}
@@ -159,7 +160,9 @@
{/if}
- {#if replying && currentUser}
+ {#if thread.self.deleted && !thread.self.cid}
+
+ {:else if replying && currentUser}
a.createdAt.localeCompare(b.createdAt);
+// stand-in for a deleted parent
+export function deletedComment(uri: string, createdAt: string, cid?: string): CommentView {
+ return {
+ uri,
+ cid,
+ rkey: uri.split("/").pop() ?? "",
+ authorDid: "",
+ authorHandle: "",
+ createdAt,
+ body: "",
+ bodyHtml: null,
+ deleted: true
+ };
+}
+
export function buildCommentThreads(inputs: ThreadInput[]): CommentThread[] {
const threads = new Map();
- const orphanReplies: ThreadInput[] = [];
- for (const input of inputs) {
- if (input.replyTo === null) {
- threads.set(input.comment.uri, { self: input.comment, replies: [] });
+ for (const { comment, replyTo } of inputs) {
+ if (replyTo === null) {
+ threads.set(comment.uri, { self: comment, replies: [] });
}
}
- for (const input of inputs) {
- if (input.replyTo === null) continue;
- const parent = threads.get(input.replyTo);
- if (parent) {
- parent.replies.push(input.comment);
- } else {
- // parent not found (e.g. legacy/cross-collection ref): surface as top-level
- // so nothing is dropped.
- orphanReplies.push(input);
+ for (const { comment, replyTo, replyToCid } of inputs) {
+ if (replyTo === null) continue;
+ let parent = threads.get(replyTo);
+ if (!parent) {
+ parent = { self: deletedComment(replyTo, comment.createdAt), replies: [] };
+ threads.set(replyTo, parent);
}
+ if (parent.self.deleted && replyToCid && !parent.self.cid) {
+ parent.self.cid = replyToCid;
+ }
+ parent.replies.push(comment);
}
- for (const orphan of orphanReplies) {
- threads.set(orphan.comment.uri, { self: orphan.comment, replies: [] });
- }
-
- const list = [...threads.values()];
- list.sort((a, b) => byCreatedAt(a.self, b.self));
+ const list = [...threads.values()].sort((a, b) => byCreatedAt(a.self, b.self));
for (const thread of list) thread.replies.sort(byCreatedAt);
return list;
}
diff --git a/web/src/routes/[handle]/[repo]/issues/[aturi]/+page.svelte b/web/src/routes/[handle]/[repo]/issues/[aturi]/+page.svelte
index 6b5118d6..f24b0ca1 100644
--- a/web/src/routes/[handle]/[repo]/issues/[aturi]/+page.svelte
+++ b/web/src/routes/[handle]/[repo]/issues/[aturi]/+page.svelte
@@ -53,11 +53,17 @@
const removeComment = (uri: string) => {
comments = comments
- .filter((thread) => thread.self.uri !== uri)
- .map((thread) => ({
- ...thread,
- replies: thread.replies.filter((reply) => reply.uri !== uri)
- }));
+ .map((thread) => {
+ // a deleted parent that still has replies stays as a tombstone so its
+ // replies remain grouped; otherwise drop it entirely
+ if (thread.self.uri === uri) {
+ return thread.replies.length
+ ? { ...thread, self: { ...thread.self, body: "", bodyHtml: null, deleted: true } }
+ : null;
+ }
+ return { ...thread, replies: thread.replies.filter((reply) => reply.uri !== uri) };
+ })
+ .filter((thread) => thread !== null);
};
const handleSaved = async (saved: RecordView) => {
diff --git a/web/src/routes/[handle]/[repo]/issues/[aturi]/+page.ts b/web/src/routes/[handle]/[repo]/issues/[aturi]/+page.ts
index 83ccd604..e7fea91b 100644
--- a/web/src/routes/[handle]/[repo]/issues/[aturi]/+page.ts
+++ b/web/src/routes/[handle]/[repo]/issues/[aturi]/+page.ts
@@ -58,7 +58,8 @@ export const load: PageLoad = async (event) => {
body: commentBody,
bodyHtml: commentBodyHtml
},
- replyTo: item.value.replyTo?.uri ?? null
+ replyTo: item.value.replyTo?.uri ?? null,
+ replyToCid: item.value.replyTo?.cid
};
})
);