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.

refactor: extract shared orphaned comment filter

DRY up the orphan exclusion SQL into a shared NOT_ORPHANED constant
and countComments helper in server/hydrate/comments.ts, used by both
gallery hydration and the thread endpoint.

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

+29 -9
+24
server/hydrate/comments.ts
··· 1 + import type { BaseContext } from "$hatk"; 2 + 3 + /** SQL condition that excludes orphaned replies (parent comment was deleted). */ 4 + export const NOT_ORPHANED = `(c.reply_to IS NULL OR EXISTS ( 5 + SELECT 1 FROM "social.grain.comment" p WHERE p.uri = c.reply_to 6 + ))`; 7 + 8 + /** Count non-orphaned comments grouped by subject URI. */ 9 + export async function countComments( 10 + db: BaseContext["db"], 11 + subjectUris: string[], 12 + ): Promise<Map<string, number>> { 13 + if (subjectUris.length === 0) return new Map(); 14 + const placeholders = subjectUris.map((_, i) => `$${i + 1}`).join(","); 15 + const rows = (await db.query( 16 + `SELECT c.subject, COUNT(*) as count FROM "social.grain.comment" c 17 + WHERE c.subject IN (${placeholders}) AND ${NOT_ORPHANED} 18 + GROUP BY c.subject`, 19 + subjectUris, 20 + )) as { subject: string; count: number }[]; 21 + const m = new Map<string, number>(); 22 + for (const r of rows) m.set(r.subject, Number(r.count)); 23 + return m; 24 + }
+2 -1
server/hydrate/galleries.ts
··· 2 2 import type { GrainActorProfile, Photo, Gallery, Label } from "$hatk"; 3 3 import type { PhotoView, GalleryView, ExifView } from "$hatk"; 4 4 import type { BaseContext, Row } from "$hatk"; 5 + import { countComments } from "./comments.ts"; 5 6 6 7 const SCALE = 1_000_000; 7 8 ··· 115 116 return m; 116 117 }) 117 118 : Promise.resolve(new Map<string, number>()), 118 - ctx.count("social.grain.comment", "subject", galleryUris), 119 + countComments(ctx.db, galleryUris), 119 120 ctx.labels(galleryUris) as Promise<Map<string, Label[]>>, 120 121 galleryUris.length > 0 121 122 ? (ctx.db.query(
+3 -8
server/xrpc/getGalleryThread.ts
··· 1 1 import { defineQuery } from "$hatk"; 2 2 import type { GrainActorProfile, Photo } from "$hatk"; 3 3 import { views } from "$hatk"; 4 + import { NOT_ORPHANED } from "../hydrate/comments.ts"; 4 5 5 6 export default defineQuery("social.grain.unspecced.getGalleryThread", async (ctx) => { 6 7 const { ok, params, db, lookup, blobUrl, getRecords } = ctx; ··· 9 10 // Count total comments for this gallery, excluding orphaned replies 10 11 const countRows = (await db.query( 11 12 `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 - ))`, 13 + WHERE c.subject = $1 AND ${NOT_ORPHANED}`, 16 14 [gallery], 17 15 )) as { cnt: number }[]; 18 16 const totalCount = countRows[0]?.cnt ?? 0; ··· 20 18 // Fetch comments with cursor-based pagination (oldest first), excluding orphaned replies 21 19 let query = `SELECT c.uri, c.did, c.cid, c.text, c.facets, c.focus, c.reply_to, c.created_at 22 20 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 - ))`; 21 + WHERE c.subject = $1 AND ${NOT_ORPHANED}`; 27 22 const queryParams: any[] = [gallery]; 28 23 29 24 if (cursor) {