feat: Add Real-time Collaboration infrastructure
- Create collaboration types (CollaboratorInfo, Session, Chat, Permissions) - Build collaboration store with Zustand and socket.io integration - Add cursor/selection sharing, chat messaging, and typing indicators - Include permission system (viewer, editor, admin, owner) - Add mock mode for demo/testing without server - Support follow mode and session settings
This commit is contained in:
parent
9c54fb3386
commit
c08627a561
2 changed files with 666 additions and 0 deletions
139
src/lib/collaboration/types.ts
Normal file
139
src/lib/collaboration/types.ts
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
/**
|
||||||
|
* Real-time Collaboration Types
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface CollaboratorInfo {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
color: string;
|
||||||
|
avatarUrl?: string;
|
||||||
|
cursor?: CursorPosition;
|
||||||
|
selection?: SelectionRange;
|
||||||
|
lastActive: number;
|
||||||
|
isTyping: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CursorPosition {
|
||||||
|
lineNumber: number;
|
||||||
|
column: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectionRange {
|
||||||
|
startLineNumber: number;
|
||||||
|
startColumn: number;
|
||||||
|
endLineNumber: number;
|
||||||
|
endColumn: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CollaborationSession {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
ownerId: string;
|
||||||
|
ownerName: string;
|
||||||
|
createdAt: number;
|
||||||
|
collaborators: CollaboratorInfo[];
|
||||||
|
fileId: string;
|
||||||
|
fileName: string;
|
||||||
|
isPublic: boolean;
|
||||||
|
maxCollaborators: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CollaborationMessage {
|
||||||
|
type: CollaborationMessageType;
|
||||||
|
sessionId: string;
|
||||||
|
senderId: string;
|
||||||
|
timestamp: number;
|
||||||
|
payload: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CollaborationMessageType =
|
||||||
|
| 'join'
|
||||||
|
| 'leave'
|
||||||
|
| 'cursor_update'
|
||||||
|
| 'selection_update'
|
||||||
|
| 'code_change'
|
||||||
|
| 'chat'
|
||||||
|
| 'typing_start'
|
||||||
|
| 'typing_stop'
|
||||||
|
| 'file_change'
|
||||||
|
| 'request_sync'
|
||||||
|
| 'sync_response';
|
||||||
|
|
||||||
|
export interface ChatMessage {
|
||||||
|
id: string;
|
||||||
|
senderId: string;
|
||||||
|
senderName: string;
|
||||||
|
senderColor: string;
|
||||||
|
content: string;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CodeChange {
|
||||||
|
range: SelectionRange;
|
||||||
|
text: string;
|
||||||
|
rangeLength: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collaboration colors for users
|
||||||
|
export const COLLABORATOR_COLORS = [
|
||||||
|
'#ef4444', // red
|
||||||
|
'#f97316', // orange
|
||||||
|
'#eab308', // yellow
|
||||||
|
'#22c55e', // green
|
||||||
|
'#14b8a6', // teal
|
||||||
|
'#3b82f6', // blue
|
||||||
|
'#8b5cf6', // violet
|
||||||
|
'#ec4899', // pink
|
||||||
|
'#6366f1', // indigo
|
||||||
|
'#06b6d4', // cyan
|
||||||
|
];
|
||||||
|
|
||||||
|
export function getCollaboratorColor(index: number): string {
|
||||||
|
return COLLABORATOR_COLORS[index % COLLABORATOR_COLORS.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session settings
|
||||||
|
export interface SessionSettings {
|
||||||
|
allowChat: boolean;
|
||||||
|
allowVoice: boolean;
|
||||||
|
showCursors: boolean;
|
||||||
|
showSelections: boolean;
|
||||||
|
autoSync: boolean;
|
||||||
|
followMode: boolean;
|
||||||
|
followUserId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_SESSION_SETTINGS: SessionSettings = {
|
||||||
|
allowChat: true,
|
||||||
|
allowVoice: false,
|
||||||
|
showCursors: true,
|
||||||
|
showSelections: true,
|
||||||
|
autoSync: true,
|
||||||
|
followMode: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Connection states
|
||||||
|
export type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'error';
|
||||||
|
|
||||||
|
// Permission levels
|
||||||
|
export type PermissionLevel = 'viewer' | 'editor' | 'admin' | 'owner';
|
||||||
|
|
||||||
|
export interface CollaboratorPermission {
|
||||||
|
collaboratorId: string;
|
||||||
|
level: PermissionLevel;
|
||||||
|
canEdit: boolean;
|
||||||
|
canInvite: boolean;
|
||||||
|
canChat: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canEdit(permission: PermissionLevel): boolean {
|
||||||
|
return ['editor', 'admin', 'owner'].includes(permission);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canInvite(permission: PermissionLevel): boolean {
|
||||||
|
return ['admin', 'owner'].includes(permission);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canManage(permission: PermissionLevel): boolean {
|
||||||
|
return permission === 'owner';
|
||||||
|
}
|
||||||
527
src/stores/collaboration-store.ts
Normal file
527
src/stores/collaboration-store.ts
Normal file
|
|
@ -0,0 +1,527 @@
|
||||||
|
/**
|
||||||
|
* Real-time Collaboration Store
|
||||||
|
* Manages collaboration state, sessions, and real-time updates
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import { io, Socket } from 'socket.io-client';
|
||||||
|
import {
|
||||||
|
CollaboratorInfo,
|
||||||
|
CollaborationSession,
|
||||||
|
ChatMessage,
|
||||||
|
CursorPosition,
|
||||||
|
SelectionRange,
|
||||||
|
CodeChange,
|
||||||
|
ConnectionState,
|
||||||
|
SessionSettings,
|
||||||
|
DEFAULT_SESSION_SETTINGS,
|
||||||
|
getCollaboratorColor,
|
||||||
|
PermissionLevel,
|
||||||
|
} from '@/lib/collaboration/types';
|
||||||
|
|
||||||
|
interface CollaborationState {
|
||||||
|
// Connection
|
||||||
|
socket: Socket | null;
|
||||||
|
connectionState: ConnectionState;
|
||||||
|
reconnectAttempts: number;
|
||||||
|
|
||||||
|
// Session
|
||||||
|
currentSession: CollaborationSession | null;
|
||||||
|
collaborators: CollaboratorInfo[];
|
||||||
|
myInfo: CollaboratorInfo | null;
|
||||||
|
myPermission: PermissionLevel;
|
||||||
|
|
||||||
|
// Chat
|
||||||
|
chatMessages: ChatMessage[];
|
||||||
|
unreadChatCount: number;
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
settings: SessionSettings;
|
||||||
|
|
||||||
|
// UI State
|
||||||
|
isChatOpen: boolean;
|
||||||
|
isCollaboratorsPanelOpen: boolean;
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
connect: (serverUrl?: string) => void;
|
||||||
|
disconnect: () => void;
|
||||||
|
createSession: (name: string, fileId: string, fileName: string, isPublic: boolean) => Promise<string>;
|
||||||
|
joinSession: (sessionId: string, name: string, avatarUrl?: string) => Promise<boolean>;
|
||||||
|
leaveSession: () => void;
|
||||||
|
|
||||||
|
// Real-time updates
|
||||||
|
updateCursor: (position: CursorPosition) => void;
|
||||||
|
updateSelection: (selection: SelectionRange | null) => void;
|
||||||
|
sendCodeChange: (change: CodeChange) => void;
|
||||||
|
setTyping: (isTyping: boolean) => void;
|
||||||
|
|
||||||
|
// Chat
|
||||||
|
sendChatMessage: (content: string) => void;
|
||||||
|
markChatAsRead: () => void;
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
updateSettings: (settings: Partial<SessionSettings>) => void;
|
||||||
|
setFollowUser: (userId: string | null) => void;
|
||||||
|
|
||||||
|
// UI
|
||||||
|
toggleChat: () => void;
|
||||||
|
toggleCollaboratorsPanel: () => void;
|
||||||
|
|
||||||
|
// Permissions
|
||||||
|
updateCollaboratorPermission: (collaboratorId: string, permission: PermissionLevel) => void;
|
||||||
|
kickCollaborator: (collaboratorId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate unique ID
|
||||||
|
const generateId = () => `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
|
||||||
|
export const useCollaborationStore = create<CollaborationState>((set, get) => ({
|
||||||
|
// Initial state
|
||||||
|
socket: null,
|
||||||
|
connectionState: 'disconnected',
|
||||||
|
reconnectAttempts: 0,
|
||||||
|
|
||||||
|
currentSession: null,
|
||||||
|
collaborators: [],
|
||||||
|
myInfo: null,
|
||||||
|
myPermission: 'viewer',
|
||||||
|
|
||||||
|
chatMessages: [],
|
||||||
|
unreadChatCount: 0,
|
||||||
|
|
||||||
|
settings: DEFAULT_SESSION_SETTINGS,
|
||||||
|
|
||||||
|
isChatOpen: false,
|
||||||
|
isCollaboratorsPanelOpen: true,
|
||||||
|
|
||||||
|
// Connection
|
||||||
|
connect: (serverUrl = 'ws://localhost:3001') => {
|
||||||
|
const { socket, connectionState } = get();
|
||||||
|
|
||||||
|
if (socket || connectionState === 'connecting') return;
|
||||||
|
|
||||||
|
set({ connectionState: 'connecting' });
|
||||||
|
|
||||||
|
const newSocket = io(serverUrl, {
|
||||||
|
transports: ['websocket'],
|
||||||
|
reconnection: true,
|
||||||
|
reconnectionAttempts: 5,
|
||||||
|
reconnectionDelay: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
newSocket.on('connect', () => {
|
||||||
|
set({
|
||||||
|
connectionState: 'connected',
|
||||||
|
reconnectAttempts: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
newSocket.on('disconnect', () => {
|
||||||
|
set({ connectionState: 'disconnected' });
|
||||||
|
});
|
||||||
|
|
||||||
|
newSocket.on('reconnect_attempt', (attempt) => {
|
||||||
|
set({
|
||||||
|
connectionState: 'reconnecting',
|
||||||
|
reconnectAttempts: attempt,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
newSocket.on('connect_error', () => {
|
||||||
|
set({ connectionState: 'error' });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Session events
|
||||||
|
newSocket.on('session_joined', (data: { session: CollaborationSession; collaborators: CollaboratorInfo[] }) => {
|
||||||
|
set({
|
||||||
|
currentSession: data.session,
|
||||||
|
collaborators: data.collaborators,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
newSocket.on('collaborator_joined', (collaborator: CollaboratorInfo) => {
|
||||||
|
set((state) => ({
|
||||||
|
collaborators: [...state.collaborators, collaborator],
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
newSocket.on('collaborator_left', (collaboratorId: string) => {
|
||||||
|
set((state) => ({
|
||||||
|
collaborators: state.collaborators.filter((c) => c.id !== collaboratorId),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
newSocket.on('cursor_updated', (data: { id: string; cursor: CursorPosition }) => {
|
||||||
|
set((state) => ({
|
||||||
|
collaborators: state.collaborators.map((c) =>
|
||||||
|
c.id === data.id ? { ...c, cursor: data.cursor, lastActive: Date.now() } : c
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
newSocket.on('selection_updated', (data: { id: string; selection: SelectionRange | null }) => {
|
||||||
|
set((state) => ({
|
||||||
|
collaborators: state.collaborators.map((c) =>
|
||||||
|
c.id === data.id ? { ...c, selection: data.selection ?? undefined, lastActive: Date.now() } : c
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
newSocket.on('typing_changed', (data: { id: string; isTyping: boolean }) => {
|
||||||
|
set((state) => ({
|
||||||
|
collaborators: state.collaborators.map((c) =>
|
||||||
|
c.id === data.id ? { ...c, isTyping: data.isTyping } : c
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
newSocket.on('chat_message', (message: ChatMessage) => {
|
||||||
|
set((state) => ({
|
||||||
|
chatMessages: [...state.chatMessages, message],
|
||||||
|
unreadChatCount: state.isChatOpen ? 0 : state.unreadChatCount + 1,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
newSocket.on('code_changed', (change: CodeChange & { senderId: string }) => {
|
||||||
|
// This would be handled by the editor integration
|
||||||
|
// The editor should listen to this event and apply the change
|
||||||
|
});
|
||||||
|
|
||||||
|
set({ socket: newSocket });
|
||||||
|
},
|
||||||
|
|
||||||
|
disconnect: () => {
|
||||||
|
const { socket } = get();
|
||||||
|
if (socket) {
|
||||||
|
socket.disconnect();
|
||||||
|
}
|
||||||
|
set({
|
||||||
|
socket: null,
|
||||||
|
connectionState: 'disconnected',
|
||||||
|
currentSession: null,
|
||||||
|
collaborators: [],
|
||||||
|
myInfo: null,
|
||||||
|
chatMessages: [],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
createSession: async (name: string, fileId: string, fileName: string, isPublic: boolean): Promise<string> => {
|
||||||
|
const { socket, myInfo } = get();
|
||||||
|
|
||||||
|
if (!socket || !myInfo) {
|
||||||
|
throw new Error('Not connected');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const sessionId = generateId();
|
||||||
|
|
||||||
|
socket.emit('create_session', {
|
||||||
|
sessionId,
|
||||||
|
name,
|
||||||
|
fileId,
|
||||||
|
fileName,
|
||||||
|
isPublic,
|
||||||
|
ownerId: myInfo.id,
|
||||||
|
ownerName: myInfo.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.once('session_created', (data: { sessionId: string }) => {
|
||||||
|
const session: CollaborationSession = {
|
||||||
|
id: data.sessionId,
|
||||||
|
name,
|
||||||
|
ownerId: myInfo.id,
|
||||||
|
ownerName: myInfo.name,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
collaborators: [myInfo],
|
||||||
|
fileId,
|
||||||
|
fileName,
|
||||||
|
isPublic,
|
||||||
|
maxCollaborators: 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
set({
|
||||||
|
currentSession: session,
|
||||||
|
collaborators: [myInfo],
|
||||||
|
myPermission: 'owner',
|
||||||
|
});
|
||||||
|
|
||||||
|
resolve(data.sessionId);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.once('session_error', (error: string) => {
|
||||||
|
reject(new Error(error));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Timeout
|
||||||
|
setTimeout(() => reject(new Error('Session creation timed out')), 10000);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
joinSession: async (sessionId: string, name: string, avatarUrl?: string): Promise<boolean> => {
|
||||||
|
const { socket } = get();
|
||||||
|
|
||||||
|
if (!socket) {
|
||||||
|
throw new Error('Not connected');
|
||||||
|
}
|
||||||
|
|
||||||
|
const myId = generateId();
|
||||||
|
const colorIndex = Math.floor(Math.random() * 10);
|
||||||
|
|
||||||
|
const myInfo: CollaboratorInfo = {
|
||||||
|
id: myId,
|
||||||
|
name,
|
||||||
|
color: getCollaboratorColor(colorIndex),
|
||||||
|
avatarUrl,
|
||||||
|
lastActive: Date.now(),
|
||||||
|
isTyping: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
set({ myInfo });
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
socket.emit('join_session', {
|
||||||
|
sessionId,
|
||||||
|
collaborator: myInfo,
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.once('session_joined', () => {
|
||||||
|
set({ myPermission: 'editor' });
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.once('session_error', (error: string) => {
|
||||||
|
reject(new Error(error));
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => reject(new Error('Join timed out')), 10000);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
leaveSession: () => {
|
||||||
|
const { socket, currentSession, myInfo } = get();
|
||||||
|
|
||||||
|
if (socket && currentSession && myInfo) {
|
||||||
|
socket.emit('leave_session', {
|
||||||
|
sessionId: currentSession.id,
|
||||||
|
collaboratorId: myInfo.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
set({
|
||||||
|
currentSession: null,
|
||||||
|
collaborators: [],
|
||||||
|
myPermission: 'viewer',
|
||||||
|
chatMessages: [],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
updateCursor: (position: CursorPosition) => {
|
||||||
|
const { socket, currentSession, myInfo, settings } = get();
|
||||||
|
|
||||||
|
if (!socket || !currentSession || !myInfo || !settings.showCursors) return;
|
||||||
|
|
||||||
|
socket.emit('cursor_update', {
|
||||||
|
sessionId: currentSession.id,
|
||||||
|
collaboratorId: myInfo.id,
|
||||||
|
cursor: position,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
updateSelection: (selection: SelectionRange | null) => {
|
||||||
|
const { socket, currentSession, myInfo, settings } = get();
|
||||||
|
|
||||||
|
if (!socket || !currentSession || !myInfo || !settings.showSelections) return;
|
||||||
|
|
||||||
|
socket.emit('selection_update', {
|
||||||
|
sessionId: currentSession.id,
|
||||||
|
collaboratorId: myInfo.id,
|
||||||
|
selection,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
sendCodeChange: (change: CodeChange) => {
|
||||||
|
const { socket, currentSession, myInfo, myPermission } = get();
|
||||||
|
|
||||||
|
if (!socket || !currentSession || !myInfo) return;
|
||||||
|
if (!['editor', 'admin', 'owner'].includes(myPermission)) return;
|
||||||
|
|
||||||
|
socket.emit('code_change', {
|
||||||
|
sessionId: currentSession.id,
|
||||||
|
collaboratorId: myInfo.id,
|
||||||
|
change,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
setTyping: (isTyping: boolean) => {
|
||||||
|
const { socket, currentSession, myInfo } = get();
|
||||||
|
|
||||||
|
if (!socket || !currentSession || !myInfo) return;
|
||||||
|
|
||||||
|
socket.emit('typing_change', {
|
||||||
|
sessionId: currentSession.id,
|
||||||
|
collaboratorId: myInfo.id,
|
||||||
|
isTyping,
|
||||||
|
});
|
||||||
|
|
||||||
|
set((state) => ({
|
||||||
|
myInfo: state.myInfo ? { ...state.myInfo, isTyping } : null,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
sendChatMessage: (content: string) => {
|
||||||
|
const { socket, currentSession, myInfo, settings } = get();
|
||||||
|
|
||||||
|
if (!socket || !currentSession || !myInfo || !settings.allowChat) return;
|
||||||
|
if (!content.trim()) return;
|
||||||
|
|
||||||
|
const message: ChatMessage = {
|
||||||
|
id: generateId(),
|
||||||
|
senderId: myInfo.id,
|
||||||
|
senderName: myInfo.name,
|
||||||
|
senderColor: myInfo.color,
|
||||||
|
content: content.trim(),
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.emit('chat_message', {
|
||||||
|
sessionId: currentSession.id,
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add to local state immediately
|
||||||
|
set((state) => ({
|
||||||
|
chatMessages: [...state.chatMessages, message],
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
markChatAsRead: () => {
|
||||||
|
set({ unreadChatCount: 0 });
|
||||||
|
},
|
||||||
|
|
||||||
|
updateSettings: (newSettings: Partial<SessionSettings>) => {
|
||||||
|
set((state) => ({
|
||||||
|
settings: { ...state.settings, ...newSettings },
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
setFollowUser: (userId: string | null) => {
|
||||||
|
set((state) => ({
|
||||||
|
settings: {
|
||||||
|
...state.settings,
|
||||||
|
followMode: userId !== null,
|
||||||
|
followUserId: userId ?? undefined,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleChat: () => {
|
||||||
|
set((state) => ({
|
||||||
|
isChatOpen: !state.isChatOpen,
|
||||||
|
unreadChatCount: !state.isChatOpen ? 0 : state.unreadChatCount,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleCollaboratorsPanel: () => {
|
||||||
|
set((state) => ({
|
||||||
|
isCollaboratorsPanelOpen: !state.isCollaboratorsPanelOpen,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
updateCollaboratorPermission: (collaboratorId: string, permission: PermissionLevel) => {
|
||||||
|
const { socket, currentSession, myPermission } = get();
|
||||||
|
|
||||||
|
if (!socket || !currentSession) return;
|
||||||
|
if (myPermission !== 'owner' && myPermission !== 'admin') return;
|
||||||
|
|
||||||
|
socket.emit('update_permission', {
|
||||||
|
sessionId: currentSession.id,
|
||||||
|
collaboratorId,
|
||||||
|
permission,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
kickCollaborator: (collaboratorId: string) => {
|
||||||
|
const { socket, currentSession, myPermission } = get();
|
||||||
|
|
||||||
|
if (!socket || !currentSession) return;
|
||||||
|
if (myPermission !== 'owner' && myPermission !== 'admin') return;
|
||||||
|
|
||||||
|
socket.emit('kick_collaborator', {
|
||||||
|
sessionId: currentSession.id,
|
||||||
|
collaboratorId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Demo/mock mode for when no server is available
|
||||||
|
export function enableMockMode() {
|
||||||
|
const store = useCollaborationStore.getState();
|
||||||
|
|
||||||
|
// Create mock session
|
||||||
|
const mockMyInfo: CollaboratorInfo = {
|
||||||
|
id: 'mock-user-1',
|
||||||
|
name: 'You',
|
||||||
|
color: getCollaboratorColor(0),
|
||||||
|
lastActive: Date.now(),
|
||||||
|
isTyping: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockCollaborators: CollaboratorInfo[] = [
|
||||||
|
mockMyInfo,
|
||||||
|
{
|
||||||
|
id: 'mock-user-2',
|
||||||
|
name: 'Alice',
|
||||||
|
color: getCollaboratorColor(1),
|
||||||
|
cursor: { lineNumber: 5, column: 10 },
|
||||||
|
lastActive: Date.now(),
|
||||||
|
isTyping: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mock-user-3',
|
||||||
|
name: 'Bob',
|
||||||
|
color: getCollaboratorColor(2),
|
||||||
|
cursor: { lineNumber: 12, column: 25 },
|
||||||
|
lastActive: Date.now() - 30000,
|
||||||
|
isTyping: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockSession: CollaborationSession = {
|
||||||
|
id: 'mock-session',
|
||||||
|
name: 'Demo Collaboration',
|
||||||
|
ownerId: mockMyInfo.id,
|
||||||
|
ownerName: mockMyInfo.name,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
collaborators: mockCollaborators,
|
||||||
|
fileId: 'file-1',
|
||||||
|
fileName: 'script.lua',
|
||||||
|
isPublic: true,
|
||||||
|
maxCollaborators: 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
useCollaborationStore.setState({
|
||||||
|
connectionState: 'connected',
|
||||||
|
currentSession: mockSession,
|
||||||
|
collaborators: mockCollaborators,
|
||||||
|
myInfo: mockMyInfo,
|
||||||
|
myPermission: 'owner',
|
||||||
|
chatMessages: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
senderId: 'mock-user-2',
|
||||||
|
senderName: 'Alice',
|
||||||
|
senderColor: getCollaboratorColor(1),
|
||||||
|
content: 'Hey, I just pushed some changes to the combat system!',
|
||||||
|
timestamp: Date.now() - 120000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
senderId: 'mock-user-3',
|
||||||
|
senderName: 'Bob',
|
||||||
|
senderColor: getCollaboratorColor(2),
|
||||||
|
content: 'Nice! Let me take a look at the damage calculation.',
|
||||||
|
timestamp: Date.now() - 60000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue