import { useCallback, useEffect, useState } from 'react'; import { clearWhiteboardStrokes, insertWhiteboardStroke, listWhiteboardStrokes, type WhiteboardStroke, } from '@chat-app/shared/chat'; import { supabase } from '../lib/supabase'; interface State { strokes: WhiteboardStroke[]; loading: boolean; error: string | null; } export function useWhiteboardStrokes(whiteboardId: string | null): { strokes: WhiteboardStroke[]; loading: boolean; error: string | null; insertStroke: (strokeJson: unknown) => Promise; clearAll: () => Promise; } { const [state, setState] = useState({ strokes: [], loading: true, error: null }); useEffect(() => { if (!whiteboardId) { setState({ strokes: [], loading: false, error: null }); return; } let cancelled = false; void (async () => { try { setState((s) => ({ ...s, loading: true, error: null })); const list = await listWhiteboardStrokes(supabase, whiteboardId); if (!cancelled) setState({ strokes: list, loading: false, error: null }); } catch (err) { if (!cancelled) { setState({ strokes: [], loading: false, error: err instanceof Error ? err.message : 'failed to load strokes', }); } } })(); const channel = supabase .channel('whiteboard:' + whiteboardId) .on( 'postgres_changes', { event: 'INSERT', schema: 'public', table: 'whiteboard_strokes', filter: 'whiteboard_id=eq.' + whiteboardId, }, (payload) => { const row = payload.new as { id?: string; whiteboard_id?: string; author_user_id?: string; stroke_json?: unknown; created_at?: string; } | null; if (!row?.id || !row.whiteboard_id || !row.author_user_id || !row.created_at) return; const next: WhiteboardStroke = { id: row.id, whiteboardId: row.whiteboard_id, authorUserId: row.author_user_id, strokeJson: row.stroke_json, createdAt: row.created_at, }; setState((s) => { if (s.strokes.some((x) => x.id === next.id)) return s; return { ...s, strokes: [...s.strokes, next] }; }); }, ) .on( 'postgres_changes', { event: 'DELETE', schema: 'public', table: 'whiteboard_strokes', filter: 'whiteboard_id=eq.' + whiteboardId, }, () => { // Bulk delete via "Clear all" — drop everything; future inserts // come back via the INSERT branch above. setState((s) => ({ ...s, strokes: [] })); }, ) .subscribe(); return () => { cancelled = true; void supabase.removeChannel(channel); }; }, [whiteboardId]); const insertStroke = useCallback( async (strokeJson: unknown) => { if (!whiteboardId) return; try { await insertWhiteboardStroke(supabase, { whiteboardId, strokeJson }); // No optimistic append — realtime echoes the row back in <150ms. } catch (err) { console.error('insertWhiteboardStroke failed', err); setState((s) => ({ ...s, error: err instanceof Error ? err.message : 'stroke insert failed', })); } }, [whiteboardId], ); const clearAll = useCallback(async () => { if (!whiteboardId) return; await clearWhiteboardStrokes(supabase, whiteboardId); }, [whiteboardId]); return { strokes: state.strokes, loading: state.loading, error: state.error, insertStroke, clearAll, }; }