Something went wrong. Try again.
Monorepo for Tangled
Something went wrong. Try again.
Svelte
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151<script lang="ts"> import { page } from "$app/state"; import { SvelteSet } from "svelte/reactivity"; import { now as tidNow } from "@atcute/tid"; import { putReaction, deleteReaction } from "$lib/api/reaction"; import { createBobbinClient } from "$lib/api/client"; import { awaitDeletedRecord, awaitIndexedOrSaved } from "$lib/api/awaitRecord"; import { getAuth } from "$lib/auth.svelte"; import type { ReactionRecord } from "$lib/api/reaction"; import Reaction from "$lib/components/reaction/Reaction.svelte"; import ReactionPicker from "$lib/components/reaction/ReactionPicker.svelte"; import { withViewerReaction, withoutViewerReaction, upsertViewerReaction, type ReactionGroup, type ReactionKind } from "$lib/components/reaction/reactions";
interface Props { reactions: ReactionGroup[]; subjectUri: string; class?: string; onchange?: (next: ReactionGroup[]) => void; showPicker?: boolean; alwaysShow?: boolean; }
let { reactions, subjectUri, class: className = "", onchange, showPicker = true, alwaysShow = false }: Props = $props();
const auth = getAuth();
let groups = $derived(reactions); const pending = new SvelteSet<ReactionKind>();
const commit = (next: ReactionGroup[]) => { groups = next; onchange?.(next); };
const setPending = (kind: ReactionKind, on: boolean) => { if (on) pending.add(kind); else pending.delete(kind); };
const toggle = async (group: ReactionGroup) => { const agent = auth.agent; const viewer = auth.currentUser; if (!agent || !viewer || pending.has(group.kind)) return;
const bobbinUrl = page.data.publicConfig?.bobbinUrl; const ctx = bobbinUrl ? createBobbinClient({ serviceUrl: bobbinUrl }) : null;
setPending(group.kind, true); try { if (group.isReacted) { if (group.viewerRkey) { const { uri } = await deleteReaction(agent, group.viewerRkey); if (ctx) { awaitDeletedRecord(ctx, uri).catch((cause) => console.error("failed to wait for unreaction index", cause) ); } } commit( groups .map((g) => g.kind === group.kind ? withoutViewerReaction(g, viewer.handle) : g ) .filter((g) => g.count > 0) ); } else { const rkey = tidNow(); const record: ReactionRecord = { $type: "sh.tangled.feed.reaction", subject: subjectUri as ReactionRecord["subject"], reaction: group.kind, createdAt: new Date().toISOString() }; const written = await putReaction(agent, rkey, record); if (ctx) { // todo: needs a design pass for surfacing index wait failures awaitIndexedOrSaved(ctx, written).catch((cause) => console.error("failed to wait for reaction index", cause) ); } commit( groups.map((g) => g.kind === group.kind ? withViewerReaction(g, viewer.handle, rkey) : g ) ); } } catch { // leave the rendered state untouched on failure; a reload reconciles it } finally { setPending(group.kind, false); } };
const canReact = $derived(auth.currentUser !== null);
// kinds the viewer reacted with, mapped to their record's rkey so the picker can toggle const reacted = $derived( new Map( groups .filter((g) => g.isReacted && g.viewerRkey) .map((g) => [g.kind, g.viewerRkey!] as [ReactionKind, string]) ) );
const onreacted = (kind: ReactionKind, rkey: string) => { const viewer = auth.currentUser; if (!viewer) return; commit(upsertViewerReaction(groups, kind, viewer.handle, rkey)); };
const onunreacted = (kind: ReactionKind) => { const viewer = auth.currentUser; if (!viewer) return; commit( groups .map((g) => (g.kind === kind ? withoutViewerReaction(g, viewer.handle) : g)) .filter((g) => g.count > 0) ); };
const pickerAlways = $derived(alwaysShow && canReact && showPicker);</script>
{#if groups.length || pickerAlways} <div class={`flex flex-wrap items-center gap-2xs ${className}`}> {#each groups as reaction (reaction.kind)} <Reaction {reaction} ontoggle={canReact ? () => toggle(reaction) : undefined} pending={pending.has(reaction.kind)} /> {/each} {#if canReact && showPicker} <ReactionPicker {subjectUri} {reacted} {onreacted} {onunreacted} /> {/if} </div>{/if}