use-collaborative-editor-mode.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. import { useEffect, useState } from 'react';
  2. import { keymap } from '@codemirror/view';
  3. import { GlobalSocketEventName, type IUserHasId } from '@growi/core/dist/interfaces';
  4. import { useGlobalSocket, GLOBAL_SOCKET_NS } from '@growi/core/dist/swr';
  5. import { yCollab, yUndoManagerKeymap } from 'y-codemirror.next';
  6. import { SocketIOProvider } from 'y-socket.io';
  7. import * as Y from 'yjs';
  8. import { userColor } from '../consts';
  9. import { UseCodeMirrorEditor } from '../services';
  10. type UserLocalState = {
  11. name: string;
  12. user?: IUserHasId;
  13. color: string;
  14. colorLight: string;
  15. }
  16. export const useCollaborativeEditorMode = (
  17. user?: IUserHasId,
  18. pageId?: string,
  19. initialValue?: string,
  20. onEditorsUpdated?: (userList: IUserHasId[]) => void,
  21. codeMirrorEditor?: UseCodeMirrorEditor,
  22. ): void => {
  23. const [ydoc, setYdoc] = useState<Y.Doc | null>(null);
  24. const [provider, setProvider] = useState<SocketIOProvider | null>(null);
  25. const [cPageId, setCPageId] = useState(pageId);
  26. const { data: socket } = useGlobalSocket();
  27. const cleanupYDoc = () => {
  28. if (cPageId === pageId) {
  29. return;
  30. }
  31. ydoc?.destroy();
  32. setYdoc(null);
  33. // NOTICE: Destroying the provider leaves awareness in the other user's connection,
  34. // so only awareness is destroyed here
  35. provider?.awareness.destroy();
  36. // TODO: catch ydoc:sync:error GlobalSocketEventName.YDocSyncError
  37. socket?.off(GlobalSocketEventName.YDocSync);
  38. setCPageId(pageId);
  39. // reset editors
  40. onEditorsUpdated?.([]);
  41. };
  42. const setupYDoc = () => {
  43. if (ydoc != null) {
  44. return;
  45. }
  46. // NOTICE: Old provider destroy at the time of ydoc setup,
  47. // because the awareness destroying is not sync to other clients
  48. provider?.destroy();
  49. setProvider(null);
  50. const _ydoc = new Y.Doc();
  51. setYdoc(_ydoc);
  52. };
  53. const setupProvider = () => {
  54. if (provider != null || ydoc == null || socket == null || onEditorsUpdated == null) {
  55. return;
  56. }
  57. const socketIOProvider = new SocketIOProvider(
  58. GLOBAL_SOCKET_NS,
  59. `yjs/${pageId}`,
  60. ydoc,
  61. { autoConnect: true },
  62. );
  63. const userLocalState: UserLocalState = {
  64. name: user?.name ? `${user.name}` : `Guest User ${Math.floor(Math.random() * 100)}`,
  65. user,
  66. color: userColor.color,
  67. colorLight: userColor.light,
  68. };
  69. socketIOProvider.awareness.setLocalStateField('user', userLocalState);
  70. socketIOProvider.on('sync', (isSync: boolean) => {
  71. if (isSync) {
  72. socket.emit(GlobalSocketEventName.YDocSync, { pageId, initialValue });
  73. const userList: IUserHasId[] = Array.from(socketIOProvider.awareness.states.values(), value => value.user.user && value.user.user);
  74. onEditorsUpdated(userList);
  75. }
  76. });
  77. // update args type see: SocketIOProvider.Awareness.awarenessUpdate
  78. socketIOProvider.awareness.on('update', (update: any) => {
  79. const { added, removed } = update;
  80. if (added.length > 0 || removed.length > 0) {
  81. const userList: IUserHasId[] = Array.from(socketIOProvider.awareness.states.values(), value => value.user.user && value.user.user);
  82. onEditorsUpdated(userList);
  83. }
  84. });
  85. setProvider(socketIOProvider);
  86. };
  87. const setupYDocExtensions = () => {
  88. if (ydoc == null || provider == null || codeMirrorEditor == null) {
  89. return;
  90. }
  91. const ytext = ydoc.getText('codemirror');
  92. const undoManager = new Y.UndoManager(ytext);
  93. codeMirrorEditor.initDoc(ytext.toString());
  94. const cleanupYUndoManagerKeymap = codeMirrorEditor.appendExtensions([
  95. keymap.of(yUndoManagerKeymap),
  96. ]);
  97. const cleanupYCollab = codeMirrorEditor.appendExtensions([
  98. yCollab(ytext, provider.awareness, { undoManager }),
  99. ]);
  100. return () => {
  101. cleanupYUndoManagerKeymap?.();
  102. cleanupYCollab?.();
  103. };
  104. };
  105. useEffect(cleanupYDoc, [cPageId, onEditorsUpdated, pageId, provider, socket, ydoc]);
  106. useEffect(setupYDoc, [provider, ydoc]);
  107. useEffect(setupProvider, [initialValue, onEditorsUpdated, pageId, provider, socket, user, ydoc]);
  108. useEffect(setupYDocExtensions, [codeMirrorEditor, provider, ydoc]);
  109. };