Signed-off-by: Seongmin Lee git@boltless.me
+137
Diff
Round #0
+109
web/src/routes/timeline/+page.svelte
+109
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
+
// starEvent => event.uri (at-uri)
20
+
// repoEvent
21
+
22
+
const toRepoCard = (repo: ShTangledRepoDefs.RepoViewBasic): RepoCardData => ({
23
+
name: repo.slug,
24
+
repoDid: repo.did,
25
+
ownerHandle: repo.owner.handle,
26
+
description: repo.description,
27
+
createdAt: repo.createdAt,
28
+
stars: repo.starCount,
29
+
viewerStarRkey: repo.viewer?.star ? rkeyFromUri(repo.viewer.star) : null
30
+
});
31
+
32
+
const toPerson = (profile: ShTangledActorDefs.ProfileViewDetailed): PersonData => ({
33
+
did: profile.did,
34
+
handle: profile.handle,
35
+
avatar: profile.avatar,
36
+
followers: profile.followersCount,
37
+
following: profile.followsCount,
38
+
viewerFollowRkey: profile.viewer?.following ? rkeyFromUri(profile.viewer.following) : null
39
+
});
40
+
41
+
const repoHref = (repo: ShTangledRepoDefs.RepoViewBasic) =>
42
+
resolve(`/${repo.owner.handle}/${repo.slug}` as "/");
43
+
</script>
44
+
45
+
<svelte:head>
46
+
<title>Timeline · Tangled</title>
47
+
</svelte:head>
48
+
49
+
<section class="mx-auto flex w-full max-w-screen-md flex-col gap-4 px-6 py-8">
50
+
<div class="flex items-center justify-between">
51
+
<h1 class="flex items-center gap-2 text-2xl font-semibold">
52
+
<GalleryVertical class="size-5 text-foreground-subtle" aria-hidden="true" />
53
+
Timeline
54
+
</h1>
55
+
{#if data.signedIn}
56
+
<ButtonGroup>
57
+
<Button
58
+
href={resolve("/timeline" as "/")}
59
+
variant={data.followingOnly ? "ghost" : "default"}>Global</Button
60
+
>
61
+
<Button
62
+
href={resolve("/timeline?following=true" as "/")}
63
+
variant={data.followingOnly ? "default" : "ghost"}>Following</Button
64
+
>
65
+
</ButtonGroup>
66
+
{/if}
67
+
</div>
68
+
69
+
{#each data.feed as item (item.event.uri)}
70
+
{@const event = item.event}
71
+
<div class="flex flex-col gap-2">
72
+
<div class="flex flex-wrap items-center gap-2 text-sm text-foreground-muted">
73
+
{#if event.$type === "sh.tangled.feed.getTimeline#repoEvent"}
74
+
<User handle={event.repo.owner.handle} src={event.repo.owner.avatar} size="sm" />
75
+
{#if event.source}
76
+
<span>forked</span>
77
+
<a href={repoHref(event.source)} class="text-foreground-default"
78
+
>{event.source.owner.handle}/{event.source.slug}</a
79
+
>
80
+
<span>to {event.repo.slug}</span>
81
+
{:else}
82
+
<span>created {event.repo.slug}</span>
83
+
{/if}
84
+
{:else if event.$type === "sh.tangled.feed.getTimeline#starEvent"}
85
+
<User handle={event.actor.handle} src={event.actor.avatar} size="sm" />
86
+
<span>starred</span>
87
+
<a href={repoHref(event.repo)} class="text-foreground-default"
88
+
>{event.repo.owner.handle}/{event.repo.slug}</a
89
+
>
90
+
{:else if event.$type === "sh.tangled.feed.getTimeline#followEvent"}
91
+
<User handle={event.actor.handle} src={event.actor.avatar} size="sm" />
92
+
<span>followed</span>
93
+
<User handle={event.subject.handle} src={event.subject.avatar} size="sm" />
94
+
{/if}
95
+
<span class="text-foreground-subtle">{relativeTime(item.eventAt)}</span>
96
+
</div>
97
+
98
+
{#if event.$type === "sh.tangled.feed.getTimeline#followEvent"}
99
+
<FollowCard person={toPerson(event.subject)} />
100
+
{:else}
101
+
<RepoCard repo={toRepoCard(event.repo)} />
102
+
{/if}
103
+
</div>
104
+
{:else}
105
+
<EmptyState
106
+
message={data.followingOnly ? "Nobody you follow has been busy." : "Nothing here yet."}
107
+
/>
108
+
{/each}
109
+
</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>