Something went wrong. Try again.
Monorepo for Tangled
Something went wrong. Try again.
Svelte
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210<script lang="ts"> import { resolve } from "$app/paths"; import { page } from "$app/state"; import Star from "$icon/star"; import TriangleAlert from "$icon/triangle-alert"; import { getAuth } from "$lib/auth.svelte"; import { loginWithReturn } from "$lib/auth/guards"; import { createStar, deleteStar, type StarSubject } from "$lib/api/graph"; import { createBobbinClient } from "$lib/api/client"; import { awaitDeletedRecord, awaitIndexedOrSaved } from "$lib/api/awaitRecord"; import Button from "$lib/components/ui/Button.svelte"; import ButtonGroup from "$lib/components/ui/ButtonGroup.svelte"; import Tooltip from "$lib/components/ui/Tooltip.svelte"; import { getProfileCounts } from "$lib/components/profile/counts.svelte"; import { createOptimisticCount, createOptimisticRelation, seededValue } from "$lib/optimistic.svelte"; import { formatCount } from "$lib/format"; import Bones from "$lib/components/ui/Bones.svelte"; import ToggleFaces from "$lib/components/ui/ToggleFaces.svelte"; import bonesLoadingprimitivesCount from "$lib/bones/bones-loadingprimitives--count.bones.json"; import type { StarChange } from "$lib/components/profile/types"; import type { RepoCounts } from "$lib/components/repo/types";
interface Props { subject: StarSubject; initialCount?: number | RepoCounts | Promise<number> | Promise<RepoCounts>; initialRkey?: string | null | Promise<string | null>; hideCount?: boolean; onAdjust?: (delta: number) => void; onCommit?: (change: StarChange) => void; class?: string; } let { subject, initialCount, initialRkey, hideCount = false, onAdjust, onCommit, class: className = "" }: Props = $props();
const subjectKey = $derived(subject.kind === "repo" ? subject.did : subject.uri);
const auth = getAuth(); const profileCounts = getProfileCounts(); const signedIn = $derived(Boolean(auth.currentDid)); const initialRkeySync = ( val: string | null | Promise<string | null> | undefined ): string | null | undefined => (typeof val === "string" || val === null ? val : undefined);
const initialCountSync = ( val: number | RepoCounts | Promise<number> | Promise<RepoCounts> | undefined ): number | undefined => { if (typeof val === "number") return val; if (typeof val === "object" && val !== null && !("then" in (val as unknown as object))) { return (val as RepoCounts).stars; } return undefined; };
const rkeySeed = seededValue(() => initialRkey, initialRkeySync, null); const countSeed = seededValue(() => initialCount, initialCountSync, 0); const loadedRkey = $derived(rkeySeed.value); const loadedCount = $derived(countSeed.value);
const relation = createOptimisticRelation({ key: () => `${auth.currentDid ?? ""}:${subjectKey}`, loadedRkey: () => loadedRkey }); const starCount = createOptimisticCount({ key: () => subjectKey, loaded: () => loadedCount }); const starred = $derived(relation.active); const failed = $derived(relation.failed || starCount.failed); $effect(() => { if (!failed) return; const timer = setTimeout(() => { relation.resetFailure(); starCount.resetFailure(); }, 2000); return () => clearTimeout(timer); }); const loginHref = $derived( resolve(loginWithReturn(page.url.pathname + page.url.search) as "/login") ); const starsHref = $derived( subject.kind === "repo" && subject.ownerHandle && subject.name ? resolve(`/${subject.ownerHandle}/${subject.name}/stars` as "/") : undefined );
const toggle = async () => { const agent = auth.agent; if (!agent || !relation.known || relation.loading || !subjectKey) return; const wasStarred = relation.active; const existingRkey = relation.rkey; const delta = wasStarred ? -1 : 1; const undo = wasStarred ? 1 : -1; relation.begin(!wasStarred); starCount.resetFailure(); starCount.adjust(delta); onAdjust?.(delta); if (subject.kind === "repo") { profileCounts?.adjust(agent.sub, "stars", delta); } const bobbinUrl = page.data.publicConfig?.bobbinUrl; const ctx = bobbinUrl ? createBobbinClient({ serviceUrl: bobbinUrl }) : null; try { if (wasStarred && existingRkey) { const { uri } = await deleteStar(agent, existingRkey); relation.deleted(); if (ctx) { awaitDeletedRecord(ctx, uri).catch((cause) => console.error("failed to wait for unstar index", cause) ); } if (subject.kind === "repo") { onCommit?.({ viewerDid: agent.sub, repoDid: subject.did, starred: false, rkey: null, delta }); } } else { const written = await createStar(agent, subject); const rkey = written.rkey; relation.created(rkey); if (ctx) { // todo: needs a design pass for surfacing index wait failures awaitIndexedOrSaved(ctx, written).catch((cause) => console.error("failed to wait for star index", cause) ); } if (subject.kind === "repo") { onCommit?.({ viewerDid: agent.sub, repoDid: subject.did, starred: true, rkey, delta }); } } } catch { relation.fail(); starCount.adjust(undo); onAdjust?.(undo); starCount.fail(); if (subject.kind === "repo") { profileCounts?.adjust(agent.sub, "stars", undo); } } };</script>
<div class={["flex flex-col items-start font-normal", className]}> <Tooltip content="Something went wrong. Try again." disabled={!failed}> <ButtonGroup class="w-full"> <Button variant={failed ? "danger" : "default"} insetShadow={true} href={signedIn ? undefined : loginHref} disabled={signedIn && (!relation.known || !subjectKey)} aria-busy={relation.loading} onclick={signedIn ? toggle : undefined} class="min-w-22 flex-1" > <ToggleFaces on={starred} {failed} size="small" offLabel="Star" onLabel="Starred"> {#snippet off(cls)} <Star class={cls} /> {/snippet} {#snippet onIcon(cls)} <Star class={[cls, "[&_path]:fill-current"]} /> {/snippet} {#snippet err(cls)} <TriangleAlert class={cls} /> {/snippet} </ToggleFaces> </Button> {#if !hideCount} <Button href={starsHref} variant="ghost" size="small" title="Starred by" class={starsHref ? undefined : "pointer-events-none"} tabindex={starsHref ? undefined : -1} insetShadow={true} > {#if loadedCount === undefined} <Bones capture={bonesLoadingprimitivesCount} class="inline-block w-8 align-middle" /> {:else} {formatCount(starCount.value)} {/if} </Button> {/if} </ButtonGroup> </Tooltip></div>