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
JavaScript
// 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;
}