[feature] Live cursors MVP (#137)

* Adds very basic live cursors

* Adds ability to hide pages / menu
This commit is contained in:
Steve Ruiz 2021-10-09 14:57:44 +01:00 committed by GitHub
parent 99730b4fe2
commit d6b38ed79e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 455 additions and 154 deletions

View file

@ -8,11 +8,12 @@ import {
useCameraCss,
useKeyEvents,
} 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 { ErrorBoundary } from '+components/error-boundary'
import { Brush } from '+components/brush'
import { Page } from '+components/page'
import { Users } from '+components/users'
import { useResizeObserver } from '+hooks/useResizeObserver'
import { inputs } from '+inputs'
@ -23,6 +24,8 @@ function resetError() {
interface CanvasProps<T extends TLShape, M extends Record<string, unknown>> {
page: TLPage<T, TLBinding>
pageState: TLPageState
users?: TLUsers
userId?: string
hideBounds?: boolean
hideHandles?: boolean
hideIndicators?: boolean
@ -34,6 +37,8 @@ export function Canvas<T extends TLShape, M extends Record<string, unknown>>({
id,
page,
pageState,
users,
userId,
meta,
hideHandles = false,
hideBounds = false,
@ -83,6 +88,7 @@ export function Canvas<T extends TLShape, M extends Record<string, unknown>>({
meta={meta}
/>
{pageState.brush && <Brush brush={pageState.brush} />}
{users && <Users userId={userId} users={users} />}
</div>
</ErrorBoundary>
</div>

View file

@ -12,7 +12,7 @@ import type {
import { Canvas } from '../canvas'
import { Inputs } from '../../inputs'
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>
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.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
shapeUtils: Record<T['type'], TLShapeUtil<T, E, M>>
/**
* 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
/**
* 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>
/**
* 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
/**
* 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
/**
* 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,
*/
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.
*/
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
/**
@ -82,6 +89,8 @@ export function Renderer<T extends TLShape, E extends Element, M extends Record<
shapeUtils,
page,
pageState,
users,
userId,
theme,
meta,
hideHandles = false,
@ -118,6 +127,8 @@ export function Renderer<T extends TLShape, E extends Element, M extends Record<
id={id}
page={page}
pageState={pageState}
users={users}
userId={userId}
hideBounds={hideBounds}
hideIndicators={hideIndicators}
hideHandles={hideHandles}

View file

@ -0,0 +1 @@
export * from './user'

View 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)`,
}}
/>
)
}

View file

@ -0,0 +1 @@
export * from './users'

View 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} />
))}
</>
)
}

View file

@ -233,6 +233,15 @@ const tlcss = css`
pointer-events: none;
}
.tl-user {
left: -4px;
top: -4px;
height: 8px;
width: 8px;
border-radius: 100%;
pointer-events: none;
}
.tl-selected {
fill: transparent;
stroke: var(--tl-selectStroke);

View file

@ -16,6 +16,8 @@ export interface TLPage<T extends TLShape, B extends TLBinding> {
bindings: Record<string, B>
}
export type TLUsers<U extends TLUser = TLUser> = Record<string, U>
export interface TLPageState {
id: string
selectedIds: string[]
@ -32,6 +34,12 @@ export interface TLPageState {
currentParentId?: string | null
}
export interface TLUser {
id: string
color: string
point: number[]
}
export interface TLHandle {
id: string
index: number

1
packages/dev/.env.local Normal file
View file

@ -0,0 +1 @@
LIVEBLOCKS_PUBLIC_API_KEY=pk_live_1LJGGaqBSNLjLT-4Jalkl-U9

View file

@ -23,6 +23,7 @@ esbuild
incremental: isDevServer,
target: ['chrome58', 'firefox57', 'safari11', 'edge18'],
define: {
'process.env.LIVEBLOCKS_PUBLIC_API_KEY': process.env.LIVEBLOCKS_PUBLIC_API_KEY,
'process.env.NODE_ENV': isDevServer ? '"development"' : '"production"',
},
watch: isDevServer && {

View file

@ -4,8 +4,8 @@ import Basic from './basic'
import Controlled from './controlled'
import Imperative from './imperative'
import Embedded from './embedded'
import NoSizeEmbedded from '+no-size-embedded'
import LiveBlocks from './liveblocks'
import NoSizeEmbedded from './no-size-embedded'
import { Multiplayer } from './multiplayer'
import ChangingId from './changing-id'
import Core from './core'
import './styles.css'
@ -35,8 +35,8 @@ export default function App(): JSX.Element {
<Route path="/no-size-embedded">
<NoSizeEmbedded />
</Route>
<Route path="/liveblocks">
<LiveBlocks />
<Route path="/multiplayer">
<Multiplayer />
</Route>
<Route path="/">
<ul>
@ -62,7 +62,7 @@ export default function App(): JSX.Element {
<Link to="/no-size-embedded">embedded (no size)</Link>
</li>
<li>
<Link to="/liveblocks">liveblocks</Link>
<Link to="/multiplayer">multiplayer</Link>
</li>
</ul>
</Route>

View file

@ -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>
)
}

View file

@ -0,0 +1,3 @@
export function Cursors() {
return <div>hi</div>
}

View file

@ -0,0 +1 @@
export * from './multiplayer'

View 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>
)
}

View file

@ -31,6 +31,8 @@ const isSelectedShapeWithHandlesSelector = (s: Data) => {
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 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.
*/
id?: string
/**
* (optional) The document to load or update from.
*/
document?: TLDrawDocument
/**
* (optional) The current page id.
*/
@ -55,10 +59,22 @@ export interface TLDrawProps {
* (optional) Whether the editor should immediately receive focus. Defaults to true.
*/
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.
*/
onMount?: (state: TLDrawState) => void
/**
* (optional) A callback to run when the component's state changes.
*/
@ -70,6 +86,8 @@ export function TLDraw({
document,
currentPageId,
autofocus = true,
showMenu = true,
showPages = true,
onMount,
onChange,
}: TLDrawProps) {
@ -98,6 +116,8 @@ export function TLDraw({
currentPageId={currentPageId}
document={document}
autofocus={autofocus}
showPages={showPages}
showMenu={showMenu}
/>
</IdProvider>
</TLDrawContext.Provider>
@ -108,11 +128,15 @@ function InnerTldraw({
id,
currentPageId,
autofocus,
showPages,
showMenu,
document,
}: {
id?: string
currentPageId?: string
autofocus?: boolean
autofocus: boolean
showPages: boolean
showMenu: boolean
document?: TLDrawDocument
}) {
const { tlstate, useSelector } = useTLDrawContext()
@ -125,6 +149,8 @@ function InnerTldraw({
const pageState = useSelector(pageStateSelector)
const users = useSelector(usersSelector)
const isDarkMode = useSelector(isDarkModeSelector)
const isFocusMode = useSelector(isFocusModeSelector)
@ -188,6 +214,8 @@ function InnerTldraw({
id={id}
page={page}
pageState={pageState}
users={users}
userId={tlstate.state.room.userId}
shapeUtils={tldrawShapeUtils}
theme={theme}
meta={meta}
@ -253,8 +281,8 @@ function InnerTldraw({
) : (
<>
<div className={menuButtons()}>
<Menu />
<PagePanel />
{showMenu && <Menu />}
{showPages && <PagePanel />}
</div>
<div className={spacer()} />
<StylePanel />

View file

@ -37,6 +37,7 @@ import {
TLDrawBinding,
GroupShape,
TLDrawCommand,
TLDrawUser,
} from '~types'
import { TLDR } from './tldr'
import { defaultStyle } from '~shape'
@ -66,6 +67,8 @@ const defaultDocument: TLDrawDocument = {
},
}
const uuid = Utils.uniqueId()
const defaultState: Data = {
settings: {
isPenMode: false,
@ -94,6 +97,17 @@ const defaultState: Data = {
},
},
document: defaultDocument,
room: {
id: 'local',
userId: uuid,
users: {
[uuid]: {
id: uuid,
color: 'dodgerBlue',
point: [100, 100],
},
},
},
}
export class TLDrawState extends StateManager<Data> {
@ -144,15 +158,10 @@ export class TLDrawState extends StateManager<Data> {
onChange?: (tlstate: TLDrawState, data: Data, reason: string) => void,
onMount?: (tlstate: TLDrawState) => void
) {
super(defaultState, id, 2, (prev, next, prevVersion) => {
const state = { ...prev }
if (prevVersion === 1)
state.settings = {
...state.settings,
isZoomSnap: next.settings.isZoomSnap,
}
return state
})
super(defaultState, id, 2.3, (prev, next) => ({
...next,
...prev,
}))
this._onChange = onChange
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
// 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
}
@ -543,6 +555,31 @@ export class TLDrawState extends StateManager<Data> {
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.
* @param document
@ -619,15 +656,6 @@ export class TLDrawState extends StateManager<Data> {
currentPageStates[this.currentPageId].selectedIds = [editingId]
}
console.log('next state', {
...this.state,
appState: nextAppState,
document: {
...document,
pageStates: currentPageStates,
},
})
return this.replaceState(
{
...this.state,
@ -2567,6 +2595,20 @@ export class TLDrawState extends StateManager<Data> {
/* ----------------- Pointer Events ----------------- */
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
this.updateOnPointerMove(info)
}

View file

@ -2,7 +2,7 @@
/* eslint-disable @typescript-eslint/ban-types */
import type { TLBinding, TLShapeProps } 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 { Command, Patch } from 'rko'
@ -29,10 +29,21 @@ export interface TLDrawSettings {
isFocusMode: boolean
}
export enum TLUserStatus {
Idle = 'idle',
Connecting = 'connecting',
Connected = 'connected',
Disconnected = 'disconnected',
}
export interface TLDrawMeta {
isDarkMode: boolean
}
export interface TLDrawUser extends TLUser {
id: string
}
export type TLDrawShapeProps<T extends TLDrawShape, E extends Element> = TLShapeProps<
T,
E,
@ -40,7 +51,6 @@ export type TLDrawShapeProps<T extends TLDrawShape, E extends Element> = TLShape
>
export interface Data {
document: TLDrawDocument
settings: TLDrawSettings
appState: {
selectedStyle: ShapeStyles
@ -55,6 +65,12 @@ export interface Data {
isEmptyCanvas: boolean
status: { current: TLDrawStatus; previous: TLDrawStatus }
}
document: TLDrawDocument
room: {
id: string
userId: string
users: Record<string, TLDrawUser>
}
}
export type TLDrawPatch = Patch<Data>

View file

@ -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 React from 'react'
import { createClient } from '@liveblocks/client'
import { createClient, Presence } from '@liveblocks/client'
import { LiveblocksProvider, RoomProvider, useObject, useErrorListener } from '@liveblocks/react'
import { Utils } from '@tldraw/core'
interface TLDrawPresence extends Presence {
user: TLDrawUser
}
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 }) {
@ -20,19 +24,18 @@ export default function MultiplayerEditor({ id }: { id: string }) {
}
function Editor({ id }: { id: string }) {
const [uuid] = React.useState(() => Utils.uniqueId())
const [error, setError] = React.useState<Error>(null)
const [docId] = React.useState(() => Utils.uniqueId())
const [error, setError] = React.useState<Error>()
const [tlstate, setTlstate] = React.useState<TLDrawState>()
useErrorListener((err) => {
console.log(err)
setError(err)
})
useErrorListener((err) => setError(err))
const doc = useObject<{ uuid: string; document: TLDrawDocument }>('doc', {
uuid,
uuid: docId,
document: {
id,
id: 'test-room',
pages: {
page: {
id: 'page',
@ -61,9 +64,9 @@ function Editor({ id }: { id: string }) {
setTlstate(tlstate)
}, [])
// Send events to gtag as actions.
const handleChange = React.useCallback(
(_tlstate: TLDrawState, state: Data, reason: string) => {
// If the client updates its document, update the room's document / gtag
if (reason.startsWith('command')) {
gtag.event({
action: reason,
@ -72,37 +75,86 @@ function Editor({ id }: { id: string }) {
value: 0,
})
if (doc) {
doc.update({ uuid, document: state.document })
}
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(id)
if (!room) return
const { userId, users } = state.room
room.updatePresence({ id: userId, user: users[userId] })
}
},
[uuid, doc]
[docId, doc, id]
)
React.useEffect(() => {
const room = client.getRoom(id)
if (!room) return
if (!doc) return
if (!tlstate) return
function updateState() {
// 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 (!tlstate) return
const docObject = doc.toObject()
if (docObject.uuid === uuid) return
tlstate.mergeDocument(docObject.document)
// Only merge the change if it caused by someone else
if (docObject.uuid !== docId) {
tlstate.mergeDocument(docObject.document)
}
}
updateState()
doc.subscribe(updateState)
function handleExit() {
room?.broadcastEvent({ name: 'exit', userId: tlstate?.state.room.userId })
}
return () => doc.unsubscribe(updateState)
}, [doc, uuid, tlstate])
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, id])
if (error) return <div>Error: {error.message}</div>
if (doc === null) return <div>loading...</div>
if (doc === null) return <div>Loading...</div>
return (
<div className="tldraw">
<TLDraw onMount={handleMount} onChange={handleChange} autofocus />
<TLDraw onMount={handleMount} onChange={handleChange} showPages={false} autofocus />
</div>
)
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long