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