grain.social is a photo sharing platform built on atproto. grain.social
atproto photography appview
57
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: allow replies on nested comments and filter orphaned comments

- Reply button now shows on second-level comments, targeting the root
parent to keep threads at two levels max
- Orphaned replies (whose parent was deleted) are excluded from both
the total count and query results

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+17 -10
+2 -2
app/lib/components/molecules/Comment.svelte
··· 29 29 </div> 30 30 <div class="meta"> 31 31 <span class="time">{timeStr}</span> 32 - {#if onReply && !isReply} 33 - <button class="meta-btn" onclick={() => onReply?.(comment.uri, comment.author?.handle ?? '')}>Reply</button> 32 + {#if onReply} 33 + <button class="meta-btn" onclick={() => onReply?.(comment.replyTo ?? comment.uri, comment.author?.handle ?? '')}>Reply</button> 34 34 {/if} 35 35 {#if isOwner && onDelete} 36 36 <button class="meta-btn delete" onclick={() => onDelete?.(comment.uri)}>Delete</button>
+15 -8
server/xrpc/getGalleryThread.ts
··· 6 6 const { ok, params, db, lookup, blobUrl, getRecords } = ctx; 7 7 const { gallery, limit = 20, cursor } = params; 8 8 9 - // Count total comments for this gallery 9 + // Count total comments for this gallery, excluding orphaned replies 10 10 const countRows = (await db.query( 11 - `SELECT count(*) as cnt FROM "social.grain.comment" WHERE subject = $1`, 11 + `SELECT count(*) as cnt FROM "social.grain.comment" c 12 + WHERE c.subject = $1 13 + AND (c.reply_to IS NULL OR EXISTS ( 14 + SELECT 1 FROM "social.grain.comment" p WHERE p.uri = c.reply_to 15 + ))`, 12 16 [gallery], 13 17 )) as { cnt: number }[]; 14 18 const totalCount = countRows[0]?.cnt ?? 0; 15 19 16 - // Fetch comments with cursor-based pagination (oldest first) 17 - let query = `SELECT uri, did, cid, text, facets, focus, reply_to, created_at 18 - FROM "social.grain.comment" 19 - WHERE subject = $1`; 20 + // Fetch comments with cursor-based pagination (oldest first), excluding orphaned replies 21 + let query = `SELECT c.uri, c.did, c.cid, c.text, c.facets, c.focus, c.reply_to, c.created_at 22 + FROM "social.grain.comment" c 23 + WHERE c.subject = $1 24 + AND (c.reply_to IS NULL OR EXISTS ( 25 + SELECT 1 FROM "social.grain.comment" p WHERE p.uri = c.reply_to 26 + ))`; 20 27 const queryParams: any[] = [gallery]; 21 28 22 29 if (cursor) { 23 - query += ` AND created_at > $2`; 30 + query += ` AND c.created_at > $2`; 24 31 queryParams.push(cursor); 25 32 } 26 33 27 - query += ` ORDER BY created_at ASC LIMIT $${queryParams.length + 1}`; 34 + query += ` ORDER BY c.created_at ASC LIMIT $${queryParams.length + 1}`; 28 35 queryParams.push(limit + 1); // fetch one extra for cursor 29 36 30 37 const rows = (await db.query(query, queryParams)) as Array<{