Monorepo for Tangled tangled.org
3

Configure Feed

Select the types of activity you want to include in your feed.

Labels

None yet.

Participants 1
AT URI
at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.pull/3mrmrvzsvaf22
+137
Diff #0
+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 &middot; 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
··· 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
Sign up or Login to add to the discussion
1 commit
Expand
web: timeline
Checking mergeability…
Expand 0 comments
boltless.me submitted #0
1 commit
Expand
web: timeline
Expand 0 comments