import type { RealtimeChannel } from '@supabase/supabase-js'; import { useCallback, useEffect, useRef, useState } from 'react'; import { supabase } from './supabase'; // Typing events live on a Realtime BROADCAST channel — no DB writes. // Each typer pings once every 2s while actively typing. Receivers keep a // per-user timestamp and show the indicator for 4s after the last ping. const SEND_THROTTLE_MS = 2000; const RECEIVE_TTL_MS = 4000; export interface UseTypingChannel { typingUserIds: string[]; notifyTyping: () => void; notifyStopTyping: () => void; } export function useTypingChannel( conversationId: string | undefined, myId: string | undefined, ): UseTypingChannel { const [typingUserIds, setTypingUserIds] = useState([]); const channelRef = useRef(null); const lastSentRef = useRef(0); const receivedRef = useRef>(new Map()); useEffect(() => { receivedRef.current = new Map(); setTypingUserIds([]); if (!conversationId || !myId) return; const channel = supabase.channel('typing:' + conversationId, { config: { broadcast: { self: false } }, }); channelRef.current = channel; channel.on('broadcast', { event: 'typing' }, (msg) => { const payload = msg.payload as { userId?: string; stop?: boolean }; const uid = payload.userId; if (!uid || uid === myId) return; if (payload.stop) { receivedRef.current.delete(uid); } else { receivedRef.current.set(uid, Date.now()); } setTypingUserIds(collectRecent(receivedRef.current)); }); void channel.subscribe(); const interval = window.setInterval(() => { const active = collectRecent(receivedRef.current); setTypingUserIds((prev) => { if (prev.length === active.length && prev.every((v, i) => v === active[i])) return prev; return active; }); }, 1000); return () => { window.clearInterval(interval); void supabase.removeChannel(channel); channelRef.current = null; }; }, [conversationId, myId]); const notifyTyping = useCallback(() => { const ch = channelRef.current; if (!ch || !myId) return; const now = Date.now(); if (now - lastSentRef.current < SEND_THROTTLE_MS) return; lastSentRef.current = now; void ch.send({ type: 'broadcast', event: 'typing', payload: { userId: myId } }); }, [myId]); const notifyStopTyping = useCallback(() => { const ch = channelRef.current; if (!ch || !myId) return; lastSentRef.current = 0; void ch.send({ type: 'broadcast', event: 'typing', payload: { userId: myId, stop: true } }); }, [myId]); return { typingUserIds, notifyTyping, notifyStopTyping }; } function collectRecent(map: Map): string[] { const now = Date.now(); const out: string[] = []; for (const [uid, ts] of map) { if (now - ts < RECEIVE_TTL_MS) out.push(uid); else map.delete(uid); } return out.sort(); }