Просмотр исходного кода

ydoc operation moves to code mirror editor main

ryoji-s 2 лет назад
Родитель
Сommit
604c28fb0e

+ 22 - 96
apps/app/src/components/PageEditor/PageEditor.tsx

@@ -12,8 +12,6 @@ import detectIndent from 'detect-indent';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
 import { throttle, debounce } from 'throttle-debounce';
 import { throttle, debounce } from 'throttle-debounce';
-import { SocketIOProvider } from 'y-socket.io';
-import * as Y from 'yjs';
 
 
 import { useUpdateStateAfterSave, useSaveOrUpdate } from '~/client/services/page-operation';
 import { useUpdateStateAfterSave, useSaveOrUpdate } from '~/client/services/page-operation';
 import { apiGet, apiPostForm } from '~/client/util/apiv1-client';
 import { apiGet, apiPostForm } from '~/client/util/apiv1-client';
@@ -21,7 +19,7 @@ import { toastError, toastSuccess } from '~/client/util/toastr';
 import { OptionsToSave } from '~/interfaces/page-operation';
 import { OptionsToSave } from '~/interfaces/page-operation';
 import { SocketEventName } from '~/interfaces/websocket';
 import { SocketEventName } from '~/interfaces/websocket';
 import {
 import {
-  useDefaultIndentSize,
+  useDefaultIndentSize, useCurrentUser,
   useCurrentPathname, useIsEnabledAttachTitleHeader,
   useCurrentPathname, useIsEnabledAttachTitleHeader,
   useIsEditable, useIsUploadableFile, useIsUploadableImage, useIsIndentSizeForced,
   useIsEditable, useIsUploadableFile, useIsUploadableImage, useIsIndentSizeForced,
 } from '~/stores/context';
 } from '~/stores/context';
@@ -48,7 +46,7 @@ import {
   EditorMode,
   EditorMode,
   useEditorMode, useSelectedGrant,
   useEditorMode, useSelectedGrant,
 } from '~/stores/ui';
 } from '~/stores/ui';
-import { useGlobalSocket, GLOBAL_SOCKET_NS } from '~/stores/websocket';
+import { useGlobalSocket } from '~/stores/websocket';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 
 
@@ -115,6 +113,7 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
   const { mutate: mutateRemoteRevisionId } = useRemoteRevisionBody();
   const { mutate: mutateRemoteRevisionId } = useRemoteRevisionBody();
   const { mutate: mutateRemoteRevisionLastUpdatedAt } = useRemoteRevisionLastUpdatedAt();
   const { mutate: mutateRemoteRevisionLastUpdatedAt } = useRemoteRevisionLastUpdatedAt();
   const { mutate: mutateRemoteRevisionLastUpdateUser } = useRemoteRevisionLastUpdateUser();
   const { mutate: mutateRemoteRevisionLastUpdateUser } = useRemoteRevisionLastUpdateUser();
+  const { data: user } = useCurrentUser();
 
 
   const { data: socket } = useGlobalSocket();
   const { data: socket } = useGlobalSocket();
 
 
@@ -470,84 +469,6 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
 
 
   }, [mutateCurrentPage, mutateEditingMarkdown, mutateIsConflict, mutateTagsInfo, syncTagsInfoForEditor]);
   }, [mutateCurrentPage, mutateEditingMarkdown, mutateIsConflict, mutateTagsInfo, syncTagsInfoForEditor]);
 
 
