import { addReaction, listReactionsForMessages, type MessageReaction, removeReaction, } from '@chat-app/shared/chat'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { supabase } from './supabase'; export interface AggregatedReaction { emoji: string; count: number; userIds: string[]; mine: boolean; } export interface UseMessageReactionsResult { byMessage: Map; toggle: (messageId: string, emoji: string) => Promise; } // Batch-fetches reactions for the given message ids + subscribes to the // message_reactions table. Re-pulls on any change (batch is cheap). export function useMessageReactions( messageIds: string[], myId: string | undefined, ): UseMessageReactionsResult { const idsKey = useMemo(() => messageIds.join(','), [messageIds]); const [rows, setRows] = useState([]); const refresh = useCallback(async () => { if (messageIds.length === 0) { setRows([]); return; } try { const data = await listReactionsForMessages(supabase, messageIds); setRows(data); } catch (err: unknown) { console.error('listReactionsForMessages failed', err); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [idsKey]); useEffect(() => { void refresh(); if (messageIds.length === 0) return; const channel = supabase .channel('reactions:' + idsKey.slice(0, 32)) .on( 'postgres_changes', { event: '*', schema: 'public', table: 'message_reactions' }, (payload: { new: Record; old: Record }) => { const mid = (payload.new?.message_id as string | undefined) ?? (payload.old?.message_id as string | undefined); if (mid && messageIds.includes(mid)) { void refresh(); } }, ) .subscribe(); return () => { void supabase.removeChannel(channel); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [idsKey, refresh]); const byMessage = useMemo(() => { const out = new Map(); for (const r of rows) { const list = out.get(r.messageId) ?? []; const existing = list.find((a) => a.emoji === r.emoji); if (existing) { existing.count += 1; existing.userIds.push(r.userId); if (r.userId === myId) existing.mine = true; } else { list.push({ emoji: r.emoji, count: 1, userIds: [r.userId], mine: r.userId === myId, }); } out.set(r.messageId, list); } return out; }, [rows, myId]); const toggle = useCallback( async (messageId: string, emoji: string) => { if (!myId) return; const current = byMessage.get(messageId) ?? []; const existing = current.find((a) => a.emoji === emoji); if (existing?.mine) { await removeReaction(supabase, messageId, emoji); } else { await addReaction(supabase, messageId, emoji); } await refresh(); }, [byMessage, myId, refresh], ); return { byMessage, toggle }; }