use-collaborative-editor-mode.ts 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. import { useEffect, useState } from 'react';
  2. import { GlobalSocketEventName, type IUserHasId } 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. type UserLocalState = {
  13. name: string;
  14. user?: IUserHasId;
  15. color: string;
  16. colorLight: string;
  17. }
  18. export const useCollaborativeEditorMode = (
  19. user?: IUserHasId,
  20. pageId?: string,
  21. initialValue?: string,
  22. onEditorsUpdated?: (userList: IUserHasId[]) => void,
  23. codeMirrorEditor?: UseCodeMirrorEditor,
  24. ): void => {
  25. const [ydoc, setYdoc] = useState<Y.Doc | null>(null);
  26. const [provider, setProvider] = useState<SocketIOProvider | null>(null);
  27. const [cPageId, setCPageId] = useState(pageId);
  28. const { data: socket } = useGlobalSocket();
  29. const cleanupYDoc = () => {
  30. if (cPageId === pageId) {
  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. };
  44. const setupYDoc = () => {
  45. if (ydoc != null) {
  46. return;
  47. }
  48. // NOTICE: Old provider destroy at the time of ydoc setup,
  49. // because the awareness destroying is not sync to other clients
  50. provider?.destroy();
  51. setProvider(null);
  52. const _ydoc = new Y.Doc();
  53. setYdoc(_ydoc);
  54. };
  55. const setupProvider = () => {
  56. if (provider != null || ydoc == null || socket == null || onEditorsUpdated == null) {
  57. return;
  58. }
  59. const socketIOProvider = new SocketIOProvider(
  60. GLOBAL_SOCKET_NS,
  61. `yjs/${pageId}`,
  62. ydoc,
  63. { autoConnect: true },
  64. );
  65. const userLocalState: UserLocalState = {
  66. name: user?.name ? `${user.name}` : `Guest User ${Math.floor(Math.random() * 100)}`,
  67. user,
  68. color: userColor.color,
  69. colorLight: userColor.light,
  70. };
  71. socketIOProvider.awareness.setLocalStateField('user', userLocalState);
  72. socketIOProvider.on('sync', (isSync: boolean) => {
  73. if (isSync) {
  74. socket.emit(GlobalSocketEventName.YDocSync, { pageId, initialValue });
  75. const userList: IUserHasId[] = Array.from(socketIOProvider.awareness.states.values(), value => value.user.user && value.user.user);
  76. onEditorsUpdated(userList);
  77. }
  78. });
  79. // update args type see: SocketIOProvider.Awareness.awarenessUpdate
  80. socketIOProvider.awareness.on('update', (update: any) => {
  81. const { added, removed } = update;
  82. if (added.length > 0 || removed.length > 0) {
  83. const userList: IUserHasId[] = Array.from(socketIOProvider.awareness.states.values(), value => value.user.user && value.user.user);
  84. onEditorsUpdated(userList);
  85. }
  86. });
  87. setProvider(socketIOProvider);
  88. };
  89. const setupYDocExtensions = () => {
  90. if (ydoc == null || provider == null || codeMirrorEditor == null) {
  91. return;
  92. }
  93. const ytext = ydoc.getText('codemirror');
  94. const undoManager = new Y.UndoManager(ytext);
  95. codeMirrorEditor.initDoc(ytext.toString());
  96. const cleanup = codeMirrorEditor.appendExtensions([
  97. yCollab(ytext, provider.awareness, { undoManager }),
  98. ]);
  99. return cleanup;
  100. };
  101. useEffect(cleanupYDoc, [cPageId, onEditorsUpdated, pageId, provider, socket, ydoc]);
  102. useEffect(setupYDoc, [provider, ydoc]);
  103. useEffect(setupProvider, [initialValue, onEditorsUpdated, pageId, provider, socket, user, ydoc]);
  104. useEffect(setupYDocExtensions, [codeMirrorEditor, provider, ydoc]);
  105. };