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";
import type { Fetch } from "#lib/fetch.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<Feed> => {
const params = { followingOnly, limit: PAGE_LIMIT };
if (!viewer || followingOnly) {
const { feed } = await jsonGet<OrgTangledFeedGetTimeline.$output>(
ctx,
"org.tangled.feed.getTimeline",
params
);
return feed;
}
const { output, data } = await enrich<OrgTangledFeedGetTimeline.$output>(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: Fetch, timeout = 2500): Promise<BlogPost[]> => {
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
}));
};