This repository has no description
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="py-2 flex cursor-pointer items-center justify-center rounded 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 border border-border-default bg-background-default text-foreground-default shadow-menu [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>