UNPKG

sourabhrealtime

Version:

ROBUST RICH TEXT EDITOR: Single-pane contentEditable with direct text selection formatting, speech features, undo/redo, professional UI - Perfect TipTap alternative

237 lines (203 loc) 6.01 kB
// Enhanced real-time features for cross-browser collaboration export class RealtimeFeatures { constructor(socket, storage) { this.socket = socket; this.storage = storage; this.cursors = new Map(); this.userPresence = new Map(); this.typingUsers = new Set(); this.mouseTracking = true; this.init(); } init() { this.setupSocketListeners(); this.startPresenceHeartbeat(); this.startMouseTracking(); } setupSocketListeners() { // User presence this.socket.on('user-presence-update', (data) => { this.userPresence.set(data.userId, { ...data, lastSeen: Date.now(), browser: data.browser || 'Unknown' }); this.onPresenceUpdate?.(Array.from(this.userPresence.values())); }); // Real-time cursors this.socket.on('cursor-position', (data) => { this.cursors.set(data.userId, { ...data, timestamp: Date.now() }); this.onCursorUpdate?.(data); }); // Typing indicators this.socket.on('typing-indicator', (data) => { if (data.isTyping) { this.typingUsers.add(data.userId); } else { this.typingUsers.delete(data.userId); } this.onTypingUpdate?.(Array.from(this.typingUsers)); }); // Real-time invitations this.socket.on('real-time-invitation', (invitation) => { this.storage.saveInvitation(invitation); this.onInvitationReceived?.(invitation); }); // Live notifications this.socket.on('live-notification', (notification) => { this.onNotificationReceived?.(notification); }); // Project updates this.socket.on('project-updated', (project) => { this.storage.saveProject(project); this.onProjectUpdated?.(project); }); } // Send user presence updatePresence(user, status = 'online') { const presenceData = { userId: user.id, name: user.name, email: user.email, role: user.role, status, browser: this.storage.getBrowserInfo(), timestamp: Date.now() }; this.socket.emit('presence-update', presenceData); this.storage.saveUserSession(user.id, presenceData); } // Send cursor position updateCursor(projectId, position, user) { if (!this.mouseTracking) return; const cursorData = { projectId, userId: user.id, position, user: { name: user.name, color: this.getUserColor(user.id) }, timestamp: Date.now() }; this.socket.emit('cursor-update', cursorData); } // Send typing status updateTyping(projectId, isTyping, user) { const typingData = { projectId, userId: user.id, isTyping, user: { name: user.name, color: this.getUserColor(user.id) }, timestamp: Date.now() }; this.socket.emit('typing-update', typingData); } // Send real-time invitation sendInvitation(invitation) { this.socket.emit('send-real-time-invitation', invitation); this.storage.saveInvitation(invitation); } // Send live notification sendNotification(notification) { this.socket.emit('send-live-notification', notification); } // Mouse tracking startMouseTracking() { if (typeof document === 'undefined') return; let mouseTimeout; document.addEventListener('mousemove', (e) => { if (!this.mouseTracking || !this.currentProject || !this.currentUser) return; clearTimeout(mouseTimeout); mouseTimeout = setTimeout(() => { const position = { x: e.clientX, y: e.clientY, pageX: e.pageX, pageY: e.pageY }; this.updateCursor(this.currentProject.id, position, this.currentUser); }, 50); // Throttle to 20fps }); } // Presence heartbeat startPresenceHeartbeat() { setInterval(() => { if (this.currentUser) { this.updatePresence(this.currentUser, 'online'); } }, 30000); // Every 30 seconds } // Get consistent user color getUserColor(userId) { const colors = [ '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9', '#F8C471', '#82E0AA', '#F1948A', '#85C1E9', '#D7BDE2' ]; let hash = 0; for (let i = 0; i < userId.length; i++) { hash = userId.charCodeAt(i) + ((hash << 5) - hash); } return colors[Math.abs(hash) % colors.length]; } // Set current context setContext(user, project) { this.currentUser = user; this.currentProject = project; } // Get active users getActiveUsers() { return Array.from(this.userPresence.values()) .filter(user => Date.now() - user.lastSeen < 60000); // Active in last minute } // Get typing users getTypingUsers() { return Array.from(this.typingUsers); } // Get cursor positions getCursorPositions() { const now = Date.now(); const activeCursors = []; for (const [userId, cursor] of this.cursors.entries()) { if (now - cursor.timestamp < 5000) { // Active in last 5 seconds activeCursors.push(cursor); } } return activeCursors; } // Toggle mouse tracking toggleMouseTracking() { this.mouseTracking = !this.mouseTracking; return this.mouseTracking; } // Clean up old data cleanup() { const now = Date.now(); // Clean old cursors for (const [userId, cursor] of this.cursors.entries()) { if (now - cursor.timestamp > 10000) { this.cursors.delete(userId); } } // Clean old presence for (const [userId, presence] of this.userPresence.entries()) { if (now - presence.lastSeen > 120000) { // 2 minutes this.userPresence.delete(userId); } } } // Event handlers (to be set by the main component) onPresenceUpdate = null; onCursorUpdate = null; onTypingUpdate = null; onInvitationReceived = null; onNotificationReceived = null; onProjectUpdated = null; }