0
2
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<Gallery>(
"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 } : {}) });
});