UNPKG

realtimecursor

Version:

Real-time collaboration system with cursor tracking and approval workflow

331 lines 12.6 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.useRealtimeCursor = useRealtimeCursor; exports.useProject = useProject; exports.useCursorTracking = useCursorTracking; const react_1 = require("react"); const legacy_1 = require("./legacy"); function useRealtimeCursor(config, options = {}) { const [client] = (0, react_1.useState)(() => new legacy_1.RealtimeCursorSDK(config)); const [user, setUser] = (0, react_1.useState)(null); const [loading, setLoading] = (0, react_1.useState)(false); const [error, setError] = (0, react_1.useState)(null); const [isAuthenticated, setIsAuthenticated] = (0, react_1.useState)(!!config.token); // Check authentication status on mount (0, react_1.useEffect)(() => { if (config.token && options.autoConnect !== false) { setLoading(true); client.getCurrentUser() .then(user => { setUser(user); setIsAuthenticated(true); }) .catch(err => { setError(err.message); setIsAuthenticated(false); }) .finally(() => { setLoading(false); }); } }, []); const login = (email, password) => __awaiter(this, void 0, void 0, function* () { setLoading(true); setError(null); try { const user = yield client.login(email, password); setUser(user); setIsAuthenticated(true); return user; } catch (err) { setError(err.message); throw err; } finally { setLoading(false); } }); const register = (userData) => __awaiter(this, void 0, void 0, function* () { setLoading(true); setError(null); try { const user = yield client.register(userData); setUser(user); setIsAuthenticated(true); return user; } catch (err) { setError(err.message); throw err; } finally { setLoading(false); } }); const logout = () => { client.logout(); setUser(null); setIsAuthenticated(false); }; return { client, user, loading, error, isAuthenticated, login, register, logout }; } function useProject(client, projectId, user) { const [project, setProject] = (0, react_1.useState)(null); const [content, setContent] = (0, react_1.useState)(''); const [comments, setComments] = (0, react_1.useState)([]); const [stagedChanges, setStagedChanges] = (0, react_1.useState)([]); const [history, setHistory] = (0, react_1.useState)([]); const [collaborators, setCollaborators] = (0, react_1.useState)([]); const [cursors, setCursors] = (0, react_1.useState)({}); const [loading, setLoading] = (0, react_1.useState)(false); const [error, setError] = (0, react_1.useState)(null); const socketConnected = (0, react_1.useRef)(false); // Load project data when projectId changes (0, react_1.useEffect)(() => { if (!projectId) return; const loadProjectData = () => __awaiter(this, void 0, void 0, function* () { setLoading(true); setError(null); try { const project = yield client.getProject(projectId); setProject(project); setContent(project.content || ''); // Load comments const comments = yield client.getComments(projectId); setComments(comments); // Load history const history = yield client.getHistory(projectId); setHistory(history); // If user is superadmin, load staged changes try { const stagedChanges = yield client.getStagedChanges(projectId); setStagedChanges(stagedChanges); } catch (err) { // Ignore errors - user might not have permission } } catch (err) { setError(err.message); } finally { setLoading(false); } }); loadProjectData(); }, [projectId, client]); // Connect to real-time collaboration (0, react_1.useEffect)(() => { if (!projectId || !project || socketConnected.current) return; try { // Create a default user object if none is provided const userInfo = user || { id: 'anonymous', email: 'anonymous@example.com' }; client.connectToProject(projectId, { id: userInfo.id, name: userInfo.fullName || userInfo.email }); socketConnected.current = true; // Set up event listeners client.onRoomUsers(users => { setCollaborators(users); }); client.onUserJoined(({ user }) => { setCollaborators(prev => [...prev, user]); }); client.onUserLeft(({ socketId }) => { setCollaborators(prev => prev.filter(u => u.socketId !== socketId)); setCursors(prev => { const newCursors = Object.assign({}, prev); delete newCursors[socketId]; return newCursors; }); }); client.onContentUpdate(({ content: newContent, user }) => { setContent(newContent); }); client.onCursorUpdate((data) => { const { user, x, y, textPosition } = data; // The socketId is added by the server, but not in the type definition const socketId = data.socketId; setCursors(prev => (Object.assign(Object.assign({}, prev), { [socketId]: { x, y, user, textPosition } }))); }); return () => { client.disconnect(); socketConnected.current = false; }; } catch (err) { setError(`Failed to connect to real-time collaboration: ${err.message}`); } }, [projectId, project, client]); const updateContent = (newContent) => __awaiter(this, void 0, void 0, function* () { setContent(newContent); // Update content in real-time client.updateContent(newContent); // Save content to server try { const result = yield client.updateProjectContent(projectId, newContent); if (result.staged) { // Content was staged for approval return { staged: true, changeId: result.changeId }; } // Content was saved directly // Create a default user object if none is provided const userInfo = user || { id: 'anonymous', email: 'anonymous@example.com' }; client.notifyContentSaved(userInfo.fullName || userInfo.email); client.notifyHistoryUpdate(); // Refresh history const history = yield client.getHistory(projectId); setHistory(history); return { staged: false }; } catch (err) { setError(err.message); throw err; } }); const updateCursor = (position) => { client.updateCursor(position); }; const addComment = (commentData) => __awaiter(this, void 0, void 0, function* () { try { const comment = yield client.addComment(projectId, commentData); setComments(prev => [...prev, comment]); return comment; } catch (err) { setError(err.message); throw err; } }); const reviewChange = (changeId, approve, feedback) => __awaiter(this, void 0, void 0, function* () { try { const result = yield client.reviewChange(changeId, approve, feedback); // Refresh staged changes const stagedChanges = yield client.getStagedChanges(projectId); setStagedChanges(stagedChanges); // Refresh project data if change was approved if (approve) { const project = yield client.getProject(projectId); setProject(project); setContent(project.content || ''); } // Refresh history const history = yield client.getHistory(projectId); setHistory(history); return result; } catch (err) { setError(err.message); throw err; } }); return { project, content, comments, stagedChanges, history, collaborators, cursors, loading, error, updateContent, updateCursor, addComment, reviewChange }; } function useCursorTracking(editorRef, client) { (0, react_1.useEffect)(() => { if (!editorRef.current) return; const handleMouseMove = (e) => { if (!editorRef.current) return; const rect = editorRef.current.getBoundingClientRect(); const relativeX = Math.max(0, e.clientX - rect.left); const relativeY = Math.max(0, e.clientY - rect.top); // Calculate text position const textPosition = getTextPositionFromCoords(editorRef.current, relativeX, relativeY); client.updateCursor({ x: e.clientX, y: e.clientY, textPosition }); }; const handleKeyUp = (e) => { if (!editorRef.current) return; const cursorPosition = editorRef.current.selectionStart; client.updateCursorPosition(cursorPosition); }; const handleFocus = () => { client.setTyping(true); }; const handleBlur = () => { client.setTyping(false); }; editorRef.current.addEventListener('mousemove', handleMouseMove); editorRef.current.addEventListener('keyup', handleKeyUp); editorRef.current.addEventListener('focus', handleFocus); editorRef.current.addEventListener('blur', handleBlur); return () => { if (editorRef.current) { editorRef.current.removeEventListener('mousemove', handleMouseMove); editorRef.current.removeEventListener('keyup', handleKeyUp); editorRef.current.removeEventListener('focus', handleFocus); editorRef.current.removeEventListener('blur', handleBlur); } }; }, [editorRef, client]); } // Helper function to calculate text position from coordinates function getTextPositionFromCoords(element, x, y) { const content = element.value; if (!content) return 0; try { const lineHeight = 24; // Approximate line height in pixels const charWidth = 8.5; // Approximate character width in pixels const line = Math.max(0, Math.floor(y / lineHeight)); const char = Math.max(0, Math.floor(x / charWidth)); const lines = content.split('\n'); if (lines.length === 0) return 0; let position = 0; for (let i = 0; i < line && i < lines.length; i++) { position += (lines[i] || '').length + 1; } if (line < lines.length && lines[line]) { position += Math.min(char, lines[line].length); } return Math.max(0, Math.min(position, content.length)); } catch (error) { console.warn('Error calculating text position:', error); return 0; } } //# sourceMappingURL=hooks.js.map