Tldraw/example/src/multiplayer/multiplayer.tsx

163 wiersze
4.1 KiB
TypeScript

/* eslint-disable @typescript-eslint/no-non-null-assertion */
import * as React from 'react'
import { TLDraw, TLDrawState, TLDrawDocument, TLDrawUser } from '@tldraw/tldraw'
import { createClient, Presence } from '@liveblocks/client'
import { LiveblocksProvider, RoomProvider, useErrorListener, useObject } from '@liveblocks/react'
import { Utils } from '@tldraw/core'
interface TLDrawUserPresence extends Presence {
user: TLDrawUser
}
const client = createClient({
publicApiKey: process.env.LIVEBLOCKS_PUBLIC_API_KEY || '',
throttle: 80,
})
const roomId = 'mp-test-2'
export function Multiplayer() {
return (
<LiveblocksProvider client={client}>
<RoomProvider id={roomId}>
<TLDrawWrapper />
</RoomProvider>
</LiveblocksProvider>
)
}
function TLDrawWrapper() {
const [docId] = React.useState(() => Utils.uniqueId())
const [error, setError] = React.useState<Error>()
const [state, setstate] = React.useState<TLDrawState>()
useErrorListener((err) => setError(err))
const doc = useObject<{ uuid: string; document: TLDrawDocument }>('doc', {
uuid: docId,
document: {
...TLDrawState.defaultDocument,
id: roomId,
},
})
// Put the state into the window, for debugging.
const handleMount = React.useCallback(
(state: TLDrawState) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
window.state = state
state.loadRoom(roomId)
setstate(state)
},
[roomId]
)
React.useEffect(() => {
const room = client.getRoom(roomId)
if (!room) return
if (!doc) return
if (!state) return
if (!state.state.room) return
// Update the user's presence with the user from state
const { users, userId } = state.state.room
room.updatePresence({ id: userId, user: users[userId] })
// Subscribe to presence changes; when others change, update the state
room.subscribe<TLDrawUserPresence>('others', (others) => {
state.updateUsers(
others
.toArray()
.filter((other) => other.presence)
.map((other) => other.presence!.user)
.filter(Boolean)
)
})
room.subscribe('event', (event) => {
if (event.event?.name === 'exit') {
state.removeUser(event.event.userId)
}
})
function handleDocumentUpdates() {
if (!doc) return
if (!state) return
if (!state.state.room) return
const docObject = doc.toObject()
// Only merge the change if it caused by someone else
if (docObject.uuid !== docId) {
state.mergeDocument(docObject.document)
} else {
state.updateUsers(
Object.values(state.state.room.users).map((user) => {
return {
...user,
selectedIds: user.selectedIds,
}
})
)
}
}
function handleExit() {
if (!(state && state.state.room)) return
room?.broadcastEvent({ name: 'exit', userId: state.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) {
state.loadDocument(newDocument)
}
return () => {
window.removeEventListener('beforeunload', handleExit)
doc.unsubscribe(handleDocumentUpdates)
}
}, [doc, docId, state])
const handlePersist = React.useCallback(
(state: TLDrawState) => {
doc?.update({ uuid: docId, document: state.document })
},
[docId, doc]
)
const handleUserChange = React.useCallback(
(state: TLDrawState, user: TLDrawUser) => {
const room = client.getRoom(roomId)
room?.updatePresence({ id: state.state.room?.userId, user })
},
[client]
)
if (error) return <div>Error: {error.message}</div>
if (doc === null) return <div>Loading...</div>
return (
<div className="tldraw">
<TLDraw
onMount={handleMount}
onPersist={handlePersist}
onUserChange={handleUserChange}
showPages={false}
/>
</div>
)
}