Upgrade Liveblocks to 0.17 (#738)
* Upgrade Liveblocks packages to 0.17 * Convert app to recommended Liveblocks 0.17 setup * Convert multiplayer example to recommended Liveblocks 0.17 setup * Convert multiplayer-with-images example to recommended Liveblocks 0.17 setup * Fix React rendering issue for multiplayer app This bug could manifest after _navigating_ internally to the Multiplayer example app. Liveblocks Storage would seem to remain empty, but Presence would still seem to work. In other words, you'd see cursors flying, but no document contents. This did not happen when doing a full page load. This bug only occurs in React strict mode. * update onPatch and onCommand * "Add event callbacks for `onSessionStart` and `onSessionEnd`" * Adds edit state * Pass callbacks to app * Remove console logs * interpolate cursor only when not in session * Update multiplayer icon * Fix a few things Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
17
.eslintrc
|
@ -8,7 +8,22 @@
|
|||
// enable the rule specifically for TypeScript files
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
"rules": {
|
||||
"@typescript-eslint/explicit-module-boundary-types": [0]
|
||||
"@typescript-eslint/explicit-module-boundary-types": [0],
|
||||
"no-non-null-assertion": "off",
|
||||
"no-fallthrough": "off",
|
||||
"@typescript-eslint/no-fallthrough": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"argsIgnorePattern": "^_",
|
||||
"varsIgnorePattern": "^_",
|
||||
"destructuredArrayIgnorePattern": "^_",
|
||||
"caughtErrorsIgnorePattern": "^_"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { createClient } from '@liveblocks/client'
|
||||
import { LiveblocksProvider, RoomProvider } from '@liveblocks/react'
|
||||
import { RoomProvider } from '../utils/liveblocks'
|
||||
import { Tldraw, useFileSystem } from '@tldraw/tldraw'
|
||||
import { useAccountHandlers } from 'hooks/useAccountHandlers'
|
||||
import { useMultiplayerAssets } from 'hooks/useMultiplayerAssets'
|
||||
|
@ -8,11 +7,6 @@ import { useUploadAssets } from 'hooks/useUploadAssets'
|
|||
import React, { FC } from 'react'
|
||||
import { styled } from 'styles'
|
||||
|
||||
const client = createClient({
|
||||
publicApiKey: process.env.NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_API_KEY || '',
|
||||
throttle: 80,
|
||||
})
|
||||
|
||||
interface Props {
|
||||
roomId: string
|
||||
isUser: boolean
|
||||
|
@ -29,11 +23,9 @@ const MultiplayerEditor: FC<Props> = ({
|
|||
isSponsor: boolean
|
||||
}) => {
|
||||
return (
|
||||
<LiveblocksProvider client={client}>
|
||||
<RoomProvider id={roomId}>
|
||||
<Editor roomId={roomId} isSponsor={isSponsor} isUser={isUser} />
|
||||
</RoomProvider>
|
||||
</LiveblocksProvider>
|
||||
<RoomProvider id={roomId}>
|
||||
<Editor roomId={roomId} isSponsor={isSponsor} isUser={isUser} />
|
||||
</RoomProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,21 +1,20 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import React, { useState, useRef, useCallback } from 'react'
|
||||
import type { TldrawApp, TDUser, TDShape, TDBinding, TDDocument, TDAsset } from '@tldraw/tldraw'
|
||||
import { useRedo, useUndo, useRoom, useUpdateMyPresence } from '@liveblocks/react'
|
||||
import { LiveMap, LiveObject, Lson, LsonObject } from '@liveblocks/client'
|
||||
import type { TldrawApp, TDUser, TDShape, TDBinding, TDAsset } from '@tldraw/tldraw'
|
||||
import {
|
||||
Storage,
|
||||
useRedo,
|
||||
useUndo,
|
||||
useRoom,
|
||||
useHistory,
|
||||
useUpdateMyPresence,
|
||||
} from '../utils/liveblocks'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { LiveMap } from '@liveblocks/client'
|
||||
|
||||
declare const window: Window & { app: TldrawApp }
|
||||
|
||||
type TDLsonShape = TDShape & Lson
|
||||
type TDLsonBinding = TDBinding & Lson
|
||||
type TDLsonAsset = TDAsset & Lson
|
||||
type LsonDoc = {
|
||||
uuid: string
|
||||
document: TDDocument
|
||||
migrated?: boolean
|
||||
} & LsonObject
|
||||
|
||||
export function useMultiplayerState(roomId: string) {
|
||||
const [app, setApp] = useState<TldrawApp>()
|
||||
const [error, setError] = useState<Error>()
|
||||
|
@ -26,9 +25,11 @@ export function useMultiplayerState(roomId: string) {
|
|||
const onRedo = useRedo()
|
||||
const updateMyPresence = useUpdateMyPresence()
|
||||
|
||||
const rLiveShapes = useRef<LiveMap<string, TDLsonShape>>()
|
||||
const rLiveBindings = useRef<LiveMap<string, TDLsonBinding>>()
|
||||
const rLiveAssets = useRef<LiveMap<string, TDLsonAsset>>()
|
||||
const rIsPaused = useRef(false)
|
||||
|
||||
const rLiveShapes = useRef<Storage['shapes']>()
|
||||
const rLiveBindings = useRef<Storage['bindings']>()
|
||||
const rLiveAssets = useRef<Storage['assets']>()
|
||||
|
||||
// Callbacks --------------
|
||||
|
||||
|
@ -62,7 +63,7 @@ export function useMultiplayerState(roomId: string) {
|
|||
if (!shape) {
|
||||
lShapes.delete(id)
|
||||
} else {
|
||||
lShapes.set(shape.id, shape as TDLsonShape)
|
||||
lShapes.set(shape.id, shape)
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -70,7 +71,7 @@ export function useMultiplayerState(roomId: string) {
|
|||
if (!binding) {
|
||||
lBindings.delete(id)
|
||||
} else {
|
||||
lBindings.set(binding.id, binding as TDLsonBinding)
|
||||
lBindings.set(binding.id, binding)
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -78,7 +79,7 @@ export function useMultiplayerState(roomId: string) {
|
|||
if (!asset) {
|
||||
lAssets.delete(id)
|
||||
} else {
|
||||
lAssets.set(asset.id, asset as TDLsonAsset)
|
||||
lAssets.set(asset.id, asset)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
@ -104,7 +105,7 @@ export function useMultiplayerState(roomId: string) {
|
|||
|
||||
// Handle changes to other users' presence
|
||||
unsubs.push(
|
||||
room.subscribe<{ id: string; user: TDUser }>('others', (others, event) => {
|
||||
room.subscribe('others', (others, event) => {
|
||||
if (event.type === 'leave') {
|
||||
if (event.user.presence) {
|
||||
app?.removeUser(event.user.presence.id)
|
||||
|
@ -125,30 +126,30 @@ export function useMultiplayerState(roomId: string) {
|
|||
|
||||
// Setup the document's storage and subscriptions
|
||||
async function setupDocument() {
|
||||
const storage = await room.getStorage<any>()
|
||||
const storage = await room.getStorage()
|
||||
|
||||
// Migrate previous versions
|
||||
const version = storage.root.get('version')
|
||||
|
||||
// Initialize (get or create) maps for shapes/bindings/assets
|
||||
|
||||
let lShapes: LiveMap<string, TDLsonShape> = storage.root.get('shapes')
|
||||
let lShapes = storage.root.get('shapes')
|
||||
if (!lShapes || !('_serialize' in lShapes)) {
|
||||
storage.root.set('shapes', new LiveMap<string, TDLsonShape>())
|
||||
storage.root.set('shapes', new LiveMap())
|
||||
lShapes = storage.root.get('shapes')
|
||||
}
|
||||
rLiveShapes.current = lShapes
|
||||
|
||||
let lBindings: LiveMap<string, TDLsonBinding> = storage.root.get('bindings')
|
||||
let lBindings = storage.root.get('bindings')
|
||||
if (!lBindings || !('_serialize' in lBindings)) {
|
||||
storage.root.set('bindings', new LiveMap<string, TDLsonBinding>())
|
||||
storage.root.set('bindings', new LiveMap())
|
||||
lBindings = storage.root.get('bindings')
|
||||
}
|
||||
rLiveBindings.current = lBindings
|
||||
|
||||
let lAssets: LiveMap<string, TDLsonAsset> = storage.root.get('assets')
|
||||
let lAssets = storage.root.get('assets')
|
||||
if (!lAssets || !('_serialize' in lAssets)) {
|
||||
storage.root.set('assets', new LiveMap<string, TDLsonAsset>())
|
||||
storage.root.set('assets', new LiveMap())
|
||||
lAssets = storage.root.get('assets')
|
||||
}
|
||||
rLiveAssets.current = lAssets
|
||||
|
@ -159,7 +160,7 @@ export function useMultiplayerState(roomId: string) {
|
|||
// document was a single LiveObject named 'doc'. If we find a doc,
|
||||
// then we need to move the shapes and bindings over to the new structures
|
||||
// and then mark the doc as migrated.
|
||||
const doc = storage.root.get('doc') as LiveObject<LsonDoc>
|
||||
const doc = storage.root.get('doc')
|
||||
|
||||
// No doc? No problem. This was likely a newer document
|
||||
if (doc) {
|
||||
|
@ -172,11 +173,9 @@ export function useMultiplayerState(roomId: string) {
|
|||
},
|
||||
} = doc.toObject()
|
||||
|
||||
Object.values(shapes).forEach((shape) => lShapes.set(shape.id, shape as TDLsonShape))
|
||||
Object.values(bindings).forEach((binding) =>
|
||||
lBindings.set(binding.id, binding as TDLsonBinding)
|
||||
)
|
||||
Object.values(assets).forEach((asset) => lAssets.set(asset.id, asset as TDLsonAsset))
|
||||
Object.values(shapes).forEach((shape) => lShapes.set(shape.id, shape))
|
||||
Object.values(bindings).forEach((binding) => lBindings.set(binding.id, binding))
|
||||
Object.values(assets).forEach((asset) => lAssets.set(asset.id, asset))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -219,13 +218,43 @@ export function useMultiplayerState(roomId: string) {
|
|||
const onSessionStart = React.useCallback(() => {
|
||||
if (!room) return
|
||||
room.history.pause()
|
||||
rIsPaused.current = true
|
||||
}, [room])
|
||||
|
||||
const onSessionEnd = React.useCallback(() => {
|
||||
if (!room) return
|
||||
room.history.resume()
|
||||
rIsPaused.current = false
|
||||
}, [room])
|
||||
|
||||
useHotkeys(
|
||||
'ctrl+shift+l;,⌘+shift+l',
|
||||
() => {
|
||||
if (window.confirm('Reset the document?')) {
|
||||
room.batch(() => {
|
||||
const lShapes = rLiveShapes.current
|
||||
const lBindings = rLiveBindings.current
|
||||
const lAssets = rLiveAssets.current
|
||||
|
||||
if (!(lShapes && lBindings && lAssets)) return
|
||||
|
||||
lShapes.forEach((shape) => {
|
||||
lShapes.delete(shape.id)
|
||||
})
|
||||
|
||||
lBindings.forEach((shape) => {
|
||||
lBindings.delete(shape.id)
|
||||
})
|
||||
|
||||
lAssets.forEach((shape) => {
|
||||
lAssets.delete(shape.id)
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
return {
|
||||
onUndo,
|
||||
onRedo,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const withPWA = require('next-pwa')
|
||||
const withTM = require('next-transpile-modules')
|
||||
const SentryWebpackPlugin = require('@sentry/webpack-plugin')
|
||||
|
||||
const {
|
||||
|
@ -20,57 +21,59 @@ const isProduction = NODE_ENV === 'production'
|
|||
|
||||
const basePath = ''
|
||||
|
||||
module.exports = withPWA({
|
||||
reactStrictMode: true,
|
||||
pwa: {
|
||||
disable: !isProduction,
|
||||
dest: 'public',
|
||||
},
|
||||
productionBrowserSourceMaps: true,
|
||||
env: {
|
||||
NEXT_PUBLIC_COMMIT_SHA: VERCEL_GIT_COMMIT_SHA,
|
||||
GA_MEASUREMENT_ID,
|
||||
GITHUB_ID,
|
||||
GITHUB_API_SECRET,
|
||||
},
|
||||
webpack: (config, options) => {
|
||||
if (!options.isServer) {
|
||||
config.resolve.alias['@sentry/node'] = '@sentry/browser'
|
||||
}
|
||||
module.exports = withTM(['@tldraw/tldraw', '@tldraw/core'])(
|
||||
withPWA({
|
||||
reactStrictMode: true,
|
||||
pwa: {
|
||||
disable: !isProduction,
|
||||
dest: 'public',
|
||||
},
|
||||
productionBrowserSourceMaps: true,
|
||||
env: {
|
||||
NEXT_PUBLIC_COMMIT_SHA: VERCEL_GIT_COMMIT_SHA,
|
||||
GA_MEASUREMENT_ID,
|
||||
GITHUB_ID,
|
||||
GITHUB_API_SECRET,
|
||||
},
|
||||
webpack: (config, options) => {
|
||||
if (!options.isServer) {
|
||||
config.resolve.alias['@sentry/node'] = '@sentry/browser'
|
||||
}
|
||||
|
||||
config.plugins.push(
|
||||
new options.webpack.DefinePlugin({
|
||||
'process.env.NEXT_IS_SERVER': JSON.stringify(options.isServer.toString()),
|
||||
})
|
||||
)
|
||||
|
||||
config.module.rules.push({
|
||||
test: /.*packages.*\.js$/,
|
||||
use: ['source-map-loader'],
|
||||
enforce: 'pre',
|
||||
})
|
||||
|
||||
if (
|
||||
SENTRY_DSN &&
|
||||
SENTRY_ORG &&
|
||||
SENTRY_PROJECT &&
|
||||
SENTRY_AUTH_TOKEN &&
|
||||
VERCEL_GIT_COMMIT_SHA &&
|
||||
isProduction
|
||||
) {
|
||||
config.plugins.push(
|
||||
new SentryWebpackPlugin({
|
||||
include: '.next',
|
||||
ignore: ['node_modules'],
|
||||
stripPrefix: ['webpack://_N_E/'],
|
||||
urlPrefix: `~${basePath}/_next`,
|
||||
release: VERCEL_GIT_COMMIT_SHA,
|
||||
authToken: SENTRY_AUTH_TOKEN,
|
||||
org: SENTRY_PROJECT,
|
||||
project: SENTRY_ORG,
|
||||
new options.webpack.DefinePlugin({
|
||||
'process.env.NEXT_IS_SERVER': JSON.stringify(options.isServer.toString()),
|
||||
})
|
||||
)
|
||||
}
|
||||
return config
|
||||
},
|
||||
})
|
||||
|
||||
config.module.rules.push({
|
||||
test: /.*packages.*\.js$/,
|
||||
use: ['source-map-loader'],
|
||||
enforce: 'pre',
|
||||
})
|
||||
|
||||
if (
|
||||
SENTRY_DSN &&
|
||||
SENTRY_ORG &&
|
||||
SENTRY_PROJECT &&
|
||||
SENTRY_AUTH_TOKEN &&
|
||||
VERCEL_GIT_COMMIT_SHA &&
|
||||
isProduction
|
||||
) {
|
||||
config.plugins.push(
|
||||
new SentryWebpackPlugin({
|
||||
include: '.next',
|
||||
ignore: ['node_modules'],
|
||||
stripPrefix: ['webpack://_N_E/'],
|
||||
urlPrefix: `~${basePath}/_next`,
|
||||
release: VERCEL_GIT_COMMIT_SHA,
|
||||
authToken: SENTRY_AUTH_TOKEN,
|
||||
org: SENTRY_PROJECT,
|
||||
project: SENTRY_ORG,
|
||||
})
|
||||
)
|
||||
}
|
||||
return config
|
||||
},
|
||||
})
|
||||
)
|
||||
|
|
|
@ -18,23 +18,22 @@
|
|||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sentry/webpack-plugin": "^1.17.1",
|
||||
"@types/next-auth": "^3.15.0",
|
||||
"@types/react": "^18.0.12",
|
||||
"@types/react-dom": "^18.0.5",
|
||||
"eslint": "^8.8.0",
|
||||
"eslint-config-next": "^12.0.10",
|
||||
"typescript": "^4.7.3",
|
||||
"@liveblocks/client": "^0.16.17",
|
||||
"@liveblocks/react": "^0.16.17",
|
||||
"@liveblocks/client": "^0.17.0-beta2",
|
||||
"@liveblocks/react": "^0.17.0-beta2",
|
||||
"@sentry/integrations": "^6.13.2",
|
||||
"@sentry/node": "^6.13.2",
|
||||
"@sentry/react": "^6.13.2",
|
||||
"@sentry/tracing": "^6.13.2",
|
||||
"@sentry/webpack-plugin": "^1.17.1",
|
||||
"@stitches/react": "^1.2.8",
|
||||
"@tldraw/core": "*",
|
||||
"@tldraw/tldraw": "*",
|
||||
"@types/next-auth": "^3.15.0",
|
||||
"@types/react": "^18.0.12",
|
||||
"@types/react-dom": "^18.0.5",
|
||||
"aws-sdk": "^2.1053.0",
|
||||
"eslint": "^8.8.0",
|
||||
"eslint-config-next": "^12.0.10",
|
||||
"lz-string": "^1.4.4",
|
||||
"nanoid": "^3.3.4",
|
||||
"next": "^12.1.6",
|
||||
|
@ -42,7 +41,12 @@
|
|||
"next-pwa": "^5.5.4",
|
||||
"next-themes": "^0.0.15",
|
||||
"react": "^18.1.0",
|
||||
"react-dom": "^18.1.0"
|
||||
"react-dom": "^18.1.0",
|
||||
"react-hotkeys-hook": "^3.4.6",
|
||||
"typescript": "^4.7.3"
|
||||
},
|
||||
"gitHead": "838fabdbff1a66d4d7ee8aa5c5d117bc55acbff2"
|
||||
"gitHead": "838fabdbff1a66d4d7ee8aa5c5d117bc55acbff2",
|
||||
"devDependencies": {
|
||||
"next-transpile-modules": "^9.0.0"
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.5 2.49538C12.2239 2.49538 12 2.71923 12 2.99538V5.49559H9.49979C9.22364 5.49559 8.99979 5.71945 8.99979 5.99559C8.99979 6.27173 9.22364 6.49559 9.49979 6.49559H12.5C12.7761 6.49559 13 6.27173 13 5.99559V2.99538C13 2.71923 12.7761 2.49538 12.5 2.49538Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.69698 2.04877C6.62345 1.89773 5.52991 2.09968 4.58113 2.62417C3.63236 3.14867 2.87973 3.9673 2.43667 4.95673C1.99361 5.94616 1.8841 7.05278 2.12465 8.10985C2.3652 9.16693 2.94278 10.1172 3.77036 10.8175C4.59794 11.5177 5.63069 11.9301 6.713 11.9924C7.79531 12.0547 8.86855 11.7635 9.77101 11.1628C10.6735 10.5621 11.3563 9.68441 11.7165 8.66191C11.8083 8.40146 11.6715 8.11593 11.4111 8.02417C11.1506 7.93241 10.8651 8.06916 10.7733 8.32961C10.4851 9.14762 9.93888 9.84981 9.21691 10.3304C8.49493 10.811 7.63632 11.0439 6.77046 10.994C5.9046 10.9442 5.07839 10.6143 4.41631 10.0541C3.75424 9.49386 3.29217 8.73363 3.09972 7.88796C2.90728 7.04229 2.99488 6.15698 3.34934 5.36542C3.7038 4.57387 4.30591 3.91895 5.06494 3.49935C5.82398 3.07974 6.69882 2.91819 7.55765 3.03902C8.41649 3.15985 9.21279 3.55653 9.82658 4.16928L9.83745 4.17981L12.1576 6.35996C12.3588 6.54906 12.6753 6.53921 12.8644 6.33797C13.0535 6.13673 13.0436 5.8203 12.8424 5.63121L10.5276 3.4561C9.76111 2.69329 8.76794 2.19945 7.69698 2.04877Z" fill="black"/>
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M12.5 2.49538C12.2239 2.49538 12 2.71923 12 2.99538V5.49559H9.49979C9.22364 5.49559 8.99979 5.71945 8.99979 5.99559C8.99979 6.27173 9.22364 6.49559 9.49979 6.49559H12.5C12.7761 6.49559 13 6.27173 13 5.99559V2.99538C13 2.71923 12.7761 2.49538 12.5 2.49538Z" fill="black"/>
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M7.69698 2.04877C6.62345 1.89773 5.52991 2.09968 4.58113 2.62417C3.63236 3.14867 2.87973 3.9673 2.43667 4.95673C1.99361 5.94616 1.8841 7.05278 2.12465 8.10985C2.3652 9.16693 2.94278 10.1172 3.77036 10.8175C4.59794 11.5177 5.63069 11.9301 6.713 11.9924C7.79531 12.0547 8.86855 11.7635 9.77101 11.1628C10.6735 10.5621 11.3563 9.68441 11.7165 8.66191C11.8083 8.40146 11.6715 8.11593 11.4111 8.02417C11.1506 7.93241 10.8651 8.06916 10.7733 8.32961C10.4851 9.14762 9.93888 9.84981 9.21691 10.3304C8.49493 10.811 7.63632 11.0439 6.77046 10.994C5.9046 10.9442 5.07839 10.6143 4.41631 10.0541C3.75424 9.49386 3.29217 8.73363 3.09972 7.88796C2.90728 7.04229 2.99488 6.15698 3.34934 5.36542C3.7038 4.57387 4.30591 3.91895 5.06494 3.49935C5.82398 3.07974 6.69882 2.91819 7.55765 3.03902C8.41649 3.15985 9.21279 3.55653 9.82658 4.16928L9.83745 4.17981L12.1576 6.35996C12.3588 6.54906 12.6753 6.53921 12.8644 6.33797C13.0535 6.13673 13.0436 5.8203 12.8424 5.63121L10.5276 3.4561C9.76111 2.69329 8.76794 2.19945 7.69698 2.04877Z" fill="black"/>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
@ -1,6 +1,6 @@
|
|||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 4.65555C2 4.37941 2.22386 4.15555 2.5 4.15555H12.2C12.4761 4.15555 12.7 4.37941 12.7 4.65555C12.7 4.93169 12.4761 5.15555 12.2 5.15555H2.5C2.22386 5.15555 2 4.93169 2 4.65555Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.27208 3C6.11885 3 5.97189 3.06087 5.86353 3.16923C5.75518 3.27758 5.6943 3.42454 5.6943 3.57778V4.15556H9.00542V3.57778C9.00542 3.42454 8.94454 3.27758 8.83619 3.16923C8.72783 3.06087 8.58087 3 8.42764 3H6.27208ZM10.0054 4.15556V3.57778C10.0054 3.15933 9.83919 2.75801 9.54329 2.46212C9.2474 2.16623 8.84609 2 8.42764 2H6.27208C5.85363 2 5.45232 2.16623 5.15642 2.46212C4.86053 2.75801 4.6943 3.15933 4.6943 3.57778V4.15556H3.57764C3.30149 4.15556 3.07764 4.37941 3.07764 4.65556V12.2C3.07764 12.6185 3.24387 13.0198 3.53976 13.3157C3.83565 13.6115 4.23696 13.7778 4.65541 13.7778H10.0443C10.4628 13.7778 10.8641 13.6115 11.16 13.3157C11.4559 13.0198 11.6221 12.6185 11.6221 12.2V4.65556C11.6221 4.37941 11.3982 4.15556 11.1221 4.15556H10.0054ZM4.07764 5.15556V12.2C4.07764 12.3532 4.13851 12.5002 4.24686 12.6086C4.35522 12.7169 4.50218 12.7778 4.65541 12.7778H10.0443C10.1975 12.7778 10.3445 12.7169 10.4529 12.6086C10.5612 12.5002 10.6221 12.3532 10.6221 12.2V5.15556H4.07764Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.27246 6.85001C6.5486 6.85001 6.77246 7.07386 6.77246 7.35001V10.5833C6.77246 10.8595 6.5486 11.0833 6.27246 11.0833C5.99632 11.0833 5.77246 10.8595 5.77246 10.5833V7.35001C5.77246 7.07386 5.99632 6.85001 6.27246 6.85001Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.42773 6.85001C8.70388 6.85001 8.92773 7.07386 8.92773 7.35001V10.5833C8.92773 10.8595 8.70388 11.0833 8.42773 11.0833C8.15159 11.0833 7.92773 10.8595 7.92773 10.5833V7.35001C7.92773 7.07386 8.15159 6.85001 8.42773 6.85001Z" fill="black"/>
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M2 4.65555C2 4.37941 2.22386 4.15555 2.5 4.15555H12.2C12.4761 4.15555 12.7 4.37941 12.7 4.65555C12.7 4.93169 12.4761 5.15555 12.2 5.15555H2.5C2.22386 5.15555 2 4.93169 2 4.65555Z" fill="black"/>
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M6.27208 3C6.11885 3 5.97189 3.06087 5.86353 3.16923C5.75518 3.27758 5.6943 3.42454 5.6943 3.57778V4.15556H9.00542V3.57778C9.00542 3.42454 8.94454 3.27758 8.83619 3.16923C8.72783 3.06087 8.58087 3 8.42764 3H6.27208ZM10.0054 4.15556V3.57778C10.0054 3.15933 9.83919 2.75801 9.54329 2.46212C9.2474 2.16623 8.84609 2 8.42764 2H6.27208C5.85363 2 5.45232 2.16623 5.15642 2.46212C4.86053 2.75801 4.6943 3.15933 4.6943 3.57778V4.15556H3.57764C3.30149 4.15556 3.07764 4.37941 3.07764 4.65556V12.2C3.07764 12.6185 3.24387 13.0198 3.53976 13.3157C3.83565 13.6115 4.23696 13.7778 4.65541 13.7778H10.0443C10.4628 13.7778 10.8641 13.6115 11.16 13.3157C11.4559 13.0198 11.6221 12.6185 11.6221 12.2V4.65556C11.6221 4.37941 11.3982 4.15556 11.1221 4.15556H10.0054ZM4.07764 5.15556V12.2C4.07764 12.3532 4.13851 12.5002 4.24686 12.6086C4.35522 12.7169 4.50218 12.7778 4.65541 12.7778H10.0443C10.1975 12.7778 10.3445 12.7169 10.4529 12.6086C10.5612 12.5002 10.6221 12.3532 10.6221 12.2V5.15556H4.07764Z" fill="black"/>
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M6.27246 6.85001C6.5486 6.85001 6.77246 7.07386 6.77246 7.35001V10.5833C6.77246 10.8595 6.5486 11.0833 6.27246 11.0833C5.99632 11.0833 5.77246 10.8595 5.77246 10.5833V7.35001C5.77246 7.07386 5.99632 6.85001 6.27246 6.85001Z" fill="black"/>
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M8.42773 6.85001C8.70388 6.85001 8.92773 7.07386 8.92773 7.35001V10.5833C8.92773 10.8595 8.70388 11.0833 8.42773 11.0833C8.15159 11.0833 7.92773 10.8595 7.92773 10.5833V7.35001C7.92773 7.07386 8.15159 6.85001 8.42773 6.85001Z" fill="black"/>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
@ -1,4 +1,4 @@
|
|||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.5 2.49538C2.77614 2.49538 3 2.71923 3 2.99538V5.49559H5.50021C5.77636 5.49559 6.00021 5.71945 6.00021 5.99559C6.00021 6.27173 5.77636 6.49559 5.50021 6.49559H2.5C2.22386 6.49559 2 6.27173 2 5.99559V2.99538C2 2.71923 2.22386 2.49538 2.5 2.49538Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.30302 2.04877C8.37655 1.89773 9.47009 2.09968 10.4189 2.62417C11.3676 3.14867 12.1203 3.9673 12.5633 4.95673C13.0064 5.94616 13.1159 7.05278 12.8753 8.10985C12.6348 9.16693 12.0572 10.1172 11.2296 10.8175C10.4021 11.5177 9.36931 11.9301 8.287 11.9924C7.20469 12.0547 6.13145 11.7635 5.22899 11.1628C4.32653 10.5621 3.64374 9.68441 3.2835 8.66191C3.19174 8.40146 3.32849 8.11593 3.58894 8.02417C3.84939 7.93241 4.13492 8.06916 4.22668 8.32961C4.51488 9.14762 5.06112 9.84981 5.78309 10.3304C6.50507 10.811 7.36368 11.0439 8.22954 10.994C9.0954 10.9442 9.92161 10.6143 10.5837 10.0541C11.2458 9.49386 11.7078 8.73363 11.9003 7.88796C12.0927 7.04229 12.0051 6.15698 11.6507 5.36542C11.2962 4.57387 10.6941 3.91895 9.93506 3.49935C9.17602 3.07974 8.30118 2.91819 7.44235 3.03902C6.58351 3.15985 5.78721 3.55653 5.17342 4.16928L5.16255 4.17981L2.84239 6.35996C2.64115 6.54906 2.32472 6.53921 2.13562 6.33797C1.94653 6.13673 1.95637 5.8203 2.15761 5.63121L4.47241 3.4561C5.23889 2.69329 6.23206 2.19945 7.30302 2.04877Z" fill="black"/>
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M2.5 2.49538C2.77614 2.49538 3 2.71923 3 2.99538V5.49559H5.50021C5.77636 5.49559 6.00021 5.71945 6.00021 5.99559C6.00021 6.27173 5.77636 6.49559 5.50021 6.49559H2.5C2.22386 6.49559 2 6.27173 2 5.99559V2.99538C2 2.71923 2.22386 2.49538 2.5 2.49538Z" fill="black"/>
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M7.30302 2.04877C8.37655 1.89773 9.47009 2.09968 10.4189 2.62417C11.3676 3.14867 12.1203 3.9673 12.5633 4.95673C13.0064 5.94616 13.1159 7.05278 12.8753 8.10985C12.6348 9.16693 12.0572 10.1172 11.2296 10.8175C10.4021 11.5177 9.36931 11.9301 8.287 11.9924C7.20469 12.0547 6.13145 11.7635 5.22899 11.1628C4.32653 10.5621 3.64374 9.68441 3.2835 8.66191C3.19174 8.40146 3.32849 8.11593 3.58894 8.02417C3.84939 7.93241 4.13492 8.06916 4.22668 8.32961C4.51488 9.14762 5.06112 9.84981 5.78309 10.3304C6.50507 10.811 7.36368 11.0439 8.22954 10.994C9.0954 10.9442 9.92161 10.6143 10.5837 10.0541C11.2458 9.49386 11.7078 8.73363 11.9003 7.88796C12.0927 7.04229 12.0051 6.15698 11.6507 5.36542C11.2962 4.57387 10.6941 3.91895 9.93506 3.49935C9.17602 3.07974 8.30118 2.91819 7.44235 3.03902C6.58351 3.15985 5.78721 3.55653 5.17342 4.16928L5.16255 4.17981L2.84239 6.35996C2.64115 6.54906 2.32472 6.53921 2.13562 6.33797C1.94653 6.13673 1.95637 5.8203 2.15761 5.63121L4.47241 3.4561C5.23889 2.69329 6.23206 2.19945 7.30302 2.04877Z" fill="black"/>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="35px" height="35px" viewBox="0 0 35 35" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs></defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fillRule="evenodd">
|
||||
<g id="openhand">
|
||||
<g id="bg-copy" fill="#FFFFFF" opacity="1">
|
||||
<rect id="bg" x="0" y="0" width="35" height="35"></rect>
|
||||
|
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="35px" height="35px" viewBox="0 0 35 35" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs></defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fillRule="evenodd">
|
||||
<g id="pointer">
|
||||
<g id="bg" fill="#FFFFFF" opacity="0.00999999978">
|
||||
<rect x="0" y="0" width="35" height="35"></rect>
|
||||
|
|
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 1 KiB |
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="35px" height="35px" viewBox="0 0 35 35" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs></defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fillRule="evenodd">
|
||||
<g id="resizenortheastsouthwest">
|
||||
<g id="bg-copy" fill="#FFFFFF" opacity="1">
|
||||
<rect id="bg" x="0" y="0" width="35" height="35"></rect>
|
||||
|
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
52
apps/www/utils/liveblocks.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
import { createClient } from '@liveblocks/client'
|
||||
import type { EnsureJson, LiveMap, LiveObject } from '@liveblocks/client'
|
||||
import { createRoomContext } from '@liveblocks/react'
|
||||
import type { TDUser, TDShape, TDBinding, TDDocument, TDAsset } from '@tldraw/tldraw'
|
||||
|
||||
const client = createClient({
|
||||
publicApiKey: process.env.NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_API_KEY || '',
|
||||
throttle: 80,
|
||||
})
|
||||
|
||||
// Presence represents the properties that will exist on every User in the
|
||||
// Liveblocks Room and that will automatically be kept in sync. Accessible
|
||||
// through the `user.presence` property.
|
||||
type Presence = {
|
||||
id?: string
|
||||
user: EnsureJson<TDUser>
|
||||
}
|
||||
|
||||
// Storage represents the shared document that persists in the Room, even after
|
||||
// all Users leave, and for which updates are automatically persisted and
|
||||
// synced to all connected clients.
|
||||
export type Storage = {
|
||||
version: number
|
||||
doc: LiveObject<{
|
||||
uuid: string
|
||||
document: EnsureJson<TDDocument>
|
||||
migrated?: boolean
|
||||
}>
|
||||
shapes: LiveMap<string, EnsureJson<TDShape>>
|
||||
bindings: LiveMap<string, EnsureJson<TDBinding>>
|
||||
assets: LiveMap<string, EnsureJson<TDAsset>>
|
||||
}
|
||||
|
||||
// Optionally, UserMeta represents static/readonly metadata on each User, as
|
||||
// provided by your own custom auth backend. This isn't used for TLDraw.
|
||||
// type UserMeta = {
|
||||
// id?: string, // Accessible through `user.id`
|
||||
// info?: Json, // Accessible through `user.info`
|
||||
// };
|
||||
|
||||
// Optionally, the type of custom events broadcasted and listened for in this
|
||||
// room.
|
||||
// type RoomEvent = {};
|
||||
|
||||
const { RoomProvider, useHistory, useRedo, useUndo, useRoom, useUpdateMyPresence } =
|
||||
createRoomContext<
|
||||
Presence,
|
||||
Storage
|
||||
/* UserMeta, RoomEvent */
|
||||
>(client)
|
||||
|
||||
export { RoomProvider, useHistory, useRedo, useUndo, useRoom, useUpdateMyPresence }
|
|
@ -15,8 +15,8 @@
|
|||
"build": "node scripts/build.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@liveblocks/client": "^0.14.0",
|
||||
"@liveblocks/react": "^0.14.0",
|
||||
"@liveblocks/client": "^0.17.0-beta2",
|
||||
"@liveblocks/react": "^0.17.0-beta2",
|
||||
"@types/node": "^17.0.14",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"concurrently": "^7.0.0",
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
import { createClient } from '@liveblocks/client'
|
||||
import type { BaseUserMeta, EnsureJson } from '@liveblocks/client'
|
||||
import { createRoomContext } from '@liveblocks/react'
|
||||
import { LiveMap, LiveObject } from '@liveblocks/client'
|
||||
import type { TDAsset, TDBinding, TDDocument, TDShape, TDUser } from '@tldraw/tldraw'
|
||||
|
||||
const client = createClient({
|
||||
publicApiKey: process.env.LIVEBLOCKS_PUBLIC_API_KEY || '',
|
||||
throttle: 100,
|
||||
})
|
||||
|
||||
// Presence represents the properties that will exist on every User in the
|
||||
// Liveblocks Room and that will automatically be synchronized between
|
||||
// connected Users. Accessible through the `user.presence` property.
|
||||
type Presence = {
|
||||
id?: string
|
||||
user: EnsureJson<TDUser>
|
||||
}
|
||||
|
||||
// Storage represents the shared document that persists in the Room, even after
|
||||
// all Users leave. All Live structures here are automatically kept in sync
|
||||
// between connected clients.
|
||||
export type Storage = {
|
||||
version: number
|
||||
doc: LiveObject<{
|
||||
uuid: string
|
||||
document: EnsureJson<TDDocument>
|
||||
migrated?: boolean
|
||||
}>
|
||||
assets: LiveMap<string, EnsureJson<TDAsset>>
|
||||
bindings: LiveMap<string, EnsureJson<TDBinding>>
|
||||
shapes: LiveMap<string, EnsureJson<TDShape>>
|
||||
}
|
||||
|
||||
// Optionally, UserMeta represents static/readonly metadata on each User, as
|
||||
// provided by a potential custom Liveblocks auth backend. This isn't used for
|
||||
// TLDraw.
|
||||
//
|
||||
// type UserMeta = {
|
||||
// id?: string, // Accessible through `user.id`
|
||||
// info?: Json, // Accessible through `user.info`
|
||||
// };
|
||||
//
|
||||
type UserMeta = BaseUserMeta
|
||||
|
||||
// Custom Events that TLDraw broadcasts between connected users
|
||||
type RoomEvent =
|
||||
// | { name: 'click', x: number, y: number }
|
||||
// | { name: 'like', emoji: string }
|
||||
// | etc.
|
||||
{ name: 'exit'; userId: string }
|
||||
|
||||
const { RoomProvider, useRedo, useUndo, useRoom, useUpdateMyPresence } = createRoomContext<
|
||||
Presence,
|
||||
Storage,
|
||||
UserMeta,
|
||||
RoomEvent
|
||||
>(client)
|
||||
|
||||
export { RoomProvider, useRedo, useUndo, useRoom, useUpdateMyPresence }
|
|
@ -1,28 +1,20 @@
|
|||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import * as React from 'react'
|
||||
import { TDShape, Tldraw } from '@tldraw/tldraw'
|
||||
import { createClient } from '@liveblocks/client'
|
||||
import { LiveblocksProvider, RoomProvider } from '@liveblocks/react'
|
||||
import { Tldraw } from '@tldraw/tldraw'
|
||||
import { RoomProvider } from './liveblocks.config'
|
||||
import { useMultiplayerState } from './useMultiplayerState'
|
||||
// import { initializeApp } from 'firebase/app'
|
||||
// import firebaseConfig from '../firebase.config'
|
||||
// import { useMemo } from 'react'
|
||||
// import { getStorage, ref, uploadBytes, getDownloadURL, deleteObject } from 'firebase/storage'
|
||||
|
||||
const client = createClient({
|
||||
publicApiKey: process.env.LIVEBLOCKS_PUBLIC_API_KEY || '',
|
||||
throttle: 100,
|
||||
})
|
||||
|
||||
const roomId = 'mp-test-images-1'
|
||||
|
||||
export function Multiplayer() {
|
||||
return (
|
||||
<LiveblocksProvider client={client}>
|
||||
<RoomProvider id={roomId}>
|
||||
<Editor roomId={roomId} />
|
||||
</RoomProvider>
|
||||
</LiveblocksProvider>
|
||||
<RoomProvider id={roomId}>
|
||||
<Editor roomId={roomId} />
|
||||
</RoomProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import * as React from 'react'
|
||||
import type { TldrawApp, TDUser, TDShape, TDBinding, TDDocument, TDAsset } from '@tldraw/tldraw'
|
||||
import { useRedo, useUndo, useRoom, useUpdateMyPresence } from '@liveblocks/react'
|
||||
import { LiveMap, LiveObject } from '@liveblocks/client'
|
||||
import type { TldrawApp, TDUser, TDShape, TDBinding, TDAsset } from '@tldraw/tldraw'
|
||||
import { useRedo, useUndo, useRoom, useUpdateMyPresence } from './liveblocks.config'
|
||||
import { LiveMap } from '@liveblocks/client'
|
||||
import type { Storage } from './liveblocks.config'
|
||||
|
||||
declare const window: Window & { app: TldrawApp }
|
||||
|
||||
|
@ -17,9 +18,9 @@ export function useMultiplayerState(roomId: string) {
|
|||
const onRedo = useRedo()
|
||||
const updateMyPresence = useUpdateMyPresence()
|
||||
|
||||
const rLiveShapes = React.useRef<LiveMap<string, TDShape>>()
|
||||
const rLiveBindings = React.useRef<LiveMap<string, TDBinding>>()
|
||||
const rLiveAssets = React.useRef<LiveMap<string, TDAsset>>()
|
||||
const rLiveShapes = React.useRef<Storage['shapes'] | undefined>()
|
||||
const rLiveBindings = React.useRef<Storage['bindings'] | undefined>()
|
||||
const rLiveAssets = React.useRef<Storage['assets'] | undefined>()
|
||||
|
||||
// Callbacks --------------
|
||||
|
||||
|
@ -108,17 +109,14 @@ export function useMultiplayerState(roomId: string) {
|
|||
|
||||
// Handle events from the room
|
||||
unsubs.push(
|
||||
room.subscribe(
|
||||
'event',
|
||||
(e: { connectionId: number; event: { name: string; userId: string } }) => {
|
||||
switch (e.event.name) {
|
||||
case 'exit': {
|
||||
app?.removeUser(e.event.userId)
|
||||
break
|
||||
}
|
||||
room.subscribe('event', (e) => {
|
||||
switch (e.event.name) {
|
||||
case 'exit': {
|
||||
app?.removeUser(e.event.userId)
|
||||
break
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
// Send the exit event when the tab closes
|
||||
|
@ -134,27 +132,27 @@ export function useMultiplayerState(roomId: string) {
|
|||
|
||||
// Setup the document's storage and subscriptions
|
||||
async function setupDocument() {
|
||||
const storage = await room.getStorage<any>()
|
||||
const storage = await room.getStorage()
|
||||
|
||||
// Initialize (get or create) shapes and bindings maps
|
||||
|
||||
let lShapes: LiveMap<string, TDShape> = storage.root.get('shapes')
|
||||
let lShapes = storage.root.get('shapes')
|
||||
if (!lShapes) {
|
||||
storage.root.set('shapes', new LiveMap<string, TDShape>())
|
||||
storage.root.set('shapes', new LiveMap())
|
||||
lShapes = storage.root.get('shapes')
|
||||
}
|
||||
rLiveShapes.current = lShapes
|
||||
|
||||
let lBindings: LiveMap<string, TDBinding> = storage.root.get('bindings')
|
||||
let lBindings = storage.root.get('bindings')
|
||||
if (!lBindings) {
|
||||
storage.root.set('bindings', new LiveMap<string, TDBinding>())
|
||||
storage.root.set('bindings', new LiveMap())
|
||||
lBindings = storage.root.get('bindings')
|
||||
}
|
||||
rLiveBindings.current = lBindings
|
||||
|
||||
let lAssets: LiveMap<string, TDAsset> = storage.root.get('assets')
|
||||
let lAssets = storage.root.get('assets')
|
||||
if (!lAssets) {
|
||||
storage.root.set('assets', new LiveMap<string, TDAsset>())
|
||||
storage.root.set('assets', new LiveMap())
|
||||
lAssets = storage.root.get('assets')
|
||||
}
|
||||
rLiveAssets.current = lAssets
|
||||
|
@ -168,11 +166,7 @@ export function useMultiplayerState(roomId: string) {
|
|||
// document was a single LiveObject named 'doc'. If we find a doc,
|
||||
// then we need to move the shapes and bindings over to the new structures
|
||||
// and then mark the doc as migrated.
|
||||
const doc = storage.root.get('doc') as LiveObject<{
|
||||
uuid: string
|
||||
document: TDDocument
|
||||
migrated?: boolean
|
||||
}>
|
||||
const doc = storage.root.get('doc')
|
||||
|
||||
// No doc? No problem. This was likely a newer document
|
||||
if (doc) {
|
||||
|
@ -218,7 +212,7 @@ export function useMultiplayerState(roomId: string) {
|
|||
stillAlive = false
|
||||
unsubs.forEach((unsub) => unsub())
|
||||
}
|
||||
}, [app])
|
||||
}, [app, room])
|
||||
|
||||
return {
|
||||
onUndo,
|
||||
|
|
59
examples/tldraw-example/src/multiplayer/liveblocks.config.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
import { createClient } from '@liveblocks/client'
|
||||
import type { BaseUserMeta, EnsureJson } from '@liveblocks/client'
|
||||
import { createRoomContext } from '@liveblocks/react'
|
||||
import { LiveMap, LiveObject } from '@liveblocks/client'
|
||||
import type { TDUser, TDShape, TDBinding, TDDocument } from '@tldraw/tldraw'
|
||||
|
||||
const client = createClient({
|
||||
publicApiKey: process.env.LIVEBLOCKS_PUBLIC_API_KEY || '',
|
||||
throttle: 100,
|
||||
})
|
||||
|
||||
// Presence represents the properties that will exist on every User in the
|
||||
// Liveblocks Room and that will automatically be synchronized between
|
||||
// connected Users. Accessible through the `user.presence` property.
|
||||
type Presence = {
|
||||
id?: string
|
||||
user: EnsureJson<TDUser>
|
||||
}
|
||||
|
||||
// Storage represents the shared document that persists in the Room, even after
|
||||
// all Users leave. All Live structures here are automatically kept in sync
|
||||
// between connected clients.
|
||||
export type Storage = {
|
||||
version: number
|
||||
doc: LiveObject<{
|
||||
uuid: string
|
||||
document: EnsureJson<TDDocument>
|
||||
migrated?: boolean
|
||||
}>
|
||||
bindings: LiveMap<string, EnsureJson<TDBinding>>
|
||||
shapes: LiveMap<string, EnsureJson<TDShape>>
|
||||
}
|
||||
|
||||
// Optionally, UserMeta represents static/readonly metadata on each User, as
|
||||
// provided by a potential custom Liveblocks auth backend. This isn't used for
|
||||
// TLDraw.
|
||||
//
|
||||
// type UserMeta = {
|
||||
// id?: string, // Accessible through `user.id`
|
||||
// info?: Json, // Accessible through `user.info`
|
||||
// };
|
||||
//
|
||||
type UserMeta = BaseUserMeta
|
||||
|
||||
// Custom Events that TLDraw broadcasts between connected users
|
||||
type RoomEvent =
|
||||
// | { name: 'click', x: number, y: number }
|
||||
// | { name: 'like', emoji: string }
|
||||
// | etc.
|
||||
{ name: 'exit'; userId: string }
|
||||
|
||||
const { RoomProvider, useRedo, useUndo, useRoom, useUpdateMyPresence } = createRoomContext<
|
||||
Presence,
|
||||
Storage,
|
||||
UserMeta,
|
||||
RoomEvent
|
||||
>(client)
|
||||
|
||||
export { RoomProvider, useRedo, useUndo, useRoom, useUpdateMyPresence }
|
|
@ -1,24 +1,16 @@
|
|||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import * as React from 'react'
|
||||
import { Tldraw } from '@tldraw/tldraw'
|
||||
import { createClient } from '@liveblocks/client'
|
||||
import { LiveblocksProvider, RoomProvider } from '@liveblocks/react'
|
||||
import { RoomProvider } from './liveblocks.config'
|
||||
import { useMultiplayerState } from './useMultiplayerState'
|
||||
|
||||
const client = createClient({
|
||||
publicApiKey: process.env.LIVEBLOCKS_PUBLIC_API_KEY || '',
|
||||
throttle: 100,
|
||||
})
|
||||
|
||||
const roomId = 'mp-test-8'
|
||||
|
||||
export function Multiplayer() {
|
||||
return (
|
||||
<LiveblocksProvider client={client}>
|
||||
<RoomProvider id={roomId}>
|
||||
<Editor roomId={roomId} />
|
||||
</RoomProvider>
|
||||
</LiveblocksProvider>
|
||||
<RoomProvider id={roomId}>
|
||||
<Editor roomId={roomId} />
|
||||
</RoomProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import * as React from 'react'
|
||||
import type { TldrawApp, TDUser, TDShape, TDBinding, TDDocument } from '@tldraw/tldraw'
|
||||
import { useRedo, useUndo, useRoom, useUpdateMyPresence } from '@liveblocks/react'
|
||||
import { LiveMap, LiveObject } from '@liveblocks/client'
|
||||
import type { TldrawApp, TDUser, TDShape, TDBinding } from '@tldraw/tldraw'
|
||||
import { LiveMap } from '@liveblocks/client'
|
||||
|
||||
import { useRedo, useUndo, useRoom, useUpdateMyPresence } from './liveblocks.config'
|
||||
import type { Storage } from './liveblocks.config'
|
||||
|
||||
declare const window: Window & { app: TldrawApp }
|
||||
|
||||
|
@ -17,8 +19,8 @@ export function useMultiplayerState(roomId: string) {
|
|||
const onRedo = useRedo()
|
||||
const updateMyPresence = useUpdateMyPresence()
|
||||
|
||||
const rLiveShapes = React.useRef<LiveMap<string, TDShape>>()
|
||||
const rLiveBindings = React.useRef<LiveMap<string, TDBinding>>()
|
||||
const rLiveShapes = React.useRef<Storage['shapes'] | undefined>()
|
||||
const rLiveBindings = React.useRef<Storage['bindings'] | undefined>()
|
||||
|
||||
// Callbacks --------------
|
||||
|
||||
|
@ -97,17 +99,14 @@ export function useMultiplayerState(roomId: string) {
|
|||
|
||||
// Handle events from the room
|
||||
unsubs.push(
|
||||
room.subscribe(
|
||||
'event',
|
||||
(e: { connectionId: number; event: { name: string; userId: string } }) => {
|
||||
switch (e.event.name) {
|
||||
case 'exit': {
|
||||
app?.removeUser(e.event.userId)
|
||||
break
|
||||
}
|
||||
room.subscribe('event', (e) => {
|
||||
switch (e.event.name) {
|
||||
case 'exit': {
|
||||
app?.removeUser(e.event.userId)
|
||||
break
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
// Send the exit event when the tab closes
|
||||
|
@ -123,20 +122,20 @@ export function useMultiplayerState(roomId: string) {
|
|||
|
||||
// Setup the document's storage and subscriptions
|
||||
async function setupDocument() {
|
||||
const storage = await room.getStorage<any>()
|
||||
const storage = await room.getStorage()
|
||||
|
||||
// Initialize (get or create) shapes and bindings maps
|
||||
|
||||
let lShapes: LiveMap<string, TDShape> = storage.root.get('shapes')
|
||||
let lShapes = storage.root.get('shapes')
|
||||
if (!lShapes) {
|
||||
storage.root.set('shapes', new LiveMap<string, TDShape>())
|
||||
storage.root.set('shapes', new LiveMap())
|
||||
lShapes = storage.root.get('shapes')
|
||||
}
|
||||
rLiveShapes.current = lShapes
|
||||
|
||||
let lBindings: LiveMap<string, TDBinding> = storage.root.get('bindings')
|
||||
let lBindings = storage.root.get('bindings')
|
||||
if (!lBindings) {
|
||||
storage.root.set('bindings', new LiveMap<string, TDBinding>())
|
||||
storage.root.set('bindings', new LiveMap())
|
||||
lBindings = storage.root.get('bindings')
|
||||
}
|
||||
rLiveBindings.current = lBindings
|
||||
|
@ -150,11 +149,7 @@ export function useMultiplayerState(roomId: string) {
|
|||
// document was a single LiveObject named 'doc'. If we find a doc,
|
||||
// then we need to move the shapes and bindings over to the new structures
|
||||
// and then mark the doc as migrated.
|
||||
const doc = storage.root.get('doc') as LiveObject<{
|
||||
uuid: string
|
||||
document: TDDocument
|
||||
migrated?: boolean
|
||||
}>
|
||||
const doc = storage.root.get('doc')
|
||||
|
||||
// No doc? No problem. This was likely a newer document
|
||||
if (doc) {
|
||||
|
@ -198,7 +193,7 @@ export function useMultiplayerState(roomId: string) {
|
|||
stillAlive = false
|
||||
unsubs.forEach((unsub) => unsub())
|
||||
}
|
||||
}, [app])
|
||||
}, [app, room])
|
||||
|
||||
return {
|
||||
onUndo,
|
||||
|
|
|
@ -11,7 +11,8 @@ interface UserProps {
|
|||
|
||||
export function User({ user }: UserProps) {
|
||||
const rCursor = React.useRef<SVGSVGElement>(null)
|
||||
useCursorAnimation(rCursor, user.point)
|
||||
useCursorAnimation(rCursor, user.point, user.session)
|
||||
|
||||
return (
|
||||
<svg
|
||||
ref={rCursor}
|
||||
|
|
|
@ -13,7 +13,7 @@ type Animation = {
|
|||
duration: number
|
||||
}
|
||||
|
||||
export function useCursorAnimation(ref: any, point: number[]) {
|
||||
export function useCursorAnimation(ref: any, point: number[], skip = false) {
|
||||
const rState = React.useRef<AnimationState>('idle')
|
||||
const rPrevPoint = React.useRef(point)
|
||||
const rQueue = React.useRef<Animation[]>([])
|
||||
|
@ -22,9 +22,19 @@ export function useCursorAnimation(ref: any, point: number[]) {
|
|||
const rTimeoutId = React.useRef<any>(0)
|
||||
const [spline] = React.useState(() => new Spline())
|
||||
|
||||
// Animate an animation
|
||||
const animateNext = React.useCallback(
|
||||
(animation: Animation) => {
|
||||
// When the point changes, add a new animation
|
||||
React.useLayoutEffect(() => {
|
||||
if (skip) {
|
||||
const elm = ref.current
|
||||
if (!elm) return
|
||||
|
||||
rState.current = 'stopped'
|
||||
rPrevPoint.current = point
|
||||
elm.style.setProperty('transform', `translate(${point[0]}px, ${point[1]}px)`)
|
||||
return
|
||||
}
|
||||
|
||||
const animateNext = (animation: Animation) => {
|
||||
const start = performance.now()
|
||||
function loop() {
|
||||
const t = (performance.now() - start) / animation.duration
|
||||
|
@ -50,19 +60,18 @@ export function useCursorAnimation(ref: any, point: number[]) {
|
|||
}
|
||||
}
|
||||
loop()
|
||||
},
|
||||
[spline]
|
||||
)
|
||||
}
|
||||
|
||||
// When the point changes, add a new animation
|
||||
React.useLayoutEffect(() => {
|
||||
const now = performance.now()
|
||||
|
||||
if (rState.current === 'stopped') {
|
||||
rTimestamp.current = now
|
||||
rPrevPoint.current = point
|
||||
spline.clear()
|
||||
}
|
||||
|
||||
spline.addPoint(point)
|
||||
|
||||
const animation: Animation = {
|
||||
distance: spline.totalLength,
|
||||
curve: spline.points.length > 3,
|
||||
|
@ -72,8 +81,9 @@ export function useCursorAnimation(ref: any, point: number[]) {
|
|||
timeStamp: now,
|
||||
duration: Math.min(now - rTimestamp.current, 300),
|
||||
}
|
||||
rPrevPoint.current = point
|
||||
|
||||
rTimestamp.current = now
|
||||
|
||||
switch (rState.current) {
|
||||
case 'stopped': {
|
||||
rPrevPoint.current = point
|
||||
|
@ -86,12 +96,21 @@ export function useCursorAnimation(ref: any, point: number[]) {
|
|||
break
|
||||
}
|
||||
case 'animating': {
|
||||
rPrevPoint.current = point
|
||||
rQueue.current.push(animation)
|
||||
break
|
||||
}
|
||||
}
|
||||
return () => clearTimeout(rTimeoutId.current)
|
||||
}, [point, spline])
|
||||
}, [skip, point, spline])
|
||||
|
||||
// React.useLayoutEffect(() => {
|
||||
// const cursor = rCursor.current
|
||||
// if (!cursor) return
|
||||
|
||||
// const [x, y] = user.point
|
||||
// cursor.style.transform = `translate(${x}px, ${y}px)`
|
||||
// }, [skip, point])
|
||||
}
|
||||
|
||||
class Spline {
|
||||
|
@ -126,6 +145,11 @@ class Spline {
|
|||
q2 = 3 * ttt - 5 * tt + 2,
|
||||
q3 = -3 * ttt + 4 * tt + t,
|
||||
q4 = ttt - tt
|
||||
|
||||
if (!(points[p0] && points[p1] && points[p2] && points[p3])) {
|
||||
return [0, 0]
|
||||
}
|
||||
|
||||
return [
|
||||
0.5 * (points[p0][0] * q1 + points[p1][0] * q2 + points[p2][0] * q3 + points[p3][0] * q4),
|
||||
0.5 * (points[p0][1] * q1 + points[p1][1] * q2 + points[p2][1] * q3 + points[p3][1] * q4),
|
||||
|
|
|
@ -52,6 +52,7 @@ export interface TLUser<T extends TLShape> {
|
|||
color: string
|
||||
point: number[]
|
||||
selectedIds: string[]
|
||||
session?: boolean
|
||||
}
|
||||
|
||||
export type TLUsers<T extends TLShape, U extends TLUser<T> = TLUser<T>> = Record<string, U>
|
||||
|
|
|
@ -138,6 +138,8 @@ export function Tldraw({
|
|||
onAssetCreate,
|
||||
onAssetDelete,
|
||||
onAssetUpload,
|
||||
onSessionStart,
|
||||
onSessionEnd,
|
||||
onExport,
|
||||
}: TldrawProps) {
|
||||
const [sId, setSId] = React.useState(id)
|
||||
|
@ -164,6 +166,8 @@ export function Tldraw({
|
|||
onAssetDelete,
|
||||
onAssetCreate,
|
||||
onAssetUpload,
|
||||
onSessionStart,
|
||||
onSessionEnd,
|
||||
})
|
||||
return app
|
||||
})
|
||||
|
@ -192,6 +196,8 @@ export function Tldraw({
|
|||
onAssetCreate,
|
||||
onAssetUpload,
|
||||
onExport,
|
||||
onSessionStart,
|
||||
onSessionEnd,
|
||||
})
|
||||
|
||||
setSId(id)
|
||||
|
@ -262,6 +268,8 @@ export function Tldraw({
|
|||
onAssetCreate,
|
||||
onAssetUpload,
|
||||
onExport,
|
||||
onSessionStart,
|
||||
onSessionEnd,
|
||||
}
|
||||
}, [
|
||||
onMount,
|
||||
|
@ -284,6 +292,8 @@ export function Tldraw({
|
|||
onAssetCreate,
|
||||
onAssetUpload,
|
||||
onExport,
|
||||
onSessionStart,
|
||||
onSessionEnd,
|
||||
])
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import * as React from 'react'
|
||||
|
||||
export function MultiplayerIcon2() {
|
||||
return (
|
||||
<svg
|
||||
width="15"
|
||||
height="15"
|
||||
viewBox="0 0 15 15"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
>
|
||||
<path d="M1.36482 4.71089C1.36482 3.21371 2.57853 2 4.07572 2C5.5729 2 6.78661 3.21371 6.78661 4.71089C6.78661 5.76842 6.18106 6.68452 5.29779 7.13136C5.80465 7.24611 6.26407 7.44186 6.66028 7.73182C7.18674 8.11711 7.57215 8.64722 7.81073 9.30983C7.81452 9.30606 7.81832 9.3023 7.82212 9.29855C8.30448 8.82317 8.91325 8.52463 9.60905 8.37275C8.68812 7.922 8.05394 6.97569 8.05394 5.88126C8.05394 4.35017 9.29513 3.10898 10.8262 3.10898C12.3573 3.10898 13.5985 4.35017 13.5985 5.88126C13.5985 6.97561 12.9644 7.92186 12.0436 8.37265C12.7396 8.52449 13.3486 8.82306 13.831 9.29854C14.5795 10.0361 14.9481 11.1249 14.9481 12.5176C14.9481 12.7799 14.7354 12.9926 14.473 12.9926C14.2107 12.9926 13.9981 12.7799 13.9981 12.5175C13.9981 11.2848 13.6738 10.4774 13.1642 9.97518C12.6532 9.4716 11.8802 9.20024 10.8266 9.20024C9.77294 9.20024 8.99993 9.4716 8.48896 9.97518C7.97939 10.4774 7.65513 11.2848 7.65513 12.5176C7.65513 12.7799 7.44247 12.9926 7.18013 12.9926C6.9178 12.9926 6.70513 12.7799 6.70513 12.5176C6.70513 11.6734 6.84053 10.941 7.11384 10.3307C7.0922 10.2895 7.0763 10.2444 7.06737 10.1962C6.91739 9.38749 6.57392 8.84586 6.09923 8.49845C5.61626 8.14499 4.94481 7.95427 4.07574 7.95427C3.05232 7.95427 2.30368 8.21784 1.80952 8.70485C1.31675 9.19047 1.00176 9.97257 1.00176 11.1702C1.00176 11.4326 0.789093 11.6452 0.526758 11.6452C0.264423 11.6452 0.0517578 11.4326 0.0517578 11.1702C0.0517578 9.81263 0.411052 8.74925 1.14268 8.02821C1.60624 7.57137 2.18892 7.28191 2.85378 7.13142C1.97043 6.68461 1.36482 5.76848 1.36482 4.71089ZM4.07572 2.95C3.1032 2.95 2.31482 3.73838 2.31482 4.71089C2.31482 5.68341 3.1032 6.47178 4.07572 6.47178C5.04823 6.47178 5.83661 5.68341 5.83661 4.71089C5.83661 3.73838 5.04823 2.95 4.07572 2.95ZM10.8262 4.05898C9.8198 4.05898 9.00394 4.87484 9.00394 5.88126C9.00394 6.88768 9.8198 7.70355 10.8262 7.70355C11.8326 7.70355 12.6485 6.88768 12.6485 5.88126C12.6485 4.87484 11.8326 4.05898 10.8262 4.05898Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
|
@ -1,14 +1,14 @@
|
|||
import * as React from 'react'
|
||||
import { CheckIcon, ClipboardIcon, CursorArrowIcon, PersonIcon } from '@radix-ui/react-icons'
|
||||
import { CheckIcon, ClipboardIcon } from '@radix-ui/react-icons'
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import { useTldrawApp } from '~hooks'
|
||||
import { DMItem, DMContent, DMDivider, DMTriggerIcon } from '~components/Primitives/DropdownMenu'
|
||||
import { SmallIcon } from '~components/Primitives/SmallIcon'
|
||||
import { MultiplayerIcon } from '~components/Primitives/icons'
|
||||
import { TDAssetType, TDSnapshot } from '~types'
|
||||
import { TLDR } from '~state/TLDR'
|
||||
import { Utils } from '@tldraw/core'
|
||||
import { FormattedMessage } from 'react-intl'
|
||||
import { MultiplayerIcon2 } from '~components/Primitives/icons/MultiplayerIcon2'
|
||||
|
||||
const roomSelector = (state: TDSnapshot) => state.room
|
||||
|
||||
|
@ -96,7 +96,7 @@ export const MultiplayerMenu = React.memo(function MultiplayerMenu() {
|
|||
return (
|
||||
<DropdownMenu.Root dir="ltr">
|
||||
<DMTriggerIcon id="TD-MultiplayerMenuIcon" isActive={!!room}>
|
||||
<PersonIcon />
|
||||
<MultiplayerIcon2 />
|
||||
</DMTriggerIcon>
|
||||
<DMContent variant="menu" align="start" id="TD-MultiplayerMenu">
|
||||
<DMItem id="TD-Multiplayer-CopyInviteLink" onClick={handleCopySelect} disabled={!room}>
|
||||
|
|
|
@ -126,11 +126,11 @@ export class StateManager<T extends Record<string, any>> {
|
|||
/**
|
||||
* Save the current state to indexdb.
|
||||
*/
|
||||
protected persist = (id?: string): void | Promise<void> => {
|
||||
protected persist = (patch: Patch<T>, id?: string): void | Promise<void> => {
|
||||
if (this._status !== 'ready') return
|
||||
|
||||
if (this.onPersist) {
|
||||
this.onPersist(this._state, id)
|
||||
this.onPersist(this._state, patch, id)
|
||||
}
|
||||
|
||||
if (this._idbId) {
|
||||
|
@ -201,7 +201,7 @@ export class StateManager<T extends Record<string, any>> {
|
|||
patchState = (patch: Patch<T>, id?: string): this => {
|
||||
this.applyPatch(patch, id)
|
||||
if (this.onPatch) {
|
||||
this.onPatch(this._state, id)
|
||||
this.onPatch(this._state, patch, id)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
@ -240,8 +240,8 @@ export class StateManager<T extends Record<string, any>> {
|
|||
this.stack.push({ ...command, id })
|
||||
this.pointer = this.stack.length - 1
|
||||
this.applyPatch(command.after, id)
|
||||
if (this.onCommand) this.onCommand(this._state, id)
|
||||
this.persist(id)
|
||||
if (this.onCommand) this.onCommand(this._state, command, id)
|
||||
this.persist(command.after, id)
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -264,17 +264,17 @@ export class StateManager<T extends Record<string, any>> {
|
|||
/**
|
||||
* A callback fired when a patch is applied.
|
||||
*/
|
||||
public onPatch?: (state: T, id?: string) => void
|
||||
public onPatch?: (state: T, patch: Patch<T>, id?: string) => void
|
||||
|
||||
/**
|
||||
* A callback fired when a patch is applied.
|
||||
*/
|
||||
public onCommand?: (state: T, id?: string) => void
|
||||
public onCommand?: (state: T, command: Command<T>, id?: string) => void
|
||||
|
||||
/**
|
||||
* A callback fired when the state is persisted.
|
||||
*/
|
||||
public onPersist?: (state: T, id?: string) => void
|
||||
public onPersist?: (state: T, patch: Patch<T>, id?: string) => void
|
||||
|
||||
/**
|
||||
* A callback fired when the state is replaced.
|
||||
|
@ -311,7 +311,7 @@ export class StateManager<T extends Record<string, any>> {
|
|||
this._state = this.initialState
|
||||
this.store.setState(this._state, true)
|
||||
this.resetHistory()
|
||||
this.persist('reset')
|
||||
this.persist({}, 'reset')
|
||||
if (this.onStateDidChange) {
|
||||
this.onStateDidChange(this._state, 'reset')
|
||||
}
|
||||
|
@ -357,7 +357,7 @@ export class StateManager<T extends Record<string, any>> {
|
|||
const command = this.stack[this.pointer]
|
||||
this.pointer--
|
||||
this.applyPatch(command.before, `undo`)
|
||||
this.persist('undo')
|
||||
this.persist(command.before, 'undo')
|
||||
}
|
||||
if (this.onUndo) this.onUndo(this._state)
|
||||
return this
|
||||
|
@ -372,7 +372,7 @@ export class StateManager<T extends Record<string, any>> {
|
|||
this.pointer++
|
||||
const command = this.stack[this.pointer]
|
||||
this.applyPatch(command.after, 'redo')
|
||||
this.persist('undo')
|
||||
this.persist(command.after, 'undo')
|
||||
}
|
||||
if (this.onRedo) this.onRedo(this._state)
|
||||
return this
|
||||
|
|
|
@ -41,6 +41,7 @@ import {
|
|||
TDExport,
|
||||
ArrowShape,
|
||||
TDExportType,
|
||||
TldrawPatch,
|
||||
} from '~types'
|
||||
import {
|
||||
migrate,
|
||||
|
@ -125,11 +126,11 @@ export interface TDCallbacks {
|
|||
/**
|
||||
* (optional) A callback to run when the state is patched.
|
||||
*/
|
||||
onPatch?: (app: TldrawApp, reason?: string) => void
|
||||
onPatch?: (app: TldrawApp, patch: TldrawPatch, reason?: string) => void
|
||||
/**
|
||||
* (optional) A callback to run when the state is changed with a command.
|
||||
*/
|
||||
onCommand?: (app: TldrawApp, reason?: string) => void
|
||||
onCommand?: (app: TldrawApp, command: TldrawCommand, reason?: string) => void
|
||||
/**
|
||||
* (optional) A callback to run when the state is persisted.
|
||||
*/
|
||||
|
@ -149,7 +150,8 @@ export interface TDCallbacks {
|
|||
app: TldrawApp,
|
||||
shapes: Record<string, TDShape | undefined>,
|
||||
bindings: Record<string, TDBinding | undefined>,
|
||||
assets: Record<string, TDAsset | undefined>
|
||||
assets: Record<string, TDAsset | undefined>,
|
||||
addToHistory: boolean
|
||||
) => void
|
||||
/**
|
||||
* (optional) A callback to run when the user creates a new project.
|
||||
|
@ -510,14 +512,58 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
return next
|
||||
}
|
||||
|
||||
onPatch = (app: TDSnapshot, id?: string) => {
|
||||
this.callbacks.onPatch?.(this, id)
|
||||
private broadcastPatch = (patch: TldrawPatch, addToHistory: boolean) => {
|
||||
const changedShapes: Record<string, TDShape | undefined> = {}
|
||||
const changedBindings: Record<string, TDBinding | undefined> = {}
|
||||
const changedAssets: Record<string, TDAsset | undefined> = {}
|
||||
|
||||
const shapes = patch?.document?.pages?.[this.currentPageId]?.shapes
|
||||
const bindings = patch?.document?.pages?.[this.currentPageId]?.bindings
|
||||
const assets = patch?.document?.assets
|
||||
|
||||
if (shapes) {
|
||||
Object.keys(shapes).forEach((id) => {
|
||||
changedShapes[id!] = this.getShape(id, this.currentPageId)
|
||||
})
|
||||
}
|
||||
|
||||
if (bindings) {
|
||||
Object.keys(bindings).forEach((id) => {
|
||||
changedBindings[id] = this.getBinding(id, this.currentPageId)
|
||||
})
|
||||
}
|
||||
|
||||
if (assets) {
|
||||
Object.keys(assets).forEach((id) => {
|
||||
changedAssets[id] = this.document.assets[id]
|
||||
})
|
||||
}
|
||||
|
||||
this.callbacks.onChangePage?.(this, changedShapes, changedBindings, changedAssets, addToHistory)
|
||||
}
|
||||
|
||||
onCommand = (app: TDSnapshot, id?: string) => {
|
||||
onPatch = (state: TDSnapshot, patch: TldrawPatch, id?: string) => {
|
||||
if (
|
||||
(this.callbacks.onChangePage && patch?.document?.pages?.[this.currentPageId]) ||
|
||||
patch?.document?.assets
|
||||
) {
|
||||
if (
|
||||
this.session &&
|
||||
this.session.type !== SessionType.Brush &&
|
||||
this.session.type !== SessionType.Erase &&
|
||||
this.session.type !== SessionType.Draw
|
||||
) {
|
||||
this.broadcastPatch(patch, false)
|
||||
}
|
||||
}
|
||||
|
||||
this.callbacks.onPatch?.(this, patch, id)
|
||||
}
|
||||
|
||||
onCommand = (state: TDSnapshot, command: TldrawCommand, id?: string) => {
|
||||
this.clearSelectHistory()
|
||||
this.isDirty = true
|
||||
this.callbacks.onCommand?.(this, id)
|
||||
this.callbacks.onCommand?.(this, command, id)
|
||||
}
|
||||
|
||||
onReplace = () => {
|
||||
|
@ -535,12 +581,11 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
this.callbacks.onRedo?.(this)
|
||||
}
|
||||
|
||||
onPersist = () => {
|
||||
onPersist = (state: TDSnapshot, patch: TldrawPatch) => {
|
||||
// If we are part of a room, send our changes to the server
|
||||
if (this.callbacks.onChangePage) {
|
||||
this.broadcastPageChanges()
|
||||
}
|
||||
|
||||
this.callbacks.onPersist?.(this)
|
||||
this.broadcastPatch(patch, true)
|
||||
}
|
||||
|
||||
private prevSelectedIds = this.selectedIds
|
||||
|
@ -550,13 +595,14 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
* @param state
|
||||
* @param id
|
||||
*/
|
||||
protected onStateDidChange = (_app: TDSnapshot, id?: string): void => {
|
||||
protected onStateDidChange = (_state: TDSnapshot, id?: string): void => {
|
||||
this.callbacks.onChange?.(this, id)
|
||||
|
||||
if (this.room && this.selectedIds !== this.prevSelectedIds) {
|
||||
this.callbacks.onChangePresence?.(this, {
|
||||
...this.room.users[this.room.userId],
|
||||
selectedIds: this.selectedIds,
|
||||
session: !!this.session,
|
||||
})
|
||||
this.prevSelectedIds = this.selectedIds
|
||||
}
|
||||
|
@ -569,69 +615,69 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
private prevBindings = this.page.bindings
|
||||
private prevAssets = this.document.assets
|
||||
|
||||
private broadcastPageChanges = () => {
|
||||
const visited = new Set<string>()
|
||||
// private broadcastPageChanges = () => {
|
||||
// const visited = new Set<string>()
|
||||
|
||||
const changedShapes: Record<string, TDShape | undefined> = {}
|
||||
const changedBindings: Record<string, TDBinding | undefined> = {}
|
||||
const changedAssets: Record<string, TDAsset | undefined> = {}
|
||||
// const changedShapes: Record<string, TDShape | undefined> = {}
|
||||
// const changedBindings: Record<string, TDBinding | undefined> = {}
|
||||
// const changedAssets: Record<string, TDAsset | undefined> = {}
|
||||
|
||||
this.shapes.forEach((shape) => {
|
||||
visited.add(shape.id)
|
||||
if (this.prevShapes[shape.id] !== shape) {
|
||||
changedShapes[shape.id] = shape
|
||||
}
|
||||
})
|
||||
// this.shapes.forEach((shape) => {
|
||||
// visited.add(shape.id)
|
||||
// if (this.prevShapes[shape.id] !== shape) {
|
||||
// changedShapes[shape.id] = shape
|
||||
// }
|
||||
// })
|
||||
|
||||
Object.keys(this.prevShapes)
|
||||
.filter((id) => !visited.has(id))
|
||||
.forEach((id) => {
|
||||
// After visiting all the current shapes, if we haven't visited a
|
||||
// previously present shape, then it was deleted
|
||||
changedShapes[id] = undefined
|
||||
})
|
||||
// Object.keys(this.prevShapes)
|
||||
// .filter((id) => !visited.has(id))
|
||||
// .forEach((id) => {
|
||||
// // After visiting all the current shapes, if we haven't visited a
|
||||
// // previously present shape, then it was deleted
|
||||
// changedShapes[id] = undefined
|
||||
// })
|
||||
|
||||
this.bindings.forEach((binding) => {
|
||||
visited.add(binding.id)
|
||||
if (this.prevBindings[binding.id] !== binding) {
|
||||
changedBindings[binding.id] = binding
|
||||
}
|
||||
})
|
||||
// this.bindings.forEach((binding) => {
|
||||
// visited.add(binding.id)
|
||||
// if (this.prevBindings[binding.id] !== binding) {
|
||||
// changedBindings[binding.id] = binding
|
||||
// }
|
||||
// })
|
||||
|
||||
Object.keys(this.prevBindings)
|
||||
.filter((id) => !visited.has(id))
|
||||
.forEach((id) => {
|
||||
// After visiting all the current bindings, if we haven't visited a
|
||||
// previously present shape, then it was deleted
|
||||
changedBindings[id] = undefined
|
||||
})
|
||||
// Object.keys(this.prevBindings)
|
||||
// .filter((id) => !visited.has(id))
|
||||
// .forEach((id) => {
|
||||
// // After visiting all the current bindings, if we haven't visited a
|
||||
// // previously present shape, then it was deleted
|
||||
// changedBindings[id] = undefined
|
||||
// })
|
||||
|
||||
this.assets.forEach((asset) => {
|
||||
visited.add(asset.id)
|
||||
if (this.prevAssets[asset.id] !== asset) {
|
||||
changedAssets[asset.id] = asset
|
||||
}
|
||||
})
|
||||
// this.assets.forEach((asset) => {
|
||||
// visited.add(asset.id)
|
||||
// if (this.prevAssets[asset.id] !== asset) {
|
||||
// changedAssets[asset.id] = asset
|
||||
// }
|
||||
// })
|
||||
|
||||
Object.keys(this.prevAssets)
|
||||
.filter((id) => !visited.has(id))
|
||||
.forEach((id) => {
|
||||
changedAssets[id] = undefined
|
||||
})
|
||||
// Object.keys(this.prevAssets)
|
||||
// .filter((id) => !visited.has(id))
|
||||
// .forEach((id) => {
|
||||
// changedAssets[id] = undefined
|
||||
// })
|
||||
|
||||
// Only trigger update if shapes or bindings have changed
|
||||
if (
|
||||
Object.keys(changedBindings).length > 0 ||
|
||||
Object.keys(changedShapes).length > 0 ||
|
||||
Object.keys(changedAssets).length > 0
|
||||
) {
|
||||
this.justSent = true
|
||||
this.callbacks.onChangePage?.(this, changedShapes, changedBindings, changedAssets)
|
||||
this.prevShapes = this.page.shapes
|
||||
this.prevBindings = this.page.bindings
|
||||
this.prevAssets = this.document.assets
|
||||
}
|
||||
}
|
||||
// // Only trigger update if shapes or bindings have changed
|
||||
// if (
|
||||
// Object.keys(changedBindings).length > 0 ||
|
||||
// Object.keys(changedShapes).length > 0 ||
|
||||
// Object.keys(changedAssets).length > 0
|
||||
// ) {
|
||||
// this.justSent = true
|
||||
// this.callbacks.onChangePage?.(this, changedShapes, changedBindings, changedAssets,)
|
||||
// this.prevShapes = this.page.shapes
|
||||
// this.prevBindings = this.page.bindings
|
||||
// this.prevAssets = this.document.assets
|
||||
// }
|
||||
// }
|
||||
|
||||
getReservedContent = (coreReservedIds: string[], pageId = this.currentPageId) => {
|
||||
const { bindings } = this.document.pages[pageId]
|
||||
|
@ -928,10 +974,22 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
* Set or clear the editing id
|
||||
* @param id [string]
|
||||
*/
|
||||
setEditingId = (id?: string) => {
|
||||
setEditingId = (id?: string, isCreating = false) => {
|
||||
if (this.readOnly) return
|
||||
|
||||
if (id) {
|
||||
// Start a new editing session
|
||||
this.startSession(SessionType.Edit, id, isCreating)
|
||||
} else {
|
||||
// If we're clearing the editing id and we don't have one, bail
|
||||
if (!this.pageState.editingId) return
|
||||
|
||||
// If we're clearing the editing id and we do have one, complete the session
|
||||
this.completeSession()
|
||||
}
|
||||
|
||||
this.editingStartTime = performance.now()
|
||||
|
||||
this.patchState(
|
||||
{
|
||||
document: {
|
||||
|
@ -978,15 +1036,15 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
): this => {
|
||||
if (this.session) return this
|
||||
|
||||
this.patchState(
|
||||
{
|
||||
settings: {
|
||||
[name]: typeof value === 'function' ? value(this.settings[name] as V) : value,
|
||||
},
|
||||
const patch = {
|
||||
settings: {
|
||||
[name]: typeof value === 'function' ? value(this.settings[name] as V) : value,
|
||||
},
|
||||
`settings:${name}`
|
||||
)
|
||||
this.persist()
|
||||
}
|
||||
|
||||
this.patchState(patch, `settings:${name}`)
|
||||
|
||||
this.persist(patch)
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -995,15 +1053,15 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
*/
|
||||
toggleFocusMode = (): this => {
|
||||
if (this.session) return this
|
||||
this.patchState(
|
||||
{
|
||||
settings: {
|
||||
isFocusMode: !this.settings.isFocusMode,
|
||||
},
|
||||
const patch = {
|
||||
settings: {
|
||||
isFocusMode: !this.settings.isFocusMode,
|
||||
},
|
||||
`settings:toggled_focus_mode`
|
||||
)
|
||||
this.persist()
|
||||
}
|
||||
|
||||
this.patchState(patch, `settings:toggled_focus_mode`)
|
||||
|
||||
this.persist(patch)
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -1012,15 +1070,13 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
*/
|
||||
togglePenMode = (): this => {
|
||||
if (this.session) return this
|
||||
this.patchState(
|
||||
{
|
||||
settings: {
|
||||
isPenMode: !this.settings.isPenMode,
|
||||
},
|
||||
const patch = {
|
||||
settings: {
|
||||
isPenMode: !this.settings.isPenMode,
|
||||
},
|
||||
`settings:toggled_pen_mode`
|
||||
)
|
||||
this.persist()
|
||||
}
|
||||
this.patchState(patch, `settings:toggled_pen_mode`)
|
||||
this.persist(patch)
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -1029,11 +1085,9 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
*/
|
||||
toggleDarkMode = (): this => {
|
||||
if (this.session) return this
|
||||
this.patchState(
|
||||
{ settings: { isDarkMode: !this.settings.isDarkMode } },
|
||||
`settings:toggled_dark_mode`
|
||||
)
|
||||
this.persist()
|
||||
const patch = { settings: { isDarkMode: !this.settings.isDarkMode } }
|
||||
this.patchState(patch, `settings:toggled_dark_mode`)
|
||||
this.persist(patch)
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -1042,11 +1096,9 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
*/
|
||||
toggleZoomSnap = () => {
|
||||
if (this.session) return this
|
||||
this.patchState(
|
||||
{ settings: { isZoomSnap: !this.settings.isZoomSnap } },
|
||||
`settings:toggled_zoom_snap`
|
||||
)
|
||||
this.persist()
|
||||
const patch = { settings: { isZoomSnap: !this.settings.isZoomSnap } }
|
||||
this.patchState(patch, `settings:toggled_zoom_snap`)
|
||||
this.persist(patch)
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -1055,11 +1107,9 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
*/
|
||||
toggleDebugMode = () => {
|
||||
if (this.session) return this
|
||||
this.patchState(
|
||||
{ settings: { isDebugMode: !this.settings.isDebugMode } },
|
||||
`settings:toggled_debug`
|
||||
)
|
||||
this.persist()
|
||||
const patch = { settings: { isDebugMode: !this.settings.isDebugMode } }
|
||||
this.patchState(patch, `settings:toggled_debug`)
|
||||
this.persist(patch)
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -1067,8 +1117,9 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
* Toggles the state if menu is opened
|
||||
*/
|
||||
setMenuOpen = (isOpen: boolean): this => {
|
||||
this.patchState({ appState: { isMenuOpen: isOpen } }, 'ui:toggled_menu_opened')
|
||||
this.persist()
|
||||
const patch = { appState: { isMenuOpen: isOpen } }
|
||||
this.patchState(patch, 'ui:toggled_menu_opened')
|
||||
this.persist(patch)
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -1076,8 +1127,9 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
* Toggles the state if something is loading
|
||||
*/
|
||||
setIsLoading = (isLoading: boolean): this => {
|
||||
this.patchState({ appState: { isLoading } }, 'ui:toggled_is_loading')
|
||||
this.persist()
|
||||
const patch = { appState: { isLoading } }
|
||||
this.patchState(patch, 'ui:toggled_is_loading')
|
||||
this.persist(patch)
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -1103,8 +1155,9 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
*/
|
||||
toggleGrid = (): this => {
|
||||
if (this.session) return this
|
||||
this.patchState({ settings: { showGrid: !this.settings.showGrid } }, 'settings:toggled_grid')
|
||||
this.persist()
|
||||
const patch = { settings: { showGrid: !this.settings.showGrid } }
|
||||
this.patchState(patch, 'settings:toggled_grid')
|
||||
this.persist(patch)
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -1175,7 +1228,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
this.resetHistory()
|
||||
.clearSelectHistory()
|
||||
.loadDocument(migrate(TldrawApp.defaultDocument, TldrawApp.version))
|
||||
.persist()
|
||||
.persist({})
|
||||
|
||||
return this
|
||||
}
|
||||
|
@ -1420,7 +1473,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
this.fileSystemHandle
|
||||
)
|
||||
this.fileSystemHandle = fileHandle
|
||||
this.persist()
|
||||
this.persist({})
|
||||
this.isDirty = false
|
||||
} catch (e: any) {
|
||||
// Likely cancelled
|
||||
|
@ -1436,7 +1489,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
try {
|
||||
const fileHandle = await saveToFileSystem(this.document, null)
|
||||
this.fileSystemHandle = fileHandle
|
||||
this.persist()
|
||||
this.persist({})
|
||||
this.isDirty = false
|
||||
} catch (e: any) {
|
||||
// Likely cancelled
|
||||
|
@ -1462,11 +1515,11 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
this.loadDocument(document)
|
||||
this.fileSystemHandle = fileHandle
|
||||
this.zoomToFit()
|
||||
this.persist()
|
||||
this.persist({})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
this.persist()
|
||||
this.persist({})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1482,7 +1535,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
this.persist()
|
||||
this.persist({})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2700,28 +2753,28 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
|
||||
/**
|
||||
* Start a new session.
|
||||
* @param session The new session
|
||||
* @param type The session type
|
||||
* @param args arguments of the session's start method.
|
||||
*/
|
||||
startSession = <T extends SessionType>(type: T, ...args: SessionArgsOfType<T>): this => {
|
||||
if (this.readOnly && type !== SessionType.Brush) return this
|
||||
|
||||
if (this.session) {
|
||||
TLDR.warn(`Already in a session! (${this.session.constructor.name})`)
|
||||
this.cancelSession()
|
||||
}
|
||||
|
||||
const Session = getSession(type)
|
||||
|
||||
// @ts-ignore
|
||||
const Session = getSession(type) as any
|
||||
this.session = new Session(this, ...args)
|
||||
|
||||
const result = this.session.start()
|
||||
const result = this.session!.start()
|
||||
|
||||
if (result) {
|
||||
this.patchState(result, `session:start_${this.session.constructor.name}`)
|
||||
this.callbacks.onSessionStart?.(this, this.session.constructor.name)
|
||||
this.patchState(result, `session:start_${this.session!.constructor.name}`)
|
||||
}
|
||||
|
||||
this.callbacks.onSessionStart?.(this, this.session!.constructor.name)
|
||||
|
||||
return this
|
||||
// return this.setStatus(this.session.status)
|
||||
}
|
||||
|
@ -2754,9 +2807,12 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
|
||||
if (result) {
|
||||
this.patchState(result, `session:cancel:${session.constructor.name}`)
|
||||
this.callbacks.onSessionEnd?.(this, session.constructor.name)
|
||||
}
|
||||
|
||||
this.setEditingId()
|
||||
|
||||
this.callbacks.onSessionEnd?.(this, session.constructor.name)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -2768,6 +2824,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
const { session } = this
|
||||
|
||||
if (!session) return this
|
||||
|
||||
this.session = undefined
|
||||
const result = session.complete()
|
||||
|
||||
|
@ -2791,9 +2848,6 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
},
|
||||
`session:complete:${session.constructor.name}`
|
||||
)
|
||||
|
||||
this.callbacks.onSessionEnd?.(this, session.constructor.name)
|
||||
return this
|
||||
} else if ('after' in result) {
|
||||
// Session ended with a command
|
||||
|
||||
|
@ -2870,6 +2924,8 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
)
|
||||
}
|
||||
|
||||
this.callbacks.onSessionEnd?.(this, session.constructor.name)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -2910,7 +2966,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
)
|
||||
}
|
||||
|
||||
createTextShapeAtPoint(point: number[], id?: string): this {
|
||||
createTextShapeAtPoint(point: number[], id?: string, patch?: boolean): this {
|
||||
const {
|
||||
shapes,
|
||||
appState: { currentPageId, currentStyle },
|
||||
|
@ -2935,8 +2991,14 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
|
||||
const bounds = Text.getBounds(newShape)
|
||||
newShape.point = Vec.sub(newShape.point, [bounds.width / 2, bounds.height / 2])
|
||||
this.createShapes(newShape)
|
||||
this.setEditingId(newShape.id)
|
||||
|
||||
if (patch) {
|
||||
this.patchCreate([TLDR.getShapeUtil(newShape.type).create(newShape)])
|
||||
} else {
|
||||
this.createShapes(newShape)
|
||||
}
|
||||
|
||||
this.setEditingId(newShape.id, true)
|
||||
|
||||
return this
|
||||
}
|
||||
|
@ -3668,6 +3730,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
this.callbacks.onChangePresence?.(this, {
|
||||
...users[userId],
|
||||
point: this.getPagePoint(info.point),
|
||||
session: !!this.session,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -3894,7 +3957,11 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
}
|
||||
|
||||
onShapeChange = (shape: { id: string } & Partial<TDShape>) => {
|
||||
this.updateShapes(shape)
|
||||
const pageShapes = this.document.pages[this.currentPageId].shapes
|
||||
const shapeToUpdate = { ...(pageShapes[shape.id] as any), ...shape }
|
||||
const patch = Commands.updateShapes(this, [shapeToUpdate], this.currentPageId).after
|
||||
return this.patchState(patch, 'patched_shapes')
|
||||
// this.updateShapes(shape)
|
||||
}
|
||||
|
||||
onShapeBlur = () => {
|
||||
|
|
|
@ -227,7 +227,7 @@ TldrawTestApp {
|
|||
"align": [Function],
|
||||
"altKey": false,
|
||||
"applyPatch": [Function],
|
||||
"broadcastPageChanges": [Function],
|
||||
"broadcastPatch": [Function],
|
||||
"callbacks": Object {},
|
||||
"cancel": [Function],
|
||||
"cancelSession": [Function],
|
||||
|
@ -293,6 +293,7 @@ TldrawTestApp {
|
|||
"deletePage": [Function],
|
||||
"distribute": [Function],
|
||||
"doubleClickBoundHandle": [Function],
|
||||
"doubleClickCanvas": [Function],
|
||||
"doubleClickShape": [Function],
|
||||
"duplicate": [Function],
|
||||
"duplicatePage": [Function],
|
||||
|
|
|
@ -92,6 +92,17 @@ export class BrushSession extends BaseSession {
|
|||
|
||||
const afterSelectedIds = didChange ? Array.from(selectedIds.values()) : currentSelectedIds
|
||||
|
||||
if (!didChange)
|
||||
return {
|
||||
document: {
|
||||
pageStates: {
|
||||
[this.app.currentPageId]: {
|
||||
brush,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
appState: {
|
||||
selectByContain,
|
||||
|
|
|
@ -0,0 +1,174 @@
|
|||
import { mockDocument, TldrawTestApp } from '~test'
|
||||
import {
|
||||
ColorStyle,
|
||||
DashStyle,
|
||||
SessionType,
|
||||
SizeStyle,
|
||||
TDDocument,
|
||||
TDShapeType,
|
||||
TextShape,
|
||||
} from '~types'
|
||||
|
||||
const textDoc: TDDocument = {
|
||||
version: 0,
|
||||
id: 'doc',
|
||||
name: 'New Document',
|
||||
pages: {
|
||||
page1: {
|
||||
id: 'page1',
|
||||
shapes: {
|
||||
text1: {
|
||||
id: 'text1',
|
||||
parentId: 'page1',
|
||||
name: 'Text',
|
||||
childIndex: 1,
|
||||
type: TDShapeType.Text,
|
||||
point: [0, 0],
|
||||
text: 'Hello',
|
||||
style: {
|
||||
dash: DashStyle.Draw,
|
||||
size: SizeStyle.Medium,
|
||||
color: ColorStyle.Blue,
|
||||
},
|
||||
label: '',
|
||||
},
|
||||
},
|
||||
bindings: {},
|
||||
},
|
||||
},
|
||||
pageStates: {
|
||||
page1: {
|
||||
id: 'page1',
|
||||
selectedIds: [],
|
||||
camera: {
|
||||
point: [0, 0],
|
||||
zoom: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
assets: {},
|
||||
}
|
||||
|
||||
describe('When creating a shape...', () => {
|
||||
it('begins, updateSession', () => {
|
||||
const app = new TldrawTestApp().selectTool(TDShapeType.Text).doubleClickCanvas()
|
||||
|
||||
// We should be in the editing session
|
||||
expect(app.session?.type).toBe(SessionType.Edit)
|
||||
|
||||
// We should be able to edit the shape
|
||||
const id = app.shapes[0].id
|
||||
|
||||
app.onShapeChange({
|
||||
id,
|
||||
text: 'Hello',
|
||||
})
|
||||
|
||||
expect(app.getShape<TextShape>(id).text).toBe('Hello')
|
||||
})
|
||||
|
||||
it('cancels session', () => {
|
||||
const app = new TldrawTestApp().selectTool(TDShapeType.Text).doubleClickCanvas()
|
||||
|
||||
const id = app.shapes[0].id
|
||||
|
||||
app
|
||||
.onShapeChange({
|
||||
id,
|
||||
text: 'Hello',
|
||||
})
|
||||
.cancel()
|
||||
|
||||
// Removes the editing shape
|
||||
expect(app.session?.type).toBeUndefined()
|
||||
expect(app.getShape(id)).toBeUndefined()
|
||||
|
||||
// The shape was never added to the undo stack
|
||||
app.undo()
|
||||
expect(app.getShape(id)).toBeUndefined()
|
||||
app.redo()
|
||||
expect(app.getShape(id)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('completes session', () => {
|
||||
const app = new TldrawTestApp().selectTool(TDShapeType.Text).doubleClickCanvas()
|
||||
|
||||
const id = app.shapes[0].id
|
||||
|
||||
app
|
||||
.onShapeChange({
|
||||
id,
|
||||
text: 'Hello',
|
||||
})
|
||||
.completeSession()
|
||||
|
||||
// Removes the editing shape
|
||||
expect(app.session?.type).toBeUndefined()
|
||||
expect(app.getShape(id)).toBeDefined()
|
||||
|
||||
// The shape was never added to the undo stack
|
||||
app.undo()
|
||||
expect(app.getShape(id)).toBeUndefined()
|
||||
|
||||
// The shape was added to the undo stack
|
||||
app.redo()
|
||||
expect(app.getShape(id)).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('When editing an existing a shape...', () => {
|
||||
it('begins, updateSession', () => {
|
||||
const app = new TldrawTestApp().loadDocument(textDoc)
|
||||
|
||||
app.doubleClickShape('text1')
|
||||
|
||||
expect(app.session?.type).toBe(SessionType.Edit)
|
||||
})
|
||||
|
||||
it('cancels session', () => {
|
||||
const app = new TldrawTestApp()
|
||||
.loadDocument(textDoc)
|
||||
|
||||
.doubleClickShape('text1')
|
||||
.onShapeChange({
|
||||
id: 'text1',
|
||||
text: 'Hello World!',
|
||||
})
|
||||
.cancel()
|
||||
|
||||
// Cancelling will cancel the session and restore the original text
|
||||
expect(app.session?.type).toBeUndefined()
|
||||
expect(app.getShape<TextShape>('text1').text).toBe('Hello')
|
||||
|
||||
// The changes were never added to the undo stack
|
||||
app.undo()
|
||||
expect(app.getShape<TextShape>('text1').text).toBe('Hello')
|
||||
|
||||
// The redo will restore the shape
|
||||
app.redo()
|
||||
expect(app.getShape<TextShape>('text1').text).toBe('Hello')
|
||||
})
|
||||
|
||||
it('completes session', () => {
|
||||
const app = new TldrawTestApp()
|
||||
.loadDocument(textDoc)
|
||||
.doubleClickShape('text1')
|
||||
.onShapeChange({
|
||||
id: 'text1',
|
||||
text: 'Hello World!',
|
||||
})
|
||||
.completeSession()
|
||||
|
||||
// Cancelling will cancel the session and restore the original text
|
||||
expect(app.session?.type).toBeUndefined()
|
||||
expect(app.getShape<TextShape>('text1').text).toBe('Hello World!')
|
||||
|
||||
// The changes were never added to the undo stack
|
||||
app.undo()
|
||||
expect(app.getShape<TextShape>('text1').text).toBe('Hello')
|
||||
|
||||
// The redo will restore the shape
|
||||
app.redo()
|
||||
expect(app.getShape<TextShape>('text1').text).toBe('Hello World!')
|
||||
})
|
||||
})
|
|
@ -0,0 +1,88 @@
|
|||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { TDShape, SessionType, TldrawPatch, TldrawCommand } from '~types'
|
||||
import { BaseSession } from '../BaseSession'
|
||||
import type { TldrawApp } from '../../internal'
|
||||
|
||||
export class EditSession extends BaseSession {
|
||||
type = SessionType.Edit
|
||||
performanceMode = undefined
|
||||
|
||||
initialShape: TDShape
|
||||
initialSelectedIds: string[]
|
||||
currentPageId: string
|
||||
isCreating: boolean
|
||||
|
||||
constructor(app: TldrawApp, id: string, isCreating: boolean) {
|
||||
super(app)
|
||||
this.initialShape = app.getShape(id, app.currentPageId)
|
||||
this.currentPageId = app.currentPageId
|
||||
this.isCreating = isCreating
|
||||
this.initialSelectedIds = [...app.selectedIds]
|
||||
}
|
||||
|
||||
start = (): TldrawPatch | undefined => void null
|
||||
|
||||
update = (): TldrawPatch | undefined => void null
|
||||
|
||||
cancel = (): TldrawPatch | undefined => {
|
||||
return {
|
||||
document: {
|
||||
pages: {
|
||||
[this.currentPageId]: {
|
||||
shapes: {
|
||||
[this.initialShape.id]: this.isCreating ? undefined : this.initialShape,
|
||||
},
|
||||
},
|
||||
},
|
||||
pageStates: {
|
||||
[this.currentPageId]: {
|
||||
selectedIds: this.isCreating ? [] : this.initialSelectedIds,
|
||||
editingId: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
complete = (): TldrawPatch | TldrawCommand | undefined => {
|
||||
const shape = this.app.getShape(this.initialShape.id)
|
||||
|
||||
return {
|
||||
id: 'edit',
|
||||
before: {
|
||||
document: {
|
||||
pages: {
|
||||
[this.currentPageId]: {
|
||||
shapes: {
|
||||
[this.initialShape.id]: this.isCreating ? undefined : this.initialShape,
|
||||
},
|
||||
},
|
||||
},
|
||||
pageStates: {
|
||||
[this.currentPageId]: {
|
||||
selectedIds: this.isCreating ? [] : this.initialSelectedIds,
|
||||
editingId: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
after: {
|
||||
document: {
|
||||
pages: {
|
||||
[this.currentPageId]: {
|
||||
shapes: {
|
||||
[this.initialShape.id]: shape,
|
||||
},
|
||||
},
|
||||
},
|
||||
pageStates: {
|
||||
[this.currentPageId]: {
|
||||
selectedIds: [shape.id],
|
||||
editingId: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
1
packages/tldraw/src/state/sessions/EditSession/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './EditSession'
|
|
@ -9,6 +9,7 @@ import { TransformSingleSession } from './TransformSingleSession'
|
|||
import { TranslateSession } from './TranslateSession'
|
||||
import { EraseSession } from './EraseSession'
|
||||
import { GridSession } from './GridSession'
|
||||
import { EditSession } from './EditSession'
|
||||
|
||||
export type TldrawSession =
|
||||
| ArrowSession
|
||||
|
@ -21,6 +22,7 @@ export type TldrawSession =
|
|||
| TranslateSession
|
||||
| EraseSession
|
||||
| GridSession
|
||||
| EditSession
|
||||
|
||||
export interface SessionsMap {
|
||||
[SessionType.Arrow]: typeof ArrowSession
|
||||
|
@ -33,6 +35,7 @@ export interface SessionsMap {
|
|||
[SessionType.TransformSingle]: typeof TransformSingleSession
|
||||
[SessionType.Translate]: typeof TranslateSession
|
||||
[SessionType.Grid]: typeof GridSession
|
||||
[SessionType.Edit]: typeof EditSession
|
||||
}
|
||||
|
||||
export type SessionOfType<K extends SessionType> = SessionsMap[K]
|
||||
|
@ -52,6 +55,7 @@ export const sessions: { [K in SessionType]: SessionsMap[K] } = {
|
|||
[SessionType.TransformSingle]: TransformSingleSession,
|
||||
[SessionType.Translate]: TranslateSession,
|
||||
[SessionType.Grid]: GridSession,
|
||||
[SessionType.Edit]: EditSession,
|
||||
}
|
||||
|
||||
export const getSession = <K extends SessionType>(type: K): SessionOfType<K> => {
|
||||
|
|
|
@ -60,6 +60,39 @@ export class TextUtil extends TDShapeUtil<T, E> {
|
|||
const rInput = React.useRef<HTMLTextAreaElement>(null)
|
||||
const rIsMounted = React.useRef(false)
|
||||
|
||||
const rEditedText = React.useRef(text)
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
if (text !== rEditedText.current) {
|
||||
let delta = [0, 0]
|
||||
this.texts.set(shape.id, text)
|
||||
const currentBounds = this.getBounds(shape)
|
||||
const nextBounds = this.getBounds(shape)
|
||||
switch (shape.style.textAlign) {
|
||||
case AlignStyle.Start: {
|
||||
break
|
||||
}
|
||||
case AlignStyle.Middle: {
|
||||
delta = Vec.div([nextBounds.width - currentBounds.width, 0], 2)
|
||||
break
|
||||
}
|
||||
case AlignStyle.End: {
|
||||
delta = [nextBounds.width - currentBounds.width, 0]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
rEditedText.current = text
|
||||
|
||||
onShapeChange?.({
|
||||
...shape,
|
||||
id: shape.id,
|
||||
point: Vec.sub(shape.point, delta),
|
||||
text,
|
||||
})
|
||||
}
|
||||
}, [text])
|
||||
|
||||
const handleChange = React.useCallback(
|
||||
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
let delta = [0, 0]
|
||||
|
@ -83,6 +116,9 @@ export class TextUtil extends TDShapeUtil<T, E> {
|
|||
break
|
||||
}
|
||||
}
|
||||
|
||||
rEditedText.current = newText
|
||||
|
||||
onShapeChange?.({
|
||||
...shape,
|
||||
id: shape.id,
|
||||
|
@ -102,6 +138,13 @@ export class TextUtil extends TDShapeUtil<T, E> {
|
|||
return
|
||||
}
|
||||
|
||||
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
rInput.current!.blur()
|
||||
return
|
||||
}
|
||||
|
||||
if (!(e.key === 'Meta' || e.metaKey)) {
|
||||
e.stopPropagation()
|
||||
} else if (e.key === 'z' && e.metaKey) {
|
||||
|
|
|
@ -50,7 +50,7 @@ export class SelectTool extends BaseTool<Status> {
|
|||
/* --------------------- Methods -------------------- */
|
||||
|
||||
private deselect(id: string) {
|
||||
this.app.select(...this.app.selectedIds.filter(oid => oid !== id))
|
||||
this.app.select(...this.app.selectedIds.filter((oid) => oid !== id))
|
||||
}
|
||||
|
||||
private select(id: string) {
|
||||
|
@ -59,7 +59,7 @@ export class SelectTool extends BaseTool<Status> {
|
|||
|
||||
private pushSelect(id: string) {
|
||||
const shape = this.app.getShape(id)
|
||||
this.app.select(...this.app.selectedIds.filter(oid => oid !== shape.parentId), id)
|
||||
this.app.select(...this.app.selectedIds.filter((oid) => oid !== shape.parentId), id)
|
||||
}
|
||||
|
||||
private selectNone() {
|
||||
|
@ -77,7 +77,7 @@ export class SelectTool extends BaseTool<Status> {
|
|||
clonePaint = (point: number[]) => {
|
||||
if (this.app.selectedIds.length === 0) return
|
||||
|
||||
const shapes = this.app.selectedIds.map(id => this.app.getShape(id))
|
||||
const shapes = this.app.selectedIds.map((id) => this.app.getShape(id))
|
||||
|
||||
const bounds = Utils.expandBounds(Utils.getCommonBounds(shapes.map(TLDR.getBounds)), 16)
|
||||
|
||||
|
@ -92,7 +92,7 @@ export class SelectTool extends BaseTool<Status> {
|
|||
|
||||
const centeredBounds = Utils.centerBounds(bounds, gridPoint)
|
||||
|
||||
const hit = this.app.shapes.some(shape =>
|
||||
const hit = this.app.shapes.some((shape) =>
|
||||
TLDR.getShapeUtil(shape).hitTestBounds(shape, centeredBounds)
|
||||
)
|
||||
|
||||
|
@ -171,12 +171,12 @@ export class SelectTool extends BaseTool<Status> {
|
|||
/* ----------------- Event Handlers ----------------- */
|
||||
|
||||
onCancel = () => {
|
||||
if (this.app.pageState.editingId) {
|
||||
this.app.setEditingId()
|
||||
if (this.app.session) {
|
||||
this.app.cancelSession()
|
||||
} else {
|
||||
this.selectNone()
|
||||
}
|
||||
this.app.cancelSession()
|
||||
|
||||
this.setStatus(Status.Idle)
|
||||
}
|
||||
|
||||
|
@ -265,7 +265,7 @@ export class SelectTool extends BaseTool<Status> {
|
|||
} else {
|
||||
// Stat a transform session
|
||||
this.setStatus(Status.Transforming)
|
||||
const idsToTransform = this.app.selectedIds.flatMap(id =>
|
||||
const idsToTransform = this.app.selectedIds.flatMap((id) =>
|
||||
TLDR.getDocumentBranch(this.app.state, id, this.app.currentPageId)
|
||||
)
|
||||
if (idsToTransform.length === 1) {
|
||||
|
@ -372,7 +372,7 @@ export class SelectTool extends BaseTool<Status> {
|
|||
}
|
||||
}
|
||||
|
||||
onPointerUp: TLPointerEventHandler = info => {
|
||||
onPointerUp: TLPointerEventHandler = (info) => {
|
||||
if (this.status === Status.TranslatingClone || this.status === Status.PointingClone) {
|
||||
if (this.pointedId) {
|
||||
this.app.completeSession()
|
||||
|
@ -415,11 +415,18 @@ export class SelectTool extends BaseTool<Status> {
|
|||
}
|
||||
|
||||
// Complete the current session, if any; and reset the status
|
||||
this.app.completeSession()
|
||||
|
||||
this.setStatus(Status.Idle)
|
||||
this.pointedBoundsHandle = undefined
|
||||
this.pointedHandleId = undefined
|
||||
this.pointedId = undefined
|
||||
|
||||
// Don't complete a session if we've just started one
|
||||
if (this.app.session?.type === SessionType.Edit) {
|
||||
return
|
||||
}
|
||||
|
||||
this.app.completeSession()
|
||||
}
|
||||
|
||||
// Canvas
|
||||
|
@ -529,7 +536,7 @@ export class SelectTool extends BaseTool<Status> {
|
|||
}
|
||||
}
|
||||
|
||||
onDoubleClickShape: TLPointerEventHandler = info => {
|
||||
onDoubleClickShape: TLPointerEventHandler = (info) => {
|
||||
if (this.app.readOnly) return
|
||||
|
||||
const shape = this.app.getShape(info.target)
|
||||
|
@ -556,17 +563,17 @@ export class SelectTool extends BaseTool<Status> {
|
|||
this.app.select(info.target)
|
||||
}
|
||||
|
||||
onRightPointShape: TLPointerEventHandler = info => {
|
||||
onRightPointShape: TLPointerEventHandler = (info) => {
|
||||
if (!this.app.isSelected(info.target)) {
|
||||
this.app.select(info.target)
|
||||
}
|
||||
}
|
||||
|
||||
onHoverShape: TLPointerEventHandler = info => {
|
||||
onHoverShape: TLPointerEventHandler = (info) => {
|
||||
this.app.setHoveredId(info.target)
|
||||
}
|
||||
|
||||
onUnhoverShape: TLPointerEventHandler = info => {
|
||||
onUnhoverShape: TLPointerEventHandler = (info) => {
|
||||
const { currentPageId: oldCurrentPageId } = this.app
|
||||
|
||||
// Wait a frame; and if we haven't changed the hovered id,
|
||||
|
@ -583,7 +590,7 @@ export class SelectTool extends BaseTool<Status> {
|
|||
|
||||
/* --------------------- Bounds --------------------- */
|
||||
|
||||
onPointBounds: TLBoundsEventHandler = info => {
|
||||
onPointBounds: TLBoundsEventHandler = (info) => {
|
||||
if (info.metaKey) {
|
||||
if (!info.shiftKey) {
|
||||
this.selectNone()
|
||||
|
@ -612,12 +619,12 @@ export class SelectTool extends BaseTool<Status> {
|
|||
|
||||
/* ----------------- Bounds Handles ----------------- */
|
||||
|
||||
onPointBoundsHandle: TLBoundsHandleEventHandler = info => {
|
||||
onPointBoundsHandle: TLBoundsHandleEventHandler = (info) => {
|
||||
this.pointedBoundsHandle = info.target
|
||||
this.setStatus(Status.PointingBoundsHandle)
|
||||
}
|
||||
|
||||
onDoubleClickBoundsHandle: TLBoundsHandleEventHandler = info => {
|
||||
onDoubleClickBoundsHandle: TLBoundsHandleEventHandler = (info) => {
|
||||
switch (info.target) {
|
||||
case 'center':
|
||||
case 'left':
|
||||
|
@ -650,12 +657,12 @@ export class SelectTool extends BaseTool<Status> {
|
|||
|
||||
/* --------------------- Handles -------------------- */
|
||||
|
||||
onPointHandle: TLPointerEventHandler = info => {
|
||||
onPointHandle: TLPointerEventHandler = (info) => {
|
||||
this.pointedHandleId = info.target as 'start' | 'end'
|
||||
this.setStatus(Status.PointingHandle)
|
||||
}
|
||||
|
||||
onDoubleClickHandle: TLPointerEventHandler = info => {
|
||||
onDoubleClickHandle: TLPointerEventHandler = (info) => {
|
||||
if (info.target === 'bend') {
|
||||
const { selectedIds } = this.app
|
||||
if (selectedIds.length !== 1) return
|
||||
|
@ -678,7 +685,7 @@ export class SelectTool extends BaseTool<Status> {
|
|||
|
||||
/* ---------------------- Misc ---------------------- */
|
||||
|
||||
onShapeClone: TLShapeCloneHandler = info => {
|
||||
onShapeClone: TLShapeCloneHandler = (info) => {
|
||||
const selectedShapeId = this.app.selectedIds[0]
|
||||
|
||||
const clonedShape = this.getShapeClone(selectedShapeId, info.target)
|
||||
|
|
|
@ -50,7 +50,7 @@ export class StickyTool extends BaseTool {
|
|||
|
||||
newShape.point = Vec.sub(newShape.point, [bounds.width / 2, bounds.height / 2])
|
||||
|
||||
this.app.createShapes(newShape)
|
||||
this.app.patchCreate([newShape])
|
||||
|
||||
this.app.startSession(SessionType.Translate)
|
||||
|
||||
|
|
|
@ -39,7 +39,11 @@ export class TextTool extends BaseTool {
|
|||
settings: { showGrid },
|
||||
} = this.app
|
||||
|
||||
this.app.createTextShapeAtPoint(showGrid ? Vec.snap(currentPoint, currentGrid) : currentPoint)
|
||||
this.app.createTextShapeAtPoint(
|
||||
showGrid ? Vec.snap(currentPoint, currentGrid) : currentPoint,
|
||||
undefined,
|
||||
true
|
||||
)
|
||||
this.setStatus(Status.Creating)
|
||||
return
|
||||
}
|
||||
|
@ -60,7 +64,7 @@ export class TextTool extends BaseTool {
|
|||
}
|
||||
|
||||
onShapeBlur = () => {
|
||||
if (this.app.readOnly) return
|
||||
if (this.app.readOnly) return
|
||||
this.stopEditingShape()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,6 +44,19 @@ export class TldrawTestApp extends TldrawApp {
|
|||
return this
|
||||
}
|
||||
|
||||
doubleClickCanvas = (options?: PointerOptions | number[]) => {
|
||||
this.onPointerDown(
|
||||
inputs.pointerDown(this.getPoint(options), 'canvas'),
|
||||
{} as React.PointerEvent
|
||||
)
|
||||
this.onDoubleClickCanvas(
|
||||
inputs.pointerDown(this.getPoint(options), 'canvas'),
|
||||
{} as React.PointerEvent
|
||||
)
|
||||
this.onPointerUp(inputs.pointerUp(this.getPoint(options), 'canvas'), {} as React.PointerEvent)
|
||||
return this
|
||||
}
|
||||
|
||||
doubleClickShape = (id: string, options?: PointerOptions | number[]) => {
|
||||
this.onPointerDown(
|
||||
inputs.pointerDown(this.getPoint(options), 'canvas'),
|
||||
|
|
|
@ -178,6 +178,7 @@ export enum TDUserStatus {
|
|||
export interface TDUser extends TLUser<TDShape> {
|
||||
activeShapes: TDShape[]
|
||||
status: TDUserStatus
|
||||
session?: boolean
|
||||
}
|
||||
|
||||
export type Theme = 'dark' | 'light'
|
||||
|
@ -193,6 +194,7 @@ export enum SessionType {
|
|||
Rotate = 'rotate',
|
||||
Handle = 'handle',
|
||||
Grid = 'grid',
|
||||
Edit = 'edit',
|
||||
}
|
||||
|
||||
export enum TDStatus {
|
||||
|
|
54
yarn.lock
|
@ -1863,25 +1863,15 @@
|
|||
"@jridgewell/resolve-uri" "^3.0.3"
|
||||
"@jridgewell/sourcemap-codec" "^1.4.10"
|
||||
|
||||
"@liveblocks/client@^0.14.0":
|
||||
version "0.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@liveblocks/client/-/client-0.14.0.tgz#2a5f7bd243d3aea7b95cf62737e49dc13df1b69b"
|
||||
integrity sha512-I1nykCqYSpuBQhP1kZplYqL6L0+C1JocW01UKgPz+tthOOGdTdsNBHPcMigxou4vsOQutUuEJUcaDsd1or4A+Q==
|
||||
"@liveblocks/client@^0.17.0-beta2":
|
||||
version "0.17.0-beta2"
|
||||
resolved "https://registry.yarnpkg.com/@liveblocks/client/-/client-0.17.0-beta2.tgz#0cd14dd29350a29765bfa41c5d7a2185010ab791"
|
||||
integrity sha512-V8LOv6BIDQvLl2LLcdDWIK6Ow3hMSpmIYk5T+NbNC34MM04bdhBU3C4EHg4qRnTPtOxm4NylRiVbNDwfwmMNjw==
|
||||
|
||||
"@liveblocks/client@^0.16.17":
|
||||
version "0.16.17"
|
||||
resolved "https://registry.yarnpkg.com/@liveblocks/client/-/client-0.16.17.tgz#38f8392d7baaf20b34237d06fe3cb53586cea8f0"
|
||||
integrity sha512-mWX/EGQNoWwzkEdUfdt82w9OMA5kKV3BraBt2YhZO4Zn04mfNmcOkr7V4S+EmJ4LyQ5QVNKYvA4gJQU1ahn5mQ==
|
||||
|
||||
"@liveblocks/react@^0.14.0":
|
||||
version "0.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@liveblocks/react/-/react-0.14.0.tgz#1a29ff10e88ea04d029f553f8b14a6cff4584f70"
|
||||
integrity sha512-KGuEEmifh3A9OEHTYaR3+yxIQOhdAQG59d0eOqKbyV/I0X6IoX4kAtXZjnZllrWrTwnnSY2aPFESthtt19t6EQ==
|
||||
|
||||
"@liveblocks/react@^0.16.17":
|
||||
version "0.16.17"
|
||||
resolved "https://registry.yarnpkg.com/@liveblocks/react/-/react-0.16.17.tgz#e3d5c591ad284f84cf5689ad2b8f4d0d0e1ef1ba"
|
||||
integrity sha512-I0WvNI+X/I+ymbVF/+W4GOCaZnsaqje01Mp9BGt7u7joL1ZNBQuva1zWPINE6pNWTk75y/pmG4o56YoC+ek4FQ==
|
||||
"@liveblocks/react@^0.17.0-beta2":
|
||||
version "0.17.0-beta2"
|
||||
resolved "https://registry.yarnpkg.com/@liveblocks/react/-/react-0.17.0-beta2.tgz#e148f9c1eb03a40db350ca2ccef180248e6d3afc"
|
||||
integrity sha512-g7XSias4M2nl6eao8JxS5ynyjTvkV4FF8PavvAd9aO5sLz++1qrPW0XCXq2jle2sOyM9act/nMX35FedXvXPFA==
|
||||
|
||||
"@malept/cross-spawn-promise@^1.1.0":
|
||||
version "1.1.1"
|
||||
|
@ -5411,6 +5401,14 @@ enhanced-resolve@^5.0.0:
|
|||
graceful-fs "^4.2.4"
|
||||
tapable "^2.2.0"
|
||||
|
||||
enhanced-resolve@^5.7.0:
|
||||
version "5.9.3"
|
||||
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.9.3.tgz#44a342c012cbc473254af5cc6ae20ebd0aae5d88"
|
||||
integrity sha512-Bq9VSor+kjvW3f9/MiiR4eE3XYgOl7/rS8lnSxbRbF3kS0B2r+Y9w5krBWxZgDxASVZbdYrn5wT4j/Wb0J9qow==
|
||||
dependencies:
|
||||
graceful-fs "^4.2.4"
|
||||
tapable "^2.2.0"
|
||||
|
||||
enhanced-resolve@^5.8.3:
|
||||
version "5.9.0"
|
||||
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.9.0.tgz#49ac24953ac8452ed8fed2ef1340fc8e043667ee"
|
||||
|
@ -6769,6 +6767,11 @@ hotkeys-js@3.8.7:
|
|||
resolved "https://registry.yarnpkg.com/hotkeys-js/-/hotkeys-js-3.8.7.tgz#c16cab978b53d7242f860ca3932e976b92399981"
|
||||
integrity sha512-ckAx3EkUr5XjDwjEHDorHxRO2Kb7z6Z2Sxul4MbBkN8Nho7XDslQsgMJT+CiJ5Z4TgRxxvKHEpuLE3imzqy4Lg==
|
||||
|
||||
hotkeys-js@3.9.3:
|
||||
version "3.9.3"
|
||||
resolved "https://registry.yarnpkg.com/hotkeys-js/-/hotkeys-js-3.9.3.tgz#4b755cc695b388d7f93a83aff4b0c2a45719996c"
|
||||
integrity sha512-s+f0xyvDmf6+DyrFQ2SY+eA7lbvMbjqkqi0I0SpMgnN5tZx7DeH8nsWhkJR4KEq3pxDPHJppDUhdt1rZFW5LeQ==
|
||||
|
||||
html-encoding-sniffer@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3"
|
||||
|
@ -8678,6 +8681,14 @@ next-themes@^0.0.15:
|
|||
resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.0.15.tgz#ab0cee69cd763b77d41211f631e108beab39bf7d"
|
||||
integrity sha512-LTmtqYi03c4gMTJmWwVK9XkHL7h0/+XrtR970Ujvtu3s0kZNeJN24aJsi4rkZOI8i19+qq6f8j+8Duwy5jqcrQ==
|
||||
|
||||
next-transpile-modules@^9.0.0:
|
||||
version "9.0.0"
|
||||
resolved "https://registry.yarnpkg.com/next-transpile-modules/-/next-transpile-modules-9.0.0.tgz#133b1742af082e61cc76b02a0f12ffd40ce2bf90"
|
||||
integrity sha512-VCNFOazIAnXn1hvgYYSTYMnoWgKgwlYh4lm1pKbSfiB3kj5ZYLcKVhfh3jkPOg1cnd9DP+pte9yCUocdPEUBTQ==
|
||||
dependencies:
|
||||
enhanced-resolve "^5.7.0"
|
||||
escalade "^3.1.1"
|
||||
|
||||
next@^12.1.6:
|
||||
version "12.1.6"
|
||||
resolved "https://registry.yarnpkg.com/next/-/next-12.1.6.tgz#eb205e64af1998651f96f9df44556d47d8bbc533"
|
||||
|
@ -9553,6 +9564,13 @@ react-hotkeys-hook@^3.4.4:
|
|||
dependencies:
|
||||
hotkeys-js "3.8.7"
|
||||
|
||||
react-hotkeys-hook@^3.4.6:
|
||||
version "3.4.6"
|
||||
resolved "https://registry.yarnpkg.com/react-hotkeys-hook/-/react-hotkeys-hook-3.4.6.tgz#21eda8e97121583a14056479e3eea9e51d2e2a69"
|
||||
integrity sha512-SiGKHnauaAQglRA7qeiW5LTa0KoT2ssv8YGYKZQoM3P9v5JFEHJdXOSFml1N6K86oKQ8dLCLlxqBqGlSJWGmxQ==
|
||||
dependencies:
|
||||
hotkeys-js "3.9.3"
|
||||
|
||||
react-intl@^6.0.3:
|
||||
version "6.0.3"
|
||||
resolved "https://registry.yarnpkg.com/react-intl/-/react-intl-6.0.3.tgz#eb5857f2fd525c83255bf6c8339562a7fea9f970"
|
||||
|
|