Spaces:
Sleeping
Sleeping
| 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<string, RemotePointer>; | |
| remoteBlockDrags: Record<string, RemoteBlockDrag>; | |
| 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<CollaborationStore>((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; | |
| }, | |
| })); | |