import { useEffect, useRef, useState } from 'react'; import { useBuilder } from '../store/builderStore'; import { useAuth } from '../auth/AuthContext'; const colors = [ '#38bdf8', '#3d7bfa', '#34d399', '#3b7a57', '#fbbf24', '#f97316', '#e0564f', '#a78bfa', ]; const stringToColor = (str: string) => { let hash = 0; for (let i = 0; i < str.length; i++) { hash = str.charCodeAt(i) + ((hash << 5) - hash); } const index = Math.abs(hash) % colors.length; return colors[index]; }; export interface RemoteCursor { position: [number, number, number]; userName: string; color: string; activeRoomId: string; } export function useCollab() { const { user } = useAuth(); const parts = useBuilder((s) => s.parts); const projectName = useBuilder((s) => s.projectName); const [remoteCursors, setRemoteCursors] = useState>({}); const wsRef = useRef(null); const localPartsRef = useRef(parts); const isIncomingUpdateRef = useRef(false); useEffect(() => { if (!user) return; const wsProto = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsPort = window.location.port === '5173' ? '3000' : window.location.port; const wsUrl = `${wsProto}//${window.location.hostname}${wsPort ? `:${wsPort}` : ''}`; const ws = new WebSocket(wsUrl); wsRef.current = ws; ws.onopen = () => { const color = stringToColor(user.email); ws.send( JSON.stringify({ type: 'join', projectRoom: projectName, userId: user.id, userName: user.email, color: color, activeRoomId: useBuilder.getState().activeRoomId, }) ); }; ws.onmessage = (event) => { try { const data = JSON.parse(event.data); if (data.type === 'mutation') { isIncomingUpdateRef.current = true; localPartsRef.current = data.parts; useBuilder.getState().syncParts(data.parts); } else if (data.type === 'cursor') { setRemoteCursors((prev) => ({ ...prev, [data.userId]: { position: data.position, userName: data.userName, color: data.color, activeRoomId: data.activeRoomId, }, })); } else if (data.type === 'leave') { setRemoteCursors((prev) => { const next = { ...prev }; delete next[data.userId]; return next; }); } } catch (err) { console.error('Error handling WebSocket message:', err); } }; ws.onclose = () => { console.log('Collaboration WebSocket disconnected'); }; ws.onerror = (err) => { console.error('Collaboration WebSocket error:', err); }; return () => { ws.close(); wsRef.current = null; }; }, [user, projectName]); // Sync local changes to remote useEffect(() => { if (isIncomingUpdateRef.current) { isIncomingUpdateRef.current = false; localPartsRef.current = parts; return; } if (parts !== localPartsRef.current) { localPartsRef.current = parts; const ws = wsRef.current; if (ws && ws.readyState === WebSocket.OPEN) { ws.send( JSON.stringify({ type: 'mutation', parts, }) ); } } }, [parts]); // Sync cursor movement useEffect(() => { let lastSent = 0; const handleCursor = (e: Event) => { const now = Date.now(); if (now - lastSent < 50) return; // rate limit to 20fps lastSent = now; const pos = (e as CustomEvent).detail; const ws = wsRef.current; if (ws && ws.readyState === WebSocket.OPEN) { ws.send( JSON.stringify({ type: 'cursor', position: [pos.x, pos.y, pos.z], activeRoomId: useBuilder.getState().activeRoomId, }) ); } }; window.addEventListener('collab:cursor', handleCursor); return () => { window.removeEventListener('collab:cursor', handleCursor); }; }, []); return { remoteCursors }; }