-  const [ydoc, setYdoc] = useState<Y.Doc | null>(null);
-  const [provider, setProvider] = useState<SocketIOProvider | null>(null);
-  const [cPageId, setCPageId] = useState(pageId);
-
-  // cleanup ydoc and provider
-  useEffect(() => {
-    if (cPageId === pageId) {
-      return;
-    }
-    if (!provider || !ydoc || socket == null) {
-      return;
-    }
-
-    ydoc.destroy();
-    setYdoc(null);
-
-    provider.destroy();
-    provider.disconnect();
-    setProvider(null);
-
-    socket.off('ydoc:sync');
-
-    setCPageId(pageId);
-  }, [cPageId, pageId, provider, socket, ydoc]);
-
-  // setup ydoc
-  useEffect(() => {
-    if (ydoc != null) {
-      return;
-    }
-
-    const _ydoc = new Y.Doc();
-    setYdoc(_ydoc);
-  }, [initialValue, ydoc]);
-
-  // setup socketIOProvider
-  useEffect(() => {
-    if (ydoc == null || provider != null || socket == null) {
-      return;
-    }
-
-    const socketIOProvider = new SocketIOProvider(
-      GLOBAL_SOCKET_NS,
-      `yjs/${pageId}`,
-      ydoc,
-      { autoConnect: true },
-    );
-    // TODO: apply provider awareness
-    // https://redmine.weseek.co.jp/issues/130770
-    socketIOProvider.awareness.setLocalState({ id: Math.random(), name: 'Perico' });
-    socketIOProvider.on('sync', (isSync: boolean) => {
-      // TODO: check behavior when ydoc diff is retrieved once from server but sync is false exactly 5 seconds later
-      setTimeout(() => {
-        if (!isSync) {
-          ydoc.getText('codemirror').insert(0, initialValue);
-        }
-      }, 5000);
-
-      if (isSync) {
-        socket.emit('ydoc:sync', { pageId, initialValue });
-      }
-    });
-    socketIOProvider.on('status', ({ status: _status }: { status: string }) => {
-      if (_status) console.log(_status);
-    });
-    setProvider(socketIOProvider);
-  }, [initialValue, pageId, provider, socket, ydoc]);
-
-  // initialize
-  useEffect(() => {
-    if (ydoc == null) {
-      return;
-    }
-    codeMirrorEditor?.initDoc(ydoc.getText('codemirror').toString());
-    setMarkdownToPreview(ydoc.getText('codemirror').toString());
-    mutateIsEnabledUnsavedWarning(false);
-  }, [codeMirrorEditor, initialValue, mutateIsEnabledUnsavedWarning, ydoc]);
-
   // initial caret line
   // initial caret line
   useEffect(() => {
   useEffect(() => {
     codeMirrorEditor?.setCaretLine();
     codeMirrorEditor?.setCaretLine();
@@ -602,19 +523,21 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
     }
     }
   }, [initialValue, isIndentSizeForced, mutateCurrentIndentSize]);
   }, [initialValue, isIndentSizeForced, mutateCurrentIndentSize]);
 
 
