This repository has no description
1import { didFromUri, rkeyFromUri } from "$lib/api/uri";
2import type { ReactionRecord, RecordView } from "$lib/api/records";
3
4export const ORDERED_REACTION_KINDS = ["👍", "👎", "😆", "🎉", "🫤", "❤️", "🚀", "👀"] as const;
5export type ReactionKind = (typeof ORDERED_REACTION_KINDS)[number];
6
7const KIND_SET = new Set<string>(ORDERED_REACTION_KINDS);
8
9export interface ReactionGroup {
10 kind: ReactionKind;
11 count: number;
12 users: string[];
13 isReacted: boolean;
14 viewerRkey?: string;
15}
16
17// groups flat reaction records by emoji, keeping OrderedReactionKinds order and
18// dropping kinds nobody used. handleOf resolves a reactor did to a display handle.
19export function buildReactions(
20 items: RecordView<ReactionRecord>[],
21 viewerDid?: string,
22 handleOf?: (did: string) => string
23): ReactionGroup[] {
24 const groups = new Map<ReactionKind, ReactionGroup>();
25
26 for (const item of items) {
27 const kind = item.value.reaction;
28 if (!KIND_SET.has(kind)) continue;
29 const did = didFromUri(item.uri);
30 let group = groups.get(kind as ReactionKind);
31 if (!group) {
32 group = { kind: kind as ReactionKind, count: 0, users: [], isReacted: false };
33 groups.set(kind as ReactionKind, group);
34 }
35 group.count++;
36 group.users.push(handleOf ? handleOf(did) : did);
37 if (viewerDid && did === viewerDid) {
38 group.isReacted = true;
39 group.viewerRkey = rkeyFromUri(item.uri);
40 }
41 }
42
43 return ORDERED_REACTION_KINDS.filter((k) => groups.has(k)).map((k) => groups.get(k)!);
44}
45
46// optimistic addition for a reaction group
47export function withViewerReaction(
48 group: ReactionGroup,
49 viewerHandle: string,
50 rkey: string
51): ReactionGroup {
52 return {
53 ...group,
54 count: group.count + 1,
55 users: [...group.users, viewerHandle],
56 isReacted: true,
57 viewerRkey: rkey
58 };
59}
60
61// optimistic removal for a reaction group
62export function withoutViewerReaction(group: ReactionGroup, viewerHandle: string): ReactionGroup {
63 // drop one occurrence of the viewer; count never dips below zero
64 const idx = group.users.indexOf(viewerHandle);
65 const users = idx === -1 ? group.users : group.users.filter((_, i) => i !== idx);
66 return {
67 ...group,
68 count: Math.max(0, group.count - 1),
69 users,
70 isReacted: false,
71 viewerRkey: undefined
72 };
73}
74
75// optimistic add of a fresh reaction (e.g. from the picker): bumps the group if the
76// kind is already present, otherwise inserts a new one, keeping canonical emoji order.
77export function upsertViewerReaction(
78 groups: ReactionGroup[],
79 kind: ReactionKind,
80 viewerHandle: string,
81 rkey: string
82): ReactionGroup[] {
83 const exists = groups.some((g) => g.kind === kind);
84 const next = exists
85 ? groups.map((g) => (g.kind === kind ? withViewerReaction(g, viewerHandle, rkey) : g))
86 : [...groups, { kind, count: 1, users: [viewerHandle], isReacted: true, viewerRkey: rkey }];
87 return ORDERED_REACTION_KINDS.filter((k) => next.some((g) => g.kind === k)).map((k) =>
88 next.find((g) => g.kind === k)!
89 );
90}