mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-03-21 23:51:39 +00:00
Lock editor until yCollab extension is loaded (#2136)
* Lock editor until yCollab extension is loaded Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
70dc2ac09b
commit
cf892a11a0
8 changed files with 141 additions and 52 deletions
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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<ScrollProps> = ({ scrollState, onScroll, onMakeScrollSource }) => {
|
||||
const ligaturesEnabled = useApplicationState((state) => state.editorConfig.ligatures)
|
||||
|
@ -57,13 +60,14 @@ export const EditorPane: React.FC<ScrollProps> = ({ 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<ScrollProps> = ({ scrollState, onScroll, onMak
|
|||
cursorActivityExtension,
|
||||
updateViewContext,
|
||||
yjsExtension,
|
||||
...(mockContentExtension ? [mockContentExtension] : [])
|
||||
firstEditorUpdateExtension
|
||||
],
|
||||
[
|
||||
editorScrollExtension,
|
||||
|
@ -88,7 +92,7 @@ export const EditorPane: React.FC<ScrollProps> = ({ scrollState, onScroll, onMak
|
|||
cursorActivityExtension,
|
||||
updateViewContext,
|
||||
yjsExtension,
|
||||
mockContentExtension
|
||||
firstEditorUpdateExtension
|
||||
]
|
||||
)
|
||||
|
||||
|
@ -107,10 +111,11 @@ export const EditorPane: React.FC<ScrollProps> = ({ scrollState, onScroll, onMak
|
|||
onTouchStart={onMakeScrollSource}
|
||||
onMouseEnter={onMakeScrollSource}
|
||||
{...cypressId('editor-pane')}
|
||||
{...cypressAttribute('editor-ready', String(codeMirrorRef !== undefined))}>
|
||||
{...cypressAttribute('editor-ready', String(firstUpdateHappened && connectionSynced))}>
|
||||
<MaxLengthWarning />
|
||||
<ToolBar />
|
||||
<ReactCodeMirror
|
||||
editable={firstUpdateHappened && connectionSynced}
|
||||
placeholder={t('editor.placeholder')}
|
||||
extensions={extensions}
|
||||
width={'100%'}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import { YDocMessageTransporter } from '@hedgedoc/realtime'
|
||||
import type { Doc } from 'yjs'
|
||||
import type { Awareness } from 'y-protocols/awareness'
|
||||
import { MARKDOWN_CONTENT_CHANNEL_NAME } from './use-markdown-content-y-text'
|
||||
|
||||
/**
|
||||
* A mocked connection that doesn't send or receive any data and is instantly ready.
|
||||
|
@ -16,7 +17,17 @@ export class MockConnection extends YDocMessageTransporter {
|
|||
super(doc, awareness)
|
||||
this.onOpen()
|
||||
this.emit('ready')
|
||||
this.markAsSynced()
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulates a complete sync from the server by inserting the given content at position 0 of the editor yText channel.
|
||||
*
|
||||
* @param content The content to insert
|
||||
*/
|
||||
public simulateFirstSync(content: string): void {
|
||||
const yText = this.doc.getText(MARKDOWN_CONTENT_CHANNEL_NAME)
|
||||
yText.insert(0, content)
|
||||
super.markAsSynced()
|
||||
}
|
||||
|
||||
disconnect(): void {
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { isMockMode } from '../../../../../utils/test-modes'
|
||||
import { getGlobalState } from '../../../../../redux'
|
||||
import type { YText } from 'yjs/dist/src/types/YText'
|
||||
import type { Extension } from '@codemirror/state'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
|
||||
/**
|
||||
* When in mock mode this hook inserts the current markdown content into the given yText to write it into the editor.
|
||||
* This happens only one time because after that the editor writes it 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 yText The yText in which the content should be inserted
|
||||
*/
|
||||
export const useInsertInitialNoteContentIntoEditorInMockMode = (yText: YText): Extension | undefined => {
|
||||
const [firstUpdateHappened, setFirstUpdateHappened] = useState<boolean>(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])
|
||||
}
|
|
@ -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])
|
||||
}
|
|
@ -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<boolean>(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
|
||||
}
|
|
@ -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])
|
||||
}
|
|
@ -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<boolean>(false)
|
||||
const extension = useMemo(() => EditorView.updateListener.of(() => setFirstUpdateHappened(true)), [])
|
||||
return [extension, firstUpdateHappened]
|
||||
}
|
Loading…
Reference in a new issue