import { didFromUri, rkeyFromUri } from "$lib/api/uri"; import type { ReactionRecord } from "$lib/api/reaction"; import type { RecordView } from "$lib/api/records"; export const ORDERED_REACTION_KINDS = ["👍", "👎", "😆", "🎉", "🫤", "❤️", "🚀", "👀"] as const; export type ReactionKind = (typeof ORDERED_REACTION_KINDS)[number]; const KIND_SET = new Set(ORDERED_REACTION_KINDS); export interface ReactionGroup { kind: ReactionKind; count: number; users: string[]; isReacted: boolean; viewerRkey?: string; } // groups flat reaction records by emoji, keeping OrderedReactionKinds order and // dropping kinds nobody used. handleOf resolves a reactor did to a display handle. export function buildReactions( items: RecordView[], viewerDid?: string, handleOf?: (did: string) => string ): ReactionGroup[] { const groups = new Map(); for (const item of items) { const kind = item.value.reaction; if (!KIND_SET.has(kind)) continue; const did = didFromUri(item.uri); let group = groups.get(kind as ReactionKind); if (!group) { group = { kind: kind as ReactionKind, count: 0, users: [], isReacted: false }; groups.set(kind as ReactionKind, group); } group.count++; group.users.push(handleOf ? handleOf(did) : did); if (viewerDid && did === viewerDid) { group.isReacted = true; group.viewerRkey = rkeyFromUri(item.uri); } } return ORDERED_REACTION_KINDS.filter((k) => groups.has(k)).map((k) => groups.get(k)!); } // optimistic addition for a reaction group export function withViewerReaction( group: ReactionGroup, viewerHandle: string, rkey: string ): ReactionGroup { return { ...group, count: group.count + 1, users: [...group.users, viewerHandle], isReacted: true, viewerRkey: rkey }; } // optimistic removal for a reaction group export function withoutViewerReaction(group: ReactionGroup, viewerHandle: string): ReactionGroup { // drop one occurrence of the viewer; count never dips below zero const idx = group.users.indexOf(viewerHandle); const users = idx === -1 ? group.users : group.users.filter((_, i) => i !== idx); return { ...group, count: Math.max(0, group.count - 1), users, isReacted: false, viewerRkey: undefined }; } // optimistic add of a fresh reaction (e.g. from the picker): bumps the group if the // kind is already present, otherwise inserts a new one, keeping canonical emoji order. export function upsertViewerReaction( groups: ReactionGroup[], kind: ReactionKind, viewerHandle: string, rkey: string ): ReactionGroup[] { const exists = groups.some((g) => g.kind === kind); const next = exists ? groups.map((g) => (g.kind === kind ? withViewerReaction(g, viewerHandle, rkey) : g)) : [...groups, { kind, count: 1, users: [viewerHandle], isReacted: true, viewerRkey: rkey }]; return ORDERED_REACTION_KINDS.filter((k) => next.some((g) => g.kind === k)).map((k) => next.find((g) => g.kind === k)! ); }