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 / ReactionPicker.svelte
2.7 kB 90 lines
1<script lang="ts"> 2 import { now as tidNow } from "@atcute/tid"; 3 import SmilePlus from "$icon/smile-plus"; 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 Button from "$lib/components/ui/Button.svelte"; 8 import { ORDERED_REACTION_KINDS, type ReactionKind } from "./reactions"; 9 10 interface Props { 11 subjectUri: string; 12 // kinds the viewer reacted with, mapped to their record's rkey so a repeat pick deletes it 13 reacted?: Map<ReactionKind, string>; 14 onreacted?: (kind: ReactionKind, rkey: string) => void; 15 onunreacted?: (kind: ReactionKind) => void; 16 } 17 18 let { subjectUri, reacted, onreacted, onunreacted }: Props = $props(); 19 20 const auth = getAuth(); 21 22 const rawId = $props.id(); 23 const popoverId = `reaction-picker-${rawId.replace(/[^a-zA-Z0-9]/g, "")}`; 24 const anchorName = `--${popoverId}`; 25 26 let panel = $state<HTMLElement>(); 27 let pending = $state<ReactionKind | null>(null); 28 29 const pick = async (kind: ReactionKind) => { 30 const agent = auth?.agent; 31 if (!agent || pending) return; 32 33 pending = kind; 34 try { 35 const existingRkey = reacted?.get(kind); 36 if (existingRkey) { 37 await deleteReaction(agent, existingRkey); 38 onunreacted?.(kind); 39 } else { 40 const rkey = tidNow(); 41 const record: ReactionRecord = { 42 $type: "sh.tangled.feed.reaction", 43 subject: subjectUri as ReactionRecord["subject"], 44 reaction: kind, 45 createdAt: new Date().toISOString() 46 }; 47 await putReaction(agent, rkey, record); 48 onreacted?.(kind, rkey); 49 } 50 panel?.hidePopover(); 51 } catch { 52 // leave the panel open on failure so the user can retry 53 } finally { 54 pending = null; 55 } 56 }; 57</script> 58 59<button 60 type="button" 61 popovertarget={popoverId} 62 style={`anchor-name: ${anchorName}`} 63 aria-label="Add reaction" 64 class="flex cursor-pointer items-center justify-center rounded-sm py-2 text-foreground-subtle transition hover:text-foreground-default" 65> 66 <SmilePlus class="size-3" /> 67</button> 68 69<div 70 bind:this={panel} 71 id={popoverId} 72 popover="auto" 73 style={`position-anchor: ${anchorName}`} 74 class="inset-auto mt-1 h-fit w-max rounded-sm border border-border-default bg-background-default text-foreground-default shadow-regular [position-area:bottom_center]" 75> 76 <div class="grid grid-cols-4 p-1"> 77 {#each ORDERED_REACTION_KINDS as kind (kind)} 78 <Button 79 variant="ghost" 80 size="sm" 81 aria-label={`React with ${kind}`} 82 aria-pressed={reacted?.has(kind) ?? false} 83 disabled={pending !== null} 84 onclick={() => pick(kind)} 85 > 86 {kind} 87 </Button> 88 {/each} 89 </div> 90</div>