[feature] Live cursors MVP (#137)
* Adds very basic live cursors * Adds ability to hide pages / menu
This commit is contained in:
parent
99730b4fe2
commit
d6b38ed79e
22 changed files with 455 additions and 154 deletions
|
@ -8,11 +8,12 @@ import {
|
||||||
useCameraCss,
|
useCameraCss,
|
||||||
useKeyEvents,
|
useKeyEvents,
|
||||||
} from '+hooks'
|
} from '+hooks'
|
||||||
import type { TLBinding, TLPage, TLPageState, TLShape } from '+types'
|
import type { TLBinding, TLPage, TLPageState, TLShape, TLUser, TLUsers } from '+types'
|
||||||
import { ErrorFallback } from '+components/error-fallback'
|
import { ErrorFallback } from '+components/error-fallback'
|
||||||
import { ErrorBoundary } from '+components/error-boundary'
|
import { ErrorBoundary } from '+components/error-boundary'
|
||||||
import { Brush } from '+components/brush'
|
import { Brush } from '+components/brush'
|
||||||
import { Page } from '+components/page'
|
import { Page } from '+components/page'
|
||||||
|
import { Users } from '+components/users'
|
||||||
import { useResizeObserver } from '+hooks/useResizeObserver'
|
import { useResizeObserver } from '+hooks/useResizeObserver'
|
||||||
import { inputs } from '+inputs'
|
import { inputs } from '+inputs'
|
||||||
|
|
||||||
|
@ -23,6 +24,8 @@ function resetError() {
|
||||||
interface CanvasProps<T extends TLShape, M extends Record<string, unknown>> {
|
interface CanvasProps<T extends TLShape, M extends Record<string, unknown>> {
|
||||||
page: TLPage<T, TLBinding>
|
page: TLPage<T, TLBinding>
|
||||||
pageState: TLPageState
|
pageState: TLPageState
|
||||||
|
users?: TLUsers
|
||||||
|
userId?: string
|
||||||
hideBounds?: boolean
|
hideBounds?: boolean
|
||||||
hideHandles?: boolean
|
hideHandles?: boolean
|
||||||
hideIndicators?: boolean
|
hideIndicators?: boolean
|
||||||
|
@ -34,6 +37,8 @@ export function Canvas<T extends TLShape, M extends Record<string, unknown>>({
|
||||||
id,
|
id,
|
||||||
page,
|
page,
|
||||||
pageState,
|
pageState,
|
||||||
|
users,
|
||||||
|
userId,
|
||||||
meta,
|
meta,
|
||||||
hideHandles = false,
|
hideHandles = false,
|
||||||
hideBounds = false,
|
hideBounds = false,
|
||||||
|
@ -83,6 +88,7 @@ export function Canvas<T extends TLShape, M extends Record<string, unknown>>({
|
||||||
meta={meta}
|
meta={meta}
|
||||||
/>
|
/>
|
||||||
{pageState.brush && <Brush brush={pageState.brush} />}
|
{pageState.brush && <Brush brush={pageState.brush} />}
|
||||||
|
{users && <Users userId={userId} users={users} />}
|
||||||
</div>
|
</div>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -12,7 +12,7 @@ import type {
|
||||||
import { Canvas } from '../canvas'
|
import { Canvas } from '../canvas'
|
||||||
import { Inputs } from '../../inputs'
|
import { Inputs } from '../../inputs'
|
||||||
import { useTLTheme, TLContext, TLContextType } from '../../hooks'
|
import { useTLTheme, TLContext, TLContextType } from '../../hooks'
|
||||||
import type { TLShapeUtil } from '+index'
|
import type { TLShapeUtil, TLUser, TLUsers } from '+index'
|
||||||
|
|
||||||
export interface RendererProps<T extends TLShape, E extends Element = any, M = any>
|
export interface RendererProps<T extends TLShape, E extends Element = any, M = any>
|
||||||
extends Partial<TLCallbacks<T>> {
|
extends Partial<TLCallbacks<T>> {
|
||||||
|
@ -23,7 +23,6 @@ export interface RendererProps<T extends TLShape, E extends Element = any, M = a
|
||||||
/**
|
/**
|
||||||
* An object containing instances of your shape classes.
|
* An object containing instances of your shape classes.
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
shapeUtils: Record<T['type'], TLShapeUtil<T, E, M>>
|
shapeUtils: Record<T['type'], TLShapeUtil<T, E, M>>
|
||||||
/**
|
/**
|
||||||
* The current page, containing shapes and bindings.
|
* The current page, containing shapes and bindings.
|
||||||
|
@ -34,29 +33,37 @@ export interface RendererProps<T extends TLShape, E extends Element = any, M = a
|
||||||
*/
|
*/
|
||||||
pageState: TLPageState
|
pageState: TLPageState
|
||||||
/**
|
/**
|
||||||
* An object of custom theme colors.
|
* (optional) The current users to render.
|
||||||
|
*/
|
||||||
|
users?: TLUsers
|
||||||
|
/**
|
||||||
|
* (optional) The current user's id, used to identify the user.
|
||||||
|
*/
|
||||||
|
userId?: string
|
||||||
|
/**
|
||||||
|
* (optional) An object of custom theme colors.
|
||||||
*/
|
*/
|
||||||
theme?: Partial<TLTheme>
|
theme?: Partial<TLTheme>
|
||||||
/**
|
/**
|
||||||
* When true, the renderer will not show the bounds for selected objects.
|
* (optional) When true, the renderer will not show the bounds for selected objects.
|
||||||
*/
|
*/
|
||||||
hideBounds?: boolean
|
hideBounds?: boolean
|
||||||
/**
|
/**
|
||||||
* When true, the renderer will not show the handles of shapes with handles.
|
* (optional) When true, the renderer will not show the handles of shapes with handles.
|
||||||
*/
|
*/
|
||||||
hideHandles?: boolean
|
hideHandles?: boolean
|
||||||
/**
|
/**
|
||||||
* When true, the renderer will not show indicators for selected or
|
* (optional) When true, the renderer will not show indicators for selected or
|
||||||
* hovered objects,
|
* hovered objects,
|
||||||
*/
|
*/
|
||||||
hideIndicators?: boolean
|
hideIndicators?: boolean
|
||||||
/**
|
/**
|
||||||
* When true, the renderer will ignore all inputs that were not made
|
* (optional) hen true, the renderer will ignore all inputs that were not made
|
||||||
* by a stylus or pen-type device.
|
* by a stylus or pen-type device.
|
||||||
*/
|
*/
|
||||||
isPenMode?: boolean
|
isPenMode?: boolean
|
||||||
/**
|
/**
|
||||||
* An object of custom options that should be passed to rendered shapes.
|
* (optional) An object of custom options that should be passed to rendered shapes.
|
||||||
*/
|
*/
|
||||||
meta?: M
|
meta?: M
|
||||||
/**
|
/**
|
||||||
|
@ -82,6 +89,8 @@ export function Renderer<T extends TLShape, E extends Element, M extends Record<
|
||||||
shapeUtils,
|
shapeUtils,
|
||||||
page,
|
page,
|
||||||
pageState,
|
pageState,
|
||||||
|
users,
|
||||||
|
userId,
|
||||||
theme,
|
theme,
|
||||||
meta,
|
meta,
|
||||||
hideHandles = false,
|
hideHandles = false,
|
||||||
|
@ -118,6 +127,8 @@ export function Renderer<T extends TLShape, E extends Element, M extends Record<
|
||||||
id={id}
|
id={id}
|
||||||
page={page}
|
page={page}
|
||||||
pageState={pageState}
|
pageState={pageState}
|
||||||
|
users={users}
|
||||||
|
userId={userId}
|
||||||
hideBounds={hideBounds}
|
hideBounds={hideBounds}
|
||||||
hideIndicators={hideIndicators}
|
hideIndicators={hideIndicators}
|
||||||
hideHandles={hideHandles}
|
hideHandles={hideHandles}
|
||||||
|
|
1
packages/core/src/components/user/index.ts
Normal file
1
packages/core/src/components/user/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './user'
|
21
packages/core/src/components/user/user.tsx
Normal file
21
packages/core/src/components/user/user.tsx
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import * as React from 'react'
|
||||||
|
import type { TLUser } from '+types'
|
||||||
|
|
||||||
|
interface UserProps {
|
||||||
|
user: TLUser
|
||||||
|
}
|
||||||
|
|
||||||
|
export function User({ user }: UserProps) {
|
||||||
|
const rUser = React.useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={rUser}
|
||||||
|
className="tl-absolute tl-user"
|
||||||
|
style={{
|
||||||
|
backgroundColor: user.color,
|
||||||
|
transform: `translate(${user.point[0]}px, ${user.point[1]}px)`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
1
packages/core/src/components/users/index.ts
Normal file
1
packages/core/src/components/users/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './users'
|
20
packages/core/src/components/users/users.tsx
Normal file
20
packages/core/src/components/users/users.tsx
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import * as React from 'react'
|
||||||
|
import { User } from '+components/user/user'
|
||||||
|
import type { TLUsers } from '+types'
|
||||||
|
|
||||||
|
export interface UserProps {
|
||||||
|
userId?: string
|
||||||
|
users: TLUsers
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Users({ userId, users }: UserProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{Object.values(users)
|
||||||
|
.filter((user) => user.id !== userId)
|
||||||
|
.map((user) => (
|
||||||
|
<User key={user.id} user={user} />
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -233,6 +233,15 @@ const tlcss = css`
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tl-user {
|
||||||
|
left: -4px;
|
||||||
|
top: -4px;
|
||||||
|
height: 8px;
|
||||||
|
width: 8px;
|
||||||
|
border-radius: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.tl-selected {
|
.tl-selected {
|
||||||
fill: transparent;
|
fill: transparent;
|
||||||
stroke: var(--tl-selectStroke);
|
stroke: var(--tl-selectStroke);
|
||||||
|
|
|
@ -16,6 +16,8 @@ export interface TLPage<T extends TLShape, B extends TLBinding> {
|
||||||
bindings: Record<string, B>
|
bindings: Record<string, B>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TLUsers<U extends TLUser = TLUser> = Record<string, U>
|
||||||
|
|
||||||
export interface TLPageState {
|
export interface TLPageState {
|
||||||
id: string
|
id: string
|
||||||
selectedIds: string[]
|
selectedIds: string[]
|
||||||
|
@ -32,6 +34,12 @@ export interface TLPageState {
|
||||||
currentParentId?: string | null
|
currentParentId?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TLUser {
|
||||||
|
id: string
|
||||||
|
color: string
|
||||||
|
point: number[]
|
||||||
|
}
|
||||||
|
|
||||||
export interface TLHandle {
|
export interface TLHandle {
|
||||||
id: string
|
id: string
|
||||||
index: number
|
index: number
|
||||||
|
|
1
packages/dev/.env.local
Normal file
1
packages/dev/.env.local
Normal file
|
@ -0,0 +1 @@
|
||||||
|
LIVEBLOCKS_PUBLIC_API_KEY=pk_live_1LJGGaqBSNLjLT-4Jalkl-U9
|
|
@ -23,6 +23,7 @@ esbuild
|
||||||
incremental: isDevServer,
|
incremental: isDevServer,
|
||||||
target: ['chrome58', 'firefox57', 'safari11', 'edge18'],
|
target: ['chrome58', 'firefox57', 'safari11', 'edge18'],
|
||||||
define: {
|
define: {
|
||||||
|
'process.env.LIVEBLOCKS_PUBLIC_API_KEY': process.env.LIVEBLOCKS_PUBLIC_API_KEY,
|
||||||
'process.env.NODE_ENV': isDevServer ? '"development"' : '"production"',
|
'process.env.NODE_ENV': isDevServer ? '"development"' : '"production"',
|
||||||
},
|
},
|
||||||
watch: isDevServer && {
|
watch: isDevServer && {
|
||||||
|
|
|
@ -4,8 +4,8 @@ import Basic from './basic'
|
||||||
import Controlled from './controlled'
|
import Controlled from './controlled'
|
||||||
import Imperative from './imperative'
|
import Imperative from './imperative'
|
||||||
import Embedded from './embedded'
|
import Embedded from './embedded'
|
||||||
import NoSizeEmbedded from '+no-size-embedded'
|
import NoSizeEmbedded from './no-size-embedded'
|
||||||
import LiveBlocks from './liveblocks'
|
import { Multiplayer } from './multiplayer'
|
||||||
import ChangingId from './changing-id'
|
import ChangingId from './changing-id'
|
||||||
import Core from './core'
|
import Core from './core'
|
||||||
import './styles.css'
|
import './styles.css'
|
||||||
|
@ -35,8 +35,8 @@ export default function App(): JSX.Element {
|
||||||
<Route path="/no-size-embedded">
|
<Route path="/no-size-embedded">
|
||||||
<NoSizeEmbedded />
|
<NoSizeEmbedded />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/liveblocks">
|
<Route path="/multiplayer">
|
||||||
<LiveBlocks />
|
<Multiplayer />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/">
|
<Route path="/">
|
||||||
<ul>
|
<ul>
|
||||||
|
@ -62,7 +62,7 @@ export default function App(): JSX.Element {
|
||||||
<Link to="/no-size-embedded">embedded (no size)</Link>
|
<Link to="/no-size-embedded">embedded (no size)</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Link to="/liveblocks">liveblocks</Link>
|
<Link to="/multiplayer">multiplayer</Link>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
|
@ -1,84 +0,0 @@
|
||||||
import * as React from 'react'
|
|
||||||
import {
|
|
||||||
TLDraw,
|
|
||||||
ColorStyle,
|
|
||||||
DashStyle,
|
|
||||||
TLDrawState,
|
|
||||||
SizeStyle,
|
|
||||||
TLDrawDocument,
|
|
||||||
TLDrawShapeType,
|
|
||||||
} from '@tldraw/tldraw'
|
|
||||||
import { createClient } from '@liveblocks/client'
|
|
||||||
import { LiveblocksProvider, RoomProvider, useObject } from '@liveblocks/react'
|
|
||||||
|
|
||||||
const publicAPIKey = process.env.NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_API_KEY as string
|
|
||||||
|
|
||||||
const client = createClient({
|
|
||||||
publicApiKey: publicAPIKey,
|
|
||||||
})
|
|
||||||
|
|
||||||
export default function LiveBlocks() {
|
|
||||||
return (
|
|
||||||
<LiveblocksProvider client={client}>
|
|
||||||
<RoomProvider id="room1">
|
|
||||||
<TLDrawWrapper />
|
|
||||||
</RoomProvider>
|
|
||||||
</LiveblocksProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TLDrawWrapper() {
|
|
||||||
const doc = useObject<TLDrawDocument>('doc', {
|
|
||||||
id: 'doc',
|
|
||||||
pages: {
|
|
||||||
page1: {
|
|
||||||
id: 'page1',
|
|
||||||
shapes: {
|
|
||||||
rect1: {
|
|
||||||
id: 'rect1',
|
|
||||||
type: TLDrawShapeType.Rectangle,
|
|
||||||
parentId: 'page1',
|
|
||||||
name: 'Rectangle',
|
|
||||||
childIndex: 1,
|
|
||||||
point: [100, 100],
|
|
||||||
size: [100, 100],
|
|
||||||
style: {
|
|
||||||
dash: DashStyle.Draw,
|
|
||||||
size: SizeStyle.Medium,
|
|
||||||
color: ColorStyle.Blue,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
bindings: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
pageStates: {
|
|
||||||
page1: {
|
|
||||||
id: 'page1',
|
|
||||||
selectedIds: ['rect1'],
|
|
||||||
camera: {
|
|
||||||
point: [0, 0],
|
|
||||||
zoom: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleChange = React.useCallback(
|
|
||||||
(state: TLDrawState, patch, reason) => {
|
|
||||||
if (!doc) return
|
|
||||||
if (reason.startsWith('command')) {
|
|
||||||
doc.update(patch.document)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[doc]
|
|
||||||
)
|
|
||||||
|
|
||||||
if (doc === null) return <div>loading...</div>
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="tldraw">
|
|
||||||
<TLDraw document={doc.toObject()} onChange={handleChange} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
3
packages/dev/src/multiplayer/cursors.tsx
Normal file
3
packages/dev/src/multiplayer/cursors.tsx
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export function Cursors() {
|
||||||
|
return <div>hi</div>
|
||||||
|
}
|
1
packages/dev/src/multiplayer/index.ts
Normal file
1
packages/dev/src/multiplayer/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './multiplayer'
|
164
packages/dev/src/multiplayer/multiplayer.tsx
Normal file
164
packages/dev/src/multiplayer/multiplayer.tsx
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
|
import * as React from 'react'
|
||||||
|
import { TLDraw, TLDrawState, TLDrawDocument, TLDrawUser, Data } from '@tldraw/tldraw'
|
||||||
|
import { createClient, Presence } from '@liveblocks/client'
|
||||||
|
import {
|
||||||
|
LiveblocksProvider,
|
||||||
|
RoomProvider,
|
||||||
|
useErrorListener,
|
||||||
|
useObject,
|
||||||
|
useSelf,
|
||||||
|
useOthers,
|
||||||
|
useMyPresence,
|
||||||
|
} from '@liveblocks/react'
|
||||||
|
import { Utils } from '@tldraw/core'
|
||||||
|
|
||||||
|
interface TLDrawUserPresence extends Presence {
|
||||||
|
user: TLDrawUser
|
||||||
|
}
|
||||||
|
|
||||||
|
const publicAPIKey = 'pk_live_1LJGGaqBSNLjLT-4Jalkl-U9'
|
||||||
|
|
||||||
|
const client = createClient({
|
||||||
|
publicApiKey: publicAPIKey,
|
||||||
|
})
|
||||||
|
|
||||||
|
const ROOM_ID = 'mp-test-1'
|
||||||
|
|
||||||
|
export function Multiplayer() {
|
||||||
|
return (
|
||||||
|
<LiveblocksProvider client={client}>
|
||||||
|
<RoomProvider id={ROOM_ID}>
|
||||||
|
<TLDrawWrapper />
|
||||||
|
</RoomProvider>
|
||||||
|
</LiveblocksProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TLDrawWrapper() {
|
||||||
|
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',
|
||||||
|
pages: {
|
||||||
|
page: {
|
||||||
|
id: 'page',
|
||||||
|
shapes: {},
|
||||||
|
bindings: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pageStates: {
|
||||||
|
page: {
|
||||||
|
id: 'page',
|
||||||
|
selectedIds: [],
|
||||||
|
camera: {
|
||||||
|
point: [0, 0],
|
||||||
|
zoom: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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
|
||||||
|
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')) {
|
||||||
|
doc?.update({ uuid: docId, document: state.document })
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the client updates its presence, update the room
|
||||||
|
if (reason === 'patch:room:self:update' && state.room) {
|
||||||
|
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(ROOM_ID)
|
||||||
|
|
||||||
|
if (!room) return
|
||||||
|
if (!doc) return
|
||||||
|
if (!tlstate) 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
|
||||||
|
|
||||||
|
const docObject = doc.toObject()
|
||||||
|
|
||||||
|
// Only merge the change if it caused by someone else
|
||||||
|
if (docObject.uuid !== docId) {
|
||||||
|
tlstate.mergeDocument(docObject.document)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleExit() {
|
||||||
|
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
|
||||||
|
tlstate.loadDocument(doc.toObject().document)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('beforeunload', handleExit)
|
||||||
|
doc.unsubscribe(handleDocumentUpdates)
|
||||||
|
}
|
||||||
|
}, [doc, docId, tlstate])
|
||||||
|
|
||||||
|
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} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -31,6 +31,8 @@ const isSelectedShapeWithHandlesSelector = (s: Data) => {
|
||||||
|
|
||||||
const pageSelector = (s: Data) => s.document.pages[s.appState.currentPageId]
|
const pageSelector = (s: Data) => s.document.pages[s.appState.currentPageId]
|
||||||
|
|
||||||
|
const usersSelector = (s: Data) => s.room?.users
|
||||||
|
|
||||||
const pageStateSelector = (s: Data) => s.document.pageStates[s.appState.currentPageId]
|
const pageStateSelector = (s: Data) => s.document.pageStates[s.appState.currentPageId]
|
||||||
|
|
||||||
const isDarkModeSelector = (s: Data) => s.settings.isDarkMode
|
const isDarkModeSelector = (s: Data) => s.settings.isDarkMode
|
||||||
|
@ -42,10 +44,12 @@ export interface TLDrawProps {
|
||||||
* (optional) If provided, the component will load / persist state under this key.
|
* (optional) If provided, the component will load / persist state under this key.
|
||||||
*/
|
*/
|
||||||
id?: string
|
id?: string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* (optional) The document to load or update from.
|
* (optional) The document to load or update from.
|
||||||
*/
|
*/
|
||||||
document?: TLDrawDocument
|
document?: TLDrawDocument
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* (optional) The current page id.
|
* (optional) The current page id.
|
||||||
*/
|
*/
|
||||||
|
@ -55,10 +59,22 @@ export interface TLDrawProps {
|
||||||
* (optional) Whether the editor should immediately receive focus. Defaults to true.
|
* (optional) Whether the editor should immediately receive focus. Defaults to true.
|
||||||
*/
|
*/
|
||||||
autofocus?: boolean
|
autofocus?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (optional) Whether to show the menu UI.
|
||||||
|
*/
|
||||||
|
showMenu?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (optional) Whether to show the pages UI.
|
||||||
|
*/
|
||||||
|
showPages?: boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* (optional) A callback to run when the component mounts.
|
* (optional) A callback to run when the component mounts.
|
||||||
*/
|
*/
|
||||||
onMount?: (state: TLDrawState) => void
|
onMount?: (state: TLDrawState) => void
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* (optional) A callback to run when the component's state changes.
|
* (optional) A callback to run when the component's state changes.
|
||||||
*/
|
*/
|
||||||
|
@ -70,6 +86,8 @@ export function TLDraw({
|
||||||
document,
|
document,
|
||||||
currentPageId,
|
currentPageId,
|
||||||
autofocus = true,
|
autofocus = true,
|
||||||
|
showMenu = true,
|
||||||
|
showPages = true,
|
||||||
onMount,
|
onMount,
|
||||||
onChange,
|
onChange,
|
||||||
}: TLDrawProps) {
|
}: TLDrawProps) {
|
||||||
|
@ -98,6 +116,8 @@ export function TLDraw({
|
||||||
currentPageId={currentPageId}
|
currentPageId={currentPageId}
|
||||||
document={document}
|
document={document}
|
||||||
autofocus={autofocus}
|
autofocus={autofocus}
|
||||||
|
showPages={showPages}
|
||||||
|
showMenu={showMenu}
|
||||||
/>
|
/>
|
||||||
</IdProvider>
|
</IdProvider>
|
||||||
</TLDrawContext.Provider>
|
</TLDrawContext.Provider>
|
||||||
|
@ -108,11 +128,15 @@ function InnerTldraw({
|
||||||
id,
|
id,
|
||||||
currentPageId,
|
currentPageId,
|
||||||
autofocus,
|
autofocus,
|
||||||
|
showPages,
|
||||||
|
showMenu,
|
||||||
document,
|
document,
|
||||||
}: {
|
}: {
|
||||||
id?: string
|
id?: string
|
||||||
currentPageId?: string
|
currentPageId?: string
|
||||||
autofocus?: boolean
|
autofocus: boolean
|
||||||
|
showPages: boolean
|
||||||
|
showMenu: boolean
|
||||||
document?: TLDrawDocument
|
document?: TLDrawDocument
|
||||||
}) {
|
}) {
|
||||||
const { tlstate, useSelector } = useTLDrawContext()
|
const { tlstate, useSelector } = useTLDrawContext()
|
||||||
|
@ -125,6 +149,8 @@ function InnerTldraw({
|
||||||
|
|
||||||
const pageState = useSelector(pageStateSelector)
|
const pageState = useSelector(pageStateSelector)
|
||||||
|
|
||||||
|
const users = useSelector(usersSelector)
|
||||||
|
|
||||||
const isDarkMode = useSelector(isDarkModeSelector)
|
const isDarkMode = useSelector(isDarkModeSelector)
|
||||||
|
|
||||||
const isFocusMode = useSelector(isFocusModeSelector)
|
const isFocusMode = useSelector(isFocusModeSelector)
|
||||||
|
@ -188,6 +214,8 @@ function InnerTldraw({
|
||||||
id={id}
|
id={id}
|
||||||
page={page}
|
page={page}
|
||||||
pageState={pageState}
|
pageState={pageState}
|
||||||
|
users={users}
|
||||||
|
userId={tlstate.state.room.userId}
|
||||||
shapeUtils={tldrawShapeUtils}
|
shapeUtils={tldrawShapeUtils}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
meta={meta}
|
meta={meta}
|
||||||
|
@ -253,8 +281,8 @@ function InnerTldraw({
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className={menuButtons()}>
|
<div className={menuButtons()}>
|
||||||
<Menu />
|
{showMenu && <Menu />}
|
||||||
<PagePanel />
|
{showPages && <PagePanel />}
|
||||||
</div>
|
</div>
|
||||||
<div className={spacer()} />
|
<div className={spacer()} />
|
||||||
<StylePanel />
|
<StylePanel />
|
||||||
|
|
|
@ -37,6 +37,7 @@ import {
|
||||||
TLDrawBinding,
|
TLDrawBinding,
|
||||||
GroupShape,
|
GroupShape,
|
||||||
TLDrawCommand,
|
TLDrawCommand,
|
||||||
|
TLDrawUser,
|
||||||
} from '~types'
|
} from '~types'
|
||||||
import { TLDR } from './tldr'
|
import { TLDR } from './tldr'
|
||||||
import { defaultStyle } from '~shape'
|
import { defaultStyle } from '~shape'
|
||||||
|
@ -66,6 +67,8 @@ const defaultDocument: TLDrawDocument = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const uuid = Utils.uniqueId()
|
||||||
|
|
||||||
const defaultState: Data = {
|
const defaultState: Data = {
|
||||||
settings: {
|
settings: {
|
||||||
isPenMode: false,
|
isPenMode: false,
|
||||||
|
@ -94,6 +97,17 @@ const defaultState: Data = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
document: defaultDocument,
|
document: defaultDocument,
|
||||||
|
room: {
|
||||||
|
id: 'local',
|
||||||
|
userId: uuid,
|
||||||
|
users: {
|
||||||
|
[uuid]: {
|
||||||
|
id: uuid,
|
||||||
|
color: 'dodgerBlue',
|
||||||
|
point: [100, 100],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TLDrawState extends StateManager<Data> {
|
export class TLDrawState extends StateManager<Data> {
|
||||||
|
@ -144,15 +158,10 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
onChange?: (tlstate: TLDrawState, data: Data, reason: string) => void,
|
onChange?: (tlstate: TLDrawState, data: Data, reason: string) => void,
|
||||||
onMount?: (tlstate: TLDrawState) => void
|
onMount?: (tlstate: TLDrawState) => void
|
||||||
) {
|
) {
|
||||||
super(defaultState, id, 2, (prev, next, prevVersion) => {
|
super(defaultState, id, 2.3, (prev, next) => ({
|
||||||
const state = { ...prev }
|
...next,
|
||||||
if (prevVersion === 1)
|
...prev,
|
||||||
state.settings = {
|
}))
|
||||||
...state.settings,
|
|
||||||
isZoomSnap: next.settings.isZoomSnap,
|
|
||||||
}
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
|
|
||||||
this._onChange = onChange
|
this._onChange = onChange
|
||||||
this._onMount = onMount
|
this._onMount = onMount
|
||||||
|
@ -327,6 +336,15 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove any exited users
|
||||||
|
if (data.room !== prev.room) {
|
||||||
|
Object.values(prev.room.users).forEach((user) => {
|
||||||
|
if (data.room.users[user.id] === undefined) {
|
||||||
|
delete data.room.users[user.id]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const currentPageId = data.appState.currentPageId
|
const currentPageId = data.appState.currentPageId
|
||||||
|
|
||||||
// Apply selected style change, if any
|
// Apply selected style change, if any
|
||||||
|
@ -340,12 +358,6 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that the correct page id is active (delete me?)
|
|
||||||
|
|
||||||
if (data.document.pageStates[currentPageId].id !== currentPageId) {
|
|
||||||
throw Error('Current page id is not the current page state!')
|
|
||||||
}
|
|
||||||
|
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -543,6 +555,31 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param document
|
||||||
|
*/
|
||||||
|
updateUsers = (users: TLDrawUser[], isOwnUpdate = false) => {
|
||||||
|
this.patchState(
|
||||||
|
{
|
||||||
|
room: {
|
||||||
|
users: Object.fromEntries(users.map((user) => [user.id, user])),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
isOwnUpdate ? 'room:self:update' : 'room:user:update'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
removeUser = (userId: string) => {
|
||||||
|
this.patchState({
|
||||||
|
room: {
|
||||||
|
users: {
|
||||||
|
[userId]: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Merge a new document patch into the current document.
|
* Merge a new document patch into the current document.
|
||||||
* @param document
|
* @param document
|
||||||
|
@ -619,15 +656,6 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
currentPageStates[this.currentPageId].selectedIds = [editingId]
|
currentPageStates[this.currentPageId].selectedIds = [editingId]
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('next state', {
|
|
||||||
...this.state,
|
|
||||||
appState: nextAppState,
|
|
||||||
document: {
|
|
||||||
...document,
|
|
||||||
pageStates: currentPageStates,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return this.replaceState(
|
return this.replaceState(
|
||||||
{
|
{
|
||||||
...this.state,
|
...this.state,
|
||||||
|
@ -2567,6 +2595,20 @@ export class TLDrawState extends StateManager<Data> {
|
||||||
/* ----------------- Pointer Events ----------------- */
|
/* ----------------- Pointer Events ----------------- */
|
||||||
|
|
||||||
onPointerMove: TLPointerEventHandler = (info) => {
|
onPointerMove: TLPointerEventHandler = (info) => {
|
||||||
|
if (this.state.room) {
|
||||||
|
const { users, userId } = this.state.room
|
||||||
|
|
||||||
|
this.updateUsers(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
...users[userId],
|
||||||
|
point: this.getPagePoint(info.point),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Several events (e.g. pan) can trigger the same "pointer move" behavior
|
// Several events (e.g. pan) can trigger the same "pointer move" behavior
|
||||||
this.updateOnPointerMove(info)
|
this.updateOnPointerMove(info)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
/* eslint-disable @typescript-eslint/ban-types */
|
/* eslint-disable @typescript-eslint/ban-types */
|
||||||
import type { TLBinding, TLShapeProps } from '@tldraw/core'
|
import type { TLBinding, TLShapeProps } from '@tldraw/core'
|
||||||
import type { TLShape, TLShapeUtil, TLHandle } from '@tldraw/core'
|
import type { TLShape, TLShapeUtil, TLHandle } from '@tldraw/core'
|
||||||
import type { TLPage, TLPageState } from '@tldraw/core'
|
import type { TLPage, TLUser, TLPageState } from '@tldraw/core'
|
||||||
import type { StoreApi } from 'zustand'
|
import type { StoreApi } from 'zustand'
|
||||||
import type { Command, Patch } from 'rko'
|
import type { Command, Patch } from 'rko'
|
||||||
|
|
||||||
|
@ -29,10 +29,21 @@ export interface TLDrawSettings {
|
||||||
isFocusMode: boolean
|
isFocusMode: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum TLUserStatus {
|
||||||
|
Idle = 'idle',
|
||||||
|
Connecting = 'connecting',
|
||||||
|
Connected = 'connected',
|
||||||
|
Disconnected = 'disconnected',
|
||||||
|
}
|
||||||
|
|
||||||
export interface TLDrawMeta {
|
export interface TLDrawMeta {
|
||||||
isDarkMode: boolean
|
isDarkMode: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TLDrawUser extends TLUser {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
export type TLDrawShapeProps<T extends TLDrawShape, E extends Element> = TLShapeProps<
|
export type TLDrawShapeProps<T extends TLDrawShape, E extends Element> = TLShapeProps<
|
||||||
T,
|
T,
|
||||||
E,
|
E,
|
||||||
|
@ -40,7 +51,6 @@ export type TLDrawShapeProps<T extends TLDrawShape, E extends Element> = TLShape
|
||||||
>
|
>
|
||||||
|
|
||||||
export interface Data {
|
export interface Data {
|
||||||
document: TLDrawDocument
|
|
||||||
settings: TLDrawSettings
|
settings: TLDrawSettings
|
||||||
appState: {
|
appState: {
|
||||||
selectedStyle: ShapeStyles
|
selectedStyle: ShapeStyles
|
||||||
|
@ -55,6 +65,12 @@ export interface Data {
|
||||||
isEmptyCanvas: boolean
|
isEmptyCanvas: boolean
|
||||||
status: { current: TLDrawStatus; previous: TLDrawStatus }
|
status: { current: TLDrawStatus; previous: TLDrawStatus }
|
||||||
}
|
}
|
||||||
|
document: TLDrawDocument
|
||||||
|
room: {
|
||||||
|
id: string
|
||||||
|
userId: string
|
||||||
|
users: Record<string, TLDrawUser>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TLDrawPatch = Patch<Data>
|
export type TLDrawPatch = Patch<Data>
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
import { TLDraw, TLDrawState, Data, TLDrawDocument } from '@tldraw/tldraw'
|
import { TLDraw, TLDrawState, Data, TLDrawDocument, TLDrawUser } from '@tldraw/tldraw'
|
||||||
import * as gtag from '-utils/gtag'
|
import * as gtag from '-utils/gtag'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { createClient } from '@liveblocks/client'
|
import { createClient, Presence } from '@liveblocks/client'
|
||||||
import { LiveblocksProvider, RoomProvider, useObject, useErrorListener } from '@liveblocks/react'
|
import { LiveblocksProvider, RoomProvider, useObject, useErrorListener } from '@liveblocks/react'
|
||||||
import { Utils } from '@tldraw/core'
|
import { Utils } from '@tldraw/core'
|
||||||
|
|
||||||
|
interface TLDrawPresence extends Presence {
|
||||||
|
user: TLDrawUser
|
||||||
|
}
|
||||||
|
|
||||||
const client = createClient({
|
const client = createClient({
|
||||||
publicApiKey: 'pk_live_1LJGGaqBSNLjLT-4Jalkl-U9',
|
publicApiKey: process.env.NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_API_KEY,
|
||||||
})
|
})
|
||||||
|
|
||||||
export default function MultiplayerEditor({ id }: { id: string }) {
|
export default function MultiplayerEditor({ id }: { id: string }) {
|
||||||
|
@ -20,19 +24,18 @@ export default function MultiplayerEditor({ id }: { id: string }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function Editor({ id }: { id: string }) {
|
function Editor({ id }: { id: string }) {
|
||||||
const [uuid] = React.useState(() => Utils.uniqueId())
|
const [docId] = React.useState(() => Utils.uniqueId())
|
||||||
const [error, setError] = React.useState<Error>(null)
|
|
||||||
|
const [error, setError] = React.useState<Error>()
|
||||||
|
|
||||||
const [tlstate, setTlstate] = React.useState<TLDrawState>()
|
const [tlstate, setTlstate] = React.useState<TLDrawState>()
|
||||||
|
|
||||||
useErrorListener((err) => {
|
useErrorListener((err) => setError(err))
|
||||||
console.log(err)
|
|
||||||
setError(err)
|
|
||||||
})
|
|
||||||
|
|
||||||
const doc = useObject<{ uuid: string; document: TLDrawDocument }>('doc', {
|
const doc = useObject<{ uuid: string; document: TLDrawDocument }>('doc', {
|
||||||
uuid,
|
uuid: docId,
|
||||||
document: {
|
document: {
|
||||||
id,
|
id: 'test-room',
|
||||||
pages: {
|
pages: {
|
||||||
page: {
|
page: {
|
||||||
id: 'page',
|
id: 'page',
|
||||||
|
@ -61,9 +64,9 @@ function Editor({ id }: { id: string }) {
|
||||||
setTlstate(tlstate)
|
setTlstate(tlstate)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Send events to gtag as actions.
|
|
||||||
const handleChange = React.useCallback(
|
const handleChange = React.useCallback(
|
||||||
(_tlstate: TLDrawState, state: Data, reason: string) => {
|
(_tlstate: TLDrawState, state: Data, reason: string) => {
|
||||||
|
// If the client updates its document, update the room's document / gtag
|
||||||
if (reason.startsWith('command')) {
|
if (reason.startsWith('command')) {
|
||||||
gtag.event({
|
gtag.event({
|
||||||
action: reason,
|
action: reason,
|
||||||
|
@ -72,37 +75,86 @@ function Editor({ id }: { id: string }) {
|
||||||
value: 0,
|
value: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (doc) {
|
doc?.update({ uuid: docId, document: state.document })
|
||||||
doc.update({ uuid, document: state.document })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When the client updates its presence, update the room
|
||||||
|
if (reason === 'patch:room:self:update' && state.room) {
|
||||||
|
const room = client.getRoom(id)
|
||||||
|
if (!room) return
|
||||||
|
const { userId, users } = state.room
|
||||||
|
room.updatePresence({ id: userId, user: users[userId] })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[uuid, doc]
|
[docId, doc, id]
|
||||||
)
|
)
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
const room = client.getRoom(id)
|
||||||
|
|
||||||
|
if (!room) return
|
||||||
|
if (!doc) return
|
||||||
|
if (!tlstate) 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<TLDrawPresence>('others', (others) => {
|
||||||
|
tlstate.updateUsers(
|
||||||
|
others
|
||||||
|
.toArray()
|
||||||
|
.filter((other) => other.presence)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
.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 (!doc) return
|
||||||
if (!tlstate) return
|
if (!tlstate) return
|
||||||
|
|
||||||
function updateState() {
|
|
||||||
const docObject = doc.toObject()
|
const docObject = doc.toObject()
|
||||||
if (docObject.uuid === uuid) return
|
|
||||||
|
// Only merge the change if it caused by someone else
|
||||||
|
if (docObject.uuid !== docId) {
|
||||||
tlstate.mergeDocument(docObject.document)
|
tlstate.mergeDocument(docObject.document)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updateState()
|
function handleExit() {
|
||||||
doc.subscribe(updateState)
|
room?.broadcastEvent({ name: 'exit', userId: tlstate?.state.room.userId })
|
||||||
|
}
|
||||||
|
|
||||||
return () => doc.unsubscribe(updateState)
|
window.addEventListener('beforeunload', handleExit)
|
||||||
}, [doc, uuid, tlstate])
|
|
||||||
|
// When the shared document changes, update the state
|
||||||
|
doc.subscribe(handleDocumentUpdates)
|
||||||
|
|
||||||
|
// Load the shared document
|
||||||
|
tlstate.loadDocument(doc.toObject().document)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('beforeunload', handleExit)
|
||||||
|
doc.unsubscribe(handleDocumentUpdates)
|
||||||
|
}
|
||||||
|
}, [doc, docId, tlstate, id])
|
||||||
|
|
||||||
if (error) return <div>Error: {error.message}</div>
|
if (error) return <div>Error: {error.message}</div>
|
||||||
|
|
||||||
if (doc === null) return <div>loading...</div>
|
if (doc === null) return <div>Loading...</div>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tldraw">
|
<div className="tldraw">
|
||||||
<TLDraw onMount={handleMount} onChange={handleChange} autofocus />
|
<TLDraw onMount={handleMount} onChange={handleChange} showPages={false} autofocus />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue