use-collaborative-editor-mode.ts 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. import { useEffect, useState } from 'react';
  2. import { GlobalSocketEventName } from '@growi/core/dist/interfaces';
  3. import { useGlobalSocket, GLOBAL_SOCKET_NS } from '@growi/core/dist/swr';
  4. // see: https://github.com/yjs/y-codemirror.next#example
  5. // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  6. // @ts-ignore
  7. import { yCollab } from 'y-codemirror.next';
  8. import { SocketIOProvider } from 'y-socket.io';
  9. import * as Y from 'yjs';
  10. import { userColor } from '../consts';
  11. import { UseCodeMirrorEditor } from '../services';
  12. export const useCollaborativeEditorMode = (
  13. userName?: string,
  14. pageId?: string,
  15. initialValue?: string,
  16. onOpenEditor?: (markdown: string) => void,
  17. codeMirrorEditor?: UseCodeMirrorEditor,
  18. ): void => {
  19. const [ydoc, setYdoc] = useState<Y.Doc | null>(null);
  20. const [provider, setProvider] = useState<SocketIOProvider | null>(null);
  21. const [isInit, setIsInit] = useState(false);
  22. const [cPageId, setCPageId] = useState(pageId);
  23. const { data: socket } = useGlobalSocket();
  24. const cleanupYDocAndProvider = () => {
  25. if (cPageId === pageId) {
  26. return;
  27. }
  28. ydoc?.destroy();
  29. setYdoc(null);
  30. // NOTICE: Destorying the provider leaves awareness in the other user's connection,
  31. // so only awareness is destoryed here
  32. provider?.awareness.destroy();
  33. // TODO: catch ydoc:sync:error GlobalSocketEventName.YDocSyncError
  34. socket.off(GlobalSocketEventName.YDocSync);
  35. setIsInit(false);
  36. setCPageId(pageId);
  37. };
  38. const setupYDoc = () => {
  39. if (ydoc != null) {
  40. return;
  41. }
  42. // NOTICE: Old provider destory at the time of ydoc setup,
  43. // because the awareness destroying is not sync to other clients
  44. provider?.destroy();
  45. setProvider(null);
  46. const _ydoc = new Y.Doc();
  47. setYdoc(_ydoc);
  48. };
  49. const setupProvider = () => {
  50. if (provider != null || ydoc == null || socket == null) {
  51. return;
  52. }
  53. const socketIOProvider = new SocketIOProvider(
  54. GLOBAL_SOCKET_NS,
  55. `yjs/${pageId}`,
  56. ydoc,
  57. { autoConnect: true },
  58. );
  59. socketIOProvider.awareness.setLocalStateField('user', {
  60. name: userName ? `${userName}` : `Guest User ${Math.floor(Math.random() * 100)}`,
  61. color: userColor.color,
  62. colorLight: userColor.light,
  63. });
  64. socketIOProvider.on('sync', (isSync: boolean) => {
  65. if (isSync) {
  66. socket.emit(GlobalSocketEventName.YDocSync, { pageId, initialValue });
  67. }
  68. });
  69. setProvider(socketIOProvider);
  70. };
  71. const attachYDocExtensionsToCodeMirror = () => {
  72. if (ydoc == null || provider == null) {
  73. return;
  74. }
  75. const ytext = ydoc.getText('codemirror');
  76. const undoManager = new Y.UndoManager(ytext);
  77. const cleanup = codeMirrorEditor?.appendExtensions?.([
  78. yCollab(ytext, provider.awareness, { undoManager }),
  79. ]);
  80. return cleanup;
  81. };
  82. const initializeEditor = () => {
  83. if (ydoc == null || onOpenEditor == null || isInit === true) {
  84. return;
  85. }
  86. const ytext = ydoc.getText('codemirror');
  87. codeMirrorEditor?.initDoc(ytext.toString());
  88. onOpenEditor(ytext.toString());
  89. setIsInit(true);
  90. };
  91. useEffect(cleanupYDocAndProvider, [cPageId, pageId, provider, socket, ydoc]);
  92. useEffect(setupYDoc, [provider, ydoc]);
  93. useEffect(setupProvider, [initialValue, pageId, provider, socket, userName, ydoc]);
  94. useEffect(attachYDocExtensionsToCodeMirror, [codeMirrorEditor, provider, ydoc]);
  95. useEffect(initializeEditor, [codeMirrorEditor, isInit, onOpenEditor, ydoc]);
  96. };