~james/core

core/web/src/lib/components/timeline/load.ts -rw-r--r-- 4.2 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 15 hours ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
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
		}));
};