import { listGroupDeliveriesForMessages, listGroupReadsForMessages, } from '@chat-app/shared/chat'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { supabase } from './supabase'; export interface GroupReceiptState { // message_id → set of user ids who have delivered/read. deliveredByMessage: Map>; readByMessage: Map>; refresh: () => Promise; } // Aggregated delivery + read receipts for group conversations. Returns the // set of user ids per message; consumers join with conversation members to // figure out who has not yet acknowledged. export function useGroupReceipts( messageIds: string[], selfUserId: string | undefined, active: boolean, ): GroupReceiptState { const idsKey = useMemo(() => messageIds.join(','), [messageIds]); const [deliveredByMessage, setDelivered] = useState>>(new Map()); const [readByMessage, setRead] = useState>>(new Map()); const refresh = useCallback(async () => { if (!active || !selfUserId || messageIds.length === 0) { setDelivered(new Map()); setRead(new Map()); return; } try { const [delivered, read] = await Promise.all([ listGroupDeliveriesForMessages(supabase, messageIds, selfUserId), listGroupReadsForMessages(supabase, messageIds, selfUserId), ]); setDelivered(toIdSets(delivered)); setRead(toIdSets(read)); } catch (err: unknown) { console.warn('useGroupReceipts refresh failed', err); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [active, selfUserId, idsKey]); useEffect(() => { void refresh(); if (!active || !selfUserId) return; // Listen on both tables. RLS already enforces visibility. const reads = supabase .channel('group-reads:' + selfUserId) .on( 'postgres_changes', { event: 'INSERT', schema: 'public', table: 'message_reads' }, () => { void refresh(); }, ) .subscribe(); const deliveries = supabase .channel('group-deliv:' + selfUserId) .on( 'postgres_changes', { event: 'INSERT', schema: 'public', table: 'message_deliveries' }, () => { void refresh(); }, ) .subscribe(); return () => { void supabase.removeChannel(reads); void supabase.removeChannel(deliveries); }; }, [active, selfUserId, refresh]); return { deliveredByMessage, readByMessage, refresh }; } function toIdSets(input: Map>): Map> { const out = new Map>(); for (const [mid, inner] of input) { out.set(mid, new Set(inner.keys())); } return out; }