| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136 |
- import { useEffect, useState } from 'react';
- import { keymap } from '@codemirror/view';
- import { GlobalSocketEventName, type IUserHasId } from '@growi/core/dist/interfaces';
- import { useGlobalSocket, GLOBAL_SOCKET_NS } 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 { UseCodeMirrorEditor } from '../services';
- type UserLocalState = {
- name: string;
- user?: IUserHasId;
- color: string;
- colorLight: string;
- }
- export const useCollaborativeEditorMode = (
- isEnabled: boolean,
- user?: IUserHasId,
- pageId?: string,
- initialValue?: string,
- onEditorsUpdated?: (userList: IUserHasId[]) => void,
- codeMirrorEditor?: UseCodeMirrorEditor,
- ): void => {
- const [ydoc, setYdoc] = 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);
- // NOTICE: Destroying the provider leaves awareness in the other user's connection,
- // so only awareness is destroyed here
- provider?.awareness.destroy();
- // TODO: catch ydoc:sync:error GlobalSocketEventName.YDocSyncError
- socket?.off(GlobalSocketEventName.YDocSync);
- 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]);
- // Setup provider
- useEffect(() => {
- if (provider != null || ydoc == null || socket == null || onEditorsUpdated == null) {
- return;
- }
- const socketIOProvider = new SocketIOProvider(
- GLOBAL_SOCKET_NS,
- `yjs/${pageId}`,
- ydoc,
- { autoConnect: true },
- );
- const userLocalState: UserLocalState = {
- name: user?.name ? `${user.name}` : `Guest User ${Math.floor(Math.random() * 100)}`,
- user,
- color: userColor.color,
- colorLight: userColor.light,
- };
- socketIOProvider.awareness.setLocalStateField('user', userLocalState);
- socketIOProvider.on('sync', (isSync: boolean) => {
- if (isSync) {
- socket.emit(GlobalSocketEventName.YDocSync, { pageId, initialValue });
- const userList: IUserHasId[] = Array.from(socketIOProvider.awareness.states.values(), value => value.user.user && value.user.user);
- onEditorsUpdated(userList);
- }
- });
- // update args type see: SocketIOProvider.Awareness.awarenessUpdate
- socketIOProvider.awareness.on('update', (update: any) => {
- 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);
- }
- });
- setProvider(socketIOProvider);
- }, [initialValue, onEditorsUpdated, pageId, provider, socket, user, ydoc]);
- // Setup Ydoc Extensions
- useEffect(() => {
- if (ydoc == null || provider == null || codeMirrorEditor == null) {
- return;
- }
- const ytext = ydoc.getText('codemirror');
- const undoManager = new Y.UndoManager(ytext);
- codeMirrorEditor.initDoc(ytext.toString());
- const cleanupYUndoManagerKeymap = codeMirrorEditor.appendExtensions([
- keymap.of(yUndoManagerKeymap),
- ]);
- const cleanupYCollab = codeMirrorEditor.appendExtensions([
- yCollab(ytext, provider.awareness, { undoManager }),
- ]);
- return () => {
- cleanupYUndoManagerKeymap?.();
- cleanupYCollab?.();
- };
- }, [codeMirrorEditor, provider, ydoc]);
- };
|