mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-28 14:04:43 -04:00
Add basic realtime communication (#2118)
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
3b86afc17c
commit
0da51bba67
23 changed files with 624 additions and 53 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
|
||||
*/
|
||||
|
@ -19,7 +19,7 @@ export const ApplicationLoader: React.FC<PropsWithChildren<unknown>> = ({ childr
|
|||
const initTasks = createSetUpTaskList()
|
||||
for (const task of initTasks) {
|
||||
try {
|
||||
await task.task
|
||||
await task.task()
|
||||
} catch (reason: unknown) {
|
||||
log.error('Error while initialising application', reason)
|
||||
throw new ApplicationLoaderError(task.name)
|
||||
|
|
|
@ -27,38 +27,38 @@ const customDelay: () => Promise<void> = async () => {
|
|||
|
||||
export interface InitTask {
|
||||
name: string
|
||||
task: Promise<void>
|
||||
task: () => Promise<void>
|
||||
}
|
||||
|
||||
export const createSetUpTaskList = (): InitTask[] => {
|
||||
return [
|
||||
{
|
||||
name: 'Load dark mode',
|
||||
task: loadDarkMode()
|
||||
task: loadDarkMode
|
||||
},
|
||||
{
|
||||
name: 'Load Translations',
|
||||
task: setUpI18n()
|
||||
task: setUpI18n
|
||||
},
|
||||
{
|
||||
name: 'Load config',
|
||||
task: fetchFrontendConfig()
|
||||
task: fetchFrontendConfig
|
||||
},
|
||||
{
|
||||
name: 'Fetch user information',
|
||||
task: fetchAndSetUser()
|
||||
task: fetchAndSetUser
|
||||
},
|
||||
{
|
||||
name: 'Motd',
|
||||
task: fetchMotd()
|
||||
task: fetchMotd
|
||||
},
|
||||
{
|
||||
name: 'Load history state',
|
||||
task: refreshHistoryState()
|
||||
task: refreshHistoryState
|
||||
},
|
||||
{
|
||||
name: 'Add Delay',
|
||||
task: customDelay()
|
||||
task: customDelay
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -4,13 +4,11 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import React, { useMemo } from 'react'
|
||||
import type { ScrollProps } from '../synced-scroll/scroll-props'
|
||||
import { StatusBar } from './status-bar/status-bar'
|
||||
import { ToolBar } from './tool-bar/tool-bar'
|
||||
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
||||
import { setNoteContent } from '../../../redux/note-details/methods'
|
||||
import { useNoteMarkdownContent } from '../../../hooks/common/use-note-markdown-content'
|
||||
import { MaxLengthWarning } from './max-length-warning/max-length-warning'
|
||||
import ReactCodeMirror from '@uiw/react-codemirror'
|
||||
import { useApplyScrollState } from './hooks/use-apply-scroll-state'
|
||||
|
@ -29,10 +27,14 @@ import { useCursorActivityCallback } from './hooks/use-cursor-activity-callback'
|
|||
import { useCodeMirrorReference, useSetCodeMirrorReference } from '../change-content-context/change-content-context'
|
||||
import { useCodeMirrorTablePasteExtension } from './hooks/table-paste/use-code-mirror-table-paste-extension'
|
||||
import { useOnImageUploadFromRenderer } from './hooks/image-upload-from-renderer/use-on-image-upload-from-renderer'
|
||||
import { useCodeMirrorYjsExtension } from './hooks/yjs/use-code-mirror-yjs-extension'
|
||||
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'
|
||||
|
||||
export const EditorPane: React.FC<ScrollProps> = ({ scrollState, onScroll, onMakeScrollSource }) => {
|
||||
const markdownContent = useNoteMarkdownContent()
|
||||
|
||||
const ligaturesEnabled = useApplicationState((state) => state.editorConfig.ligatures)
|
||||
|
||||
useApplyScrollState(scrollState)
|
||||
|
@ -42,10 +44,6 @@ export const EditorPane: React.FC<ScrollProps> = ({ scrollState, onScroll, onMak
|
|||
const fileInsertExtension = useCodeMirrorFileInsertExtension()
|
||||
const cursorActivityExtension = useCursorActivityCallback()
|
||||
|
||||
const onBeforeChange = useCallback((value: string): void => {
|
||||
setNoteContent(value)
|
||||
}, [])
|
||||
|
||||
const codeMirrorRef = useCodeMirrorReference()
|
||||
const setCodeMirrorReference = useSetCodeMirrorReference()
|
||||
|
||||
|
@ -57,6 +55,16 @@ export const EditorPane: React.FC<ScrollProps> = ({ scrollState, onScroll, onMak
|
|||
})
|
||||
}, [codeMirrorRef, setCodeMirrorReference])
|
||||
|
||||
const yDoc = useYDoc()
|
||||
const awareness = useAwareness(yDoc)
|
||||
const yText = useMemo(() => yDoc.getText('markdownContent'), [yDoc])
|
||||
|
||||
useWebsocketConnection(yDoc, awareness)
|
||||
useBindYTextToRedux(yText)
|
||||
|
||||
const yjsExtension = useCodeMirrorYjsExtension(yText, awareness)
|
||||
const mockContentExtension = useInsertInitialNoteContentIntoEditorInMockMode(yText)
|
||||
|
||||
const extensions = useMemo(
|
||||
() => [
|
||||
markdown({
|
||||
|
@ -69,9 +77,19 @@ export const EditorPane: React.FC<ScrollProps> = ({ scrollState, onScroll, onMak
|
|||
fileInsertExtension,
|
||||
autocompletion(),
|
||||
cursorActivityExtension,
|
||||
updateViewContext
|
||||
updateViewContext,
|
||||
yjsExtension,
|
||||
...(mockContentExtension ? [mockContentExtension] : [])
|
||||
],
|
||||
[cursorActivityExtension, fileInsertExtension, tablePasteExtensions, editorScrollExtension, updateViewContext]
|
||||
[
|
||||
editorScrollExtension,
|
||||
tablePasteExtensions,
|
||||
fileInsertExtension,
|
||||
cursorActivityExtension,
|
||||
updateViewContext,
|
||||
yjsExtension,
|
||||
mockContentExtension
|
||||
]
|
||||
)
|
||||
|
||||
useOnImageUploadFromRenderer()
|
||||
|
@ -102,8 +120,6 @@ export const EditorPane: React.FC<ScrollProps> = ({ scrollState, onScroll, onMak
|
|||
basicSetup={true}
|
||||
className={codeMirrorClassName}
|
||||
theme={oneDark}
|
||||
value={markdownContent}
|
||||
onChange={onBeforeChange}
|
||||
/>
|
||||
<StatusBar />
|
||||
</div>
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
@ -15,4 +15,12 @@
|
|||
font-variant-ligatures: none;
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.cm-widgetBuffer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
//workarounds for line break problem.. see https://github.com/yjs/y-codemirror.next/pull/12
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,9 @@ import type { ScrollState } from '../../synced-scroll/scroll-props'
|
|||
import { EditorView } from '@codemirror/view'
|
||||
import equal from 'fast-deep-equal'
|
||||
import { useCodeMirrorReference } from '../../change-content-context/change-content-context'
|
||||
import { Logger } from '../../../../utils/logger'
|
||||
|
||||
const logger = new Logger('useApplyScrollState')
|
||||
|
||||
/**
|
||||
* Applies the given {@link ScrollState scroll state} to the given {@link EditorView code mirror editor view}.
|
||||
|
@ -16,12 +19,16 @@ import { useCodeMirrorReference } from '../../change-content-context/change-cont
|
|||
* @param view The {@link EditorView view} that should be scrolled
|
||||
* @param scrollState The {@link ScrollState scroll state} that should be applied
|
||||
*/
|
||||
export const applyScrollState = (view: EditorView, scrollState: ScrollState): void => {
|
||||
const line = view.state.doc.line(scrollState.firstLineInView)
|
||||
const lineBlock = view.lineBlockAt(line.from)
|
||||
const margin = Math.floor(lineBlock.height * scrollState.scrolledPercentage) / 100
|
||||
const stateEffect = EditorView.scrollIntoView(line.from, { y: 'start', yMargin: -margin })
|
||||
view.dispatch({ effects: [stateEffect] })
|
||||
const applyScrollState = (view: EditorView, scrollState: ScrollState): void => {
|
||||
try {
|
||||
const line = view.state.doc.line(scrollState.firstLineInView)
|
||||
const lineBlock = view.lineBlockAt(line.from)
|
||||
const margin = Math.floor(lineBlock.height * scrollState.scrolledPercentage) / 100
|
||||
const stateEffect = EditorView.scrollIntoView(line.from, { y: 'start', yMargin: -margin })
|
||||
view.dispatch({ effects: [stateEffect] })
|
||||
} catch (error) {
|
||||
logger.error('Error while applying scroll status', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { YDocMessageTransporter } from '@hedgedoc/realtime'
|
||||
import type { Doc } from 'yjs'
|
||||
import type { Awareness } from 'y-protocols/awareness'
|
||||
|
||||
/**
|
||||
* A mocked connection that doesn't send or receive any data and is instantly ready.
|
||||
*/
|
||||
export class MockConnection extends YDocMessageTransporter {
|
||||
constructor(doc: Doc, awareness: Awareness) {
|
||||
super(doc, awareness)
|
||||
this.onOpen()
|
||||
this.emit('ready')
|
||||
this.markAsSynced()
|
||||
}
|
||||
|
||||
disconnect(): void {
|
||||
//Intentionally left empty because this is a mocked connection
|
||||
}
|
||||
|
||||
send(): void {
|
||||
//Intentionally left empty because this is a mocked connection
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Awareness } from 'y-protocols/awareness'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { addOnlineUser, removeOnlineUser } from '../../../../../redux/realtime/methods'
|
||||
import { ActiveIndicatorStatus } from '../../../../../redux/realtime/types'
|
||||
import { useApplicationState } from '../../../../../hooks/common/use-application-state'
|
||||
import type { Doc } from 'yjs'
|
||||
import { Logger } from '../../../../../utils/logger'
|
||||
|
||||
const ownAwarenessClientId = -1
|
||||
|
||||
interface UserAwarenessState {
|
||||
user: {
|
||||
name: string
|
||||
color: string
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: [mrdrogdrog] move this code to the server for the initial color setting.
|
||||
const userColors = [
|
||||
{ color: '#30bced', light: '#30bced33' },
|
||||
{ color: '#6eeb83', light: '#6eeb8333' },
|
||||
{ color: '#ffbc42', light: '#ffbc4233' },
|
||||
{ color: '#ecd444', light: '#ecd44433' },
|
||||
{ color: '#ee6352', light: '#ee635233' },
|
||||
{ color: '#9ac2c9', light: '#9ac2c933' },
|
||||
{ color: '#8acb88', light: '#8acb8833' },
|
||||
{ color: '#1be7ff', light: '#1be7ff33' }
|
||||
]
|
||||
|
||||
const logger = new Logger('useAwareness')
|
||||
|
||||
/**
|
||||
* Creates an {@link Awareness awareness}, sets the own values (like name, color, etc.) for other clients and writes state changes into the global application state.
|
||||
*
|
||||
* @param yDoc The {@link Doc yjs document} that handles the communication.
|
||||
* @return The created {@link Awareness awareness}
|
||||
*/
|
||||
export const useAwareness = (yDoc: Doc): Awareness => {
|
||||
const ownUsername = useApplicationState((state) => state.user?.username)
|
||||
const awareness = useMemo(() => new Awareness(yDoc), [yDoc])
|
||||
|
||||
useEffect(() => {
|
||||
const userColor = userColors[Math.floor(Math.random() * 8)]
|
||||
if (ownUsername !== undefined) {
|
||||
awareness.setLocalStateField('user', {
|
||||
name: ownUsername,
|
||||
color: userColor.color,
|
||||
colorLight: userColor.light
|
||||
})
|
||||
addOnlineUser(ownAwarenessClientId, {
|
||||
active: ActiveIndicatorStatus.ACTIVE,
|
||||
color: userColor.color,
|
||||
username: ownUsername
|
||||
})
|
||||
}
|
||||
|
||||
const awarenessCallback = ({ added, removed }: { added: number[]; removed: number[] }): void => {
|
||||
added.forEach((addedId) => {
|
||||
const state = awareness.getStates().get(addedId) as UserAwarenessState | undefined
|
||||
if (!state) {
|
||||
logger.debug('Could not find state for user')
|
||||
return
|
||||
}
|
||||
logger.debug(`added awareness ${addedId}`, state.user)
|
||||
addOnlineUser(addedId, {
|
||||
active: ActiveIndicatorStatus.ACTIVE,
|
||||
color: state.user.color,
|
||||
username: state.user.name
|
||||
})
|
||||
})
|
||||
removed.forEach((removedId) => {
|
||||
logger.debug(`remove awareness ${removedId}`)
|
||||
removeOnlineUser(removedId)
|
||||
})
|
||||
}
|
||||
awareness.on('change', awarenessCallback)
|
||||
|
||||
return () => {
|
||||
awareness.off('change', awarenessCallback)
|
||||
removeOnlineUser(ownAwarenessClientId)
|
||||
}
|
||||
}, [awareness, ownUsername])
|
||||
return awareness
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import { setNoteContent } from '../../../../../redux/note-details/methods'
|
||||
import type { YText } from 'yjs/dist/src/types/YText'
|
||||
|
||||
/**
|
||||
* One-Way-synchronizes the text of the given {@link YText y-text} into the global application state.
|
||||
*4
|
||||
* @param yText The source text
|
||||
*/
|
||||
export const useBindYTextToRedux = (yText: YText): void => {
|
||||
useEffect(() => {
|
||||
const yTextCallback = () => setNoteContent(yText.toString())
|
||||
yText.observe(yTextCallback)
|
||||
return () => yText.unobserve(yTextCallback)
|
||||
}, [yText])
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import type { Extension } from '@codemirror/state'
|
||||
import { yCollab } from 'y-codemirror.next'
|
||||
import type { Awareness } from 'y-protocols/awareness'
|
||||
import type { YText } from 'yjs/dist/src/types/YText'
|
||||
|
||||
/**
|
||||
* Creates a {@link Extension code mirror extension} that synchronizes an editor with the given {@link YText ytext} and {@link Awareness awareness}.
|
||||
*
|
||||
* @param yText The source and target for the editor content
|
||||
* @param awareness Contains cursor positions and names from other clients that will be shown
|
||||
* @return the created extension
|
||||
*/
|
||||
export const useCodeMirrorYjsExtension = (yText: YText, awareness: Awareness): Extension => {
|
||||
return useMemo(() => yCollab(yText, awareness), [awareness, yText])
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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,40 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { WebsocketConnection } from './websocket-connection'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { useWebsocketUrl } from './use-websocket-url'
|
||||
import type { Doc } from 'yjs'
|
||||
import type { Awareness } from 'y-protocols/awareness'
|
||||
import { isMockMode } from '../../../../../utils/test-modes'
|
||||
import { MockConnection } from './mock-connection'
|
||||
import type { YDocMessageTransporter } from '@hedgedoc/realtime'
|
||||
|
||||
/**
|
||||
* Creates a {@link WebsocketConnection websocket connection handler } that handles the realtime communication with the backend.
|
||||
*
|
||||
* @param yDoc The {@link Doc y-doc} that should be synchronized with the backend
|
||||
* @param awareness The {@link Awareness awareness} that should be synchronized with the backend.
|
||||
* @return the created connection handler
|
||||
*/
|
||||
export const useWebsocketConnection = (yDoc: Doc, awareness: Awareness): YDocMessageTransporter => {
|
||||
const websocketUrl = useWebsocketUrl()
|
||||
|
||||
const websocketConnection: YDocMessageTransporter = useMemo(() => {
|
||||
return isMockMode ? new MockConnection(yDoc, awareness) : new WebsocketConnection(websocketUrl, yDoc, awareness)
|
||||
}, [awareness, websocketUrl, yDoc])
|
||||
|
||||
useEffect(() => {
|
||||
const disconnectCallback = () => websocketConnection.disconnect()
|
||||
window.addEventListener('beforeunload', disconnectCallback)
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', disconnectCallback)
|
||||
disconnectCallback()
|
||||
}
|
||||
}, [websocketConnection])
|
||||
|
||||
return websocketConnection
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import { backendUrl } from '../../../../../utils/backend-url'
|
||||
import { isMockMode } from '../../../../../utils/test-modes'
|
||||
import { useApplicationState } from '../../../../../hooks/common/use-application-state'
|
||||
|
||||
const LOCAL_FALLBACK_URL = 'ws://localhost:8080/realtime/'
|
||||
|
||||
/**
|
||||
* Provides the URL for the realtime endpoint.
|
||||
*/
|
||||
export const useWebsocketUrl = (): URL => {
|
||||
const noteId = useApplicationState((state) => state.noteDetails.id)
|
||||
|
||||
const baseUrl = useMemo(() => {
|
||||
if (isMockMode) {
|
||||
return process.env.NEXT_PUBLIC_REALTIME_URL ?? LOCAL_FALLBACK_URL
|
||||
}
|
||||
try {
|
||||
const backendBaseUrlParsed = new URL(backendUrl)
|
||||
backendBaseUrlParsed.protocol = backendBaseUrlParsed.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
backendBaseUrlParsed.pathname += 'realtime'
|
||||
return backendBaseUrlParsed.toString()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return LOCAL_FALLBACK_URL
|
||||
}
|
||||
}, [])
|
||||
|
||||
return useMemo(() => {
|
||||
const url = new URL(baseUrl)
|
||||
url.search = `?noteId=${noteId}`
|
||||
return url
|
||||
}, [baseUrl, noteId])
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Doc } from 'yjs'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
|
||||
/**
|
||||
* Creates a new {@link Doc y-doc}.
|
||||
*
|
||||
* @return The created {@link Doc y-doc}
|
||||
*/
|
||||
export const useYDoc = (): Doc => {
|
||||
const yDoc = useMemo(() => new Doc(), [])
|
||||
useEffect(() => () => yDoc.destroy(), [yDoc])
|
||||
return yDoc
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import {
|
||||
encodeAwarenessUpdateMessage,
|
||||
encodeCompleteAwarenessStateRequestMessage,
|
||||
encodeDocumentUpdateMessage
|
||||
} from '@hedgedoc/realtime'
|
||||
import { WebsocketTransporter } from '@hedgedoc/realtime'
|
||||
import type { Doc } from 'yjs'
|
||||
import type { Awareness } from 'y-protocols/awareness'
|
||||
|
||||
/**
|
||||
* Handles the communication with the realtime endpoint of the backend and synchronizes the given y-doc and awareness with other clients..
|
||||
*/
|
||||
export class WebsocketConnection extends WebsocketTransporter {
|
||||
constructor(url: URL, doc: Doc, awareness: Awareness) {
|
||||
super(doc, awareness)
|
||||
this.bindYDocEvents(doc)
|
||||
this.bindAwarenessMessageEvents(awareness)
|
||||
const websocket = new WebSocket(url)
|
||||
this.setupWebsocket(websocket)
|
||||
websocket.addEventListener('open', this.onOpen.bind(this))
|
||||
}
|
||||
|
||||
private bindAwarenessMessageEvents(awareness: Awareness) {
|
||||
const updateCallback = (
|
||||
{ added, updated, removed }: { added: number[]; updated: number[]; removed: number[] },
|
||||
origin: unknown
|
||||
) => {
|
||||
if (origin !== this) {
|
||||
this.send(encodeAwarenessUpdateMessage(awareness, [...added, ...updated, ...removed]))
|
||||
}
|
||||
}
|
||||
this.on('disconnected', () => {
|
||||
awareness.destroy()
|
||||
awareness.off('update', updateCallback)
|
||||
})
|
||||
|
||||
this.on('ready', () => {
|
||||
awareness.on('update', updateCallback)
|
||||
})
|
||||
this.on('synced', () => {
|
||||
this.send(encodeCompleteAwarenessStateRequestMessage())
|
||||
this.send(encodeAwarenessUpdateMessage(awareness, [awareness.doc.clientID]))
|
||||
})
|
||||
}
|
||||
|
||||
private bindYDocEvents(doc: Doc): void {
|
||||
doc.on('destroy', () => this.disconnect())
|
||||
doc.on('update', (update: Uint8Array, origin: unknown) => {
|
||||
if (origin !== this && this.isSynced()) {
|
||||
this.send(encodeDocumentUpdateMessage(update))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue