mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: init collaboration backend & adapt editor
This commit is contained in:
parent
83760064e7
commit
953de4cc67
7 changed files with 100 additions and 44 deletions
9
apps/collaboration/app.ts
Normal file
9
apps/collaboration/app.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { Hocuspocus } from "@hocuspocus/server";
|
||||||
|
|
||||||
|
// Configure the server …
|
||||||
|
const server = new Hocuspocus({
|
||||||
|
port: 1998,
|
||||||
|
});
|
||||||
|
|
||||||
|
// … and run it!
|
||||||
|
server.listen();
|
||||||
19
apps/collaboration/package.json
Normal file
19
apps/collaboration/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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'],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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}`
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue