Something went wrong. Try again.
grain.social is a photo sharing platform built on atproto. grain.social
appview atproto photography
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174import { defineQuery } from "$hatk";import type { GrainActorProfile, Photo } from "$hatk";import { views } from "$hatk";import { NOT_ORPHANED } from "../hydrate/comments.ts";import { blockFilter } from "../filters/blockMute.ts";import { lookupHandles } from "../helpers/lookupHandles.ts";import { resolveAtUri } from "../helpers/resolveHandle.ts";
export default defineQuery("social.grain.unspecced.getCommentThread", async (ctx) => { const { ok, params, db, lookup, blobUrl, getRecords, viewer } = ctx; const { limit = 20, cursor } = params;
// A gallery or story reached by a handle link carries the handle in its AT // URI; comments are stored against the DID form. const subject = (await resolveAtUri(db, params.subject)) ?? params.subject;
const viewerDid = viewer?.did;
// Build block filter — blocked comments are removed entirely. Taken-down // accounts are hidden from everyone, matching every feed. const countParams: any[] = [subject]; let countBmParam = ""; if (viewerDid) { countParams.push(viewerDid); countBmParam = `AND ${blockFilter("c.did", `$${countParams.length}`)}`; }
// Count total comments for this subject, excluding orphaned replies const countRows = (await db.query( `SELECT count(*) as cnt FROM "social.grain.comment" c LEFT JOIN _repos r ON r.did = c.did WHERE c.subject = $1 AND (r.status IS NULL OR r.status != 'takendown') AND ${NOT_ORPHANED} ${countBmParam}`, countParams, )) as { cnt: number }[]; const totalCount = countRows[0]?.cnt ?? 0;
// Fetch comments with cursor-based pagination (oldest first), excluding orphaned replies const queryParams: any[] = [subject]; let query = `SELECT c.uri, c.did, c.cid, c.text, c.facets, c.focus, c.reply_to, c.created_at FROM "social.grain.comment" c LEFT JOIN _repos r ON r.did = c.did WHERE c.subject = $1 AND (r.status IS NULL OR r.status != 'takendown') AND ${NOT_ORPHANED}`;
if (cursor) { query += ` AND c.created_at > $2`; queryParams.push(cursor); }
if (viewerDid) { queryParams.push(viewerDid); query += ` AND ${blockFilter("c.did", `$${queryParams.length}`)}`; }
query += ` ORDER BY c.created_at ASC LIMIT $${queryParams.length + 1}`; queryParams.push(limit + 1); // fetch one extra for cursor
const rows = (await db.query(query, queryParams)) as Array<{ uri: string; did: string; cid: string; text: string; facets: string | null; focus: string | null; reply_to: string | null; created_at: string; }>;
const hasMore = rows.length > limit; const items = hasMore ? rows.slice(0, limit) : rows; const nextCursor = hasMore ? items[items.length - 1]?.created_at : undefined;
// Hydrate author profiles const dids = [...new Set(items.map((r) => r.did))]; const [profiles, handleMap] = await Promise.all([ lookup<GrainActorProfile>("social.grain.actor.profile", "did", dids), lookupHandles(db, dids), ]);
// Check which comment authors the viewer has muted let mutedDids = new Set<string>(); if (viewerDid && dids.length > 0) { const ph = dids.map((_, i) => `$${i + 2}`).join(","); const mutedRows = (await db.query( `SELECT subject FROM _mutes WHERE did = $1 AND subject IN (${ph})`, [viewerDid, ...dids], )) as { subject: string }[]; mutedDids = new Set(mutedRows.map((r) => r.subject)); }
// Hydrate focus photos const focusUris = items.map((r) => r.focus).filter(Boolean) as string[]; const focusPhotos = focusUris.length > 0 ? await getRecords<Photo>("social.grain.photo", focusUris) : new Map();
// Hydrate comment favorite counts and viewer favorites const commentUris = items.map((r) => r.uri); const [favCounts, viewerFavs] = await Promise.all([ commentUris.length > 0 ? ( db.query( `SELECT subject, COUNT(DISTINCT did) as count FROM "social.grain.favorite" WHERE subject IN (${commentUris.map((_, i) => `$${i + 1}`).join(",")}) GROUP BY subject`, commentUris, ) as Promise<{ subject: string; count: number }[]> ).then((rows) => { const m = new Map<string, number>(); for (const r of rows) m.set(r.subject, Number(r.count)); return m; }) : Promise.resolve(new Map<string, number>()), viewerDid && commentUris.length > 0 ? ( db.query( `SELECT subject, uri FROM "social.grain.favorite" WHERE did = $1 AND subject IN (${commentUris.map((_, i) => `$${i + 2}`).join(",")})`, [viewerDid, ...commentUris], ) as Promise<{ subject: string; uri: string }[]> ).then((rows) => { const m = new Map<string, string>(); for (const r of rows) m.set(r.subject, r.uri); return m; }) : Promise.resolve(new Map<string, string>()), ]);
const comments = items.map((row) => { const author = profiles.get(row.did); const parsedFacets = row.facets ? JSON.parse(row.facets) : undefined; const focusPhoto = row.focus ? focusPhotos.get(row.focus) : null;
return { ...views.commentView({ uri: row.uri, cid: row.cid, text: row.text, facets: parsedFacets, replyTo: row.reply_to ?? undefined, createdAt: row.created_at, author: author ? views.grainActorDefsProfileView({ cid: author.cid, did: author.did, handle: author.handle ?? handleMap.get(author.did) ?? author.did, displayName: author.value.displayName, avatar: blobUrl(author.did, author.value.avatar) ?? undefined, }) : views.grainActorDefsProfileView({ cid: row.cid, did: row.did, handle: handleMap.get(row.did) ?? row.did, }), ...(focusPhoto ? { focus: views.photoView({ uri: focusPhoto.uri, cid: focusPhoto.cid, thumb: blobUrl(focusPhoto.did, focusPhoto.value.photo, "feed_thumbnail") ?? "", fullsize: blobUrl(focusPhoto.did, focusPhoto.value.photo, "feed_fullsize") ?? "", alt: focusPhoto.value.alt, aspectRatio: focusPhoto.value.aspectRatio ?? { width: 4, height: 3 }, }), } : {}), }), favCount: favCounts.get(row.uri) ?? 0, ...(viewerFavs.has(row.uri) ? { viewer: { fav: viewerFavs.get(row.uri) } } : {}), ...(mutedDids.has(row.did) ? { muted: true } : {}), }; });
return ok({ comments, ...(nextCursor ? { cursor: nextCursor } : {}), totalCount });});