-  // when transitioning to a different page, if the initialValue is the same,
-  // UnControlled CodeMirror value does not reset, so explicitly set the value to initialValue
-  const onRouterChangeComplete = useCallback(() => {
-    codeMirrorEditor?.initDoc(ydoc?.getText('codemirror').toString());
-    codeMirrorEditor?.setCaretLine();
-  }, [codeMirrorEditor, ydoc]);
 
 
-  useEffect(() => {
-    router.events.on('routeChangeComplete', onRouterChangeComplete);
-    return () => {
-      router.events.off('routeChangeComplete', onRouterChangeComplete);
-    };
-  }, [onRouterChangeComplete, router.events]);
+  // TODO: Check the reproduction conditions that made this code necessary and confirm reproduction
+  // // when transitioning to a different page, if the initialValue is the same,
+  // // UnControlled CodeMirror value does not reset, so explicitly set the value to initialValue
+  // const onRouterChangeComplete = useCallback(() => {
+  //   codeMirrorEditor?.initDoc(ydoc?.getText('codemirror').toString());
+  //   codeMirrorEditor?.setCaretLine();
+  // }, [codeMirrorEditor, ydoc]);
+
+  // useEffect(() => {
+  //   router.events.on('routeChangeComplete', onRouterChangeComplete);
+  //   return () => {
+  //     router.events.off('routeChangeComplete', onRouterChangeComplete);
+  //   };
+  // }, [onRouterChangeComplete, router.events]);
 
 
   if (!isEditable) {
   if (!isEditable) {
     return <></>;
     return <></>;
@@ -645,8 +568,11 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
           onChange={markdownChangedHandler}
           onChange={markdownChangedHandler}
           onSave={saveWithShortcut}
           onSave={saveWithShortcut}
           indentSize={currentIndentSize ?? defaultIndentSize}
           indentSize={currentIndentSize ?? defaultIndentSize}
-          ydoc={ydoc}
-          provider={provider}
+          pageId={pageId}
+          userName={user?.name}
+          socket={socket}
+          initialValue={initialValue}
+          setMarkdownToPreview={setMarkdownToPreview}
         />
         />
       </div>
       </div>
       <div className="page-editor-preview-container flex-expand-vert d-none d-lg-flex">
       <div className="page-editor-preview-container flex-expand-vert d-none d-lg-flex">

+ 106 - 21
packages/editor/src/components/CodeMirrorEditorMain.tsx

@@ -1,18 +1,23 @@
-import { useEffect } from 'react';
+import { useEffect, useState } from 'react';
 
 
 import type { Extension } from '@codemirror/state';
 import type { Extension } from '@codemirror/state';
 import { keymap, scrollPastEnd } from '@codemirror/view';
 import { keymap, scrollPastEnd } from '@codemirror/view';
+import type { Socket, DefaultEventsMap } from 'socket.io-client';
 // eslint-disable-next-line @typescript-eslint/ban-ts-comment
 // eslint-disable-next-line @typescript-eslint/ban-ts-comment
 // @ts-ignore
 // @ts-ignore
 import { yCollab } from 'y-codemirror.next';
 import { yCollab } from 'y-codemirror.next';
 import { SocketIOProvider } from 'y-socket.io';
 import { SocketIOProvider } from 'y-socket.io';
 import * as Y from 'yjs';
 import * as Y from 'yjs';
 
 
-import { GlobalCodeMirrorEditorKey } from '../consts';
+import { GlobalCodeMirrorEditorKey, userColor } from '../consts';
 import { useCodeMirrorEditorIsolated } from '../stores';
 import { useCodeMirrorEditorIsolated } from '../stores';
 
 
 import { CodeMirrorEditor } from '.';
 import { CodeMirrorEditor } from '.';
 
 
+// TODO: use SocketEventName
+// import { SocketEventName } from '~/interfaces/websocket';
+// TODO: import { GLOBAL_SOCKET_NS } from '~/stores/websocket';
+const GLOBAL_SOCKET_NS = '/';
 
 
 const additionalExtensions: Extension[] = [
 const additionalExtensions: Extension[] = [
   scrollPastEnd(),
   scrollPastEnd(),
@@ -23,16 +28,112 @@ type Props = {
   onChange?: (value: string) => void,
   onChange?: (value: string) => void,
   onSave?: () => void,
   onSave?: () => void,
   indentSize?: number,
   indentSize?: number,
-  ydoc?: Y.Doc | null,
-  provider?: SocketIOProvider | null,
+  pageId?: string,
+  userName?: string,
+  socket?: Socket<DefaultEventsMap, DefaultEventsMap>,
+  initialValue: string,
+  setMarkdownToPreview: React.Dispatch<React.SetStateAction<string>>,
 }
 }
 
 
 export const CodeMirrorEditorMain = (props: Props): JSX.Element => {
 export const CodeMirrorEditorMain = (props: Props): JSX.Element => {
   const {
   const {
-    onSave, onChange, indentSize, ydoc, provider,
+    onSave, onChange, indentSize, pageId, userName, initialValue, socket, setMarkdownToPreview,
   } = props;
   } = props;
 
 
   const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(GlobalCodeMirrorEditorKey.MAIN);
   const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(GlobalCodeMirrorEditorKey.MAIN);
+  const [ydoc, setYdoc] = useState<Y.Doc | null>(null);
+  const [provider, setProvider] = useState<SocketIOProvider | null>(null);
+  const [cPageId, setCPageId] = useState(pageId);
+
+  // cleanup ydoc and socketIOProvider
+  useEffect(() => {
+    if (cPageId === pageId) {
+      return;
+    }
+    if (!provider || !ydoc || socket == null) {
+      return;
+    }
+
+    ydoc.destroy();
+    setYdoc(null);
+
+    provider.destroy();
+    provider.disconnect();
+    setProvider(null);
+
+    // TODO: use SocketEventName
+    socket.off('ydoc:sync');
+
+    setCPageId(pageId);
+  }, [cPageId, pageId, provider, socket, ydoc]);
+
+  // setup ydoc
+  useEffect(() => {
+    if (ydoc != null) {
+      return;
+    }
+
+    const _ydoc = new Y.Doc();
+    setYdoc(_ydoc);
+  }, [initialValue, ydoc]);
+
+  // setup socketIOProvider
+  useEffect(() => {
+    if (ydoc == null || provider != null || socket == null) {
+      return;
+    }
+
+    const socketIOProvider = new SocketIOProvider(
+      GLOBAL_SOCKET_NS,
+      `yjs/${pageId}`,
+      ydoc,
+      { autoConnect: true },
+    );
+
+    socketIOProvider.awareness.setLocalStateField('user', {
+      name: userName ? `${userName}` : `Guest User ${Math.floor(Math.random() * 100)}`,
+      color: userColor.color,
+      colorLight: userColor.light,
+    });
+    socketIOProvider.on('sync', (isSync: boolean) => {
+      // TODO: check behavior when ydoc diff is retrieved once from server but sync is false exactly 5 seconds later
+      setTimeout(() => {
+        if (!isSync) {
+          ydoc.getText('codemirror').insert(0, initialValue);
+        }
+      }, 5000);
+
+      if (isSync) {
+        // TODO: use SocketEventName
+        socket.emit('ydoc:sync', { pageId, initialValue });
+      }
+    });
+    // TODO: delete this code
+    socketIOProvider.on('status', ({ status: _status }: { status: string }) => {
+      if (_status) console.log(_status);
+    });
+    setProvider(socketIOProvider);
+  }, [initialValue, pageId, provider, socket, userName, ydoc]);
+
+  // setup yCollab and initialize markdown and preivew
+  useEffect(() => {
+    if (ydoc == null || provider == null) {
+      return;
+    }
+
+    const ytext = ydoc.getText('codemirror');
+    const undoManager = new Y.UndoManager(ytext);
+    codeMirrorEditor?.initDoc(ytext.toString());
+    setMarkdownToPreview(ytext.toString());
+    // TODO: Check the reproduction conditions that made this code necessary and confirm reproduction
+    // mutateIsEnabledUnsavedWarning(false);
+
+    const cleanup = codeMirrorEditor?.appendExtensions?.([
+      yCollab(ytext, provider.awareness, { undoManager }),
+    ]);
+
+    return cleanup;
+  }, [codeMirrorEditor, provider, setMarkdownToPreview, ydoc]);
 
 
   // setup additional extensions
   // setup additional extensions
   useEffect(() => {
   useEffect(() => {
@@ -64,22 +165,6 @@ export const CodeMirrorEditorMain = (props: Props): JSX.Element => {
     return cleanupFunction;
     return cleanupFunction;
   }, [codeMirrorEditor, onSave]);
   }, [codeMirrorEditor, onSave]);
 
 
-  // setup yCollab
-  useEffect(() => {
-    if (ydoc == null || provider == null) {
-      return;
-    }
-
-    const ytext = ydoc.getText('codemirror');
-    const undoManager = new Y.UndoManager(ytext);
-    codeMirrorEditor?.initDoc(ytext.toString());
-    const cleanup = codeMirrorEditor?.appendExtensions?.([
-      yCollab(ytext, provider.awareness, { undoManager }),
-    ]);
-
-    return cleanup;
-  }, [codeMirrorEditor, provider, ydoc]);
-
   return (
   return (
     <CodeMirrorEditor
     <CodeMirrorEditor
       editorKey={GlobalCodeMirrorEditorKey.MAIN}
       editorKey={GlobalCodeMirrorEditorKey.MAIN}