import { defineQuery } from "$hatk"; import type { Gallery } from "$hatk"; import { hydrateGalleries } from "../hydrate/galleries.ts"; import { hideLabelsFilter } from "../labels/_hidden.ts"; import { resolveHandle } from "../helpers/resolveHandle.ts"; export default defineQuery("social.grain.unspecced.getActorFavorites", async (ctx) => { const { db, ok, getRecords } = ctx; const actor = ctx.params.actor ? await resolveHandle(db, ctx.params.actor) : null; if (!actor) return ok({ items: [] }); // Only the actor themselves can view their favorites. Compared after // resolution, so asking by handle works for the owner too. if (ctx.viewer?.did !== actor) return ok({ items: [] }); const limit = Math.min(Number(ctx.params.limit) || 30, 100); const cursor = ctx.params.cursor as string | undefined; const queryParams: (string | number)[] = [actor, limit + 1]; let cursorClause = ""; if (cursor) { cursorClause = ` HAVING MAX(f.created_at) < $3`; queryParams.push(cursor); } // GROUP BY f.subject: an account can hold more than one favorite record for // the same gallery (verified present in prod — the records are real, they // exist in the users' repos), which would otherwise list that gallery twice. // MAX(created_at) is the sort key so a re-favorite moves the gallery up, and // the cursor compares against the same aggregate. const rows = (await db.query( `SELECT f.subject AS subject, MAX(f.created_at) AS created_at FROM "social.grain.favorite" f JOIN "social.grain.gallery" t ON t.uri = f.subject LEFT JOIN _repos r ON t.did = r.did WHERE f.did = $1 AND (r.status IS NULL OR r.status NOT IN ('takendown', 'deactivated', 'deleted')) AND ${hideLabelsFilter("t.uri")} AND (SELECT count(*) FROM "social.grain.gallery.item" gi WHERE gi.gallery = t.uri) > 0 GROUP BY f.subject ${cursorClause} ORDER BY created_at DESC LIMIT $2`, queryParams, )) as { subject: string; created_at: string }[]; const hasMore = rows.length > limit; const pageRows = hasMore ? rows.slice(0, limit) : rows; const recordsMap = await getRecords( "social.grain.gallery", pageRows.map((r) => r.subject), ); // Preserve favorite ordering const galleryRows = pageRows.map((r) => recordsMap.get(r.subject)).filter((r) => r != null); const items = await hydrateGalleries(ctx, galleryRows); const nextCursor = hasMore ? pageRows[pageRows.length - 1].created_at : undefined; return ok({ items, ...(nextCursor ? { cursor: nextCursor } : {}) }); });