diff --git a/src/context/editor.tsx b/src/context/editor.tsx index 65bcac43..e1b23665 100644 --- a/src/context/editor.tsx +++ b/src/context/editor.tsx @@ -1,9 +1,14 @@ +import { HocuspocusProvider } from '@hocuspocus/provider' import { useMatch, useNavigate } from '@solidjs/router' import { Editor } from '@tiptap/core' +import Collaboration from '@tiptap/extension-collaboration' +import CollaborationCursor from '@tiptap/extension-collaboration-cursor' import type { JSX } from 'solid-js' -import { Accessor, createContext, createSignal, onCleanup, useContext } from 'solid-js' +import { Accessor, createContext, createEffect, createSignal, on, onCleanup, useContext } from 'solid-js' import { SetStoreFunction, createStore } from 'solid-js/store' import { debounce } from 'throttle-debounce' +import uniqolor from 'uniqolor' +import { Doc } from 'yjs' import { useSnackbar } from '~/context/ui' import deleteShoutQuery from '~/graphql/mutation/core/article-delete' import updateShoutQuery from '~/graphql/mutation/core/article-update' @@ -14,6 +19,8 @@ import { useLocalize } from './localize' import { useSession } from './session' export const AUTO_SAVE_DELAY = 3000 +const yDocs: Record = {} +const providers: Record = {} export type WordCounter = { characters: number @@ -98,7 +105,7 @@ export const EditorProvider = (props: { children: JSX.Element }) => { const navigate = useNavigate() const matchEdit = useMatch(() => '/edit') const matchEditSettings = useMatch(() => '/editSettings') - const { client } = useSession() + const { client, session } = useSession() const { addFeed } = useFeed() const snackbar = useSnackbar() const [isEditorPanelVisible, setIsEditorPanelVisible] = createSignal(false) @@ -251,7 +258,7 @@ export const EditorProvider = (props: { children: JSX.Element }) => { } } catch (error) { console.error('[publishShoutById]', error) - snackbar?.showSnackbar({ type: 'error', body: localize?.t('Error') }) + snackbar?.showSnackbar({ type: 'error', body: localize?.t('Error') || '' }) } } @@ -278,6 +285,63 @@ export const EditorProvider = (props: { children: JSX.Element }) => { }) onCleanup(debouncedAutoSave.cancel) + createEffect( + on( + isCollabMode, + (x?: boolean) => () => { + const editorInstance = editing() + if (!editorInstance) return + try { + const docName = `shout-${form.shoutId}` + const token = session()?.access_token || '' + const profile = session()?.user?.app_data?.profile + + if (!(token && profile)) { + throw new Error('Missing authentication data') + } + + if (!yDocs[docName]) { + yDocs[docName] = new Doc() + } + + if (!providers[docName]) { + providers[docName] = new HocuspocusProvider({ + url: 'wss://hocuspocus.discours.io', + name: docName, + document: yDocs[docName], + token + }) + console.log(`[collab mode] HocuspocusProvider connected for ${docName}`) + } + if (x) { + const newExtensions = [ + Collaboration.configure({ document: yDocs[docName] }), + CollaborationCursor.configure({ + provider: providers[docName], + user: { name: profile.name, color: uniqolor(profile.slug).color } + }) + ] + const extensions = editing()?.options.extensions.concat(newExtensions) + editorInstance.setOptions({ ...editorInstance.options, extensions }) + providers[docName].connect() + } else if (editorInstance) { + providers[docName].disconnect() + const updatedExtensions = editorInstance.options.extensions.filter( + (ext) => ext.name !== 'collaboration' && ext.name !== 'collaborationCursor' + ) + editorInstance.setOptions({ + ...editorInstance.options, + extensions: updatedExtensions + }) + } + } catch (error) { + console.error('[collab mode] error', error) + } + }, + { defer: true } + ) + ) + const handleInputChange = (key: keyof ShoutForm, value: string) => { console.log(`[handleInputChange] ${key}: ${value}`) setForm(key, value)