import { create } from 'zustand'; import { useEditorStore } from './editorStore'; import { useAuthStore } from './authStore'; export interface Collaborator { userId: string; username: string; permission: 'view' | 'edit' | 'admin'; activeFileId: string | null; cursor: { line: number; ch: number } | null; selectedBlockId: string | null; selectedElementId: string | null; joinedAt: number; } export interface RemotePointer { x: number; y: number; username: string; } export interface RemoteBlockDrag { blockId: string; x: number; y: number; } interface CollaboratorOwner { id: string; username: string; } interface CollaborationStore { connected: boolean; collaborators: Collaborator[]; owner: CollaboratorOwner | null; lastSaved: number | null; savedBy: string | null; remotePointers: Record; remoteBlockDrags: Record; pendingBlockConnects: Array<{ childId: string; parentId: string; inputName?: string }>; pendingBlockChanges: Array<{ blockId: string; name: string; value: any }>; pendingBlockCreates: Array<{ blockId: string; xml: string }>; setConnected: (connected: boolean) => void; setCollaborators: (collaborators: Collaborator[]) => void; setOwner: (owner: CollaboratorOwner) => void; addCollaborator: (collaborator: Collaborator) => void; removeCollaborator: (userId: string) => void; updateActiveFile: (userId: string, fileId: string | null) => void; updateCursor: (userId: string, fileId: string, cursor: { line: number; ch: number } | null) => void; updateBlockSelection: (userId: string, blockId: string | null) => void; updateElementHover: (userId: string, elementId: string | null) => void; updateFile: (fileId: string, content: string) => void; setLastSaved: (timestamp: number) => void; setSavedBy: (username: string) => void; setRemotePointer: (userId: string, pointer: RemotePointer) => void; clearRemotePointer: (userId: string) => void; setRemoteBlockDrag: (userId: string, drag: RemoteBlockDrag) => void; updateRemoteBlockDrag: (userId: string, updates: { x: number; y: number }) => void; clearRemoteBlockDrag: (userId: string) => void; pushPendingBlockConnect: (data: { childId: string; parentId: string; inputName?: string }) => void; shiftPendingBlockConnect: () => { childId: string; parentId: string; inputName?: string } | undefined; pushPendingBlockChange: (data: { blockId: string; name: string; value: any }) => void; shiftPendingBlockChange: () => { blockId: string; name: string; value: any } | undefined; pushPendingBlockCreate: (data: { blockId: string; xml: string }) => void; shiftPendingBlockCreate: () => { blockId: string; xml: string } | undefined; getCollaboratorsOnFile: (fileId: string) => Collaborator[]; getCollaboratorsOnBlock: (blockId: string) => Collaborator[]; getCollaboratorsOnElement: (elementId: string) => Collaborator[]; isViewOnly: () => boolean; } export const useCollaborationStore = create((set, get) => ({ connected: false, collaborators: [], owner: null, lastSaved: null, savedBy: null, remotePointers: {}, remoteBlockDrags: {}, pendingBlockConnects: [], pendingBlockChanges: [], pendingBlockCreates: [], setConnected: (connected) => set({ connected }), setCollaborators: (collaborators) => { // Deduplicate by userId, keeping the most recent entry per user const deduped = Array.from( new Map(collaborators.map((c) => [c.userId, c])).values() ); set({ collaborators: deduped }); }, setOwner: (owner) => set({ owner }), addCollaborator: (collaborator) => set((state) => { const exists = state.collaborators.find((c) => c.userId === collaborator.userId); if (exists) return state; return { collaborators: [...state.collaborators, collaborator] }; }), removeCollaborator: (userId) => set((state) => { const { [userId]: _p, ...restPointers } = state.remotePointers; const { [userId]: _d, ...restDrags } = state.remoteBlockDrags; return { collaborators: state.collaborators.filter((c) => c.userId !== userId), remotePointers: restPointers, remoteBlockDrags: restDrags, }; }), updateActiveFile: (userId, fileId) => set((state) => ({ collaborators: state.collaborators.map((c) => c.userId === userId ? { ...c, activeFileId: fileId } : c ), })), updateCursor: (userId, fileId, cursor) => set((state) => ({ collaborators: state.collaborators.map((c) => c.userId === userId ? { ...c, cursor, activeFileId: fileId } : c ), })), updateBlockSelection: (userId, blockId) => set((state) => ({ collaborators: state.collaborators.map((c) => c.userId === userId ? { ...c, selectedBlockId: blockId } : c ), })), updateElementHover: (userId, elementId) => set((state) => ({ collaborators: state.collaborators.map((c) => c.userId === userId ? { ...c, selectedElementId: elementId } : c ), })), updateFile: (fileId, content) => { const state = useEditorStore.getState(); if (state.activeFileId === fileId) { state.setActiveFileContent(content); } state.updateFile(fileId, { content }); }, setLastSaved: (timestamp) => set({ lastSaved: timestamp }), setSavedBy: (username) => set({ savedBy: username }), getCollaboratorsOnFile: (fileId) => { return get().collaborators.filter((c) => c.activeFileId === fileId); }, getCollaboratorsOnBlock: (blockId) => { return get().collaborators.filter((c) => c.selectedBlockId === blockId); }, getCollaboratorsOnElement: (elementId) => { return get().collaborators.filter((c) => c.selectedElementId === elementId); }, isViewOnly: () => { const user = useAuthStore.getState().user; const collab = get().collaborators.find((c) => c.userId === user?.id); return collab?.permission === 'view'; }, setRemotePointer: (userId, pointer) => set((state) => ({ remotePointers: { ...state.remotePointers, [userId]: pointer }, })), clearRemotePointer: (userId) => set((state) => { const { [userId]: _, ...rest } = state.remotePointers; return { remotePointers: rest }; }), setRemoteBlockDrag: (userId, drag) => set((state) => ({ remoteBlockDrags: { ...state.remoteBlockDrags, [userId]: drag }, })), updateRemoteBlockDrag: (userId, updates) => set((state) => { const existing = state.remoteBlockDrags[userId]; if (!existing) return state; return { remoteBlockDrags: { ...state.remoteBlockDrags, [userId]: { ...existing, ...updates }, }, }; }), clearRemoteBlockDrag: (userId) => set((state) => { const { [userId]: _, ...rest } = state.remoteBlockDrags; return { remoteBlockDrags: rest }; }), pushPendingBlockConnect: (data) => set((state) => ({ pendingBlockConnects: [...state.pendingBlockConnects, data], })), shiftPendingBlockConnect: () => { const state = get(); const [first, ...rest] = state.pendingBlockConnects; if (first) { set({ pendingBlockConnects: rest }); } return first; }, pushPendingBlockChange: (data) => set((state) => ({ pendingBlockChanges: [...state.pendingBlockChanges, data], })), shiftPendingBlockChange: () => { const state = get(); const [first, ...rest] = state.pendingBlockChanges; if (first) { set({ pendingBlockChanges: rest }); } return first; }, pushPendingBlockCreate: (data) => set((state) => ({ pendingBlockCreates: [...state.pendingBlockCreates, data], })), shiftPendingBlockCreate: () => { const state = get(); const [first, ...rest] = state.pendingBlockCreates; if (first) { set({ pendingBlockCreates: rest }); } return first; }, }));