improves multiplayer presence (#160)
This commit is contained in:
parent
3e7d2c3ad9
commit
abcdcd8dae
7 changed files with 137 additions and 94 deletions
|
@ -80,7 +80,9 @@ export function Canvas<T extends TLShape, M extends Record<string, unknown>>({
|
|||
hideHandles={hideHandles}
|
||||
meta={meta}
|
||||
/>
|
||||
{users && userId && <UsersIndicators userId={userId} users={users} meta={meta} />}
|
||||
{users && userId && (
|
||||
<UsersIndicators userId={userId} users={users} page={page} meta={meta} />
|
||||
)}
|
||||
{pageState.brush && <Brush brush={pageState.brush} />}
|
||||
{users && <Users userId={userId} users={users} />}
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import * as React from 'react'
|
||||
import type { TLBinding, TLPage, TLPageState, TLShape, TLShapeUtil } from '+types'
|
||||
import { useSelection, useShapeTree, useHandles, useTLContext } from '+hooks'
|
||||
import { useSelection, useShapeTree, useTLContext } from '+hooks'
|
||||
import { Bounds } from '+components/bounds'
|
||||
import { BoundsBg } from '+components/bounds/bounds-bg'
|
||||
import { Handles } from '+components/handles'
|
||||
|
|
|
@ -1,17 +1,23 @@
|
|||
import * as React from 'react'
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { ShapeIndicator } from '+components/shape-indicator'
|
||||
import type { TLShape, TLUsers } from '+types'
|
||||
import type { TLPage, TLShape, TLUsers } from '+types'
|
||||
import Utils from '+utils'
|
||||
import { useTLContext } from '+hooks'
|
||||
|
||||
interface UserIndicatorProps<T extends TLShape> {
|
||||
page: TLPage<any, any>
|
||||
userId: string
|
||||
users: TLUsers<T>
|
||||
meta: any
|
||||
}
|
||||
|
||||
export function UsersIndicators<T extends TLShape>({ userId, users, meta }: UserIndicatorProps<T>) {
|
||||
export function UsersIndicators<T extends TLShape>({
|
||||
userId,
|
||||
users,
|
||||
meta,
|
||||
page,
|
||||
}: UserIndicatorProps<T>) {
|
||||
const { shapeUtils } = useTLContext()
|
||||
|
||||
return (
|
||||
|
@ -20,7 +26,9 @@ export function UsersIndicators<T extends TLShape>({ userId, users, meta }: User
|
|||
.filter(Boolean)
|
||||
.filter((user) => user.id !== userId && user.selectedIds.length > 0)
|
||||
.map((user) => {
|
||||
const shapes = user.activeShapes //.map((id) => page.shapes[id])
|
||||
const shapes = user.selectedIds.map((id) => page.shapes[id]).filter(Boolean)
|
||||
|
||||
if (shapes.length === 0) return null
|
||||
|
||||
const bounds = Utils.getCommonBounds(
|
||||
shapes.map((shape) => shapeUtils[shape.type].getBounds(shape))
|
||||
|
|
|
@ -11,7 +11,7 @@ export function Users({ userId, users }: UserProps) {
|
|||
return (
|
||||
<>
|
||||
{Object.values(users)
|
||||
.filter((user) => user.id !== userId)
|
||||
.filter((user) => user && user.id !== userId)
|
||||
.map((user) => (
|
||||
<User key={user.id} user={user} />
|
||||
))}
|
||||
|
|
|
@ -66,23 +66,26 @@ function TLDrawWrapper() {
|
|||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
window.tlstate = tlstate
|
||||
|
||||
tlstate.loadRoom(ROOM_ID)
|
||||
|
||||
setTlstate(tlstate)
|
||||
}, [])
|
||||
|
||||
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')) {
|
||||
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] })
|
||||
}
|
||||
// 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]
|
||||
)
|
||||
|
@ -93,9 +96,11 @@ function TLDrawWrapper() {
|
|||
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
|
||||
|
@ -118,6 +123,7 @@ function TLDrawWrapper() {
|
|||
function handleDocumentUpdates() {
|
||||
if (!doc) return
|
||||
if (!tlstate) return
|
||||
if (!tlstate.state.room) return
|
||||
|
||||
const docObject = doc.toObject()
|
||||
|
||||
|
@ -127,13 +133,13 @@ function TLDrawWrapper() {
|
|||
} 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)
|
||||
// const activeShapes = user.activeShapes
|
||||
// .map((shape) => docObject.document.pages[tlstate.currentPageId].shapes[shape.id])
|
||||
// .filter(Boolean)
|
||||
return {
|
||||
...user,
|
||||
activeShapes: activeShapes,
|
||||
selectedIds: activeShapes.map((shape) => shape.id),
|
||||
// activeShapes: activeShapes,
|
||||
selectedIds: user.selectedIds, // activeShapes.map((shape) => shape.id),
|
||||
}
|
||||
})
|
||||
)
|
||||
|
@ -141,7 +147,8 @@ function TLDrawWrapper() {
|
|||
}
|
||||
|
||||
function handleExit() {
|
||||
room?.broadcastEvent({ name: 'exit', userId: tlstate?.state.room.userId })
|
||||
if (!(tlstate && tlstate.state.room)) return
|
||||
room?.broadcastEvent({ name: 'exit', userId: tlstate.state.room.userId })
|
||||
}
|
||||
|
||||
window.addEventListener('beforeunload', handleExit)
|
||||
|
@ -158,13 +165,26 @@ function TLDrawWrapper() {
|
|||
}
|
||||
}, [doc, docId, tlstate])
|
||||
|
||||
const handleUserChange = React.useCallback(
|
||||
(tlstate: TLDrawState, user: TLDrawUser) => {
|
||||
const room = client.getRoom(ROOM_ID)
|
||||
room?.updatePresence({ id: tlstate.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 onChange={handleChange} onMount={handleMount} showPages={false} />
|
||||
<TLDraw
|
||||
onMount={handleMount}
|
||||
onChange={handleChange}
|
||||
onUserChange={handleUserChange}
|
||||
showPages={false}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as React from 'react'
|
|||
import { IdProvider } from '@radix-ui/react-id'
|
||||
import { Renderer } from '@tldraw/core'
|
||||
import css from '~styles'
|
||||
import { Data, TLDrawDocument, TLDrawStatus } from '~types'
|
||||
import { Data, TLDrawDocument, TLDrawStatus, TLDrawUser } from '~types'
|
||||
import { TLDrawState } from '~state'
|
||||
import {
|
||||
TLDrawContext,
|
||||
|
@ -83,6 +83,8 @@ export interface TLDrawProps {
|
|||
* (optional) A callback to run when the component's state changes.
|
||||
*/
|
||||
onChange?: TLDrawState['_onChange']
|
||||
|
||||
onUserChange?: (state: TLDrawState, user: TLDrawUser) => void
|
||||
}
|
||||
|
||||
export function TLDraw({
|
||||
|
@ -94,16 +96,19 @@ export function TLDraw({
|
|||
showPages = true,
|
||||
onMount,
|
||||
onChange,
|
||||
onUserChange,
|
||||
}: TLDrawProps) {
|
||||
const [sId, setSId] = React.useState(id)
|
||||
|
||||
const [tlstate, setTlstate] = React.useState(() => new TLDrawState(id, onChange, onMount))
|
||||
const [tlstate, setTlstate] = React.useState(
|
||||
() => new TLDrawState(id, onMount, onChange, onUserChange)
|
||||
)
|
||||
const [context, setContext] = React.useState(() => ({ tlstate, useSelector: tlstate.useStore }))
|
||||
|
||||
React.useEffect(() => {
|
||||
if (id === sId) return
|
||||
// If a new id is loaded, replace the entire state
|
||||
const newState = new TLDrawState(id, onChange, onMount)
|
||||
const newState = new TLDrawState(id, onMount, onChange, onUserChange)
|
||||
setTlstate(newState)
|
||||
setContext({ tlstate: newState, useSelector: newState.useStore })
|
||||
setSId(id)
|
||||
|
|
|
@ -48,8 +48,9 @@ import type { BaseTool } from './tool/BaseTool'
|
|||
const uuid = Utils.uniqueId()
|
||||
|
||||
export class TLDrawState extends StateManager<Data> {
|
||||
private _onChange?: (tlstate: TLDrawState, data: Data, reason: string) => void
|
||||
private _onMount?: (tlstate: TLDrawState) => void
|
||||
private _onChange?: (tlstate: TLDrawState, data: Data, reason: string) => void
|
||||
private _onUserChange?: (tlstate: TLDrawState, user: TLDrawUser) => void
|
||||
|
||||
inputs?: Inputs
|
||||
|
||||
|
@ -81,6 +82,9 @@ export class TLDrawState extends StateManager<Data> {
|
|||
height: 480,
|
||||
}
|
||||
|
||||
// The most recent pointer location
|
||||
pointerPoint: number[] = [0, 0]
|
||||
|
||||
private pasteInfo = {
|
||||
center: [0, 0],
|
||||
offset: [0, 0],
|
||||
|
@ -88,11 +92,12 @@ export class TLDrawState extends StateManager<Data> {
|
|||
|
||||
constructor(
|
||||
id?: string,
|
||||
onMount?: (tlstate: TLDrawState) => void,
|
||||
onChange?: (tlstate: TLDrawState, data: Data, reason: string) => void,
|
||||
onMount?: (tlstate: TLDrawState) => void
|
||||
onUserChange?: (tlstate: TLDrawState, user: TLDrawUser) => void
|
||||
) {
|
||||
super(TLDrawState.defaultState, id, TLDrawState.version, (prev, next) => {
|
||||
console.log('Migrating to a new version.')
|
||||
console.warn('Migrating to a new version.')
|
||||
return {
|
||||
...next,
|
||||
document: { ...next.document, ...prev.document },
|
||||
|
@ -101,6 +106,7 @@ export class TLDrawState extends StateManager<Data> {
|
|||
|
||||
this._onChange = onChange
|
||||
this._onMount = onMount
|
||||
this._onUserChange = onUserChange
|
||||
|
||||
this.session = undefined
|
||||
}
|
||||
|
@ -278,31 +284,33 @@ export class TLDrawState extends StateManager<Data> {
|
|||
|
||||
const currentPageId = data.appState.currentPageId
|
||||
|
||||
const currentPage = data.document.pages[currentPageId]
|
||||
|
||||
const currentPageState = data.document.pageStates[currentPageId]
|
||||
|
||||
if (data.room && prev.room && data.room !== prev.room) {
|
||||
if (data.room && data.room !== prev.room) {
|
||||
const room = { ...data.room, users: { ...data.room.users } }
|
||||
|
||||
// Remove any exited users
|
||||
Object.values(prev.room.users).forEach((user) => {
|
||||
if (room.users[user.id] === undefined) {
|
||||
delete room.users[user.id]
|
||||
}
|
||||
})
|
||||
|
||||
// Update the room presence selected ids
|
||||
// Update the room presence active shapes
|
||||
room.users[room.userId] = {
|
||||
...room.users[room.userId],
|
||||
selectedIds: currentPageState.selectedIds,
|
||||
activeShapes: currentPageState.selectedIds.map((id) => currentPage.shapes[id]),
|
||||
if (prev.room) {
|
||||
Object.values(prev.room.users)
|
||||
.filter(Boolean)
|
||||
.forEach((user) => {
|
||||
if (room.users[user.id] === undefined) {
|
||||
delete room.users[user.id]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
data.room = room
|
||||
}
|
||||
|
||||
if (data.room) {
|
||||
data.room.users[data.room.userId] = {
|
||||
...data.room.users[data.room.userId],
|
||||
point: this.pointerPoint,
|
||||
selectedIds: currentPageState.selectedIds,
|
||||
}
|
||||
}
|
||||
|
||||
// Apply selected style change, if any
|
||||
|
||||
const newSelectedStyle = TLDR.getSelectedStyle(data, currentPageId)
|
||||
|
@ -721,6 +729,28 @@ export class TLDrawState extends StateManager<Data> {
|
|||
return this.replaceState(nextState, `${reason}:${document.id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a fresh room into the state.
|
||||
* @param roomId
|
||||
*/
|
||||
loadRoom = (roomId: string) => {
|
||||
this.patchState({
|
||||
room: {
|
||||
id: roomId,
|
||||
userId: uuid,
|
||||
users: {
|
||||
[uuid]: {
|
||||
id: uuid,
|
||||
color: sample(USER_COLORS),
|
||||
point: [100, 100],
|
||||
selectedIds: [],
|
||||
activeShapes: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a new document.
|
||||
* @param document The document to load
|
||||
|
@ -1474,47 +1504,29 @@ export class TLDrawState extends StateManager<Data> {
|
|||
private setSelectedIds = (ids: string[], push = false): this => {
|
||||
const nextIds = push ? [...this.pageState.selectedIds, ...ids] : [...ids]
|
||||
|
||||
if (this.currentUser) {
|
||||
return this.patchState(
|
||||
{
|
||||
appState: {
|
||||
activeTool: 'select',
|
||||
},
|
||||
document: {
|
||||
pageStates: {
|
||||
[this.currentPageId]: {
|
||||
selectedIds: nextIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
room: {
|
||||
users: {
|
||||
[this.currentUser.id]: {
|
||||
...this.currentUser,
|
||||
selectedIds: nextIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
`selected`
|
||||
)
|
||||
} else {
|
||||
return this.patchState(
|
||||
{
|
||||
appState: {
|
||||
activeTool: 'select',
|
||||
},
|
||||
document: {
|
||||
pageStates: {
|
||||
[this.currentPageId]: {
|
||||
selectedIds: nextIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
`selected`
|
||||
)
|
||||
if (this.state.room) {
|
||||
const { users, userId } = this.state.room
|
||||
this._onUserChange?.(this, {
|
||||
...users[userId],
|
||||
selectedIds: nextIds,
|
||||
})
|
||||
}
|
||||
|
||||
return this.patchState(
|
||||
{
|
||||
appState: {
|
||||
activeTool: 'select',
|
||||
},
|
||||
document: {
|
||||
pageStates: {
|
||||
[this.currentPageId]: {
|
||||
selectedIds: nextIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
`selected`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2161,9 +2173,9 @@ export class TLDrawState extends StateManager<Data> {
|
|||
|
||||
this.pan(delta)
|
||||
|
||||
// onPan is called by onPointerMove when spaceKey is pressed,
|
||||
// so we shouldn't call this again.
|
||||
if (!info.spaceKey) {
|
||||
// onPan is called by onPointerMove when spaceKey is pressed,
|
||||
// so we shouldn't call this again.
|
||||
this.onPointerMove(info, e as unknown as React.PointerEvent)
|
||||
}
|
||||
}
|
||||
|
@ -2180,20 +2192,16 @@ export class TLDrawState extends StateManager<Data> {
|
|||
// Several events (e.g. pan) can trigger the same "pointer move" behavior
|
||||
this.currentTool.onPointerMove?.(info, e)
|
||||
|
||||
this.pointerPoint = this.getPagePoint(info.point)
|
||||
|
||||
// Move this to an emitted event
|
||||
if (this.state.room) {
|
||||
const { users, userId } = this.state.room
|
||||
|
||||
if (Object.values(users).length === 1) return
|
||||
|
||||
this.updateUsers(
|
||||
[
|
||||
{
|
||||
...users[userId],
|
||||
point: this.getPagePoint(info.point),
|
||||
},
|
||||
],
|
||||
true
|
||||
)
|
||||
this._onUserChange?.(this, {
|
||||
...users[userId],
|
||||
point: this.getPagePoint(info.point),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue