|
|
@@ -2,28 +2,23 @@ import { useEffect, useState } from 'react';
|
|
|
|
|
|
import { keymap } from '@codemirror/view';
|
|
|
import type { IUserHasId } from '@growi/core/dist/interfaces';
|
|
|
-import { useGlobalSocket } from '@growi/core/dist/swr';
|
|
|
import { yCollab, yUndoManagerKeymap } from 'y-codemirror.next';
|
|
|
import { SocketIOProvider } from 'y-socket.io';
|
|
|
import * as Y from 'yjs';
|
|
|
|
|
|
import { userColor } from '../../consts';
|
|
|
-import { CollaborativeChange } from '../../consts/collaborative-change';
|
|
|
+// import { CollaborativeChange } from '../../consts/collaborative-change';
|
|
|
+import type { EditingClient } from '../../interfaces';
|
|
|
import type { UseCodeMirrorEditor } from '../services';
|
|
|
|
|
|
-type UserLocalState = {
|
|
|
- name: string;
|
|
|
- user?: IUserHasId;
|
|
|
- color: string;
|
|
|
- colorLight: string;
|
|
|
-}
|
|
|
+import { useSecondaryYdocs } from './use-secondary-ydocs';
|
|
|
+
|
|
|
|
|
|
type Configuration = {
|
|
|
- reviewMode?: boolean,
|
|
|
user?: IUserHasId,
|
|
|
pageId?: string,
|
|
|
- initialValue?: string,
|
|
|
- onEditorsUpdated?: (userList: IUserHasId[]) => void,
|
|
|
+ reviewMode?: boolean,
|
|
|
+ onEditorsUpdated?: (clientList: EditingClient[]) => void,
|
|
|
}
|
|
|
|
|
|
export const useCollaborativeEditorMode = (
|
|
|
@@ -32,149 +27,120 @@ export const useCollaborativeEditorMode = (
|
|
|
configuration?: Configuration,
|
|
|
): void => {
|
|
|
const {
|
|
|
- user, pageId, initialValue, onEditorsUpdated, reviewMode,
|
|
|
+ user, pageId, onEditorsUpdated, reviewMode,
|
|
|
} = configuration ?? {};
|
|
|
|
|
|
- const [ydoc, setYdoc] = useState<Y.Doc | null>(null);
|
|
|
- const [reviewDoc, setReviewDoc] = useState<Y.Doc | null>(null);
|
|
|
- const [provider, setProvider] = useState<SocketIOProvider | null>(null);
|
|
|
- const [cPageId, setCPageId] = useState(pageId);
|
|
|
-
|
|
|
- const { data: socket } = useGlobalSocket();
|
|
|
-
|
|
|
- // Cleanup Ydoc
|
|
|
- useEffect(() => {
|
|
|
- if (cPageId === pageId && isEnabled) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- ydoc?.destroy();
|
|
|
- setYdoc(null);
|
|
|
+ const { primaryDoc, activeDoc } = useSecondaryYdocs({
|
|
|
+ isEnabled,
|
|
|
+ pageId,
|
|
|
+ useSecondary: reviewMode,
|
|
|
+ }) ?? {};
|
|
|
|
|
|
- // NOTICE: Destroying the provider leaves awareness in the other user's connection,
|
|
|
- // so only awareness is destroyed here
|
|
|
- provider?.awareness.destroy();
|
|
|
+ const [provider, setProvider] = useState<SocketIOProvider>();
|
|
|
|
|
|
- setCPageId(pageId);
|
|
|
|
|
|
- // reset editors
|
|
|
- onEditorsUpdated?.([]);
|
|
|
- }, [cPageId, isEnabled, onEditorsUpdated, pageId, provider?.awareness, socket, ydoc]);
|
|
|
-
|
|
|
- // Setup Ydoc
|
|
|
- useEffect(() => {
|
|
|
- if (ydoc != null || !isEnabled) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // NOTICE: Old provider destroy at the time of ydoc setup,
|
|
|
- // because the awareness destroying is not sync to other clients
|
|
|
- provider?.destroy();
|
|
|
- setProvider(null);
|
|
|
-
|
|
|
- const _ydoc = new Y.Doc();
|
|
|
- setYdoc(_ydoc);
|
|
|
- }, [isEnabled, provider, ydoc, reviewMode]);
|
|
|
-
|
|
|
- // Setup Ydoc for review mode
|
|
|
+ // reset editors
|
|
|
useEffect(() => {
|
|
|
- if (reviewDoc != null || !isEnabled || !reviewMode) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- const _reviewDoc = new Y.Doc();
|
|
|
- setReviewDoc(_reviewDoc);
|
|
|
- }, [isEnabled, reviewMode, reviewDoc]);
|
|
|
-
|
|
|
- // Apply reviewDoc to Ydoc
|
|
|
- useEffect(() => {
|
|
|
- if (reviewDoc == null || ydoc == null || !isEnabled || reviewMode) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- Y.applyUpdate(ydoc, Y.encodeStateAsUpdate(reviewDoc));
|
|
|
- }, [isEnabled, reviewMode, reviewDoc, ydoc]);
|
|
|
+ if (!isEnabled) return;
|
|
|
+ onEditorsUpdated?.([]);
|
|
|
+ }, [isEnabled, onEditorsUpdated]);
|
|
|
|
|
|
// Setup provider
|
|
|
useEffect(() => {
|
|
|
- if (provider != null || pageId == null || ydoc == null || socket == null) {
|
|
|
- return;
|
|
|
- }
|
|
|
|
|
|
- const socketIOProvider = new SocketIOProvider(
|
|
|
- '/',
|
|
|
- pageId,
|
|
|
- ydoc,
|
|
|
- {
|
|
|
- autoConnect: true,
|
|
|
- resyncInterval: 3000,
|
|
|
- },
|
|
|
- );
|
|
|
-
|
|
|
- const userLocalState: UserLocalState = {
|
|
|
- name: user?.name ? `${user.name}` : `Guest User ${Math.floor(Math.random() * 100)}`,
|
|
|
- user,
|
|
|
- color: userColor.color,
|
|
|
- colorLight: userColor.light,
|
|
|
- };
|
|
|
+ let _provider: SocketIOProvider | undefined;
|
|
|
+ let providerSyncHandler: (isSync: boolean) => void;
|
|
|
+ let updateAwarenessHandler: (update: { added: number[]; updated: number[]; removed: number[]; }) => void;
|
|
|
|
|
|
- socketIOProvider.awareness.setLocalStateField('user', userLocalState);
|
|
|
-
|
|
|
- socketIOProvider.on('sync', (isSync: boolean) => {
|
|
|
- if (isSync && onEditorsUpdated != null) {
|
|
|
- const userList: IUserHasId[] = Array.from(socketIOProvider.awareness.states.values(), value => value.user.user && value.user.user);
|
|
|
- onEditorsUpdated(userList);
|
|
|
+ setProvider(() => {
|
|
|
+ if (!isEnabled || pageId == null || primaryDoc == null) {
|
|
|
+ return undefined;
|
|
|
}
|
|
|
- });
|
|
|
-
|
|
|
- // update args type see: SocketIOProvider.Awareness.awarenessUpdate
|
|
|
- socketIOProvider.awareness.on('update', (update: { added: unknown[]; removed: unknown[]; }) => {
|
|
|
- if (onEditorsUpdated == null) return;
|
|
|
|
|
|
- const { added, removed } = update;
|
|
|
- if (added.length > 0 || removed.length > 0) {
|
|
|
- const userList: IUserHasId[] = Array.from(socketIOProvider.awareness.states.values(), value => value.user.user && value.user.user);
|
|
|
- onEditorsUpdated(userList);
|
|
|
- }
|
|
|
+ _provider = new SocketIOProvider(
|
|
|
+ '/',
|
|
|
+ pageId,
|
|
|
+ primaryDoc,
|
|
|
+ {
|
|
|
+ autoConnect: true,
|
|
|
+ resyncInterval: 3000,
|
|
|
+ },
|
|
|
+ );
|
|
|
+
|
|
|
+ const userLocalState: EditingClient = {
|
|
|
+ clientId: primaryDoc.clientID,
|
|
|
+ name: user?.name ? `${user.name}` : `Guest User ${Math.floor(Math.random() * 100)}`,
|
|
|
+ userId: user?._id,
|
|
|
+ color: userColor.color,
|
|
|
+ colorLight: userColor.light,
|
|
|
+ };
|
|
|
+
|
|
|
+ const { awareness } = _provider;
|
|
|
+ awareness.setLocalStateField('editors', userLocalState);
|
|
|
+
|
|
|
+ providerSyncHandler = (isSync: boolean) => {
|
|
|
+ if (isSync && onEditorsUpdated != null) {
|
|
|
+ const clientList: EditingClient[] = Array.from(awareness.getStates().values(), value => value.editors);
|
|
|
+ if (Array.isArray(clientList)) {
|
|
|
+ onEditorsUpdated(clientList);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ _provider.on('sync', providerSyncHandler);
|
|
|
+
|
|
|
+ // update args type see: SocketIOProvider.Awareness.awarenessUpdate
|
|
|
+ updateAwarenessHandler = (update: { added: number[]; updated: number[]; removed: number[]; }) => {
|
|
|
+ // remove the states of disconnected clients
|
|
|
+ update.removed.forEach(clientId => awareness.states.delete(clientId));
|
|
|
+
|
|
|
+ // update editor list
|
|
|
+ if (onEditorsUpdated != null) {
|
|
|
+ const clientList: EditingClient[] = Array.from(awareness.states.values(), value => value.editors);
|
|
|
+ if (Array.isArray(clientList)) {
|
|
|
+ onEditorsUpdated(clientList);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ awareness.on('update', updateAwarenessHandler);
|
|
|
+
|
|
|
+ return _provider;
|
|
|
});
|
|
|
|
|
|
- setProvider(socketIOProvider);
|
|
|
- }, [initialValue, onEditorsUpdated, pageId, provider, socket, user, ydoc]);
|
|
|
+ return () => {
|
|
|
+ _provider?.awareness.setLocalState(null);
|
|
|
+ _provider?.awareness.off('update', updateAwarenessHandler);
|
|
|
+ _provider?.off('sync', providerSyncHandler);
|
|
|
+ _provider?.disconnect();
|
|
|
+ _provider?.destroy();
|
|
|
+ };
|
|
|
+ }, [isEnabled, primaryDoc, onEditorsUpdated, pageId, user]);
|
|
|
|
|
|
// Setup Ydoc Extensions
|
|
|
useEffect(() => {
|
|
|
- if (ydoc == null || provider == null || codeMirrorEditor == null) {
|
|
|
+ if (!isEnabled || !primaryDoc || !activeDoc || !provider || !codeMirrorEditor) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- const ytext = ydoc.getText('codemirror');
|
|
|
- const reviewText = reviewMode ? reviewDoc?.getText('codemirror') : null;
|
|
|
+ const activeText = activeDoc.getText('codemirror');
|
|
|
|
|
|
// setup observer to mark collaborative changes
|
|
|
- ytext.observe((event) => {
|
|
|
- if (event.transaction.local) return;
|
|
|
-
|
|
|
- codeMirrorEditor.view?.dispatch({
|
|
|
- effects: CollaborativeChange.of(event.delta),
|
|
|
- });
|
|
|
+ // primaryDoc.getText('codemirror').observe((event) => {
|
|
|
+ // if (event.transaction.local) return;
|
|
|
+ // codeMirrorEditor.view?.dispatch({
|
|
|
+ // effects: CollaborativeChange.of(event.delta),
|
|
|
+ // });
|
|
|
+ // });
|
|
|
|
|
|
- // レビューモード時は変更をレビュー用のドキュメントにも反映
|
|
|
- if (reviewMode && reviewText != null) {
|
|
|
- Y.applyUpdate(reviewDoc!, Y.encodeStateAsUpdate(ydoc));
|
|
|
- }
|
|
|
- });
|
|
|
+ const undoManager = new Y.UndoManager(activeText);
|
|
|
|
|
|
- // const undoManager = new Y.UndoManager(ytext);
|
|
|
- const undoManager = new Y.UndoManager(reviewMode && reviewText ? reviewText : ytext);
|
|
|
+ // initialize document with activeDoc text
|
|
|
+ codeMirrorEditor.initDoc(activeText.toString());
|
|
|
|
|
|
const extensions = [
|
|
|
keymap.of(yUndoManagerKeymap),
|
|
|
- // yCollab(ytext, provider.awareness, { undoManager }),
|
|
|
- yCollab(
|
|
|
- reviewMode && reviewText ? reviewText : ytext,
|
|
|
- provider.awareness,
|
|
|
- { undoManager },
|
|
|
- ),
|
|
|
+ yCollab(activeText, provider.awareness, { undoManager }),
|
|
|
];
|
|
|
|
|
|
const cleanupFunctions = extensions.map(ext => codeMirrorEditor.appendExtensions([ext]));
|
|
|
@@ -183,6 +149,5 @@ export const useCollaborativeEditorMode = (
|
|
|
cleanupFunctions.forEach(cleanup => cleanup?.());
|
|
|
codeMirrorEditor.initDoc('');
|
|
|
};
|
|
|
- // }, [codeMirrorEditor, provider, ydoc, reviewMode]);
|
|
|
- }, [codeMirrorEditor, provider, ydoc, reviewDoc, reviewMode]);
|
|
|
+ }, [isEnabled, codeMirrorEditor, provider, primaryDoc, activeDoc, reviewMode]);
|
|
|
};
|