Signed-off-by: Seongmin Lee git@boltless.me
+134
Diff
Round #1
+106
web/src/routes/timeline/+page.svelte
+106
web/src/routes/timeline/+page.svelte
···
1
+
<script lang="ts">
2
+
import { resolve } from "$app/paths";
3
+
import GalleryVertical from "$icon/gallery-vertical";
4
+
import Button from "$lib/components/ui/Button.svelte";
5
+
import ButtonGroup from "$lib/components/ui/ButtonGroup.svelte";
6
+
import EmptyState from "$lib/components/ui/EmptyState.svelte";
7
+
import User from "$lib/components/ui/User.svelte";
8
+
import RepoCard from "$lib/components/repo/RepoCard.svelte";
9
+
import FollowCard from "$lib/components/profile/FollowCard.svelte";
10
+
import { rkeyFromUri } from "$lib/api/uri";
11
+
import { relativeTime } from "$lib/format";
12
+
import type { RepoCardData, PersonData } from "$lib/components/profile/types";
13
+
import type * as ShTangledRepoDefs from "$lib/api/lexicons/types/sh/tangled/repo/defs";
14
+
import type * as ShTangledActorDefs from "$lib/api/lexicons/types/sh/tangled/actor/defs";
15
+
import type { PageData } from "./$types";
16
+
17
+
let { data }: { data: PageData } = $props();
18
+
19
+
const toRepoCard = (repo: ShTangledRepoDefs.RepoViewBasic): RepoCardData => ({
20
+
name: repo.slug,
21
+
repoDid: repo.did,
22
+
ownerHandle: repo.owner.handle,
23
+
description: repo.description,
24
+
createdAt: repo.createdAt,
25
+
stars: repo.starCount,
26
+
viewerStarRkey: repo.viewer?.star ? rkeyFromUri(repo.viewer.star) : null
27
+
});
28
+
29
+
const toPerson = (profile: ShTangledActorDefs.ProfileViewDetailed): PersonData => ({
30
+
did: profile.did,
31
+
handle: profile.handle,
32
+
avatar: profile.avatar,
33
+
followers: profile.followersCount,
34
+
following: profile.followsCount,
35
+
viewerFollowRkey: profile.viewer?.following ? rkeyFromUri(profile.viewer.following) : null
36
+
});
37
+
38
+
const repoHref = (repo: ShTangledRepoDefs.RepoViewBasic) =>
39
+
resolve(`/${repo.owner.handle}/${repo.slug}` as "/");
40
+
</script>
41
+
42
+
<svelte:head>
43
+
<title>Timeline · Tangled</title>
44
+
</svelte:head>
45
+
46
+
<section class="mx-auto flex w-full max-w-screen-md flex-col gap-4 px-6 py-8">
47
+
<div class="flex items-center justify-between">
48
+
<h1 class="flex items-center gap-2 text-2xl font-semibold">
49
+
<GalleryVertical class="size-5 text-foreground-subtle" aria-hidden="true" />
50
+
Timeline
51
+
</h1>
52
+
{#if data.signedIn}
53
+
<ButtonGroup>
54
+
<Button
55
+
href={resolve("/timeline" as "/")}
56
+
variant={data.followingOnly ? "ghost" : "default"}>Global</Button
57
+
>
58
+
<Button
59
+
href={resolve("/timeline?following=true" as "/")}
60
+
variant={data.followingOnly ? "default" : "ghost"}>Following</Button
61
+
>
62
+
</ButtonGroup>
63
+
{/if}
64
+
</div>
65
+
66
+
{#each data.feed as item (item.event.uri)}
67
+
{@const event = item.event}
68
+
<div class="flex flex-col gap-2">
69
+
<div class="flex flex-wrap items-center gap-2 text-sm text-foreground-muted">
70
+
{#if event.$type === "sh.tangled.feed.getTimeline#repoEvent"}
71
+
<User handle={event.repo.owner.handle} src={event.repo.owner.avatar} size="sm" />
72
+
{#if event.source}
73
+
<span>forked</span>
74
+
<a href={repoHref(event.source)} class="text-foreground-default"
75
+
>{event.source.owner.handle}/{event.source.slug}</a
76
+
>
77
+
<span>to {event.repo.slug}</span>
78
+
{:else}
79
+
<span>created {event.repo.slug}</span>
80
+
{/if}
81
+
{:else if event.$type === "sh.tangled.feed.getTimeline#starEvent"}
82
+
<User handle={event.actor.handle} src={event.actor.avatar} size="sm" />
83
+
<span>starred</span>
84
+
<a href={repoHref(event.repo)} class="text-foreground-default"
85
+
>{event.repo.owner.handle}/{event.repo.slug}</a
86
+
>
87
+
{:else if event.$type === "sh.tangled.feed.getTimeline#followEvent"}
88
+
<User handle={event.actor.handle} src={event.actor.avatar} size="sm" />
89
+
<span>followed</span>
90
+
<User handle={event.subject.handle} src={event.subject.avatar} size="sm" />
91
+
{/if}
92
+
<span class="text-foreground-subtle">{relativeTime(item.eventAt)}</span>
93
+
</div>
94
+
95
+
{#if event.$type === "sh.tangled.feed.getTimeline#followEvent"}
96
+
<FollowCard person={toPerson(event.subject)} />
97
+
{:else}
98
+
<RepoCard repo={toRepoCard(event.repo)} />
99
+
{/if}
100
+
</div>
101
+
{:else}
102
+
<EmptyState
103
+
message={data.followingOnly ? "Nobody you follow has been busy." : "Nothing here yet."}
104
+
/>
105
+
{/each}
106
+
</section>
+28
web/src/routes/timeline/+page.ts
+28
web/src/routes/timeline/+page.ts
···
1
+
import { createBobbinClient } from "$lib/api/client";
2
+
import { jsonGet } from "$lib/api/_request";
3
+
import { toHttpError } from "$lib/api/load";
4
+
import type * as ShTangledFeedGetTimeline from "$lib/api/lexicons/types/sh/tangled/feed/getTimeline";
5
+
import type { PageLoad } from "./$types";
6
+
7
+
const PAGE_LIMIT = 50;
8
+
9
+
export const load: PageLoad = async (event) => {
10
+
const parent = await event.parent();
11
+
const ctx = createBobbinClient({ serviceUrl: parent.publicConfig.bobbinUrl, fetch: event.fetch });
12
+
const viewer = parent.auth?.did;
13
+
14
+
// the following feed needs a viewer to resolve the followed set; bobbin 400s without one
15
+
const followingOnly = event.url.searchParams.get("following") === "true" && viewer !== undefined;
16
+
17
+
try {
18
+
// output key is `feed`, not `items`, so the pagination.ts helpers don't apply
19
+
const { feed } = await jsonGet<ShTangledFeedGetTimeline.$output>(
20
+
ctx,
21
+
"sh.tangled.feed.getTimeline",
22
+
{ viewer, followingOnly, limit: PAGE_LIMIT }
23
+
);
24
+
return { feed, followingOnly, signedIn: viewer !== undefined };
25
+
} catch (cause) {
26
+
toHttpError(cause, "Could not load timeline");
27
+
}
28
+
};
History
2 rounds
0 comments
boltless.me
submitted
#1
1 commit
Expand
Collapse
web: timeline
Signed-off-by: Seongmin Lee <git@boltless.me>
Expand 0 comments
boltless.me
submitted
#0
1 commit
Expand
Collapse
web: timeline
Signed-off-by: Seongmin Lee <git@boltless.me>