Initial project import
This commit is contained in:
159
src/hooks/useCollab.ts
Normal file
159
src/hooks/useCollab.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
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<Record<string, RemoteCursor>>({});
|
||||
|
||||
const wsRef = useRef<WebSocket | null>(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 };
|
||||
}
|
||||
Reference in New Issue
Block a user