~ knotless Search/
↑↓ move↵ open⇧↵ all results

grain.social/grain

photography-focused photo sharing platform

grain.social
1 collaborator
0
2
85 lines · 3.2 KB Raw History
import { defineQuery } from "$hatk";
import { views } from "$hatk";
import type { GrainActorProfile } from "$hatk";
import { hideLabelsFilter, hideSelfLabelsFilter } from "../labels/_hidden.ts";
import { blockMuteFilter } from "../filters/blockMute.ts";
import { lookupHandles } from "../helpers/lookupHandles.ts";

const TWENTY_FOUR_HOURS = 24 * 60 * 60 * 1000;

export default defineQuery("social.grain.unspecced.getStoryAuthors", async (ctx) => {
  const { db, ok } = ctx;
  const viewer = ctx.viewer?.did;
  const cutoff = new Date(Date.now() - TWENTY_FOUR_HOURS).toISOString();

  const bmFilter = viewer ? `AND ${blockMuteFilter("s.did", "$2")}` : "";
  const params = viewer ? [cutoff, viewer] : [cutoff];

  // What the viewer has watched rides along on the same aggregate: the newest
  // watched story's createdAt is the high-water mark a client compares against
  // latestAt, and the unwatched count is what it draws on the ring.
  const viewJoin = viewer ? `LEFT JOIN _story_views v ON v.subject = s.uri AND v.did = $2` : "";
  const viewCols = viewer
    ? `, MAX(CASE WHEN v.subject IS NOT NULL THEN s.created_at END) AS last_viewed_at,
         SUM(CASE WHEN v.subject IS NULL THEN 1 ELSE 0 END) AS unviewed_count`
    : "";

  // Aggregate in SQL, excluding stories with hide-severity labels, whether
  // from a labeler or the author. getStories drops both before serving, so
  // counting them here would leave a ring that never greys out.
  const rows = (await db.query(
    `SELECT s.did, COUNT(*) AS story_count, MAX(s.created_at) AS latest_at ${viewCols}
       FROM "social.grain.story" s
       LEFT JOIN _repos r ON s.did = r.did
       ${viewJoin}
       WHERE s.created_at > $1
         AND (r.status IS NULL OR r.status NOT IN ('takendown', 'deactivated', 'deleted'))
         AND ${hideLabelsFilter("s.uri")}
         AND ${hideSelfLabelsFilter("social.grain.story__labels_self_labels", "s.uri")}
         ${bmFilter}
       GROUP BY s.did
       ORDER BY latest_at DESC`,
    params,
  )) as {
    did: string;
    story_count: number;
    latest_at: string;
    last_viewed_at?: string | null;
    unviewed_count?: number | null;
  }[];

  const dids = rows.map((r) => r.did);
  const [profiles, handleMap] = await Promise.all([
    ctx.lookup<GrainActorProfile>("social.grain.actor.profile", "did", dids),
    lookupHandles(db, dids),
  ]);

  const authors = rows.map((row) => {
    const author = profiles.get(row.did);
    return {
      profile: author
        ? views.grainActorDefsProfileView({
            cid: author.cid,
            did: author.did,
            handle: author.handle ?? handleMap.get(author.did) ?? author.did,
            displayName: author.value.displayName,
            avatar: ctx.blobUrl(author.did, author.value.avatar) ?? undefined,
          })
        : views.grainActorDefsProfileView({
            cid: "",
            did: row.did,
            handle: handleMap.get(row.did) ?? row.did,
          }),
      storyCount: row.story_count,
      latestAt: row.latest_at,
      ...(viewer
        ? {
            unviewedCount: Number(row.unviewed_count ?? row.story_count),
            ...(row.last_viewed_at ? { lastViewedAt: row.last_viewed_at } : {}),
          }
        : {}),
    };
  });

  return ok({ authors });
});