123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178 |
- /* eslint-disable @typescript-eslint/no-non-null-assertion */
- import { TLDraw, TLDrawState, Data, TLDrawDocument, TLDrawUser } from '@tldraw/tldraw'
- import * as React from 'react'
- import { createClient, Presence } from '@liveblocks/client'
- import { LiveblocksProvider, RoomProvider, useObject, useErrorListener } from '@liveblocks/react'
- import { Utils } from '@tldraw/core'
-
- interface TLDrawUserPresence extends Presence {
- user: TLDrawUser
- }
-
- const client = createClient({
- publicApiKey: process.env.NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_API_KEY,
- throttle: 80,
- })
-
- export default function MultiplayerEditor({ id }: { id: string }) {
- return (
- <LiveblocksProvider client={client}>
- <RoomProvider id={id}>
- <Editor id={id} />
- </RoomProvider>
- </LiveblocksProvider>
- )
- }
-
- function Editor({ id }: { id: string }) {
- const [docId] = React.useState(() => Utils.uniqueId())
-
- const [error, setError] = React.useState<Error>()
-
- const [tlstate, setTlstate] = React.useState<TLDrawState>()
-
- useErrorListener((err) => setError(err))
-
- const doc = useObject<{ uuid: string; document: TLDrawDocument }>('doc', {
- uuid: docId,
- document: {
- id: 'test-room',
- ...TLDrawState.defaultDocument,
- },
- })
-
- // Put the tlstate into the window, for debugging.
- const handleMount = React.useCallback(
- (tlstate: TLDrawState) => {
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- window.tlstate = tlstate
-
- tlstate.loadRoom(id)
-
- setTlstate(tlstate)
- },
- [id]
- )
-
- const handleChange = React.useCallback(
- (_tlstate: TLDrawState, state: Data, reason: string) => {
- // If the client updates its document, update the room's document
- if (reason.startsWith('command') || reason.startsWith('undo') || reason.startsWith('redo')) {
- doc?.update({ uuid: docId, document: state.document })
- }
-
- // When the client updates its presence, update the room
- // if (state.room && (reason === 'patch:room:self:update' || reason === 'patch:selected')) {
- // const room = client.getRoom(ROOM_ID)
- // if (!room) return
- // const { userId, users } = state.room
- // room.updatePresence({ id: userId, user: users[userId] })
- // }
- },
- [docId, doc]
- )
-
- React.useEffect(() => {
- const room = client.getRoom(id)
-
- if (!room) return
- if (!doc) return
- if (!tlstate) return
- if (!tlstate.state.room) return
-
- // Update the user's presence with the user from state
- const { users, userId } = tlstate.state.room
-
- room.updatePresence({ id: userId, user: users[userId] })
-
- // Subscribe to presence changes; when others change, update the state
- room.subscribe<TLDrawUserPresence>('others', (others) => {
- tlstate.updateUsers(
- others
- .toArray()
- .filter((other) => other.presence)
- .map((other) => other.presence!.user)
- .filter(Boolean)
- )
- })
-
- room.subscribe('event', (event) => {
- if (event.event?.name === 'exit') {
- tlstate.removeUser(event.event.userId)
- }
- })
-
- function handleDocumentUpdates() {
- if (!doc) return
- if (!tlstate) return
- if (!tlstate.state.room) return
-
- const docObject = doc.toObject()
-
- // Only merge the change if it caused by someone else
- if (docObject.uuid !== docId) {
- tlstate.mergeDocument(docObject.document)
- } else {
- tlstate.updateUsers(
- Object.values(tlstate.state.room.users).map((user) => {
- // const activeShapes = user.activeShapes
- // .map((shape) => docObject.document.pages[tlstate.currentPageId].shapes[shape.id])
- // .filter(Boolean)
- return {
- ...user,
- // activeShapes: activeShapes,
- selectedIds: user.selectedIds, // activeShapes.map((shape) => shape.id),
- }
- })
- )
- }
- }
-
- function handleExit() {
- if (!(tlstate && tlstate.state.room)) return
- room?.broadcastEvent({ name: 'exit', userId: tlstate.state.room.userId })
- }
-
- window.addEventListener('beforeunload', handleExit)
-
- // When the shared document changes, update the state
- doc.subscribe(handleDocumentUpdates)
-
- // Load the shared document
- const newDocument = doc.toObject().document
-
- if (newDocument) {
- tlstate.loadDocument(newDocument)
- }
-
- return () => {
- window.removeEventListener('beforeunload', handleExit)
- doc.unsubscribe(handleDocumentUpdates)
- }
- }, [doc, docId, tlstate, id])
-
- const handleUserChange = React.useCallback(
- (tlstate: TLDrawState, user: TLDrawUser) => {
- const room = client.getRoom(id)
- room?.updatePresence({ id: tlstate.state.room?.userId, user })
- },
- [id]
- )
-
- if (error) return <div>Error: {error.message}</div>
-
- if (doc === null) return <div>Loading...</div>
-
- return (
- <div className="tldraw">
- <TLDraw
- onMount={handleMount}
- onChange={handleChange}
- onUserChange={handleUserChange}
- showPages={false}
- autofocus
- />
- </div>
- )
- }
|