feat: init collaboration backend & adapt editor

This commit is contained in:
swve 2024-04-01 16:47:41 +02:00
parent 83760064e7
commit 953de4cc67
7 changed files with 100 additions and 44 deletions

View file

@ -0,0 +1,9 @@
import { Hocuspocus } from "@hocuspocus/server";
// Configure the server …
const server = new Hocuspocus({
port: 1998,
});
// … and run it!
server.listen();

View file

@ -0,0 +1,19 @@
{
"name": "collaboration",
"version": "1.0.0",
"description": "",
"main": "app.ts",
"scripts": {
"start": "bun app.ts"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@hocuspocus/server": "^2.11.3",
"bun": "^1.0.36",
"typescript": "5.4.3",
"y-protocols": "^1.0.6",
"yjs": "^13.6.14"
}
}

View file

@ -49,7 +49,7 @@ const EditActivity = async (params: any) => {
access_token ? access_token : null access_token ? access_token : null
) )
const org = await getOrganizationContextInfoWithId(courseInfo.org_id, { const org = await getOrganizationContextInfoWithId(courseInfo.org_id, {
revalidate: 1800, revalidate: 180,
tags: ['organizations'], tags: ['organizations'],
}) })

View file

@ -9,7 +9,6 @@ import ImageBlock from '@components/Objects/Editor/Extensions/Image/ImageBlock'
import VideoBlock from '@components/Objects/Editor/Extensions/Video/VideoBlock' import VideoBlock from '@components/Objects/Editor/Extensions/Video/VideoBlock'
import MathEquationBlock from '@components/Objects/Editor/Extensions/MathEquation/MathEquationBlock' import MathEquationBlock from '@components/Objects/Editor/Extensions/MathEquation/MathEquationBlock'
import PDFBlock from '@components/Objects/Editor/Extensions/PDF/PDFBlock' import PDFBlock from '@components/Objects/Editor/Extensions/PDF/PDFBlock'
import { OrderedList } from '@tiptap/extension-ordered-list'
import QuizBlock from '@components/Objects/Editor/Extensions/Quiz/QuizBlock' import QuizBlock from '@components/Objects/Editor/Extensions/Quiz/QuizBlock'
// Lowlight // Lowlight
@ -83,7 +82,6 @@ function Canva(props: Editor) {
controls: true, controls: true,
modestBranding: true, modestBranding: true,
}), }),
OrderedList.configure(),
CodeBlockLowlight.configure({ CodeBlockLowlight.configure({
lowlight, lowlight,
}), }),

View file

