diff --git a/apps/course-builder-web/package.json b/apps/course-builder-web/package.json index fb2b565db..e60f2575b 100644 --- a/apps/course-builder-web/package.json +++ b/apps/course-builder-web/package.json @@ -46,6 +46,7 @@ "@hookform/resolvers": "^3.3.2", "@mdx-js/loader": "^3.0.0", "@mdx-js/react": "^3.0.0", + "@msgpack/msgpack": "3.0.0-beta2", "@mux/mux-player-react": "^2.2.0", "@next/mdx": "14.0.1", "@planetscale/database": "^1.11.0", @@ -144,7 +145,8 @@ "y-prosemirror": "^1.2.1", "y-protocols": "^1.0.6", "yjs": "^13.6.10", - "zod": "^3.22.4" + "zod": "^3.22.4", + "zustand": "^4.4.7" }, "devDependencies": { "@mux/mux-node": "^7.3.2", diff --git a/apps/course-builder-web/party/index.ts b/apps/course-builder-web/party/index.ts index 8a3d4107a..eeb5c0b8f 100644 --- a/apps/course-builder-web/party/index.ts +++ b/apps/course-builder-web/party/index.ts @@ -2,6 +2,15 @@ import type * as Party from 'partykit/server' import {onConnect} from 'y-partykit' import * as Y from 'yjs' +const BROADCAST_INTERVAL = 1000 / 60 // 60fps + +const CORS = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET', + 'Access-Control-Allow-Headers': + 'Origin, X-Requested-With, Content-Type, Accept', +} + export default class Server implements Party.Server { constructor(readonly party: Party.Party) {} @@ -43,15 +52,33 @@ export default class Server implements Party.Server { this.messages = (await this.party.storage.get('messages')) ?? [] } - async onRequest(_req: Party.Request) { - const messageBody: {requestId: string; body: string; name: string} = - await _req.json() + async onRequest(req: Party.Request) { + if (req.method === 'GET') { + // For SSR, return the current presence of all connections + // const users = [...this.party.getConnections()].reduce( + // (acc, user) => ({...acc, [user.id]: this.getUser(user)}), + // {}, + // ) + return Response.json({users: []}, {status: 200, headers: CORS}) + } - this.party.broadcast(JSON.stringify(messageBody)) + // respond to cors preflight requests + if (req.method === 'OPTIONS') { + return Response.json({ok: true}, {status: 200, headers: CORS}) + } - return new Response( - `Party ${this.party.id} has received ${this.messages.length} messages`, - ) + if (req.method === 'POST') { + const messageBody: {requestId: string; body: string; name: string} = + await req.json() + + this.party.broadcast(JSON.stringify(messageBody)) + + return new Response( + `Party ${this.party.id} has received ${this.messages.length} messages`, + ) + } + + return new Response('Method Not Allowed', {status: 405}) } onMessage(message: string, sender: Party.Connection) { diff --git a/apps/course-builder-web/src/app/tips/_components/codemirror.tsx b/apps/course-builder-web/src/app/tips/_components/codemirror.tsx index cc77b478b..0e5553e91 100644 --- a/apps/course-builder-web/src/app/tips/_components/codemirror.tsx +++ b/apps/course-builder-web/src/app/tips/_components/codemirror.tsx @@ -6,6 +6,7 @@ import {EditorState, Extension} from '@codemirror/state' import {useCallback, useEffect, useState} from 'react' import {markdown} from '@codemirror/lang-markdown' import YPartyKitProvider from 'y-partykit/provider' +import {useSession} from 'next-auth/react' export const CodemirrorEditor = ({ roomName, @@ -80,6 +81,8 @@ const useCodemirror = ({ const [element, setElement] = useState() const [yUndoManager, setYUndoManager] = useState() + const {data: session} = useSession() + useEffect(() => { let view: EditorView @@ -103,6 +106,15 @@ const useCodemirror = ({ } }) + const awareness = provider.awareness + + if (session) { + awareness.setLocalStateField('user', { + ...session.user, + color: '#ffb61e', // should be a hex color + }) + } + // Set up CodeMirror and extensions const state = EditorState.create({ doc: ytext.toString(), @@ -125,7 +137,7 @@ const useCodemirror = ({ provider?.destroy() view?.destroy() } - }, [element, roomName, value]) + }, [element, roomName, value, session]) return { codemirrorElementRef: useCallback((node: HTMLElement | null) => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 16d250b69..e4dd3a507 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -173,6 +173,9 @@ importers: '@mdx-js/react': specifier: ^3.0.0 version: 3.0.0(@types/react@18.2.37)(react@18.2.0) + '@msgpack/msgpack': + specifier: 3.0.0-beta2 + version: 3.0.0-beta2 '@mux/mux-player-react': specifier: ^2.2.0 version: 2.2.0(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) @@ -470,6 +473,9 @@ importers: zod: specifier: ^3.22.4 version: 3.22.4 + zustand: + specifier: ^4.4.7 + version: 4.4.7(@types/react@18.2.37)(react@18.2.0) devDependencies: '@mux/mux-node': specifier: ^7.3.2 @@ -3123,18 +3129,10 @@ packages: dependencies: '@codemirror/language': 6.9.2 '@codemirror/state': 6.3.3 - '@codemirror/view': 6.22.0 + '@codemirror/view': 6.22.3 '@lezer/highlight': 1.2.0 dev: false - /@codemirror/view@6.22.0: - resolution: {integrity: sha512-6zLj4YIoIpfTGKrDMTbeZRpa8ih4EymMCKmddEDcJWrCdp/N1D46B38YEz4creTb4T177AVS9EyXkLeC/HL2jA==} - dependencies: - '@codemirror/state': 6.3.3 - style-mod: 4.1.0 - w3c-keyname: 2.2.8 - dev: false - /@codemirror/view@6.22.3: resolution: {integrity: sha512-rqnq+Zospwoi3x1vZ8BGV1MlRsaGljX+6qiGYmIpJ++M+LCC+wjfDaPklhwpWSgv7pr/qx29KiAKQBH5+DOn4w==} dependencies: @@ -4800,6 +4798,11 @@ packages: react: 18.2.0 dev: false + /@msgpack/msgpack@3.0.0-beta2: + resolution: {integrity: sha512-y+l1PNV0XDyY8sM3YtuMLK5vE3/hkfId+Do8pLo/OPxfxuFAUwcGz3oiiUuV46/aBpwTzZ+mRWVMtlSKbradhw==} + engines: {node: '>= 14'} + dev: false + /@mux/mux-node@7.3.3: resolution: {integrity: sha512-t98SW0rhUP56Qn3vly+NApzy7Ebo51fcMYVQL379dpU5exsCoRBYc9lf6a4V+U0fXJ4Ke2asfToXfXMvfsMC+Q==} engines: {node: '>=14'} @@ -21351,6 +21354,26 @@ packages: /zod@3.22.4: resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} + /zustand@4.4.7(@types/react@18.2.37)(react@18.2.0): + resolution: {integrity: sha512-QFJWJMdlETcI69paJwhSMJz7PPWjVP8Sjhclxmxmxv/RYI7ZOvR5BHX+ktH0we9gTWQMxcne8q1OY8xxz604gw==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + dependencies: + '@types/react': 18.2.37 + react: 18.2.0 + use-sync-external-store: 1.2.0(react@18.2.0) + dev: false + /zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} dev: false