RealBlocks / client /src /store /collaborationStore.ts
SafeSight's picture
Revert
8f9173f
Raw
History Blame Contribute Delete
7.95 kB
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;
},
}));