realtimecursor
Version:
Real-time collaboration system with cursor tracking and approval workflow
331 lines • 12.6 kB
JavaScript
;
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