mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-14 15:14:56 -04: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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -13,8 +13,17 @@ declare namespace Cypress {
|
||||||
|
|
||||||
Cypress.Commands.add('setCodemirrorContent', (content: string) => {
|
Cypress.Commands.add('setCodemirrorContent', (content: string) => {
|
||||||
const line = content.split('\n').find((value) => value !== '')
|
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) {
|
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 { useAwareness } from './hooks/yjs/use-awareness'
|
||||||
import { useWebsocketConnection } from './hooks/yjs/use-websocket-connection'
|
import { useWebsocketConnection } from './hooks/yjs/use-websocket-connection'
|
||||||
import { useBindYTextToRedux } from './hooks/yjs/use-bind-y-text-to-redux'
|
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 }) => {
|
export const EditorPane: React.FC<ScrollProps> = ({ scrollState, onScroll, onMakeScrollSource }) => {
|
||||||
const ligaturesEnabled = useApplicationState((state) => state.editorConfig.ligatures)
|
const ligaturesEnabled = useApplicationState((state) => state.editorConfig.ligatures)
|
||||||
|
@ -57,13 +60,14 @@ export const EditorPane: React.FC<ScrollProps> = ({ scrollState, onScroll, onMak
|
||||||
|
|
||||||
const yDoc = useYDoc()
|
const yDoc = useYDoc()
|
||||||
const awareness = useAwareness(yDoc)
|
const awareness = useAwareness(yDoc)
|
||||||
const yText = useMemo(() => yDoc.getText('markdownContent'), [yDoc])
|
const yText = useMarkdownContentYText(yDoc)
|
||||||
|
const websocketConnection = useWebsocketConnection(yDoc, awareness)
|
||||||
useWebsocketConnection(yDoc, awareness)
|
const connectionSynced = useIsConnectionSynced(websocketConnection)
|
||||||
useBindYTextToRedux(yText)
|
useBindYTextToRedux(yText)
|
||||||
|
|
||||||
const yjsExtension = useCodeMirrorYjsExtension(yText, awareness)
|
const yjsExtension = useCodeMirrorYjsExtension(yText, awareness)
|
||||||
const mockContentExtension = useInsertInitialNoteContentIntoEditorInMockMode(yText)
|
const [firstEditorUpdateExtension, firstUpdateHappened] = useOnFirstEditorUpdateExtension()
|
||||||
|
useInsertNoteContentIntoYTextInMockModeEffect(firstUpdateHappened, websocketConnection)
|
||||||
|
|
||||||
const extensions = useMemo(
|
const extensions = useMemo(
|
||||||
() => [
|
() => [
|
||||||
|
@ -79,7 +83,7 @@ export const EditorPane: React.FC<ScrollProps> = ({ scrollState, onScroll, onMak
|
||||||
cursorActivityExtension,
|
cursorActivityExtension,
|
||||||
updateViewContext,
|
updateViewContext,
|
||||||
yjsExtension,
|
yjsExtension,
|
||||||
...(mockContentExtension ? [mockContentExtension] : [])
|
firstEditorUpdateExtension
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
editorScrollExtension,
|
editorScrollExtension,
|
||||||
|
@ -88,7 +92,7 @@ export const EditorPane: React.FC<ScrollProps> = ({ scrollState, onScroll, onMak
|
||||||
cursorActivityExtension,
|
cursorActivityExtension,
|
||||||
updateViewContext,
|
updateViewContext,
|
||||||
yjsExtension,
|
yjsExtension,
|
||||||
mockContentExtension
|
firstEditorUpdateExtension
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -107,10 +111,11 @@ export const EditorPane: React.FC<ScrollProps> = ({ scrollState, onScroll, onMak
|
||||||
onTouchStart={onMakeScrollSource}
|
onTouchStart={onMakeScrollSource}
|
||||||
onMouseEnter={onMakeScrollSource}
|
onMouseEnter={onMakeScrollSource}
|
||||||
{...cypressId('editor-pane')}
|
{...cypressId('editor-pane')}
|
||||||
{...cypressAttribute('editor-ready', String(codeMirrorRef !== undefined))}>
|
{...cypressAttribute('editor-ready', String(firstUpdateHappened && connectionSynced))}>
|
||||||
<MaxLengthWarning />
|
<MaxLengthWarning />
|
||||||
<ToolBar />
|
<ToolBar />
|
||||||
<ReactCodeMirror
|
<ReactCodeMirror
|
||||||
|
editable={firstUpdateHappened && connectionSynced}
|
||||||
placeholder={t('editor.placeholder')}
|
placeholder={t('editor.placeholder')}
|
||||||
extensions={extensions}
|
extensions={extensions}
|
||||||
width={'100%'}
|
width={'100%'}
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
import { YDocMessageTransporter } from '@hedgedoc/realtime'
|
import { YDocMessageTransporter } from '@hedgedoc/realtime'
|
||||||
import type { Doc } from 'yjs'
|
import type { Doc } from 'yjs'
|
||||||
import type { Awareness } from 'y-protocols/awareness'
|
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.
|
* 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)
|
super(doc, awareness)
|
||||||
this.onOpen()
|
this.onOpen()
|
||||||
this.emit('ready')
|
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 {
|
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…
Add table
Add a link
Reference in a new issue