@ -15,7 +15,7 @@ import {
useAIEditorDispatch, useAIEditorDispatch,
} from '@components/Contexts/AI/AIEditorContext' } from '@components/Contexts/AI/AIEditorContext'
// extensions // Extensions
import InfoCallout from './Extensions/Callout/Info/InfoCallout' import InfoCallout from './Extensions/Callout/Info/InfoCallout'
import WarningCallout from './Extensions/Callout/Warning/WarningCallout' import WarningCallout from './Extensions/Callout/Warning/WarningCallout'
import ImageBlock from './Extensions/Image/ImageBlock' import ImageBlock from './Extensions/Image/ImageBlock'
@ -28,7 +28,7 @@ import QuizBlock from './Extensions/Quiz/QuizBlock'
import ToolTip from '@components/StyledElements/Tooltip/Tooltip' import ToolTip from '@components/StyledElements/Tooltip/Tooltip'
import Link from 'next/link' import Link from 'next/link'
import { getCourseThumbnailMediaDirectory } from '@services/media/media' import { getCourseThumbnailMediaDirectory } from '@services/media/media'
import { OrderedList } from '@tiptap/extension-ordered-list'
// Lowlight // Lowlight
import { common, createLowlight } from 'lowlight' import { common, createLowlight } from 'lowlight'
@ -45,14 +45,19 @@ import { useSession } from '@components/Contexts/SessionContext'
import AIEditorToolkit from './AI/AIEditorToolkit' import AIEditorToolkit from './AI/AIEditorToolkit'
import useGetAIFeatures from '@components/AI/Hooks/useGetAIFeatures' import useGetAIFeatures from '@components/AI/Hooks/useGetAIFeatures'
import UserAvatar from '../UserAvatar' import UserAvatar from '../UserAvatar'
import randomColor from 'randomcolor'
import Collaboration from '@tiptap/extension-collaboration'
import CollaborationCursor from '@tiptap/extension-collaboration-cursor'
interface Editor { interface Editor {
content: string content: string
ydoc: any
provider: any
activity: any activity: any
course: any course: any
org: any org: any
session: any
ydoc: any
hocuspocusProvider: any,
isCollabEnabledOnThisOrg: boolean
setContent: (content: string) => void setContent: (content: string) => void
} }
@ -63,10 +68,12 @@ function Editor(props: Editor) {
const is_ai_feature_enabled = useGetAIFeatures({ feature: 'editor' }) const is_ai_feature_enabled = useGetAIFeatures({ feature: 'editor' })
const [isButtonAvailable, setIsButtonAvailable] = React.useState(false) const [isButtonAvailable, setIsButtonAvailable] = React.useState(false)
React.useEffect(() => { React.useEffect(() => {
if (is_ai_feature_enabled) { if (is_ai_feature_enabled) {
setIsButtonAvailable(true) setIsButtonAvailable(true)
} }
}, [is_ai_feature_enabled]) }, [is_ai_feature_enabled])
// remove course_ from course_uuid // remove course_ from course_uuid
@ -89,7 +96,7 @@ function Editor(props: Editor) {
extensions: [ extensions: [
StarterKit.configure({ StarterKit.configure({
// The Collaboration extension comes with its own history handling // The Collaboration extension comes with its own history handling
// history: false, history: props.isCollabEnabledOnThisOrg ? false : undefined,
}), }),
InfoCallout.configure({ InfoCallout.configure({
editable: true, editable: true,
@ -121,26 +128,28 @@ function Editor(props: Editor) {
controls: true, controls: true,
modestBranding: true, modestBranding: true,
}), }),
OrderedList.configure(),
CodeBlockLowlight.configure({ CodeBlockLowlight.configure({
lowlight, lowlight,
}), }),
// Register the document with Tiptap // Add Collaboration and CollaborationCursor only if isCollabEnabledOnThisOrg is true
// Collaboration.configure({ ...(props.isCollabEnabledOnThisOrg ? [
// document: props.ydoc, Collaboration.configure({
// }), document: props.hocuspocusProvider?.document,
// Register the collaboration cursor extension }),
// CollaborationCursor.configure({
// provider: props.provider, CollaborationCursor.configure({
// user: { provider: props.hocuspocusProvider,
// name: auth.userInfo.username, user: {
// color: "#f783ac", name: props.session.user.first_name + ' ' + props.session.user.last_name,
// }, color: randomColor({ luminosity: 'light' }),
// }), },
}),
] : []),
], ],
content: props.content, // If collab is enabled the onSynced callback ensures initial content is set only once using editor.setContent(), preventing repetitive content insertion on editor syncs.
content: props.isCollabEnabledOnThisOrg ? null : props.content,
}) })
return ( return (

View file

@ -1,11 +1,17 @@
'use client' 'use client'
import { default as React } from 'react' import { default as React, useEffect } from 'react'
import * as Y from 'yjs'
import Editor from './Editor' import Editor from './Editor'
import { updateActivity } from '@services/courses/activities' import { updateActivity } from '@services/courses/activities'
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
import Toast from '@components/StyledElements/Toast/Toast' import Toast from '@components/StyledElements/Toast/Toast'
import { OrgProvider } from '@components/Contexts/OrgContext' import { OrgProvider } from '@components/Contexts/OrgContext'
import { useSession } from '@components/Contexts/SessionContext'
// Collaboration
import { HocuspocusProvider } from '@hocuspocus/provider'
import * as Y from 'yjs'
import { IndexeddbPersistence } from 'y-indexeddb'
import { LEARNHOUSE_COLLABORATION_WS_URL, getCollaborationServerUrl } from '@services/config/config'
interface EditorWrapperProps { interface EditorWrapperProps {
content: string content: string
@ -15,18 +21,23 @@ interface EditorWrapperProps {
} }
function EditorWrapper(props: EditorWrapperProps): JSX.Element { function EditorWrapper(props: EditorWrapperProps): JSX.Element {
// A new Y document const session = useSession() as any
const ydoc = new Y.Doc()
const [providerState, setProviderState] = React.useState<any>({})
const [ydocState, setYdocState] = React.useState<any>({})
const [isLoading, setIsLoading] = React.useState(true)
function createRTCProvider() { /* Collaboration Features */
// const provider = new WebrtcProvider(props.activity.activity_id, ydoc); const collab = getCollaborationServerUrl()
// setYdocState(ydoc); const isCollabEnabledOnThisOrg = props.org.config.config.GeneralConfig.collaboration && collab
// setProviderState(provider); const doc = new Y.Doc()
setIsLoading(false)
} // Store the Y document in the browser
new IndexeddbPersistence(props.activity.activity_uuid, doc)
const provider = isCollabEnabledOnThisOrg ? new HocuspocusProvider({
url: collab,
name: props.activity.activity_uuid,
document: doc,
preserveConnection: false,
}) : null
/* Collaboration Features */
async function setContent(content: any) { async function setContent(content: any) {
let activity = props.activity let activity = props.activity
@ -39,24 +50,28 @@ function EditorWrapper(props: EditorWrapperProps): JSX.Element {
}) })
} }
if (isLoading) { useEffect(() => {
createRTCProvider()
return <div>Loading...</div> }
} else { , [session])
{
return ( return (
<> <>
<Toast></Toast> <Toast></Toast>
<OrgProvider orgslug={props.org.slug}> <OrgProvider orgslug={props.org.slug}>
<Editor {!session.isLoading && (<Editor
org={props.org} org={props.org}
course={props.course} course={props.course}
activity={props.activity} activity={props.activity}
content={props.content} content={props.content}
setContent={setContent} setContent={setContent}
provider={providerState} session={session}
ydoc={ydocState} ydoc={doc}
></Editor> hocuspocusProvider={provider}
; isCollabEnabledOnThisOrg={isCollabEnabledOnThisOrg}
></Editor>)}
</OrgProvider> </OrgProvider>
</> </>
) )

View file

@ -3,6 +3,8 @@ export const LEARNHOUSE_HTTP_PROTOCOL =
const LEARNHOUSE_API_URL = `${process.env.NEXT_PUBLIC_LEARNHOUSE_API_URL}` const LEARNHOUSE_API_URL = `${process.env.NEXT_PUBLIC_LEARNHOUSE_API_URL}`
export const LEARNHOUSE_BACKEND_URL = `${process.env.NEXT_PUBLIC_LEARNHOUSE_BACKEND_URL}` export const LEARNHOUSE_BACKEND_URL = `${process.env.NEXT_PUBLIC_LEARNHOUSE_BACKEND_URL}`
export const LEARNHOUSE_DOMAIN = process.env.NEXT_PUBLIC_LEARNHOUSE_DOMAIN export const LEARNHOUSE_DOMAIN = process.env.NEXT_PUBLIC_LEARNHOUSE_DOMAIN
export const LEARNHOUSE_COLLABORATION_WS_URL =
process.env.NEXT_PUBLIC_LEARNHOUSE_COLLABORATION_WS_URL
export const getAPIUrl = () => LEARNHOUSE_API_URL export const getAPIUrl = () => LEARNHOUSE_API_URL
export const getBackendUrl = () => LEARNHOUSE_BACKEND_URL export const getBackendUrl = () => LEARNHOUSE_BACKEND_URL
@ -35,3 +37,7 @@ export const getOrgFromUri = () => {
export const getDefaultOrg = () => { export const getDefaultOrg = () => {
return process.env.NEXT_PUBLIC_LEARNHOUSE_DEFAULT_ORG return process.env.NEXT_PUBLIC_LEARNHOUSE_DEFAULT_ORG
} }
export const getCollaborationServerUrl = () => {
return `${LEARNHOUSE_COLLABORATION_WS_URL}`
}