Something went wrong. Try again.
Monorepo for Tangled forked from tangled.org/core
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124import { jsonGet } from "#lib/api/_request.ts";import type { BobbinContext } from "#lib/api/client.ts";import type * as OrgTangledFeedGetTimeline from "#lib/api/lexicons/types/org/tangled/feed/getTimeline.ts";import { avatarUrl, avatarWidthFor } from "#lib/avatar.ts";import type { Notification, Party } from "#lib/components/landing/hero/notifications.ts";
/* * The hero stack, live: what's happening on Tangled right now, from the configured bobbin's * timeline (org.tangled.feed.getTimeline, as /timeline reads it). Follows, stars and new repos become notifications, newest first. */
type Feed = OrgTangledFeedGetTimeline.$output["feed"];type Event = Feed[number]["event"];
/** Below this many cards the stack can't loop, so the source counts as quiet. */const ENOUGH = 6;const COUNT = 24;const AVATAR_PX = 34;
const face = (did: string) => avatarUrl(did, avatarWidthFor(AVATAR_PX));
/** The handle's first letter or digit, for someone with no face to show. */export const initialOf = (handle: string) => (/[a-z0-9]/i.exec(handle)?.[0] ?? "?").toUpperCase();
const person = (handle: string, did: string): Party => ({ name: handle, href: `/${handle}`, avatar: face(did), initial: initialOf(handle)});
const repo = (r: { slug: string; owner: { handle: string } }): Party => ({ name: r.slug, href: `/${r.owner.handle}/${r.slug}`});
const valid = (handle: string | undefined) => !!handle && handle !== "handle.invalid";
/** One timeline event as a notification, or nothing for one the stack can't show. */function fromEvent(event: Event): Notification | undefined { switch (event.$type) { case "org.tangled.feed.getTimeline#followEvent": if (!valid(event.actor.handle) || !valid(event.subject.handle)) return undefined; return { actor: person(event.actor.handle, event.actor.did), verb: "followed", subject: person(event.subject.handle, event.subject.did) }; case "org.tangled.feed.getTimeline#starEvent": if (!valid(event.actor.handle) || !valid(event.repo.owner.handle)) return undefined; return { actor: person(event.actor.handle, event.actor.did), verb: "starred", // The repo wears its owner's face. subject: { ...repo(event.repo), avatar: face(event.repo.owner.did), initial: initialOf(event.repo.owner.handle) } }; case "org.tangled.feed.getTimeline#repoEvent": { const owner = event.repo.owner; if (!valid(owner.handle)) return undefined; return { actor: person(owner.handle, owner.did), verb: event.source ? "forked" : "created", subject: repo(event.source ?? event.repo) }; } default: return undefined; }}
/** fromEvent, with an event that doesn't have the shape its type promises skipped rather than * taking the whole stack (and the landing's ssr) down with it. */function safeFromEvent(event: Event): Notification | undefined { try { return fromEvent(event); } catch { return undefined; }}
/** * The feed as notifications. Activity comes in bursts — one person starring five repos — so * a card by the same person as the one before it is skipped, to keep the stack varied. */export function fromTimeline(feed: Feed): Notification[] { const out: Notification[] = []; for (const { event, eventAt } of feed) { const n = safeFromEvent(event); if (n && n.actor.href !== out.at(-1)?.actor.href) out.push({ ...n, at: eventAt }); } return out;}
async function timeline(ctx: BobbinContext, signal: AbortSignal) { const { feed } = await jsonGet<OrgTangledFeedGetTimeline.$output>( ctx, "org.tangled.feed.getTimeline", { limit: COUNT }, { signal } ); if (feed.length < ENOUGH) throw new Error("timeline: too quiet"); return feed;}
/** The live notifications, or null when the timeline couldn't be reached in time. */export async function loadLiveActivity( ctx: BobbinContext, timeout = 2500): Promise<Notification[] | null> { const signal = AbortSignal.timeout(timeout); let feed: Feed; try { feed = await timeline(ctx, signal); } catch { return null; } const items = fromTimeline(feed); return items.length >= ENOUGH ? items : null;}