From cf892a11a06df3d1bf87df2dbaaf843591fa3dc6 Mon Sep 17 00:00:00 2001 From: Tilman Vatteroth Date: Sun, 19 Jun 2022 20:02:51 +0200 Subject: [PATCH] Lock editor until yCollab extension is loaded (#2136) * Lock editor until yCollab extension is loaded Signed-off-by: Tilman Vatteroth --- cypress/support/fill.ts | 15 +++++-- .../editor-page/editor-pane/editor-pane.tsx | 21 ++++++---- .../editor-pane/hooks/yjs/mock-connection.ts | 13 +++++- ...l-note-content-into-editor-in-mock-mode.ts | 40 ------------------- ...content-into-y-text-in-mock-mode-effect.ts | 35 ++++++++++++++++ .../hooks/yjs/use-is-connection-synced.ts | 28 +++++++++++++ .../hooks/yjs/use-markdown-content-y-text.ts | 21 ++++++++++ .../use-on-first-editor-update-extension.ts | 20 ++++++++++ 8 files changed, 141 insertions(+), 52 deletions(-) delete mode 100644 src/components/editor-page/editor-pane/hooks/yjs/use-insert-initial-note-content-into-editor-in-mock-mode.ts create mode 100644 src/components/editor-page/editor-pane/hooks/yjs/use-insert-note-content-into-y-text-in-mock-mode-effect.ts create mode 100644 src/components/editor-page/editor-pane/hooks/yjs/use-is-connection-synced.ts create mode 100644 src/components/editor-page/editor-pane/hooks/yjs/use-markdown-content-y-text.ts create mode 100644 src/components/editor-page/editor-pane/hooks/yjs/use-on-first-editor-update-extension.ts diff --git a/cypress/support/fill.ts b/cypress/support/fill.ts index 1f6909da8..486df37de 100644 --- a/cypress/support/fill.ts +++ b/cypress/support/fill.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ @@ -13,8 +13,17 @@ declare namespace Cypress { Cypress.Commands.add('setCodemirrorContent', (content: string) => { const line = content.split('\n').find((value) => value !== '') - cy.get('.cm-editor').click().get('.cm-content').fill(content) + cy.getByCypressId('editor-pane') + .should('have.attr', 'data-cypress-editor-ready', 'true') + .get('.cm-editor') + .click() + .get('.cm-content') + .fill(content) if (line) { - cy.get('.cm-editor').find('.cm-line').should('contain.text', line) + cy.getByCypressId('editor-pane') + .should('have.attr', 'data-cypress-editor-ready', 'true') + .get('.cm-editor') + .find('.cm-line') + .should('contain.text', line) } }) diff --git a/src/components/editor-page/editor-pane/editor-pane.tsx b/src/components/editor-page/editor-pane/editor-pane.tsx index c7362fa5a..7cfb201d6 100644 --- a/src/components/editor-page/editor-pane/editor-pane.tsx +++ b/src/components/editor-page/editor-pane/editor-pane.tsx @@ -32,7 +32,10 @@ import { useYDoc } from './hooks/yjs/use-y-doc' import { useAwareness } from './hooks/yjs/use-awareness' import { useWebsocketConnection } from './hooks/yjs/use-websocket-connection' import { useBindYTextToRedux } from './hooks/yjs/use-bind-y-text-to-redux' -import { useInsertInitialNoteContentIntoEditorInMockMode } from './hooks/yjs/use-insert-initial-note-content-into-editor-in-mock-mode' +import { useInsertNoteContentIntoYTextInMockModeEffect } from './hooks/yjs/use-insert-note-content-into-y-text-in-mock-mode-effect' +import { useOnFirstEditorUpdateExtension } from './hooks/yjs/use-on-first-editor-update-extension' +import { useIsConnectionSynced } from './hooks/yjs/use-is-connection-synced' +import { useMarkdownContentYText } from './hooks/yjs/use-markdown-content-y-text' export const EditorPane: React.FC = ({ scrollState, onScroll, onMakeScrollSource }) => { const ligaturesEnabled = useApplicationState((state) => state.editorConfig.ligatures) @@ -57,13 +60,14 @@ export const EditorPane: React.FC = ({ scrollState, onScroll, onMak const yDoc = useYDoc() const awareness = useAwareness(yDoc) - const yText = useMemo(() => yDoc.getText('markdownContent'), [yDoc]) - - useWebsocketConnection(yDoc, awareness) + const yText = useMarkdownContentYText(yDoc) + const websocketConnection = useWebsocketConnection(yDoc, awareness) + const connectionSynced = useIsConnectionSynced(websocketConnection) useBindYTextToRedux(yText) const yjsExtension = useCodeMirrorYjsExtension(yText, awareness) - const mockContentExtension = useInsertInitialNoteContentIntoEditorInMockMode(yText) + const [firstEditorUpdateExtension, firstUpdateHappened] = useOnFirstEditorUpdateExtension() + useInsertNoteContentIntoYTextInMockModeEffect(firstUpdateHappened, websocketConnection) const extensions = useMemo( () => [ @@ -79,7 +83,7 @@ export const EditorPane: React.FC = ({ scrollState, onScroll, onMak cursorActivityExtension, updateViewContext, yjsExtension, - ...(mockContentExtension ? [mockContentExtension] : []) + firstEditorUpdateExtension ], [ editorScrollExtension, @@ -88,7 +92,7 @@ export const EditorPane: React.FC = ({ scrollState, onScroll, onMak cursorActivityExtension, updateViewContext, yjsExtension, - mockContentExtension + firstEditorUpdateExtension ] ) @@ -107,10 +111,11 @@ export const EditorPane: React.FC = ({ scrollState, onScroll, onMak onTouchStart={onMakeScrollSource} onMouseEnter={onMakeScrollSource} {...cypressId('editor-pane')} - {...cypressAttribute('editor-ready', String(codeMirrorRef !== undefined))}> + {...cypressAttribute('editor-ready', String(firstUpdateHappened && connectionSynced))}> { - const [firstUpdateHappened, setFirstUpdateHappened] = useState(false) - - useEffect(() => { - if (firstUpdateHappened) { - yText.insert(0, getGlobalState().noteDetails.markdownContent.plain) - } - }, [firstUpdateHappened, yText]) - - return useMemo(() => { - return isMockMode && !firstUpdateHappened - ? EditorView.updateListener.of(() => setFirstUpdateHappened(true)) - : undefined - }, [firstUpdateHappened]) -} diff --git a/src/components/editor-page/editor-pane/hooks/yjs/use-insert-note-content-into-y-text-in-mock-mode-effect.ts b/src/components/editor-page/editor-pane/hooks/yjs/use-insert-note-content-into-y-text-in-mock-mode-effect.ts new file mode 100644 index 000000000..e956779e9 --- /dev/null +++ b/src/components/editor-page/editor-pane/hooks/yjs/use-insert-note-content-into-y-text-in-mock-mode-effect.ts @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { useEffect } from 'react' +import { isMockMode } from '../../../../../utils/test-modes' +import { getGlobalState } from '../../../../../redux' +import type { YDocMessageTransporter } from '@hedgedoc/realtime' +import { MockConnection } from './mock-connection' + +/** + * When in mock mode this effect inserts the current markdown content into the yDoc of the given connection to simulate a sync from the server. + * This should happen only one time because after that the editor writes its changes into the yText which writes it into the redux. + * + * Usually the CodeMirror gets its content from yjs sync via websocket. But in mock mode this connection isn't available. + * That's why this hook inserts the current markdown content, that is currently saved in the global application state + * and was saved there by the {@link NoteLoadingBoundary note loading boundary}, into the y-text to write it into the codemirror. + * This has to be done AFTER the CodeMirror sync extension (yCollab) has been loaded because the extension reacts only to updates of the yText + * and doesn't write the existing content into the editor when being loaded. + * + * @param connection The connection into whose yDoc the content should be written + * @param firstUpdateHappened Defines if the first update already happened + */ +export const useInsertNoteContentIntoYTextInMockModeEffect = ( + firstUpdateHappened: boolean, + connection: YDocMessageTransporter +): void => { + useEffect(() => { + if (firstUpdateHappened && isMockMode && connection instanceof MockConnection) { + connection.simulateFirstSync(getGlobalState().noteDetails.markdownContent.plain) + } + }, [firstUpdateHappened, connection]) +} diff --git a/src/components/editor-page/editor-pane/hooks/yjs/use-is-connection-synced.ts b/src/components/editor-page/editor-pane/hooks/yjs/use-is-connection-synced.ts new file mode 100644 index 000000000..b43c08d85 --- /dev/null +++ b/src/components/editor-page/editor-pane/hooks/yjs/use-is-connection-synced.ts @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { useEffect, useState } from 'react' +import type { YDocMessageTransporter } from '@hedgedoc/realtime' + +/** + * Checks if the given message transporter has received at least one full synchronisation. + * + * @param connection The connection whose sync status should be checked + */ +export const useIsConnectionSynced = (connection: YDocMessageTransporter): boolean => { + const [editorEnabled, setEditorEnabled] = useState(false) + + useEffect(() => { + const enableEditor = () => setEditorEnabled(true) + const disableEditor = () => setEditorEnabled(false) + connection.on('synced', enableEditor).on('disconnected', disableEditor) + return () => { + connection.off('synced', enableEditor).off('disconnected', disableEditor) + } + }, [connection]) + + return editorEnabled +} diff --git a/src/components/editor-page/editor-pane/hooks/yjs/use-markdown-content-y-text.ts b/src/components/editor-page/editor-pane/hooks/yjs/use-markdown-content-y-text.ts new file mode 100644 index 000000000..2a1f67303 --- /dev/null +++ b/src/components/editor-page/editor-pane/hooks/yjs/use-markdown-content-y-text.ts @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import type { Doc } from 'yjs' +import { useMemo } from 'react' +import type { YText } from 'yjs/dist/src/types/YText' + +export const MARKDOWN_CONTENT_CHANNEL_NAME = 'markdownContent' + +/** + * Extracts the y-text channel that saves the markdown content from the given yDoc. + * + * @param yDoc The yjs document from which the yText should be extracted + * @return the extracted yText channel + */ +export const useMarkdownContentYText = (yDoc: Doc): YText => { + return useMemo(() => yDoc.getText(MARKDOWN_CONTENT_CHANNEL_NAME), [yDoc]) +} diff --git a/src/components/editor-page/editor-pane/hooks/yjs/use-on-first-editor-update-extension.ts b/src/components/editor-page/editor-pane/hooks/yjs/use-on-first-editor-update-extension.ts new file mode 100644 index 000000000..c80b63fcf --- /dev/null +++ b/src/components/editor-page/editor-pane/hooks/yjs/use-on-first-editor-update-extension.ts @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { useMemo, useState } from 'react' +import { EditorView } from '@codemirror/view' +import type { Extension } from '@codemirror/state' + +/** + * Provides an extension that checks when the code mirror, that loads the extension, has its first update. + * + * @return [Extension, boolean] The extension that listens for editor updates and a boolean that defines if the first update already happened + */ +export const useOnFirstEditorUpdateExtension = (): [Extension, boolean] => { + const [firstUpdateHappened, setFirstUpdateHappened] = useState(false) + const extension = useMemo(() => EditorView.updateListener.of(() => setFirstUpdateHappened(true)), []) + return [extension, firstUpdateHappened] +}