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