This repository has no description
0

Configure Feed

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

core / web / src / lib / components / reaction / Reactions.svelte
3.7 kB 133 lines
1<script lang="ts"> 2 import { SvelteSet } from "svelte/reactivity"; 3 import { now as tidNow } from "@atcute/tid"; 4 import { putReaction, deleteReaction } from "$lib/api/reaction"; 5 import { getAuth } from "$lib/auth.svelte"; 6 import type { ReactionRecord } from "$lib/api/records"; 7 import Reaction from "./Reaction.svelte"; 8 import ReactionPicker from "./ReactionPicker.svelte"; 9 import { 10 withViewerReaction, 11 withoutViewerReaction, 12 upsertViewerReaction, 13 type ReactionGroup, 14 type ReactionKind 15 } from "./reactions"; 16 17 interface Props { 18 reactions: ReactionGroup[]; 19 subjectUri: string; 20 class?: string; 21 // controlled mode: report changes up instead of keeping them only in local state 22 onchange?: (next: ReactionGroup[]) => void; 23 showPicker?: boolean; 24 // keep the bar mounted even with no reactions yet 25 alwaysShow?: boolean; 26 } 27 28 let { 29 reactions, 30 subjectUri, 31 class: className = "", 32 onchange, 33 showPicker = true, 34 alwaysShow = false 35 }: Props = $props(); 36 37 const auth = getAuth(); 38 39 // seeded from the prop, reassigned optimistically, reset by svelte on fresh reactions 40 let groups = $derived(reactions); 41 // kinds with an in-flight PDS write, disabled until it settles 42 const pending = new SvelteSet<ReactionKind>(); 43 44 const commit = (next: ReactionGroup[]) => { 45 groups = next; 46 onchange?.(next); 47 }; 48 49 const setPending = (kind: ReactionKind, on: boolean) => { 50 if (on) pending.add(kind); 51 else pending.delete(kind); 52 }; 53 54 const toggle = async (group: ReactionGroup) => { 55 const agent = auth.agent; 56 const viewer = auth.currentUser; 57 if (!agent || !viewer || pending.has(group.kind)) return; 58 59 setPending(group.kind, true); 60 try { 61 if (group.isReacted) { 62 if (group.viewerRkey) await deleteReaction(agent, group.viewerRkey); 63 commit( 64 groups 65 .map((g) => (g.kind === group.kind ? withoutViewerReaction(g, viewer.handle) : g)) 66 .filter((g) => g.count > 0) 67 ); 68 } else { 69 const rkey = tidNow(); 70 const record: ReactionRecord = { 71 $type: "sh.tangled.feed.reaction", 72 subject: subjectUri as ReactionRecord["subject"], 73 reaction: group.kind, 74 createdAt: new Date().toISOString() 75 }; 76 await putReaction(agent, rkey, record); 77 commit( 78 groups.map((g) => 79 g.kind === group.kind ? withViewerReaction(g, viewer.handle, rkey) : g 80 ) 81 ); 82 } 83 } catch { 84 // leave the rendered state untouched on failure; a reload reconciles it 85 } finally { 86 setPending(group.kind, false); 87 } 88 }; 89 90 const canReact = $derived(auth.currentUser !== null); 91 92 // kinds the viewer reacted with, mapped to their record's rkey so the picker can toggle 93 const reacted = $derived( 94 new Map( 95 groups 96 .filter((g) => g.isReacted && g.viewerRkey) 97 .map((g) => [g.kind, g.viewerRkey!] as [ReactionKind, string]) 98 ) 99 ); 100 101 const onreacted = (kind: ReactionKind, rkey: string) => { 102 const viewer = auth.currentUser; 103 if (!viewer) return; 104 commit(upsertViewerReaction(groups, kind, viewer.handle, rkey)); 105 }; 106 107 const onunreacted = (kind: ReactionKind) => { 108 const viewer = auth.currentUser; 109 if (!viewer) return; 110 commit( 111 groups 112 .map((g) => (g.kind === kind ? withoutViewerReaction(g, viewer.handle) : g)) 113 .filter((g) => g.count > 0) 114 ); 115 }; 116 117 const pickerAlways = $derived(alwaysShow && canReact && showPicker); 118</script> 119 120{#if groups.length || pickerAlways} 121 <div class={`flex flex-wrap items-center gap-2 ${className}`}> 122 {#each groups as reaction (reaction.kind)} 123 <Reaction 124 {reaction} 125 ontoggle={canReact ? () => toggle(reaction) : undefined} 126 pending={pending.has(reaction.kind)} 127 /> 128 {/each} 129 {#if canReact && showPicker} 130 <ReactionPicker {subjectUri} {reacted} {onreacted} {onunreacted} /> 131 {/if} 132 </div> 133{/if}