import { jsonGet } from "#lib/api/_request.ts"; import { FOLLOW_VIEWER, STAR_VIEWER } from "#lib/api/descriptors.ts"; import { enrich, target, viewerUriOf, type Sidecar } from "#lib/api/enrich.ts"; import { getTrending } from "#lib/api/profile.ts"; import type { Auth } from "#lib/auth.svelte.ts"; import { toRepoCard } from "#lib/components/timeline/mappers.ts"; import type { BlogPost } from "#lib/components/timeline/types.ts"; import { loadBlogFeed } from "#lib/blog.ts"; import { createBobbinClient, type BobbinContext } from "#lib/api/client.ts"; import type { ResourceUri } from "@atcute/lexicons/syntax"; import type * as OrgTangledFeedGetTimeline from "#lib/api/lexicons/types/org/tangled/feed/getTimeline.ts"; import type * as OrgTangledRepoDefs from "#lib/api/lexicons/types/org/tangled/repo/defs.ts"; import type * as OrgTangledActorDefs from "#lib/api/lexicons/types/org/tangled/actor/defs.ts"; const PAGE_LIMIT = 50; type Feed = OrgTangledFeedGetTimeline.$output["feed"]; export interface LoadFeedOptions { ctx: BobbinContext; followingOnly: boolean; viewer?: string; } const withStar = ( repo: OrgTangledRepoDefs.RepoViewBasic, data: Sidecar ): OrgTangledRepoDefs.RepoViewBasic => { const star = viewerUriOf(data, repo.did, STAR_VIEWER) as ResourceUri | null | undefined; return { ...repo, viewer: { ...repo.viewer, star: star ?? undefined } }; }; const withFollow = ( profile: OrgTangledActorDefs.ProfileViewDetailed, data: Sidecar ): OrgTangledActorDefs.ProfileViewDetailed => { const following = viewerUriOf(data, profile.did, FOLLOW_VIEWER) as ResourceUri | null | undefined; return { ...profile, viewer: { ...profile.viewer, following: following ?? undefined } }; }; const withViewerState = (feed: Feed, data: Sidecar): Feed => feed.map((item) => { const event = item.event; switch (event.$type) { case "org.tangled.feed.getTimeline#repoEvent": return { ...item, event: { ...event, repo: withStar(event.repo, data), ...(event.source ? { source: withStar(event.source, data) } : {}) } }; case "org.tangled.feed.getTimeline#starEvent": return { ...item, event: { ...event, repo: withStar(event.repo, data) } }; case "org.tangled.feed.getTimeline#followEvent": return { ...item, event: { ...event, subject: withFollow(event.subject, data) } }; default: return item; } }); const fetchFeed = async ({ ctx, followingOnly, viewer }: LoadFeedOptions): Promise => { const params = { followingOnly, limit: PAGE_LIMIT }; if (!viewer || followingOnly) { const { feed } = await jsonGet( ctx, "org.tangled.feed.getTimeline", params ); return feed; } const { output, data } = await enrich(ctx, { xrpc: "org.tangled.feed.getTimeline", params, viewer, enrich: [ target(STAR_VIEWER, ["feed[].event.repo.did", "feed[].event.source.did"]), target(FOLLOW_VIEWER, ["feed[].event.subject.did"]) ] }); return withViewerState(output.feed, data); }; // shared by /timeline and the signed-in homescreen at / export const loadFeed = (options: LoadFeedOptions) => fetchFeed(options); // the agent only exists in the browser, so this can't run in a server load export const loadFollowingFeed = async (auth: Auth, bobbinUrl: string) => { const did = auth.currentDid; if (!did) return []; const agent = auth.agent ?? (await auth.agentFor(did, { relogin: false })); return loadFeed({ ctx: createBobbinClient({ serviceUrl: bobbinUrl, agent }), followingOnly: true }); }; export const loadTrendingRepos = async (ctx: BobbinContext) => { try { const trending = await getTrending(ctx); return trending.repos.map(toRepoCard); } catch { return []; } }; const ANNOUNCEMENT_TAG = "announcement"; const ANNOUNCEMENT_COUNT = 3; export const loadAnnouncements = async ( fetch: typeof globalThis.fetch, timeout = 2500 ): Promise => { const items = await loadBlogFeed(fetch, timeout); if (!items) return []; return items .filter((item) => item.tags?.includes(ANNOUNCEMENT_TAG)) .slice(0, ANNOUNCEMENT_COUNT) .map((item) => ({ url: item.url, title: item.title, subtitle: item.summary, date: item.date_published })); };