[chore] refactor user preferences (#1435)
- Remove TLUser, TLUserPresence - Add first-class support for user preferences that persists across rooms and tabs ### Change Type <!-- 💡 Indicate the type of change your pull request is. --> <!-- 🤷♀️ If you're not sure, don't select anything --> <!-- ✂️ Feel free to delete unselected options --> <!-- To select one, put an x in the box: [x] --> - [ ] `patch` — Bug Fix - [ ] `minor` — New Feature - [x] `major` — Breaking Change - [ ] `dependencies` — Dependency Update (publishes a `patch` release, for devDependencies use `internal`) - [ ] `documentation` — Changes to the documentation only (will not publish a new version) - [ ] `tests` — Changes to any testing-related code only (will not publish a new version) - [ ] `internal` — Any other changes that don't affect the published package (will not publish a new version) ### Test Plan 1. Add a step-by-step description of how to test your PR here. 2. - [ ] Unit Tests - [ ] Webdriver tests ### Release Notes - Add a brief release note for your PR here.
This commit is contained in:
parent
53b289310d
commit
356a0d1e73
60 changed files with 710 additions and 955 deletions
64
.github/workflows/webdriver-nightly.yml
vendored
64
.github/workflows/webdriver-nightly.yml
vendored
|
@ -1,64 +0,0 @@
|
||||||
# name: Webdriver nightly (browserstack)
|
|
||||||
|
|
||||||
# on:
|
|
||||||
# workflow_dispatch:
|
|
||||||
# schedule:
|
|
||||||
# - cron: '0 2 * * *' # run at 2 AM UTC
|
|
||||||
|
|
||||||
# jobs:
|
|
||||||
# test:
|
|
||||||
# name: 'nightly'
|
|
||||||
# runs-on: ${{ matrix.os }}
|
|
||||||
|
|
||||||
# strategy:
|
|
||||||
# fail-fast: false
|
|
||||||
# matrix:
|
|
||||||
# os: [ubuntu-latest-16-cores-open]
|
|
||||||
# node-version: [16]
|
|
||||||
|
|
||||||
# container:
|
|
||||||
# image: node:${{ matrix.node-version }}
|
|
||||||
# options: --network-alias testhost
|
|
||||||
# volumes:
|
|
||||||
# - /home/runner/work/_temp/e2e:/home/runner/work/_temp/e2e
|
|
||||||
|
|
||||||
# steps:
|
|
||||||
# # start browserstack
|
|
||||||
# - name: 'BrowserStack Env Setup' # Invokes the setup-env action
|
|
||||||
# uses: browserstack/github-actions/setup-env@master
|
|
||||||
# with:
|
|
||||||
# username: jamieblair_YXsTBS
|
|
||||||
# access-key: BUcyZn9PF4iwKgayXinm
|
|
||||||
# - name: 'BrowserStack Local Tunnel Setup' # Invokes the setup-local action
|
|
||||||
# uses: browserstack/github-actions/setup-local@master
|
|
||||||
# with:
|
|
||||||
# local-testing: start
|
|
||||||
# local-identifier: random
|
|
||||||
|
|
||||||
# - name: Check out code
|
|
||||||
# uses: actions/checkout@v3
|
|
||||||
# with:
|
|
||||||
# fetch-depth: 0
|
|
||||||
# submodules: true
|
|
||||||
|
|
||||||
# - name: Setup Node.js environment
|
|
||||||
# uses: actions/setup-node@v3
|
|
||||||
# with:
|
|
||||||
# node-version: 18
|
|
||||||
# cache: 'yarn'
|
|
||||||
# cache-dependency-path: 'public-yarn.lock'
|
|
||||||
|
|
||||||
# - name: Enable corepack
|
|
||||||
# run: corepack enable
|
|
||||||
|
|
||||||
# - name: Install dependencies
|
|
||||||
# run: yarn
|
|
||||||
|
|
||||||
# - run: yarn e2e test:ci nightly
|
|
||||||
# env:
|
|
||||||
# CI: true
|
|
||||||
# DOWNLOADS_DIR: '/home/runner/work/_temp/e2e/'
|
|
||||||
# TEST_URL: 'https://testhost:5421'
|
|
||||||
# WB_BUILD_NAME: 'nightly'
|
|
||||||
# BROWSERSTACK_USER: ${{ secrets.BROWSERSTACK_USER }}
|
|
||||||
# BROWSERSTACK_KEY: ${{ secrets.BROWSERSTACK_KEY }}
|
|
117
.github/workflows/webdriver-on-demand.yml
vendored
117
.github/workflows/webdriver-on-demand.yml
vendored
|
@ -1,117 +0,0 @@
|
||||||
# name: Webdriver on demand (browserstack)
|
|
||||||
|
|
||||||
# on:
|
|
||||||
# workflow_dispatch:
|
|
||||||
# inputs:
|
|
||||||
# WD_BROWSER_CHROME:
|
|
||||||
# description: 'Chrome'
|
|
||||||
# required: false
|
|
||||||
# default: true
|
|
||||||
# type: boolean
|
|
||||||
# WD_BROWSER_FIREFOX:
|
|
||||||
# description: 'Firefox'
|
|
||||||
# required: false
|
|
||||||
# default: true
|
|
||||||
# type: boolean
|
|
||||||
# WD_BROWSER_EDGE:
|
|
||||||
# description: 'Edge'
|
|
||||||
# required: false
|
|
||||||
# default: true
|
|
||||||
# type: boolean
|
|
||||||
# WD_BROWSER_SAFARI:
|
|
||||||
# description: 'Safari'
|
|
||||||
# required: false
|
|
||||||
# default: true
|
|
||||||
# type: boolean
|
|
||||||
# WD_BROWSER_SAMSUNG:
|
|
||||||
# description: 'Samsung'
|
|
||||||
# required: false
|
|
||||||
# default: true
|
|
||||||
# type: boolean
|
|
||||||
# WD_OS_WINDOWS:
|
|
||||||
# description: 'Windows'
|
|
||||||
# required: false
|
|
||||||
# default: true
|
|
||||||
# type: boolean
|
|
||||||
# WD_OS_MACOS:
|
|
||||||
# description: 'MacOS'
|
|
||||||
# required: false
|
|
||||||
# default: true
|
|
||||||
# type: boolean
|
|
||||||
# WD_OS_ANDROID:
|
|
||||||
# description: 'Android'
|
|
||||||
# required: false
|
|
||||||
# default: true
|
|
||||||
# type: boolean
|
|
||||||
# WD_OS_IOS:
|
|
||||||
# description: 'iOS'
|
|
||||||
# required: false
|
|
||||||
# default: true
|
|
||||||
# type: boolean
|
|
||||||
|
|
||||||
# jobs:
|
|
||||||
# test:
|
|
||||||
# name: 'on-demand'
|
|
||||||
# runs-on: ${{ matrix.os }}
|
|
||||||
|
|
||||||
# strategy:
|
|
||||||
# fail-fast: false
|
|
||||||
# matrix:
|
|
||||||
# os: [ubuntu-latest-16-cores-open]
|
|
||||||
# node-version: [16]
|
|
||||||
|
|
||||||
# container:
|
|
||||||
# image: node:${{ matrix.node-version }}
|
|
||||||
# options: --network-alias testhost
|
|
||||||
# volumes:
|
|
||||||
# - /home/runner/work/_temp/e2e:/home/runner/work/_temp/e2e
|
|
||||||
|
|
||||||
# steps:
|
|
||||||
# # start browserstack
|
|
||||||
# - name: 'BrowserStack Env Setup' # Invokes the setup-env action
|
|
||||||
# uses: browserstack/github-actions/setup-env@master
|
|
||||||
# with:
|
|
||||||
# username: jamieblair_YXsTBS
|
|
||||||
# access-key: BUcyZn9PF4iwKgayXinm
|
|
||||||
# - name: 'BrowserStack Local Tunnel Setup' # Invokes the setup-local action
|
|
||||||
# uses: browserstack/github-actions/setup-local@master
|
|
||||||
# with:
|
|
||||||
# local-testing: start
|
|
||||||
# local-identifier: random
|
|
||||||
|
|
||||||
# - name: Check out code
|
|
||||||
# uses: actions/checkout@v3
|
|
||||||
# with:
|
|
||||||
# fetch-depth: 0
|
|
||||||
# submodules: true
|
|
||||||
|
|
||||||
# - name: Setup Node.js environment
|
|
||||||
# uses: actions/setup-node@v3
|
|
||||||
# with:
|
|
||||||
# node-version: 18
|
|
||||||
# cache: 'yarn'
|
|
||||||
# cache-dependency-path: 'public-yarn.lock'
|
|
||||||
|
|
||||||
# - name: Enable corepack
|
|
||||||
# run: corepack enable
|
|
||||||
|
|
||||||
# - name: Install dependencies
|
|
||||||
# run: yarn
|
|
||||||
|
|
||||||
# - run: yarn e2e test:ci nightly
|
|
||||||
# env:
|
|
||||||
# CI: true
|
|
||||||
# DOWNLOADS_DIR: '/home/runner/work/_temp/e2e/'
|
|
||||||
# TEST_URL: 'https://testhost:5421'
|
|
||||||
# WD_BROWSER_CHROME: ${{ inputs.WD_BROWSER_CHROME }}
|
|
||||||
# WD_BROWSER_FIREFOX: ${{ inputs.WD_BROWSER_FIREFOX }}
|
|
||||||
# WD_BROWSER_EDGE: ${{ inputs.WD_BROWSER_EDGE }}
|
|
||||||
# WD_BROWSER_SAFARI: ${{ inputs.WD_BROWSER_SAFARI }}
|
|
||||||
# WD_BROWSER_SAMSUNG: ${{ inputs.WD_BROWSER_SAMSUNG }}
|
|
||||||
# WD_OS_WINDOWS: ${{ inputs.WD_OS_WINDOWS }}
|
|
||||||
# WD_OS_MACOS: ${{ inputs.WD_OS_MACOS }}
|
|
||||||
# WD_OS_ANDROID: ${{ inputs.WD_OS_ANDROID }}
|
|
||||||
# WD_OS_IOS: ${{ inputs.WD_OS_IOS }}
|
|
||||||
# WB_BUILD_NAME: 'ondemand'
|
|
||||||
# BROWSERSTACK_USER: ${{ secrets.BROWSERSTACK_USER }}
|
|
||||||
# BROWSERSTACK_KEY: ${{ secrets.BROWSERSTACK_KEY }}
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { Tldraw, TLInstance, TLInstancePageState, TLUser, TLUserPresence } from '@tldraw/tldraw'
|
import { Tldraw, TLInstance, TLInstancePresence } from '@tldraw/tldraw'
|
||||||
import '@tldraw/tldraw/editor.css'
|
import '@tldraw/tldraw/editor.css'
|
||||||
import '@tldraw/tldraw/ui.css'
|
import '@tldraw/tldraw/ui.css'
|
||||||
import { useRef } from 'react'
|
import { useRef } from 'react'
|
||||||
|
|
||||||
const SHOW_MOVING_CURSOR = false
|
const SHOW_MOVING_CURSOR = true
|
||||||
const CURSOR_SPEED = 0.1
|
const CURSOR_SPEED = 0.5
|
||||||
const CIRCLE_RADIUS = 100
|
const CIRCLE_RADIUS = 100
|
||||||
const UPDATE_FPS = 60
|
const UPDATE_FPS = 60
|
||||||
|
|
||||||
|
@ -15,39 +15,19 @@ export default function UserPresenceExample() {
|
||||||
<Tldraw
|
<Tldraw
|
||||||
persistenceKey="user-presence-example"
|
persistenceKey="user-presence-example"
|
||||||
onMount={(app) => {
|
onMount={(app) => {
|
||||||
// There are several records related to user presence that must be
|
// For every connected peer you should put a TLInstancePresence record in the
|
||||||
// included for each user. These are created automatically by each
|
// store with their cursor position etc.
|
||||||
// editor or editor instance, so in a "regular" multiplayer sharing
|
|
||||||
// all records will include all of these records. In this example,
|
|
||||||
// we're having to create these ourselves.
|
|
||||||
|
|
||||||
const userId = TLUser.createCustomId('user-1')
|
const peerPresence = TLInstancePresence.create({
|
||||||
|
id: TLInstancePresence.createCustomId('peer-1-presence'),
|
||||||
const user = TLUser.create({
|
currentPageId: app.currentPageId,
|
||||||
id: userId,
|
userId: 'peer-1',
|
||||||
name: 'User 1',
|
instanceId: TLInstance.createCustomId('peer-1-editor-instance'),
|
||||||
|
userName: 'Peer 1',
|
||||||
|
cursor: { x: 0, y: 0, type: 'default', rotation: 0 },
|
||||||
})
|
})
|
||||||
|
|
||||||
const userPresence = TLUserPresence.create({
|
app.store.put([peerPresence])
|
||||||
...app.userPresence,
|
|
||||||
id: TLUserPresence.createCustomId('user-1'),
|
|
||||||
cursor: { x: 0, y: 0 },
|
|
||||||
userId,
|
|
||||||
})
|
|
||||||
|
|
||||||
const instance = TLInstance.create({
|
|
||||||
...app.instanceState,
|
|
||||||
id: TLInstance.createCustomId('user-1'),
|
|
||||||
userId,
|
|
||||||
})
|
|
||||||
|
|
||||||
const instancePageState = TLInstancePageState.create({
|
|
||||||
...app.pageState,
|
|
||||||
id: TLInstancePageState.createCustomId('user-1'),
|
|
||||||
instanceId: TLInstance.createCustomId('instance-1'),
|
|
||||||
})
|
|
||||||
|
|
||||||
app.store.put([user, instance, userPresence, instancePageState])
|
|
||||||
|
|
||||||
// Make the fake user's cursor rotate in a circle
|
// Make the fake user's cursor rotate in a circle
|
||||||
if (rTimeout.current) {
|
if (rTimeout.current) {
|
||||||
|
@ -62,24 +42,21 @@ export default function UserPresenceExample() {
|
||||||
// rotate in a circle
|
// rotate in a circle
|
||||||
app.store.put([
|
app.store.put([
|
||||||
{
|
{
|
||||||
...userPresence,
|
...peerPresence,
|
||||||
cursor: {
|
cursor: {
|
||||||
x: Math.cos(t * Math.PI * 2) * CIRCLE_RADIUS,
|
...peerPresence.cursor,
|
||||||
y: Math.sin(t * Math.PI * 2) * CIRCLE_RADIUS,
|
x: 150 + Math.cos(t * Math.PI * 2) * CIRCLE_RADIUS,
|
||||||
|
y: 150 + Math.sin(t * Math.PI * 2) * CIRCLE_RADIUS,
|
||||||
},
|
},
|
||||||
lastActivityTimestamp: now,
|
lastActivityTimestamp: now,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
}, 1000 / UPDATE_FPS)
|
}, 1000 / UPDATE_FPS)
|
||||||
} else {
|
} else {
|
||||||
app.store.put([
|
app.store.put([{ ...peerPresence, lastActivityTimestamp: Date.now() }])
|
||||||
{ ...userPresence, cursor: { x: 0, y: 0 }, lastActivityTimestamp: Date.now() },
|
|
||||||
])
|
|
||||||
|
|
||||||
rTimeout.current = setInterval(() => {
|
rTimeout.current = setInterval(() => {
|
||||||
app.store.put([
|
app.store.put([{ ...peerPresence, lastActivityTimestamp: Date.now() }])
|
||||||
{ ...userPresence, cursor: { x: 0, y: 0 }, lastActivityTimestamp: Date.now() },
|
|
||||||
])
|
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import {
|
import {
|
||||||
Canvas,
|
Canvas,
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
getUserData,
|
|
||||||
TldrawEditor,
|
TldrawEditor,
|
||||||
TldrawEditorConfig,
|
TldrawEditorConfig,
|
||||||
TldrawUi,
|
TldrawUi,
|
||||||
|
@ -13,28 +12,19 @@ import '@tldraw/tldraw/ui.css'
|
||||||
|
|
||||||
const instanceId = TLInstance.createCustomId('example')
|
const instanceId = TLInstance.createCustomId('example')
|
||||||
|
|
||||||
|
// for custom config, see 3-custom-config
|
||||||
const config = new TldrawEditorConfig()
|
const config = new TldrawEditorConfig()
|
||||||
|
|
||||||
export default function Example() {
|
export default function Example() {
|
||||||
const userData = getUserData()
|
|
||||||
|
|
||||||
const syncedStore = useLocalSyncClient({
|
const syncedStore = useLocalSyncClient({
|
||||||
config,
|
config,
|
||||||
instanceId,
|
instanceId,
|
||||||
userId: userData.id,
|
|
||||||
universalPersistenceKey: 'exploded-example',
|
universalPersistenceKey: 'exploded-example',
|
||||||
// config: myConfig // for custom config, see 3-custom-config
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tldraw__editor">
|
<div className="tldraw__editor">
|
||||||
<TldrawEditor
|
<TldrawEditor instanceId={instanceId} store={syncedStore} config={config} autoFocus>
|
||||||
instanceId={instanceId}
|
|
||||||
userId={userData.id}
|
|
||||||
store={syncedStore}
|
|
||||||
config={config}
|
|
||||||
autoFocus
|
|
||||||
>
|
|
||||||
<TldrawUi>
|
<TldrawUi>
|
||||||
<ContextMenu>
|
<ContextMenu>
|
||||||
<Canvas />
|
<Canvas />
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { SyncedStore, TLInstanceId, TLUserId, useApp } from '@tldraw/editor'
|
import { SyncedStore, TLInstanceId, useApp } from '@tldraw/editor'
|
||||||
import { parseAndLoadDocument, serializeTldrawJson } from '@tldraw/file-format'
|
import { parseAndLoadDocument, serializeTldrawJson } from '@tldraw/file-format'
|
||||||
import { useDefaultHelpers } from '@tldraw/ui'
|
import { useDefaultHelpers } from '@tldraw/ui'
|
||||||
import { debounce } from '@tldraw/utils'
|
import { debounce } from '@tldraw/utils'
|
||||||
|
@ -11,11 +11,9 @@ import type { VscodeMessage } from '../../messages'
|
||||||
|
|
||||||
export const ChangeResponder = ({
|
export const ChangeResponder = ({
|
||||||
syncedStore,
|
syncedStore,
|
||||||
userId,
|
|
||||||
instanceId,
|
instanceId,
|
||||||
}: {
|
}: {
|
||||||
syncedStore: SyncedStore
|
syncedStore: SyncedStore
|
||||||
userId: TLUserId
|
|
||||||
instanceId: TLInstanceId
|
instanceId: TLInstanceId
|
||||||
}) => {
|
}) => {
|
||||||
const app = useApp()
|
const app = useApp()
|
||||||
|
@ -46,7 +44,7 @@ export const ChangeResponder = ({
|
||||||
clearToasts()
|
clearToasts()
|
||||||
window.removeEventListener('message', handleMessage)
|
window.removeEventListener('message', handleMessage)
|
||||||
}
|
}
|
||||||
}, [app, userId, instanceId, msg, addToast, clearToasts])
|
}, [app, instanceId, msg, addToast, clearToasts])
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
// When the history changes, send the new file contents to VSCode
|
// When the history changes, send the new file contents to VSCode
|
||||||
|
@ -71,7 +69,7 @@ export const ChangeResponder = ({
|
||||||
handleChange()
|
handleChange()
|
||||||
app.off('change-history', handleChange)
|
app.off('change-history', handleChange)
|
||||||
}
|
}
|
||||||
}, [app, syncedStore, userId, instanceId])
|
}, [app, syncedStore, instanceId])
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,15 @@
|
||||||
import { TLInstanceId, TLUserId, useApp } from '@tldraw/editor'
|
import { TLInstanceId, useApp } from '@tldraw/editor'
|
||||||
import { parseAndLoadDocument } from '@tldraw/file-format'
|
import { parseAndLoadDocument } from '@tldraw/file-format'
|
||||||
import { useDefaultHelpers } from '@tldraw/ui'
|
import { useDefaultHelpers } from '@tldraw/ui'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { vscode } from './utils/vscode'
|
import { vscode } from './utils/vscode'
|
||||||
|
|
||||||
export function FileOpen({
|
export function FileOpen({
|
||||||
userId,
|
|
||||||
fileContents,
|
fileContents,
|
||||||
instanceId,
|
instanceId,
|
||||||
forceDarkMode,
|
forceDarkMode,
|
||||||
}: {
|
}: {
|
||||||
instanceId: TLInstanceId
|
instanceId: TLInstanceId
|
||||||
userId: TLUserId
|
|
||||||
fileContents: string
|
fileContents: string
|
||||||
forceDarkMode: boolean
|
forceDarkMode: boolean
|
||||||
}) {
|
}) {
|
||||||
|
@ -44,17 +42,7 @@ export function FileOpen({
|
||||||
return () => {
|
return () => {
|
||||||
clearToasts()
|
clearToasts()
|
||||||
}
|
}
|
||||||
}, [
|
}, [fileContents, app, instanceId, addToast, msg, clearToasts, forceDarkMode, isFileLoaded])
|
||||||
fileContents,
|
|
||||||
app,
|
|
||||||
userId,
|
|
||||||
instanceId,
|
|
||||||
addToast,
|
|
||||||
msg,
|
|
||||||
clearToasts,
|
|
||||||
forceDarkMode,
|
|
||||||
isFileLoaded,
|
|
||||||
])
|
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ import {
|
||||||
setRuntimeOverrides,
|
setRuntimeOverrides,
|
||||||
TldrawEditor,
|
TldrawEditor,
|
||||||
TldrawEditorConfig,
|
TldrawEditorConfig,
|
||||||
TLUserId,
|
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
import { linksUiOverrides } from './utils/links'
|
import { linksUiOverrides } from './utils/links'
|
||||||
// eslint-disable-next-line import/no-internal-modules
|
// eslint-disable-next-line import/no-internal-modules
|
||||||
|
@ -97,7 +96,6 @@ export const TldrawWrapper = () => {
|
||||||
assetSrc: message.data.assetSrc,
|
assetSrc: message.data.assetSrc,
|
||||||
fileContents: message.data.fileContents,
|
fileContents: message.data.fileContents,
|
||||||
uri: message.data.uri,
|
uri: message.data.uri,
|
||||||
userId: message.data.userId as TLUserId,
|
|
||||||
isDarkMode: message.data.isDarkMode,
|
isDarkMode: message.data.isDarkMode,
|
||||||
config,
|
config,
|
||||||
})
|
})
|
||||||
|
@ -128,24 +126,15 @@ export type TLDrawInnerProps = {
|
||||||
assetSrc: string
|
assetSrc: string
|
||||||
fileContents: string
|
fileContents: string
|
||||||
uri: string
|
uri: string
|
||||||
userId: TLUserId
|
|
||||||
isDarkMode: boolean
|
isDarkMode: boolean
|
||||||
config: TldrawEditorConfig
|
config: TldrawEditorConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
function TldrawInner({
|
function TldrawInner({ uri, config, assetSrc, isDarkMode, fileContents }: TLDrawInnerProps) {
|
||||||
uri,
|
|
||||||
config,
|
|
||||||
assetSrc,
|
|
||||||
userId,
|
|
||||||
isDarkMode,
|
|
||||||
fileContents,
|
|
||||||
}: TLDrawInnerProps) {
|
|
||||||
const instanceId = TAB_ID
|
const instanceId = TAB_ID
|
||||||
const syncedStore = useLocalSyncClient({
|
const syncedStore = useLocalSyncClient({
|
||||||
universalPersistenceKey: uri,
|
universalPersistenceKey: uri,
|
||||||
instanceId,
|
instanceId,
|
||||||
userId,
|
|
||||||
config,
|
config,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -156,20 +145,14 @@ function TldrawInner({
|
||||||
config={config}
|
config={config}
|
||||||
assetUrls={assetUrls}
|
assetUrls={assetUrls}
|
||||||
instanceId={TAB_ID}
|
instanceId={TAB_ID}
|
||||||
userId={userId}
|
|
||||||
store={syncedStore}
|
store={syncedStore}
|
||||||
onCreateBookmarkFromUrl={onCreateBookmarkFromUrl}
|
onCreateBookmarkFromUrl={onCreateBookmarkFromUrl}
|
||||||
autoFocus
|
autoFocus
|
||||||
>
|
>
|
||||||
{/* <DarkModeHandler themeKind={themeKind} /> */}
|
{/* <DarkModeHandler themeKind={themeKind} /> */}
|
||||||
<TldrawUi assetUrls={assetUrls} overrides={[menuOverrides, linksUiOverrides]}>
|
<TldrawUi assetUrls={assetUrls} overrides={[menuOverrides, linksUiOverrides]}>
|
||||||
<FileOpen
|
<FileOpen instanceId={instanceId} fileContents={fileContents} forceDarkMode={isDarkMode} />
|
||||||
instanceId={instanceId}
|
<ChangeResponder syncedStore={syncedStore} instanceId={instanceId} />
|
||||||
userId={userId}
|
|
||||||
fileContents={fileContents}
|
|
||||||
forceDarkMode={isDarkMode}
|
|
||||||
/>
|
|
||||||
<ChangeResponder syncedStore={syncedStore} userId={userId} instanceId={instanceId} />
|
|
||||||
<ContextMenu>
|
<ContextMenu>
|
||||||
<Canvas />
|
<Canvas />
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
|
|
|
@ -93,10 +93,7 @@ import { TLStyleType } from '@tldraw/tlschema';
|
||||||
import { TLTextShape } from '@tldraw/tlschema';
|
import { TLTextShape } from '@tldraw/tlschema';
|
||||||
import { TLTextShapeProps } from '@tldraw/tlschema';
|
import { TLTextShapeProps } from '@tldraw/tlschema';
|
||||||
import { TLUnknownShape } from '@tldraw/tlschema';
|
import { TLUnknownShape } from '@tldraw/tlschema';
|
||||||
import { TLUser } from '@tldraw/tlschema';
|
|
||||||
import { TLUserDocument } from '@tldraw/tlschema';
|
import { TLUserDocument } from '@tldraw/tlschema';
|
||||||
import { TLUserId } from '@tldraw/tlschema';
|
|
||||||
import { TLUserPresence } from '@tldraw/tlschema';
|
|
||||||
import { TLVideoAsset } from '@tldraw/tlschema';
|
import { TLVideoAsset } from '@tldraw/tlschema';
|
||||||
import { TLVideoShape } from '@tldraw/tlschema';
|
import { TLVideoShape } from '@tldraw/tlschema';
|
||||||
import { UnknownRecord } from '@tldraw/tlstore';
|
import { UnknownRecord } from '@tldraw/tlstore';
|
||||||
|
@ -368,6 +365,7 @@ export class App extends EventEmitter<TLEventMap> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
get isToolLocked(): boolean;
|
get isToolLocked(): boolean;
|
||||||
isWithinSelection(id: TLShapeId): boolean;
|
isWithinSelection(id: TLShapeId): boolean;
|
||||||
|
get locale(): string;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
lockShapes(_ids?: TLShapeId[]): this;
|
lockShapes(_ids?: TLShapeId[]): this;
|
||||||
mark(reason?: string, onUndo?: boolean, onRedo?: boolean): string;
|
mark(reason?: string, onUndo?: boolean, onRedo?: boolean): string;
|
||||||
|
@ -467,6 +465,7 @@ export class App extends EventEmitter<TLEventMap> {
|
||||||
setHintingIds(ids: TLShapeId[]): this;
|
setHintingIds(ids: TLShapeId[]): this;
|
||||||
setHoveredId(id?: null | TLShapeId): this;
|
setHoveredId(id?: null | TLShapeId): this;
|
||||||
setInstancePageState(partial: Partial<TLInstancePageState>, ephemeral?: boolean): void;
|
setInstancePageState(partial: Partial<TLInstancePageState>, ephemeral?: boolean): void;
|
||||||
|
setLocale(locale: string): void;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
setPenMode(isPenMode: boolean): this;
|
setPenMode(isPenMode: boolean): this;
|
||||||
setProp(key: TLShapeProp, value: any, ephemeral?: boolean, squashing?: boolean): this;
|
setProp(key: TLShapeProp, value: any, ephemeral?: boolean, squashing?: boolean): this;
|
||||||
|
@ -495,7 +494,7 @@ export class App extends EventEmitter<TLEventMap> {
|
||||||
readonly snaps: SnapManager;
|
readonly snaps: SnapManager;
|
||||||
get sortedShapesArray(): TLShape[];
|
get sortedShapesArray(): TLShape[];
|
||||||
stackShapes(operation: 'horizontal' | 'vertical', ids?: TLShapeId[], gap?: number): this;
|
stackShapes(operation: 'horizontal' | 'vertical', ids?: TLShapeId[], gap?: number): this;
|
||||||
startFollowingUser: (userId: TLUserId) => this | undefined;
|
startFollowingUser: (userId: string) => this | undefined;
|
||||||
stopCameraAnimation(): this;
|
stopCameraAnimation(): this;
|
||||||
stopFollowingUser: () => this;
|
stopFollowingUser: () => this;
|
||||||
readonly store: TLStore;
|
readonly store: TLStore;
|
||||||
|
@ -511,22 +510,12 @@ export class App extends EventEmitter<TLEventMap> {
|
||||||
updateInstanceState(partial: Partial<Omit<TLInstance, 'currentPageId' | 'documentId' | 'userId'>>, ephemeral?: boolean, squashing?: boolean): this;
|
updateInstanceState(partial: Partial<Omit<TLInstance, 'currentPageId' | 'documentId' | 'userId'>>, ephemeral?: boolean, squashing?: boolean): this;
|
||||||
updatePage(partial: RequiredKeys<TLPage, 'id'>, squashing?: boolean): this;
|
updatePage(partial: RequiredKeys<TLPage, 'id'>, squashing?: boolean): this;
|
||||||
updateShapes(partials: (null | TLShapePartial | undefined)[], squashing?: boolean): this;
|
updateShapes(partials: (null | TLShapePartial | undefined)[], squashing?: boolean): this;
|
||||||
updateUser(partial: Partial<TLUser>): void;
|
|
||||||
updateUserDocumentSettings(partial: Partial<TLUserDocument>, ephemeral?: boolean): this;
|
updateUserDocumentSettings(partial: Partial<TLUserDocument>, ephemeral?: boolean): this;
|
||||||
// (undocumented)
|
|
||||||
updateUserPresence: ({ cursor, color, viewportPageBounds, }?: {
|
|
||||||
cursor?: undefined | Vec2dModel;
|
|
||||||
color?: string | undefined;
|
|
||||||
viewportPageBounds?: Box2dModel | undefined;
|
|
||||||
}) => void;
|
|
||||||
updateViewportScreenBounds(center?: boolean): this;
|
updateViewportScreenBounds(center?: boolean): this;
|
||||||
get user(): TLUser;
|
// @internal (undocumented)
|
||||||
|
readonly user: UserPreferencesManager;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
get userDocumentSettings(): TLUserDocument;
|
get userDocumentSettings(): TLUserDocument;
|
||||||
get userId(): TLUserId;
|
|
||||||
// (undocumented)
|
|
||||||
get userPresence(): TLUserPresence | undefined;
|
|
||||||
get userSettings(): TLUser;
|
|
||||||
get viewportPageBounds(): Box2d;
|
get viewportPageBounds(): Box2d;
|
||||||
get viewportPageCenter(): Vec2d;
|
get viewportPageCenter(): Vec2d;
|
||||||
get viewportScreenBounds(): Box2d;
|
get viewportScreenBounds(): Box2d;
|
||||||
|
@ -1794,10 +1783,13 @@ export class TldrawEditorConfig {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
createStore(config: {
|
createStore(config: {
|
||||||
initialData?: StoreSnapshot<TLRecord>;
|
initialData?: StoreSnapshot<TLRecord>;
|
||||||
userId: TLUserId;
|
|
||||||
instanceId: TLInstanceId;
|
instanceId: TLInstanceId;
|
||||||
}): TLStore;
|
}): TLStore;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
readonly derivePresenceState: (store: TLStore) => Signal<null | TLInstancePresence>;
|
||||||
|
// (undocumented)
|
||||||
|
readonly setUserPreferences: (userPreferences: TLUserPreferences) => void;
|
||||||
|
// (undocumented)
|
||||||
readonly shapeUtils: Record<TLShape['type'], TLShapeUtilConstructor<any>>;
|
readonly shapeUtils: Record<TLShape['type'], TLShapeUtilConstructor<any>>;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
readonly storeSchema: StoreSchema<TLRecord, TLStoreProps>;
|
readonly storeSchema: StoreSchema<TLRecord, TLStoreProps>;
|
||||||
|
@ -1805,6 +1797,8 @@ export class TldrawEditorConfig {
|
||||||
readonly TLShape: RecordType<TLShape, 'index' | 'parentId' | 'props' | 'type'>;
|
readonly TLShape: RecordType<TLShape, 'index' | 'parentId' | 'props' | 'type'>;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
readonly tools: readonly StateNodeConstructor[];
|
readonly tools: readonly StateNodeConstructor[];
|
||||||
|
// (undocumented)
|
||||||
|
readonly userPreferences: Signal<TLUserPreferences>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
|
@ -1825,7 +1819,6 @@ export interface TldrawEditorProps {
|
||||||
}>;
|
}>;
|
||||||
onMount?: (app: App) => void;
|
onMount?: (app: App) => void;
|
||||||
store?: SyncedStore | TLStore;
|
store?: SyncedStore | TLStore;
|
||||||
userId?: TLUserId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
|
@ -2658,17 +2651,20 @@ export const useApp: () => App;
|
||||||
export function useContainer(): HTMLDivElement;
|
export function useContainer(): HTMLDivElement;
|
||||||
|
|
||||||
// @internal (undocumented)
|
// @internal (undocumented)
|
||||||
export function usePeerIds(): TLUserId[];
|
export function usePeerIds(): string[];
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export function usePrefersReducedMotion(): boolean;
|
export function usePrefersReducedMotion(): boolean;
|
||||||
|
|
||||||
// @internal (undocumented)
|
// @internal (undocumented)
|
||||||
export function usePresence(userId: TLUserId): null | TLInstancePresence;
|
export function usePresence(userId: string): null | TLInstancePresence;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export function useQuickReactor(name: string, reactFn: () => void, deps?: any[]): void;
|
export function useQuickReactor(name: string, reactFn: () => void, deps?: any[]): void;
|
||||||
|
|
||||||
|
// @internal (undocumented)
|
||||||
|
export const USER_COLORS: readonly ["#FF802B", "#EC5E41", "#F2555A", "#F04F88", "#E34BA9", "#BD54C6", "#9D5BD2", "#7B66DC", "#02B1CC", "#11B3A3", "#39B178", "#55B467"];
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export function useReactor(name: string, reactFn: () => void, deps?: any[] | undefined): void;
|
export function useReactor(name: string, reactFn: () => void, deps?: any[] | undefined): void;
|
||||||
|
|
||||||
|
|
|
@ -131,6 +131,7 @@ export {
|
||||||
type ReadySyncedStore,
|
type ReadySyncedStore,
|
||||||
type SyncedStore,
|
type SyncedStore,
|
||||||
} from './lib/config/SyncedStore'
|
} from './lib/config/SyncedStore'
|
||||||
|
export { USER_COLORS } from './lib/config/TLUserPreferences'
|
||||||
export { TldrawEditorConfig } from './lib/config/TldrawEditorConfig'
|
export { TldrawEditorConfig } from './lib/config/TldrawEditorConfig'
|
||||||
export {
|
export {
|
||||||
ANIMATION_MEDIUM_MS,
|
ANIMATION_MEDIUM_MS,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { TLAsset, TLInstance, TLInstanceId, TLStore, TLUser, TLUserId } from '@tldraw/tlschema'
|
import { TLAsset, TLInstance, TLInstanceId, TLStore } from '@tldraw/tlschema'
|
||||||
import { Store } from '@tldraw/tlstore'
|
import { Store } from '@tldraw/tlstore'
|
||||||
import { annotateError } from '@tldraw/utils'
|
import { annotateError } from '@tldraw/utils'
|
||||||
import React, { useCallback, useEffect, useState, useSyncExternalStore } from 'react'
|
import React, { useCallback, useMemo, useSyncExternalStore } from 'react'
|
||||||
import { App } from './app/App'
|
import { App } from './app/App'
|
||||||
import { EditorAssetUrls, defaultEditorAssetUrls } from './assetUrls'
|
import { EditorAssetUrls, defaultEditorAssetUrls } from './assetUrls'
|
||||||
import { OptionalErrorBoundary } from './components/ErrorBoundary'
|
import { OptionalErrorBoundary } from './components/ErrorBoundary'
|
||||||
|
@ -87,8 +87,6 @@ export interface TldrawEditorProps {
|
||||||
* from a server or database.
|
* from a server or database.
|
||||||
*/
|
*/
|
||||||
store?: TLStore | SyncedStore
|
store?: TLStore | SyncedStore
|
||||||
/** The id of the current user. If not given, one will be generated. */
|
|
||||||
userId?: TLUserId
|
|
||||||
/**
|
/**
|
||||||
* The id of the app instance (e.g. a browser tab if the app will have only one tldraw app per
|
* The id of the app instance (e.g. a browser tab if the app will have only one tldraw app per
|
||||||
* tab). If not given, one will be generated.
|
* tab). If not given, one will be generated.
|
||||||
|
@ -132,38 +130,19 @@ export function TldrawEditor(props: TldrawEditorProps) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function TldrawEditorBeforeLoading({
|
function TldrawEditorBeforeLoading({ config, instanceId, store, ...props }: TldrawEditorProps) {
|
||||||
config,
|
|
||||||
userId,
|
|
||||||
instanceId,
|
|
||||||
store,
|
|
||||||
...props
|
|
||||||
}: TldrawEditorProps) {
|
|
||||||
const { done: preloadingComplete, error: preloadingError } = usePreloadAssets(
|
const { done: preloadingComplete, error: preloadingError } = usePreloadAssets(
|
||||||
props.assetUrls ?? defaultEditorAssetUrls
|
props.assetUrls ?? defaultEditorAssetUrls
|
||||||
)
|
)
|
||||||
|
|
||||||
const [_store, _setStore] = useState<TLStore | SyncedStore>(() => {
|
const _store = useMemo<TLStore | SyncedStore>(() => {
|
||||||
return (
|
return (
|
||||||
store ??
|
store ??
|
||||||
config.createStore({
|
config.createStore({
|
||||||
userId: userId ?? TLUser.createId(),
|
|
||||||
instanceId: instanceId ?? TLInstance.createId(),
|
instanceId: instanceId ?? TLInstance.createId(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
})
|
}, [store, config, instanceId])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
_setStore(() => {
|
|
||||||
return (
|
|
||||||
store ??
|
|
||||||
config.createStore({
|
|
||||||
userId: userId ?? TLUser.createId(),
|
|
||||||
instanceId: instanceId ?? TLInstance.createId(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}, [store, config, userId, instanceId])
|
|
||||||
|
|
||||||
let loadedStore: TLStore | SyncedStore
|
let loadedStore: TLStore | SyncedStore
|
||||||
if (!(_store instanceof Store)) {
|
if (!(_store instanceof Store)) {
|
||||||
|
@ -188,12 +167,6 @@ function TldrawEditorBeforeLoading({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userId && loadedStore.props.userId !== userId) {
|
|
||||||
console.error(
|
|
||||||
`The store's userId (${loadedStore.props.userId}) does not match the userId prop (${userId}). This may cause unexpected behavior.`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preloadingError) {
|
if (preloadingError) {
|
||||||
return <ErrorScreen>Could not load assets. Please refresh the page.</ErrorScreen>
|
return <ErrorScreen>Could not load assets. Please refresh the page.</ErrorScreen>
|
||||||
}
|
}
|
||||||
|
@ -208,7 +181,6 @@ function TldrawEditorBeforeLoading({
|
||||||
function TldrawEditorAfterLoading({
|
function TldrawEditorAfterLoading({
|
||||||
onMount,
|
onMount,
|
||||||
config,
|
config,
|
||||||
isDarkMode,
|
|
||||||
children,
|
children,
|
||||||
onCreateAssetFromFile,
|
onCreateAssetFromFile,
|
||||||
onCreateBookmarkFromUrl,
|
onCreateBookmarkFromUrl,
|
||||||
|
@ -257,20 +229,15 @@ function TldrawEditorAfterLoading({
|
||||||
const onMountEvent = useEvent((app: App) => {
|
const onMountEvent = useEvent((app: App) => {
|
||||||
onMount?.(app)
|
onMount?.(app)
|
||||||
app.emit('mount')
|
app.emit('mount')
|
||||||
|
window.tldrawReady = true
|
||||||
})
|
})
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (app) {
|
if (app) {
|
||||||
// Set the initial theme state.
|
|
||||||
if (isDarkMode !== undefined) {
|
|
||||||
app.updateUserDocumentSettings({ isDarkMode })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run onMount
|
// Run onMount
|
||||||
window.tldrawReady = true
|
|
||||||
onMountEvent(app)
|
onMountEvent(app)
|
||||||
}
|
}
|
||||||
}, [app, onMountEvent, isDarkMode])
|
}, [app, onMountEvent])
|
||||||
|
|
||||||
const crashingError = useSyncExternalStore(
|
const crashingError = useSyncExternalStore(
|
||||||
useCallback(
|
useCallback(
|
||||||
|
|
|
@ -40,6 +40,7 @@ import {
|
||||||
TLInstanceId,
|
TLInstanceId,
|
||||||
TLInstancePageState,
|
TLInstancePageState,
|
||||||
TLNullableShapeProps,
|
TLNullableShapeProps,
|
||||||
|
TLPOINTER_ID,
|
||||||
TLPage,
|
TLPage,
|
||||||
TLPageId,
|
TLPageId,
|
||||||
TLParentId,
|
TLParentId,
|
||||||
|
@ -52,9 +53,7 @@ import {
|
||||||
TLSizeStyle,
|
TLSizeStyle,
|
||||||
TLStore,
|
TLStore,
|
||||||
TLUnknownShape,
|
TLUnknownShape,
|
||||||
TLUser,
|
|
||||||
TLUserDocument,
|
TLUserDocument,
|
||||||
TLUserId,
|
|
||||||
TLVideoAsset,
|
TLVideoAsset,
|
||||||
Vec2dModel,
|
Vec2dModel,
|
||||||
createCustomShapeId,
|
createCustomShapeId,
|
||||||
|
@ -116,6 +115,7 @@ import { HistoryManager } from './managers/HistoryManager'
|
||||||
import { SnapManager } from './managers/SnapManager'
|
import { SnapManager } from './managers/SnapManager'
|
||||||
import { TextManager } from './managers/TextManager'
|
import { TextManager } from './managers/TextManager'
|
||||||
import { TickManager } from './managers/TickManager'
|
import { TickManager } from './managers/TickManager'
|
||||||
|
import { UserPreferencesManager } from './managers/UserPreferencesManager'
|
||||||
import { TLArrowUtil } from './shapeutils/TLArrowUtil/TLArrowUtil'
|
import { TLArrowUtil } from './shapeutils/TLArrowUtil/TLArrowUtil'
|
||||||
import { getCurvedArrowInfo } from './shapeutils/TLArrowUtil/arrow/curved-arrow'
|
import { getCurvedArrowInfo } from './shapeutils/TLArrowUtil/arrow/curved-arrow'
|
||||||
import {
|
import {
|
||||||
|
@ -185,6 +185,8 @@ export class App extends EventEmitter<TLEventMap> {
|
||||||
|
|
||||||
this.store = store
|
this.store = store
|
||||||
|
|
||||||
|
this.user = new UserPreferencesManager(this)
|
||||||
|
|
||||||
this.getContainer = getContainer ?? (() => document.body)
|
this.getContainer = getContainer ?? (() => document.body)
|
||||||
|
|
||||||
this.textMeasure = new TextManager(this)
|
this.textMeasure = new TextManager(this)
|
||||||
|
@ -355,6 +357,11 @@ export class App extends EventEmitter<TLEventMap> {
|
||||||
*/
|
*/
|
||||||
readonly snaps = new SnapManager(this)
|
readonly snaps = new SnapManager(this)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
readonly user: UserPreferencesManager
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the editor is running in Safari.
|
* Whether the editor is running in Safari.
|
||||||
*
|
*
|
||||||
|
@ -424,21 +431,6 @@ export class App extends EventEmitter<TLEventMap> {
|
||||||
*/
|
*/
|
||||||
getContainer: () => HTMLElement
|
getContainer: () => HTMLElement
|
||||||
|
|
||||||
/**
|
|
||||||
* The editor's userId (defined in its store.props).
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
*
|
|
||||||
* ```ts
|
|
||||||
* const userId = app.userId
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
get userId(): TLUserId {
|
|
||||||
return this.store.props.userId
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The editor's instanceId (defined in its store.props).
|
* The editor's instanceId (defined in its store.props).
|
||||||
*
|
*
|
||||||
|
@ -1197,7 +1189,6 @@ export class App extends EventEmitter<TLEventMap> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateUserPresence()
|
|
||||||
this.emit('update')
|
this.emit('update')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1500,16 +1491,6 @@ export class App extends EventEmitter<TLEventMap> {
|
||||||
return this.documentSettings.gridSize
|
return this.documentSettings.gridSize
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The user's global settings.
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
* @readonly
|
|
||||||
*/
|
|
||||||
get userSettings(): TLUser {
|
|
||||||
return this.store.get(this.userId)!
|
|
||||||
}
|
|
||||||
|
|
||||||
get isSnapMode() {
|
get isSnapMode() {
|
||||||
return this.userDocumentSettings.isSnapMode
|
return this.userDocumentSettings.isSnapMode
|
||||||
}
|
}
|
||||||
|
@ -1522,12 +1503,12 @@ export class App extends EventEmitter<TLEventMap> {
|
||||||
}
|
}
|
||||||
|
|
||||||
get isDarkMode() {
|
get isDarkMode() {
|
||||||
return this.userDocumentSettings.isDarkMode
|
return this.user.isDarkMode
|
||||||
}
|
}
|
||||||
|
|
||||||
setDarkMode(isDarkMode: boolean) {
|
setDarkMode(isDarkMode: boolean) {
|
||||||
if (isDarkMode !== this.isDarkMode) {
|
if (isDarkMode !== this.isDarkMode) {
|
||||||
this.updateUserDocumentSettings({ isDarkMode }, true)
|
this.user.updateUserPreferences({ isDarkMode })
|
||||||
}
|
}
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
@ -1556,7 +1537,7 @@ export class App extends EventEmitter<TLEventMap> {
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
@computed private get _userDocumentSettings() {
|
@computed private get _userDocumentSettings() {
|
||||||
return this.store.query.record('user_document', () => ({ userId: { eq: this.userId } }))
|
return this.store.query.record('user_document')
|
||||||
}
|
}
|
||||||
|
|
||||||
get userDocumentSettings(): TLUserDocument {
|
get userDocumentSettings(): TLUserDocument {
|
||||||
|
@ -1609,15 +1590,6 @@ export class App extends EventEmitter<TLEventMap> {
|
||||||
|
|
||||||
// User / User App State
|
// User / User App State
|
||||||
|
|
||||||
/**
|
|
||||||
* The current user state.
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
get user(): TLUser {
|
|
||||||
return this.store.get(this.userId)!
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The current tab state */
|
/** The current tab state */
|
||||||
get instanceState(): TLInstance {
|
get instanceState(): TLInstance {
|
||||||
return this.store.get(this.instanceId)!
|
return this.store.get(this.instanceId)!
|
||||||
|
@ -3467,7 +3439,15 @@ export class App extends EventEmitter<TLEventMap> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo: We only have to do this if there are multiple users in the document
|
// todo: We only have to do this if there are multiple users in the document
|
||||||
this.updateUserPresence({ cursor: currentPagePoint.toJson() })
|
this.store.put([
|
||||||
|
{
|
||||||
|
id: TLPOINTER_ID,
|
||||||
|
typeName: 'pointer',
|
||||||
|
x: currentPagePoint.x,
|
||||||
|
y: currentPagePoint.y,
|
||||||
|
lastActivityTimestamp: Date.now(),
|
||||||
|
},
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --------------------- Events --------------------- */
|
/* --------------------- Events --------------------- */
|
||||||
|
@ -5017,6 +4997,27 @@ export class App extends EventEmitter<TLEventMap> {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the editor's locale.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
get locale() {
|
||||||
|
return this.user.locale
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the editor's locale. This affects which translations are used when rendering UI elements.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* app.setLocale('fr')
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
setLocale(locale: string) {
|
||||||
|
this.user.updateUserPreferences({ locale })
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update a page.
|
* Update a page.
|
||||||
*
|
*
|
||||||
|
@ -5260,56 +5261,6 @@ export class App extends EventEmitter<TLEventMap> {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
|
||||||
* Set user state. Always ephemeral for now.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
*
|
|
||||||
* ```ts
|
|
||||||
* app.updateUser({ color: '#923433' })
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @param partial - The partial of the user state object containing the changes.
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
updateUser(partial: Partial<TLUser>) {
|
|
||||||
const next = { ...this.user, ...partial }
|
|
||||||
this.store.put([next])
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
@computed private get _currentUserPresence() {
|
|
||||||
return this.store.query.record('user_presence', () => ({ userId: { eq: this.userId } }))
|
|
||||||
}
|
|
||||||
|
|
||||||
get userPresence() {
|
|
||||||
return this._currentUserPresence.value
|
|
||||||
}
|
|
||||||
|
|
||||||
// when a user performs any action in the app, we update their presence record
|
|
||||||
updateUserPresence = ({
|
|
||||||
cursor,
|
|
||||||
color,
|
|
||||||
viewportPageBounds,
|
|
||||||
}: { cursor?: Vec2dModel; color?: string; viewportPageBounds?: Box2dModel } = {}) => {
|
|
||||||
const presence = this._currentUserPresence.value
|
|
||||||
if (!presence) {
|
|
||||||
console.error('No presence found for current user')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.store.put([
|
|
||||||
{
|
|
||||||
...presence,
|
|
||||||
cursor: cursor ?? presence.cursor,
|
|
||||||
color: color ?? presence.color,
|
|
||||||
viewportPageBounds: viewportPageBounds ?? presence.viewportPageBounds,
|
|
||||||
lastUsedInstanceId: this.instanceId,
|
|
||||||
lastActivityTimestamp: Date.now(),
|
|
||||||
},
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Select one or more shapes.
|
* Select one or more shapes.
|
||||||
*
|
*
|
||||||
|
@ -5577,7 +5528,7 @@ export class App extends EventEmitter<TLEventMap> {
|
||||||
scale = 1,
|
scale = 1,
|
||||||
background = false,
|
background = false,
|
||||||
padding = SVG_PADDING,
|
padding = SVG_PADDING,
|
||||||
darkMode = this.userDocumentSettings.isDarkMode,
|
darkMode = this.isDarkMode,
|
||||||
preserveAspectRatio = false,
|
preserveAspectRatio = false,
|
||||||
} = opts
|
} = opts
|
||||||
|
|
||||||
|
@ -7246,17 +7197,11 @@ export class App extends EventEmitter<TLEventMap> {
|
||||||
|
|
||||||
this.store.put([{ ...this.instanceState, currentPageId: toId }])
|
this.store.put([{ ...this.instanceState, currentPageId: toId }])
|
||||||
|
|
||||||
this.updateUserPresence({
|
|
||||||
viewportPageBounds: this.viewportPageBounds.toJson(),
|
|
||||||
})
|
|
||||||
this.updateCullingBounds()
|
this.updateCullingBounds()
|
||||||
},
|
},
|
||||||
undo: ({ fromId }) => {
|
undo: ({ fromId }) => {
|
||||||
this.store.put([{ ...this.instanceState, currentPageId: fromId }])
|
this.store.put([{ ...this.instanceState, currentPageId: fromId }])
|
||||||
|
|
||||||
this.updateUserPresence({
|
|
||||||
viewportPageBounds: this.viewportPageBounds.toJson(),
|
|
||||||
})
|
|
||||||
this.updateCullingBounds()
|
this.updateCullingBounds()
|
||||||
},
|
},
|
||||||
squash: ({ fromId }, { toId }) => {
|
squash: ({ fromId }, { toId }) => {
|
||||||
|
@ -7890,10 +7835,6 @@ export class App extends EventEmitter<TLEventMap> {
|
||||||
isPen: this.isPenMode ?? false,
|
isPen: this.isPenMode ?? false,
|
||||||
})
|
})
|
||||||
|
|
||||||
this.updateUserPresence({
|
|
||||||
viewportPageBounds: this.viewportPageBounds.toJson(),
|
|
||||||
})
|
|
||||||
|
|
||||||
this._cameraManager.tick()
|
this._cameraManager.tick()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -8494,7 +8435,7 @@ export class App extends EventEmitter<TLEventMap> {
|
||||||
* @param userId - The id of the user to follow.
|
* @param userId - The id of the user to follow.
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
startFollowingUser = (userId: TLUserId) => {
|
startFollowingUser = (userId: string) => {
|
||||||
// Currently, we get the leader's viewport page bounds from their user presence.
|
// Currently, we get the leader's viewport page bounds from their user presence.
|
||||||
// This is a placeholder until the ephemeral PR lands.
|
// This is a placeholder until the ephemeral PR lands.
|
||||||
// After that, we'll be able to get the required data from their instance presence instead.
|
// After that, we'll be able to get the required data from their instance presence instead.
|
||||||
|
@ -8502,8 +8443,14 @@ export class App extends EventEmitter<TLEventMap> {
|
||||||
userId: { eq: userId },
|
userId: { eq: userId },
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
const thisUserId = this.user.id
|
||||||
|
|
||||||
|
if (!thisUserId) {
|
||||||
|
console.warn('You should set the userId for the current instance before following a user')
|
||||||
|
}
|
||||||
|
|
||||||
// If the leader is following us, then we can't follow them
|
// If the leader is following us, then we can't follow them
|
||||||
if (leaderPresences.value.some((p) => p.followingUserId === this.userId)) {
|
if (leaderPresences.value.some((p) => p.followingUserId === thisUserId)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8554,7 +8501,7 @@ export class App extends EventEmitter<TLEventMap> {
|
||||||
// At this point, let's check if we're following someone who's following us.
|
// At this point, let's check if we're following someone who's following us.
|
||||||
// If so, we can't try to contain their entire viewport
|
// If so, we can't try to contain their entire viewport
|
||||||
// because that would become a feedback loop where we zoom, they zoom, etc.
|
// because that would become a feedback loop where we zoom, they zoom, etc.
|
||||||
const isFollowingFollower = leaderPresence.followingUserId === this.userId
|
const isFollowingFollower = leaderPresence.followingUserId === thisUserId
|
||||||
|
|
||||||
// Figure out how much to zoom
|
// Figure out how much to zoom
|
||||||
const desiredWidth = width + (leaderWidth - width) * chaseProportion
|
const desiredWidth = width + (leaderWidth - width) * chaseProportion
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { TLUserPreferences } from '../../config/TLUserPreferences'
|
||||||
|
import { App } from '../App'
|
||||||
|
|
||||||
|
export class UserPreferencesManager {
|
||||||
|
constructor(private readonly editor: App) {}
|
||||||
|
|
||||||
|
updateUserPreferences = (userPreferences: Partial<TLUserPreferences>) => {
|
||||||
|
this.editor.config.setUserPreferences({
|
||||||
|
...this.editor.config.userPreferences.value,
|
||||||
|
...userPreferences,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get isDarkMode() {
|
||||||
|
return this.editor.config.userPreferences.value.isDarkMode
|
||||||
|
}
|
||||||
|
|
||||||
|
get id() {
|
||||||
|
return this.editor.config.userPreferences.value.id
|
||||||
|
}
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return this.editor.config.userPreferences.value.name
|
||||||
|
}
|
||||||
|
|
||||||
|
get locale() {
|
||||||
|
return this.editor.config.userPreferences.value.locale
|
||||||
|
}
|
||||||
|
|
||||||
|
get color() {
|
||||||
|
return this.editor.config.userPreferences.value.color
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,8 +2,10 @@ import { createCustomShapeId, TLGeoShape, TLLineShape } from '@tldraw/tlschema'
|
||||||
import { deepCopy } from '@tldraw/utils'
|
import { deepCopy } from '@tldraw/utils'
|
||||||
import { TestApp } from '../../../test/TestApp'
|
import { TestApp } from '../../../test/TestApp'
|
||||||
|
|
||||||
|
jest.mock('nanoid', () => {
|
||||||
let i = 0
|
let i = 0
|
||||||
jest.mock('nanoid', () => ({ nanoid: () => 'id' + i++ }))
|
return { nanoid: () => 'id' + i++ }
|
||||||
|
})
|
||||||
|
|
||||||
let app: TestApp
|
let app: TestApp
|
||||||
const id = createCustomShapeId('line1')
|
const id = createCustomShapeId('line1')
|
||||||
|
|
|
@ -5,7 +5,7 @@ Object {
|
||||||
"id": "shape:line1",
|
"id": "shape:line1",
|
||||||
"index": "a1",
|
"index": "a1",
|
||||||
"isLocked": false,
|
"isLocked": false,
|
||||||
"parentId": "page:id51",
|
"parentId": "page:id50",
|
||||||
"props": Object {
|
"props": Object {
|
||||||
"color": "black",
|
"color": "black",
|
||||||
"dash": "draw",
|
"dash": "draw",
|
||||||
|
|
|
@ -33,7 +33,7 @@ export const ShapeFill = React.memo(function ShapeFill({ d, color, fill }: Shape
|
||||||
const PatternFill = function PatternFill({ d, color }: ShapeFillProps) {
|
const PatternFill = function PatternFill({ d, color }: ShapeFillProps) {
|
||||||
const app = useApp()
|
const app = useApp()
|
||||||
const zoomLevel = useValue('zoomLevel', () => app.zoomLevel, [app])
|
const zoomLevel = useValue('zoomLevel', () => app.zoomLevel, [app])
|
||||||
const isDarkMode = useValue('isDarkMode', () => app.userDocumentSettings.isDarkMode, [app])
|
const isDarkMode = useValue('isDarkMode', () => app.isDarkMode, [app])
|
||||||
|
|
||||||
const intZoom = Math.ceil(zoomLevel)
|
const intZoom = Math.ceil(zoomLevel)
|
||||||
const teenyTiny = app.zoomLevel <= 0.18
|
const teenyTiny = app.zoomLevel <= 0.18
|
||||||
|
|
|
@ -30,7 +30,7 @@ export const DefaultErrorFallback: TLErrorFallback = ({ error, app }) => {
|
||||||
() => {
|
() => {
|
||||||
try {
|
try {
|
||||||
if (app) {
|
if (app) {
|
||||||
return app.userDocumentSettings.isDarkMode
|
return app.isDarkMode
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// we're in a funky error state so this might not work for spooky
|
// we're in a funky error state so this might not work for spooky
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { TLUserId } from '@tldraw/tlschema'
|
|
||||||
import { track } from 'signia-react'
|
import { track } from 'signia-react'
|
||||||
import { useApp } from '../hooks/useApp'
|
import { useApp } from '../hooks/useApp'
|
||||||
import { useEditorComponents } from '../hooks/useEditorComponents'
|
import { useEditorComponents } from '../hooks/useEditorComponents'
|
||||||
|
@ -16,7 +15,7 @@ export const LiveCollaborators = track(function Collaborators() {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const Collaborator = track(function Collaborator({ userId }: { userId: TLUserId }) {
|
const Collaborator = track(function Collaborator({ userId }: { userId: string }) {
|
||||||
const app = useApp()
|
const app = useApp()
|
||||||
const { viewportPageBounds, zoomLevel } = app
|
const { viewportPageBounds, zoomLevel } = app
|
||||||
|
|
||||||
|
|
158
packages/editor/src/lib/config/TLUserPreferences.ts
Normal file
158
packages/editor/src/lib/config/TLUserPreferences.ts
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
import { getDefaultTranslationLocale } from '@tldraw/tlschema'
|
||||||
|
import { defineMigrations, migrate } from '@tldraw/tlstore'
|
||||||
|
import { T } from '@tldraw/tlvalidate'
|
||||||
|
import { atom } from 'signia'
|
||||||
|
import { uniqueId } from '../utils/data'
|
||||||
|
|
||||||
|
const USER_DATA_KEY = 'TLDRAW_USER_DATA_v3'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A user of tldraw
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export interface TLUserPreferences {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
locale: string
|
||||||
|
color: string
|
||||||
|
isDarkMode: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserDataSnapshot {
|
||||||
|
version: number
|
||||||
|
user: TLUserPreferences
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserChangeBroadcastMessage {
|
||||||
|
type: typeof broadcastEventKey
|
||||||
|
origin: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const userTypeValidator: T.Validator<TLUserPreferences> = T.model(
|
||||||
|
'user',
|
||||||
|
T.object({
|
||||||
|
id: T.string,
|
||||||
|
name: T.string,
|
||||||
|
locale: T.string,
|
||||||
|
color: T.string,
|
||||||
|
isDarkMode: T.boolean,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const userTypeMigrations = defineMigrations({})
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
export const USER_COLORS = [
|
||||||
|
'#FF802B',
|
||||||
|
'#EC5E41',
|
||||||
|
'#F2555A',
|
||||||
|
'#F04F88',
|
||||||
|
'#E34BA9',
|
||||||
|
'#BD54C6',
|
||||||
|
'#9D5BD2',
|
||||||
|
'#7B66DC',
|
||||||
|
'#02B1CC',
|
||||||
|
'#11B3A3',
|
||||||
|
'#39B178',
|
||||||
|
'#55B467',
|
||||||
|
] as const
|
||||||
|
|
||||||
|
function getRandomColor() {
|
||||||
|
return USER_COLORS[Math.floor(Math.random() * USER_COLORS.length)]
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFreshUserPreferences(): TLUserPreferences {
|
||||||
|
return {
|
||||||
|
id: uniqueId(),
|
||||||
|
locale: typeof window !== 'undefined' ? getDefaultTranslationLocale() : 'en',
|
||||||
|
name: 'New User',
|
||||||
|
color: getRandomColor(),
|
||||||
|
// TODO: detect dark mode
|
||||||
|
isDarkMode: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadUserPreferences(): TLUserPreferences {
|
||||||
|
const userData =
|
||||||
|
typeof window === 'undefined'
|
||||||
|
? null
|
||||||
|
: ((JSON.parse(window?.localStorage?.getItem(USER_DATA_KEY) || 'null') ??
|
||||||
|
null) as null | UserDataSnapshot)
|
||||||
|
if (userData === null) {
|
||||||
|
return getFreshUserPreferences()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!('version' in userData) || !('user' in userData)) {
|
||||||
|
return getFreshUserPreferences()
|
||||||
|
}
|
||||||
|
|
||||||
|
const migrationResult = migrate<TLUserPreferences>({
|
||||||
|
value: userData.user,
|
||||||
|
fromVersion: userData.version,
|
||||||
|
toVersion: userTypeMigrations.currentVersion ?? 0,
|
||||||
|
migrations: userTypeMigrations,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (migrationResult.type === 'error') {
|
||||||
|
return getFreshUserPreferences()
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
userTypeValidator.validate(migrationResult.value)
|
||||||
|
} catch (e) {
|
||||||
|
return getFreshUserPreferences()
|
||||||
|
}
|
||||||
|
|
||||||
|
return migrationResult.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const globalUserPreferences = atom<TLUserPreferences>('globalUserData', loadUserPreferences())
|
||||||
|
|
||||||
|
function storeUserPreferences() {
|
||||||
|
if (typeof window !== 'undefined' && window.localStorage) {
|
||||||
|
window.localStorage.setItem(
|
||||||
|
USER_DATA_KEY,
|
||||||
|
JSON.stringify({
|
||||||
|
version: userTypeMigrations.currentVersion,
|
||||||
|
user: globalUserPreferences.value,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setUserPreferences(user: TLUserPreferences) {
|
||||||
|
userTypeValidator.validate(user)
|
||||||
|
globalUserPreferences.set(user)
|
||||||
|
storeUserPreferences()
|
||||||
|
broadcastUserPreferencesChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
const isTest = typeof process !== 'undefined' && process.env.NODE_ENV === 'test'
|
||||||
|
|
||||||
|
const channel =
|
||||||
|
typeof BroadcastChannel !== 'undefined' && !isTest
|
||||||
|
? new BroadcastChannel('tldraw-user-sync')
|
||||||
|
: null
|
||||||
|
|
||||||
|
channel?.addEventListener('message', (e) => {
|
||||||
|
const data = e.data as undefined | UserChangeBroadcastMessage
|
||||||
|
if (data?.type === broadcastEventKey && data?.origin !== broadcastOrigin) {
|
||||||
|
globalUserPreferences.set(loadUserPreferences())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const broadcastOrigin = uniqueId()
|
||||||
|
const broadcastEventKey = 'tldraw-user-preferences-change' as const
|
||||||
|
|
||||||
|
function broadcastUserPreferencesChange() {
|
||||||
|
channel?.postMessage({
|
||||||
|
type: broadcastEventKey,
|
||||||
|
origin: broadcastOrigin,
|
||||||
|
} satisfies UserChangeBroadcastMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export function getUserPreferences() {
|
||||||
|
return globalUserPreferences.value
|
||||||
|
}
|
|
@ -9,12 +9,10 @@ import {
|
||||||
TLShape,
|
TLShape,
|
||||||
TLStore,
|
TLStore,
|
||||||
TLStoreProps,
|
TLStoreProps,
|
||||||
TLUser,
|
|
||||||
TLUserId,
|
|
||||||
createTLSchema,
|
createTLSchema,
|
||||||
} from '@tldraw/tlschema'
|
} from '@tldraw/tlschema'
|
||||||
import { Migrations, RecordType, Store, StoreSchema, StoreSnapshot } from '@tldraw/tlstore'
|
import { Migrations, RecordType, Store, StoreSchema, StoreSnapshot } from '@tldraw/tlstore'
|
||||||
import { Signal } from 'signia'
|
import { Signal, computed } from 'signia'
|
||||||
import { TLArrowUtil } from '../app/shapeutils/TLArrowUtil/TLArrowUtil'
|
import { TLArrowUtil } from '../app/shapeutils/TLArrowUtil/TLArrowUtil'
|
||||||
import { TLBookmarkUtil } from '../app/shapeutils/TLBookmarkUtil/TLBookmarkUtil'
|
import { TLBookmarkUtil } from '../app/shapeutils/TLBookmarkUtil/TLBookmarkUtil'
|
||||||
import { TLDrawUtil } from '../app/shapeutils/TLDrawUtil/TLDrawUtil'
|
import { TLDrawUtil } from '../app/shapeutils/TLDrawUtil/TLDrawUtil'
|
||||||
|
@ -29,6 +27,7 @@ import { TLShapeUtilConstructor } from '../app/shapeutils/TLShapeUtil'
|
||||||
import { TLTextUtil } from '../app/shapeutils/TLTextUtil/TLTextUtil'
|
import { TLTextUtil } from '../app/shapeutils/TLTextUtil/TLTextUtil'
|
||||||
import { TLVideoUtil } from '../app/shapeutils/TLVideoUtil/TLVideoUtil'
|
import { TLVideoUtil } from '../app/shapeutils/TLVideoUtil/TLVideoUtil'
|
||||||
import { StateNodeConstructor } from '../app/statechart/StateNode'
|
import { StateNodeConstructor } from '../app/statechart/StateNode'
|
||||||
|
import { TLUserPreferences, getUserPreferences, setUserPreferences } from './TLUserPreferences'
|
||||||
|
|
||||||
// Secret shape types that don't have a shape util yet
|
// Secret shape types that don't have a shape util yet
|
||||||
type ShapeTypesNotImplemented = 'icon'
|
type ShapeTypesNotImplemented = 'icon'
|
||||||
|
@ -63,6 +62,8 @@ export type TldrawEditorConfigOptions = {
|
||||||
>
|
>
|
||||||
/** @internal */
|
/** @internal */
|
||||||
derivePresenceState?: (store: TLStore) => Signal<TLInstancePresence | null>
|
derivePresenceState?: (store: TLStore) => Signal<TLInstancePresence | null>
|
||||||
|
userPreferences?: Signal<TLUserPreferences>
|
||||||
|
setUserPreferences?: (userPreferences: TLUserPreferences) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
|
@ -78,11 +79,18 @@ export class TldrawEditorConfig {
|
||||||
|
|
||||||
// The schema used for the store incorporating any custom shapes
|
// The schema used for the store incorporating any custom shapes
|
||||||
readonly storeSchema: StoreSchema<TLRecord, TLStoreProps>
|
readonly storeSchema: StoreSchema<TLRecord, TLStoreProps>
|
||||||
|
readonly derivePresenceState: (store: TLStore) => Signal<TLInstancePresence | null>
|
||||||
|
readonly userPreferences: Signal<TLUserPreferences>
|
||||||
|
readonly setUserPreferences: (userPreferences: TLUserPreferences) => void
|
||||||
|
|
||||||
constructor(opts = {} as TldrawEditorConfigOptions) {
|
constructor(opts = {} as TldrawEditorConfigOptions) {
|
||||||
const { shapes = {}, tools = [], derivePresenceState } = opts
|
const { shapes = {}, tools = [], derivePresenceState } = opts
|
||||||
|
|
||||||
this.tools = tools
|
this.tools = tools
|
||||||
|
this.derivePresenceState = derivePresenceState ?? (() => computed('presence', () => null))
|
||||||
|
this.userPreferences =
|
||||||
|
opts.userPreferences ?? computed('userPreferences', () => getUserPreferences())
|
||||||
|
this.setUserPreferences = opts.setUserPreferences ?? setUserPreferences
|
||||||
|
|
||||||
this.shapeUtils = {
|
this.shapeUtils = {
|
||||||
...DEFAULT_SHAPE_UTILS,
|
...DEFAULT_SHAPE_UTILS,
|
||||||
|
@ -91,7 +99,6 @@ export class TldrawEditorConfig {
|
||||||
|
|
||||||
this.storeSchema = createTLSchema({
|
this.storeSchema = createTLSchema({
|
||||||
customShapes: shapes,
|
customShapes: shapes,
|
||||||
derivePresenceState: derivePresenceState,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
this.TLShape = this.storeSchema.types.shape as RecordType<
|
this.TLShape = this.storeSchema.types.shape as RecordType<
|
||||||
|
@ -103,18 +110,17 @@ export class TldrawEditorConfig {
|
||||||
createStore(config: {
|
createStore(config: {
|
||||||
/** The store's initial data. */
|
/** The store's initial data. */
|
||||||
initialData?: StoreSnapshot<TLRecord>
|
initialData?: StoreSnapshot<TLRecord>
|
||||||
userId: TLUserId
|
|
||||||
instanceId: TLInstanceId
|
instanceId: TLInstanceId
|
||||||
}): TLStore {
|
}): TLStore {
|
||||||
let initialData = config.initialData
|
let initialData = config.initialData
|
||||||
if (initialData) {
|
if (initialData) {
|
||||||
initialData = CLIENT_FIXUP_SCRIPT(initialData)
|
initialData = CLIENT_FIXUP_SCRIPT(initialData)
|
||||||
}
|
}
|
||||||
return new Store({
|
|
||||||
|
return new Store<TLRecord, TLStoreProps>({
|
||||||
schema: this.storeSchema,
|
schema: this.storeSchema,
|
||||||
initialData,
|
initialData,
|
||||||
props: {
|
props: {
|
||||||
userId: config?.userId ?? TLUser.createId(),
|
|
||||||
instanceId: config?.instanceId ?? TLInstance.createId(),
|
instanceId: config?.instanceId ?? TLInstance.createId(),
|
||||||
documentId: TLDOCUMENT_ID,
|
documentId: TLDOCUMENT_ID,
|
||||||
},
|
},
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { useContainer } from './useContainer'
|
||||||
export function useDarkMode() {
|
export function useDarkMode() {
|
||||||
const app = useApp()
|
const app = useApp()
|
||||||
const container = useContainer()
|
const container = useContainer()
|
||||||
const isDarkMode = useValue('isDarkMode', () => app.userDocumentSettings.isDarkMode, [app])
|
const isDarkMode = useValue('isDarkMode', () => app.isDarkMode, [app])
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (isDarkMode) {
|
if (isDarkMode) {
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { useApp } from './useApp'
|
||||||
export function usePeerIds() {
|
export function usePeerIds() {
|
||||||
const app = useApp()
|
const app = useApp()
|
||||||
const $presences = useMemo(() => {
|
const $presences = useMemo(() => {
|
||||||
return app.store.query.records('instance_presence')
|
return app.store.query.records('instance_presence', () => ({ userId: { neq: app.user.id } }))
|
||||||
}, [app])
|
}, [app])
|
||||||
|
|
||||||
const $userIds = useComputed(
|
const $userIds = useComputed(
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { TLInstancePresence, TLUserId } from '@tldraw/tlschema'
|
import { TLInstancePresence } from '@tldraw/tlschema'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { useValue } from 'signia-react'
|
import { useValue } from 'signia-react'
|
||||||
import { useApp } from './useApp'
|
import { useApp } from './useApp'
|
||||||
|
@ -8,7 +8,7 @@ import { useApp } from './useApp'
|
||||||
* @returns The list of peer UserIDs
|
* @returns The list of peer UserIDs
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
export function usePresence(userId: TLUserId): TLInstancePresence | null {
|
export function usePresence(userId: string): TLInstancePresence | null {
|
||||||
const app = useApp()
|
const app = useApp()
|
||||||
|
|
||||||
const $presences = useMemo(() => {
|
const $presences = useMemo(() => {
|
||||||
|
|
|
@ -13,7 +13,6 @@ import {
|
||||||
TLPage,
|
TLPage,
|
||||||
TLShapeId,
|
TLShapeId,
|
||||||
TLShapePartial,
|
TLShapePartial,
|
||||||
TLUser,
|
|
||||||
createCustomShapeId,
|
createCustomShapeId,
|
||||||
createShapeId,
|
createShapeId,
|
||||||
} from '@tldraw/tlschema'
|
} from '@tldraw/tlschema'
|
||||||
|
@ -52,7 +51,6 @@ declare global {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export const TEST_INSTANCE_ID = TLInstance.createCustomId('testInstance1')
|
export const TEST_INSTANCE_ID = TLInstance.createCustomId('testInstance1')
|
||||||
export const TEST_USER_ID = TLUser.createCustomId('testUser1')
|
|
||||||
|
|
||||||
export class TestApp extends App {
|
export class TestApp extends App {
|
||||||
constructor(options = {} as Partial<Omit<AppOptions, 'store'>>) {
|
constructor(options = {} as Partial<Omit<AppOptions, 'store'>>) {
|
||||||
|
@ -62,7 +60,6 @@ export class TestApp extends App {
|
||||||
super({
|
super({
|
||||||
config,
|
config,
|
||||||
store: config.createStore({
|
store: config.createStore({
|
||||||
userId: TEST_USER_ID,
|
|
||||||
instanceId: TEST_INSTANCE_ID,
|
instanceId: TEST_INSTANCE_ID,
|
||||||
}),
|
}),
|
||||||
getContainer: () => elm,
|
getContainer: () => elm,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { render, screen } from '@testing-library/react'
|
import { render, screen } from '@testing-library/react'
|
||||||
import { TLInstance, TLUser } from '@tldraw/tlschema'
|
import { TLInstance } from '@tldraw/tlschema'
|
||||||
import { TldrawEditor } from '../TldrawEditor'
|
import { TldrawEditor } from '../TldrawEditor'
|
||||||
import { TldrawEditorConfig } from '../config/TldrawEditorConfig'
|
import { TldrawEditorConfig } from '../config/TldrawEditorConfig'
|
||||||
|
|
||||||
|
@ -25,7 +25,6 @@ describe('<Tldraw />', () => {
|
||||||
|
|
||||||
const initialStore = config.createStore({
|
const initialStore = config.createStore({
|
||||||
instanceId: TLInstance.createCustomId('test'),
|
instanceId: TLInstance.createCustomId('test'),
|
||||||
userId: TLUser.createCustomId('test'),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const onMount = jest.fn()
|
const onMount = jest.fn()
|
||||||
|
@ -54,7 +53,6 @@ describe('<Tldraw />', () => {
|
||||||
// re-render with a new store:
|
// re-render with a new store:
|
||||||
const newStore = config.createStore({
|
const newStore = config.createStore({
|
||||||
instanceId: TLInstance.createCustomId('test'),
|
instanceId: TLInstance.createCustomId('test'),
|
||||||
userId: TLUser.createCustomId('test'),
|
|
||||||
})
|
})
|
||||||
rendered.rerender(
|
rendered.rerender(
|
||||||
<TldrawEditor config={config} store={newStore} onMount={onMount} autoFocus>
|
<TldrawEditor config={config} store={newStore} onMount={onMount} autoFocus>
|
||||||
|
|
|
@ -19,8 +19,10 @@ import { TLLineTool } from '../../app/statechart/TLLineTool/TLLineTool'
|
||||||
import { TLNoteTool } from '../../app/statechart/TLNoteTool/TLNoteTool'
|
import { TLNoteTool } from '../../app/statechart/TLNoteTool/TLNoteTool'
|
||||||
import { TestApp } from '../TestApp'
|
import { TestApp } from '../TestApp'
|
||||||
|
|
||||||
|
jest.mock('nanoid', () => {
|
||||||
let i = 0
|
let i = 0
|
||||||
jest.mock('nanoid', () => ({ nanoid: () => 'id' + i++ }))
|
return { nanoid: () => 'id' + i++ }
|
||||||
|
})
|
||||||
|
|
||||||
const ids = {
|
const ids = {
|
||||||
boxA: createCustomShapeId('boxA'),
|
boxA: createCustomShapeId('boxA'),
|
||||||
|
|
|
@ -12,7 +12,6 @@ import { TldrawEditorConfig } from '@tldraw/editor';
|
||||||
import { TLInstanceId } from '@tldraw/editor';
|
import { TLInstanceId } from '@tldraw/editor';
|
||||||
import { TLStore } from '@tldraw/editor';
|
import { TLStore } from '@tldraw/editor';
|
||||||
import { TLTranslationKey } from '@tldraw/ui';
|
import { TLTranslationKey } from '@tldraw/ui';
|
||||||
import { TLUserId } from '@tldraw/editor';
|
|
||||||
import { ToastsContextType } from '@tldraw/ui';
|
import { ToastsContextType } from '@tldraw/ui';
|
||||||
import { UnknownRecord } from '@tldraw/tlstore';
|
import { UnknownRecord } from '@tldraw/tlstore';
|
||||||
|
|
||||||
|
@ -23,10 +22,9 @@ export function isV1File(data: any): boolean;
|
||||||
export function parseAndLoadDocument(app: App, document: string, msg: (id: TLTranslationKey) => string, addToast: ToastsContextType['addToast'], onV1FileLoad?: () => void, forceDarkMode?: boolean): Promise<void>;
|
export function parseAndLoadDocument(app: App, document: string, msg: (id: TLTranslationKey) => string, addToast: ToastsContextType['addToast'], onV1FileLoad?: () => void, forceDarkMode?: boolean): Promise<void>;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export function parseTldrawJsonFile({ config, json, userId, instanceId, }: {
|
export function parseTldrawJsonFile({ config, json, instanceId, }: {
|
||||||
config: TldrawEditorConfig;
|
config: TldrawEditorConfig;
|
||||||
json: string;
|
json: string;
|
||||||
userId: TLUserId;
|
|
||||||
instanceId: TLInstanceId;
|
instanceId: TLInstanceId;
|
||||||
}): Result<TLStore, TldrawFileParseError>;
|
}): Result<TLStore, TldrawFileParseError>;
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,6 @@ import {
|
||||||
TLInstanceId,
|
TLInstanceId,
|
||||||
TLRecord,
|
TLRecord,
|
||||||
TLStore,
|
TLStore,
|
||||||
TLUserId,
|
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
import {
|
import {
|
||||||
ID,
|
ID,
|
||||||
|
@ -84,12 +83,10 @@ export type TldrawFileParseError =
|
||||||
export function parseTldrawJsonFile({
|
export function parseTldrawJsonFile({
|
||||||
config,
|
config,
|
||||||
json,
|
json,
|
||||||
userId,
|
|
||||||
instanceId,
|
instanceId,
|
||||||
}: {
|
}: {
|
||||||
config: TldrawEditorConfig
|
config: TldrawEditorConfig
|
||||||
json: string
|
json: string
|
||||||
userId: TLUserId
|
|
||||||
instanceId: TLInstanceId
|
instanceId: TLInstanceId
|
||||||
}): Result<TLStore, TldrawFileParseError> {
|
}): Result<TLStore, TldrawFileParseError> {
|
||||||
// first off, we parse .json file and check it matches the general shape of
|
// first off, we parse .json file and check it matches the general shape of
|
||||||
|
@ -140,7 +137,7 @@ export function parseTldrawJsonFile({
|
||||||
// we should be able to validate them. if any of the records at this stage
|
// we should be able to validate them. if any of the records at this stage
|
||||||
// are invalid, we don't open the file
|
// are invalid, we don't open the file
|
||||||
try {
|
try {
|
||||||
return Result.ok(config.createStore({ initialData: migrationResult.value, userId, instanceId }))
|
return Result.ok(config.createStore({ initialData: migrationResult.value, instanceId }))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// junk data in the records (they're not validated yet!) could cause the
|
// junk data in the records (they're not validated yet!) could cause the
|
||||||
// migrations to crash. We treat any throw from a migration as an
|
// migrations to crash. We treat any throw from a migration as an
|
||||||
|
@ -211,7 +208,6 @@ export async function parseAndLoadDocument(
|
||||||
config: new TldrawEditorConfig(),
|
config: new TldrawEditorConfig(),
|
||||||
json: document,
|
json: document,
|
||||||
instanceId: app.instanceId,
|
instanceId: app.instanceId,
|
||||||
userId: app.userId,
|
|
||||||
})
|
})
|
||||||
if (!parseFileResult.ok) {
|
if (!parseFileResult.ok) {
|
||||||
let description
|
let description
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { createCustomShapeId, TldrawEditorConfig, TLInstance, TLUser } from '@tldraw/editor'
|
import { createCustomShapeId, TldrawEditorConfig, TLInstance } from '@tldraw/editor'
|
||||||
import { MigrationFailureReason, UnknownRecord } from '@tldraw/tlstore'
|
import { MigrationFailureReason, UnknownRecord } from '@tldraw/tlstore'
|
||||||
import { assert } from '@tldraw/utils'
|
import { assert } from '@tldraw/utils'
|
||||||
import { parseTldrawJsonFile as _parseTldrawJsonFile, TldrawFile } from '../lib/file'
|
import { parseTldrawJsonFile as _parseTldrawJsonFile, TldrawFile } from '../lib/file'
|
||||||
|
@ -7,7 +7,6 @@ const parseTldrawJsonFile = (config: TldrawEditorConfig, json: string) =>
|
||||||
_parseTldrawJsonFile({
|
_parseTldrawJsonFile({
|
||||||
config,
|
config,
|
||||||
json,
|
json,
|
||||||
userId: TLUser.createCustomId('user'),
|
|
||||||
instanceId: TLInstance.createCustomId('instance'),
|
instanceId: TLInstance.createCustomId('instance'),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -22,7 +21,7 @@ describe('parseTldrawJsonFile', () => {
|
||||||
expect(result.error.type).toBe('notATldrawFile')
|
expect(result.error.type).toBe('notATldrawFile')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns an error if the file doesnt look like a tldraw file', () => {
|
it("returns an error if the file doesn't look like a tldraw file", () => {
|
||||||
const result = parseTldrawJsonFile(
|
const result = parseTldrawJsonFile(
|
||||||
new TldrawEditorConfig(),
|
new TldrawEditorConfig(),
|
||||||
JSON.stringify({ not: 'a tldraw file' })
|
JSON.stringify({ not: 'a tldraw file' })
|
||||||
|
|
|
@ -1,12 +1,7 @@
|
||||||
import { Canvas, TldrawEditor, TldrawEditorConfig, TldrawEditorProps } from '@tldraw/editor'
|
import { Canvas, TldrawEditor, TldrawEditorConfig, TldrawEditorProps } from '@tldraw/editor'
|
||||||
import {
|
import { DEFAULT_DOCUMENT_NAME, TAB_ID, useLocalSyncClient } from '@tldraw/tlsync-client'
|
||||||
DEFAULT_DOCUMENT_NAME,
|
|
||||||
TAB_ID,
|
|
||||||
getUserData,
|
|
||||||
useLocalSyncClient,
|
|
||||||
} from '@tldraw/tlsync-client'
|
|
||||||
import { ContextMenu, TldrawUi, TldrawUiContextProviderProps } from '@tldraw/ui'
|
import { ContextMenu, TldrawUi, TldrawUiContextProviderProps } from '@tldraw/ui'
|
||||||
import { useEffect, useState } from 'react'
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function Tldraw(
|
export function Tldraw(
|
||||||
|
@ -28,31 +23,16 @@ export function Tldraw(
|
||||||
...rest
|
...rest
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const [_config, _setConfig] = useState(() => config ?? new TldrawEditorConfig())
|
const _config = useMemo(() => config ?? new TldrawEditorConfig(), [config])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
_setConfig(config ?? new TldrawEditorConfig())
|
|
||||||
}, [config])
|
|
||||||
|
|
||||||
const userData = getUserData()
|
|
||||||
|
|
||||||
const userId = props.userId ?? userData.id
|
|
||||||
|
|
||||||
const syncedStore = useLocalSyncClient({
|
const syncedStore = useLocalSyncClient({
|
||||||
instanceId,
|
instanceId,
|
||||||
userId,
|
|
||||||
config: _config,
|
config: _config,
|
||||||
universalPersistenceKey: persistenceKey,
|
universalPersistenceKey: persistenceKey,
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TldrawEditor
|
<TldrawEditor {...rest} instanceId={instanceId} store={syncedStore} config={_config}>
|
||||||
{...rest}
|
|
||||||
instanceId={instanceId}
|
|
||||||
userId={userId}
|
|
||||||
store={syncedStore}
|
|
||||||
config={_config}
|
|
||||||
>
|
|
||||||
<TldrawUi {...rest}>
|
<TldrawUi {...rest}>
|
||||||
<ContextMenu>
|
<ContextMenu>
|
||||||
<Canvas />
|
<Canvas />
|
||||||
|
|
|
@ -87,6 +87,13 @@ export function createCustomShapeId(id: string): TLShapeId;
|
||||||
// @internal (undocumented)
|
// @internal (undocumented)
|
||||||
export function createIntegrityChecker(store: TLStore): () => void;
|
export function createIntegrityChecker(store: TLStore): () => void;
|
||||||
|
|
||||||
|
// @internal (undocumented)
|
||||||
|
export const createPresenceStateDerivation: ($user: Signal<{
|
||||||
|
id: string;
|
||||||
|
color: string;
|
||||||
|
name: string;
|
||||||
|
}>) => (store: TLStore) => Signal<null | TLInstancePresence>;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export function createShapeId(): TLShapeId;
|
export function createShapeId(): TLShapeId;
|
||||||
|
|
||||||
|
@ -107,7 +114,6 @@ export function createShapeValidator<Type extends string, Props extends object>(
|
||||||
// @public
|
// @public
|
||||||
export function createTLSchema<T extends TLUnknownShape>(opts?: {
|
export function createTLSchema<T extends TLUnknownShape>(opts?: {
|
||||||
customShapes?: { [K in T["type"]]: CustomShapeInfo<T>; } | undefined;
|
customShapes?: { [K in T["type"]]: CustomShapeInfo<T>; } | undefined;
|
||||||
derivePresenceState?: ((store: TLStore) => Signal<null | TLInstancePresence>) | undefined;
|
|
||||||
}): StoreSchema<TLRecord, TLStoreProps>;
|
}): StoreSchema<TLRecord, TLStoreProps>;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
|
@ -119,9 +125,6 @@ export const cursorValidator: T.Validator<TLCursor>;
|
||||||
// @internal (undocumented)
|
// @internal (undocumented)
|
||||||
export const dashValidator: T.Validator<"dashed" | "dotted" | "draw" | "solid">;
|
export const dashValidator: T.Validator<"dashed" | "dotted" | "draw" | "solid">;
|
||||||
|
|
||||||
// @internal (undocumented)
|
|
||||||
export const defaultDerivePresenceState: (store: TLStore) => Signal<null | TLInstancePresence>;
|
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const documentTypeValidator: T.Validator<TLDocument>;
|
export const documentTypeValidator: T.Validator<TLDocument>;
|
||||||
|
|
||||||
|
@ -351,6 +354,9 @@ export const geoShapeTypeValidator: T.Validator<TLGeoShape>;
|
||||||
// @internal (undocumented)
|
// @internal (undocumented)
|
||||||
export const geoValidator: T.Validator<"arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "check-box" | "diamond" | "ellipse" | "hexagon" | "octagon" | "oval" | "pentagon" | "rectangle" | "rhombus-2" | "rhombus" | "star" | "trapezoid" | "triangle" | "x-box">;
|
export const geoValidator: T.Validator<"arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "check-box" | "diamond" | "ellipse" | "hexagon" | "octagon" | "oval" | "pentagon" | "rectangle" | "rhombus-2" | "rhombus" | "star" | "trapezoid" | "triangle" | "x-box">;
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export function getDefaultTranslationLocale(): TLTranslationLocale;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const groupShapeTypeMigrations: Migrations;
|
export const groupShapeTypeMigrations: Migrations;
|
||||||
|
|
||||||
|
@ -432,6 +438,9 @@ export const pageTypeValidator: T.Validator<TLPage>;
|
||||||
// @internal (undocumented)
|
// @internal (undocumented)
|
||||||
export const parentIdValidator: T.Validator<TLParentId>;
|
export const parentIdValidator: T.Validator<TLParentId>;
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export const pointerTypeValidator: T.Validator<TLPointer>;
|
||||||
|
|
||||||
// @internal (undocumented)
|
// @internal (undocumented)
|
||||||
export const rootShapeTypeMigrations: Migrations;
|
export const rootShapeTypeMigrations: Migrations;
|
||||||
|
|
||||||
|
@ -970,7 +979,7 @@ export interface TLInstance extends BaseRecord<'instance', TLInstanceId> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
exportBackground: boolean;
|
exportBackground: boolean;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
followingUserId: null | TLUserId;
|
followingUserId: null | string;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
isDebugMode: boolean;
|
isDebugMode: boolean;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
@ -984,13 +993,11 @@ export interface TLInstance extends BaseRecord<'instance', TLInstanceId> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
scribble: null | TLScribble;
|
scribble: null | TLScribble;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
userId: TLUserId;
|
|
||||||
// (undocumented)
|
|
||||||
zoomBrush: Box2dModel | null;
|
zoomBrush: Box2dModel | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const TLInstance: RecordType<TLInstance, "currentPageId" | "userId">;
|
export const TLInstance: RecordType<TLInstance, "currentPageId">;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export type TLInstanceId = ID<TLInstance>;
|
export type TLInstanceId = ID<TLInstance>;
|
||||||
|
@ -1047,7 +1054,7 @@ export interface TLInstancePresence extends BaseRecord<'instance_presence', TLIn
|
||||||
rotation: number;
|
rotation: number;
|
||||||
};
|
};
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
followingUserId: null | TLUserId;
|
followingUserId: null | string;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
instanceId: TLInstanceId;
|
instanceId: TLInstanceId;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
@ -1059,13 +1066,13 @@ export interface TLInstancePresence extends BaseRecord<'instance_presence', TLIn
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
selectedIds: TLShapeId[];
|
selectedIds: TLShapeId[];
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
userId: TLUserId;
|
userId: string;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
userName: string;
|
userName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const TLInstancePresence: RecordType<TLInstancePresence, "brush" | "camera" | "color" | "currentPageId" | "cursor" | "followingUserId" | "instanceId" | "lastActivityTimestamp" | "screenBounds" | "scribble" | "selectedIds" | "userId" | "userName">;
|
export const TLInstancePresence: RecordType<TLInstancePresence, "currentPageId" | "instanceId" | "userId" | "userName">;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export type TLInstancePropsForNextShape = Pick<TLShapeProps, TLStyleType>;
|
export type TLInstancePropsForNextShape = Pick<TLShapeProps, TLStyleType>;
|
||||||
|
@ -1133,8 +1140,27 @@ export type TLPageId = ID<TLPage>;
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export type TLParentId = TLPageId | TLShapeId;
|
export type TLParentId = TLPageId | TLShapeId;
|
||||||
|
|
||||||
|
// @public
|
||||||
|
export interface TLPointer extends BaseRecord<'pointer', TLPointerId> {
|
||||||
|
// (undocumented)
|
||||||
|
lastActivityTimestamp: number;
|
||||||
|
// (undocumented)
|
||||||
|
x: number;
|
||||||
|
// (undocumented)
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export type TLRecord = TLAsset | TLCamera | TLDocument | TLInstance | TLInstancePageState | TLInstancePresence | TLPage | TLShape | TLUser | TLUserDocument | TLUserPresence;
|
export const TLPointer: RecordType<TLPointer, never>;
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export const TLPOINTER_ID: TLPointerId;
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export type TLPointerId = ID<TLPointer>;
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export type TLRecord = TLAsset | TLCamera | TLDocument | TLInstance | TLInstancePageState | TLInstancePresence | TLPage | TLPointer | TLShape | TLUserDocument;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export type TLScribble = {
|
export type TLScribble = {
|
||||||
|
@ -1192,7 +1218,6 @@ export type TLStore = Store<TLRecord, TLStoreProps>;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export type TLStoreProps = {
|
export type TLStoreProps = {
|
||||||
userId: TLUserId;
|
|
||||||
instanceId: TLInstanceId;
|
instanceId: TLInstanceId;
|
||||||
documentId: typeof TLDOCUMENT_ID;
|
documentId: typeof TLDOCUMENT_ID;
|
||||||
};
|
};
|
||||||
|
@ -1262,21 +1287,8 @@ export type TLUiColorType = SetValue<typeof TL_UI_COLOR_TYPES>;
|
||||||
// @public
|
// @public
|
||||||
export type TLUnknownShape = TLBaseShape<string, object>;
|
export type TLUnknownShape = TLBaseShape<string, object>;
|
||||||
|
|
||||||
// @public
|
|
||||||
export interface TLUser extends BaseRecord<'user', TLUserId> {
|
|
||||||
// (undocumented)
|
|
||||||
locale: string;
|
|
||||||
// (undocumented)
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// @public (undocumented)
|
|
||||||
export const TLUser: RecordType<TLUser, never>;
|
|
||||||
|
|
||||||
// @public
|
// @public
|
||||||
export interface TLUserDocument extends BaseRecord<'user_document', TLUserDocumentId> {
|
export interface TLUserDocument extends BaseRecord<'user_document', TLUserDocumentId> {
|
||||||
// (undocumented)
|
|
||||||
isDarkMode: boolean;
|
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
isGridMode: boolean;
|
isGridMode: boolean;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
@ -1289,41 +1301,14 @@ export interface TLUserDocument extends BaseRecord<'user_document', TLUserDocume
|
||||||
lastUpdatedPageId: ID<TLPage> | null;
|
lastUpdatedPageId: ID<TLPage> | null;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
lastUsedTabId: ID<TLInstance> | null;
|
lastUsedTabId: ID<TLInstance> | null;
|
||||||
// (undocumented)
|
|
||||||
userId: TLUserId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const TLUserDocument: RecordType<TLUserDocument, "userId">;
|
export const TLUserDocument: RecordType<TLUserDocument, never>;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export type TLUserDocumentId = ID<TLUserDocument>;
|
export type TLUserDocumentId = ID<TLUserDocument>;
|
||||||
|
|
||||||
// @public (undocumented)
|
|
||||||
export type TLUserId = ID<TLUser>;
|
|
||||||
|
|
||||||
// @public (undocumented)
|
|
||||||
export interface TLUserPresence extends BaseRecord<'user_presence', TLUserPresenceId> {
|
|
||||||
// (undocumented)
|
|
||||||
color: string;
|
|
||||||
// (undocumented)
|
|
||||||
cursor: Vec2dModel;
|
|
||||||
// (undocumented)
|
|
||||||
lastActivityTimestamp: number;
|
|
||||||
// (undocumented)
|
|
||||||
lastUsedInstanceId: null | TLInstanceId;
|
|
||||||
// (undocumented)
|
|
||||||
userId: TLUserId;
|
|
||||||
// (undocumented)
|
|
||||||
viewportPageBounds: Box2dModel;
|
|
||||||
}
|
|
||||||
|
|
||||||
// @public (undocumented)
|
|
||||||
export const TLUserPresence: RecordType<TLUserPresence, "userId">;
|
|
||||||
|
|
||||||
// @public (undocumented)
|
|
||||||
export type TLUserPresenceId = ID<TLUserPresence>;
|
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export type TLVerticalAlignType = SetValue<typeof TL_VERTICAL_ALIGN_TYPES>;
|
export type TLVerticalAlignType = SetValue<typeof TL_VERTICAL_ALIGN_TYPES>;
|
||||||
|
|
||||||
|
@ -1354,27 +1339,12 @@ export type TLVideoShapeProps = {
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const uiColorTypeValidator: T.Validator<"accent" | "black" | "laser" | "muted-1" | "selection-fill" | "selection-stroke" | "white">;
|
export const uiColorTypeValidator: T.Validator<"accent" | "black" | "laser" | "muted-1" | "selection-fill" | "selection-stroke" | "white">;
|
||||||
|
|
||||||
// @internal (undocumented)
|
|
||||||
export const USER_COLORS: string[];
|
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const userDocumentTypeMigrations: Migrations;
|
export const userDocumentTypeMigrations: Migrations;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const userDocumentTypeValidator: T.Validator<TLUserDocument>;
|
export const userDocumentTypeValidator: T.Validator<TLUserDocument>;
|
||||||
|
|
||||||
// @internal (undocumented)
|
|
||||||
export const userIdValidator: T.Validator<TLUserId>;
|
|
||||||
|
|
||||||
// @public (undocumented)
|
|
||||||
export const userPresenceTypeMigrations: Migrations;
|
|
||||||
|
|
||||||
// @public (undocumented)
|
|
||||||
export const userPresenceTypeValidator: T.Validator<TLUserPresence>;
|
|
||||||
|
|
||||||
// @public (undocumented)
|
|
||||||
export const userTypeValidator: T.Validator<TLUser>;
|
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export interface Vec2dModel {
|
export interface Vec2dModel {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
|
|
@ -5,10 +5,9 @@ import { TLInstance } from './records/TLInstance'
|
||||||
import { TLInstancePageState } from './records/TLInstancePageState'
|
import { TLInstancePageState } from './records/TLInstancePageState'
|
||||||
import { TLInstancePresence } from './records/TLInstancePresence'
|
import { TLInstancePresence } from './records/TLInstancePresence'
|
||||||
import { TLPage } from './records/TLPage'
|
import { TLPage } from './records/TLPage'
|
||||||
|
import { TLPointer } from './records/TLPointer'
|
||||||
import { TLShape } from './records/TLShape'
|
import { TLShape } from './records/TLShape'
|
||||||
import { TLUser } from './records/TLUser'
|
|
||||||
import { TLUserDocument } from './records/TLUserDocument'
|
import { TLUserDocument } from './records/TLUserDocument'
|
||||||
import { TLUserPresence } from './records/TLUserPresence'
|
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLRecord =
|
export type TLRecord =
|
||||||
|
@ -19,7 +18,6 @@ export type TLRecord =
|
||||||
| TLInstancePageState
|
| TLInstancePageState
|
||||||
| TLPage
|
| TLPage
|
||||||
| TLShape
|
| TLShape
|
||||||
| TLUser
|
|
||||||
| TLUserDocument
|
| TLUserDocument
|
||||||
| TLUserPresence
|
|
||||||
| TLInstancePresence
|
| TLInstancePresence
|
||||||
|
| TLPointer
|
||||||
|
|
|
@ -6,9 +6,8 @@ import { TLDOCUMENT_ID, TLDocument } from './records/TLDocument'
|
||||||
import { TLInstance, TLInstanceId } from './records/TLInstance'
|
import { TLInstance, TLInstanceId } from './records/TLInstance'
|
||||||
import { TLInstancePageState } from './records/TLInstancePageState'
|
import { TLInstancePageState } from './records/TLInstancePageState'
|
||||||
import { TLPage } from './records/TLPage'
|
import { TLPage } from './records/TLPage'
|
||||||
import { TLUser, TLUserId } from './records/TLUser'
|
import { TLPOINTER_ID, TLPointer } from './records/TLPointer'
|
||||||
import { TLUserDocument } from './records/TLUserDocument'
|
import { TLUserDocument } from './records/TLUserDocument'
|
||||||
import { TLUserPresence } from './records/TLUserPresence'
|
|
||||||
|
|
||||||
function sortByIndex<T extends { index: string }>(a: T, b: T) {
|
function sortByIndex<T extends { index: string }>(a: T, b: T) {
|
||||||
if (a.index < b.index) {
|
if (a.index < b.index) {
|
||||||
|
@ -19,22 +18,6 @@ function sortByIndex<T extends { index: string }>(a: T, b: T) {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
export const USER_COLORS = [
|
|
||||||
'#FF802B',
|
|
||||||
'#EC5E41',
|
|
||||||
'#F2555A',
|
|
||||||
'#F04F88',
|
|
||||||
'#E34BA9',
|
|
||||||
'#BD54C6',
|
|
||||||
'#9D5BD2',
|
|
||||||
'#7B66DC',
|
|
||||||
'#02B1CC',
|
|
||||||
'#11B3A3',
|
|
||||||
'#39B178',
|
|
||||||
'#55B467',
|
|
||||||
]
|
|
||||||
|
|
||||||
function redactRecordForErrorReporting(record: any) {
|
function redactRecordForErrorReporting(record: any) {
|
||||||
if (record.typeName === 'asset') {
|
if (record.typeName === 'asset') {
|
||||||
if ('src' in record) {
|
if ('src' in record) {
|
||||||
|
@ -55,7 +38,6 @@ export type TLStoreSnapshot = StoreSnapshot<TLRecord>
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLStoreProps = {
|
export type TLStoreProps = {
|
||||||
userId: TLUserId
|
|
||||||
instanceId: TLInstanceId
|
instanceId: TLInstanceId
|
||||||
documentId: typeof TLDOCUMENT_ID
|
documentId: typeof TLDOCUMENT_ID
|
||||||
}
|
}
|
||||||
|
@ -90,10 +72,6 @@ export const onValidationFailure: StoreSchemaOptions<
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRandomColor() {
|
|
||||||
return USER_COLORS[Math.floor(Math.random() * USER_COLORS.length)]
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDefaultPages() {
|
function getDefaultPages() {
|
||||||
return [TLPage.create({ name: 'Page 1', index: 'a1' })]
|
return [TLPage.create({ name: 'Page 1', index: 'a1' })]
|
||||||
}
|
}
|
||||||
|
@ -101,32 +79,32 @@ function getDefaultPages() {
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export function createIntegrityChecker(store: TLStore): () => void {
|
export function createIntegrityChecker(store: TLStore): () => void {
|
||||||
const $pages = store.query.records('page')
|
const $pages = store.query.records('page')
|
||||||
const $userDocumentSettings = store.query.record('user_document', () => ({
|
const $userDocumentSettings = store.query.record('user_document')
|
||||||
userId: { eq: store.props.userId },
|
|
||||||
}))
|
|
||||||
|
|
||||||
const $instanceState = store.query.record('instance', () => ({
|
const $instanceState = store.query.record('instance', () => ({
|
||||||
id: { eq: store.props.instanceId },
|
id: { eq: store.props.instanceId },
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const $user = store.query.record('user', () => ({ id: { eq: store.props.userId } }))
|
|
||||||
|
|
||||||
const $userPresences = store.query.records('user_presence')
|
|
||||||
const $instancePageStates = store.query.records('instance_page_state')
|
const $instancePageStates = store.query.records('instance_page_state')
|
||||||
|
|
||||||
const ensureStoreIsUsable = (): void => {
|
const ensureStoreIsUsable = (): void => {
|
||||||
const { userId, instanceId: tabId } = store.props
|
const { instanceId: tabId } = store.props
|
||||||
// make sure we have exactly one document
|
// make sure we have exactly one document
|
||||||
if (!store.has(TLDOCUMENT_ID)) {
|
if (!store.has(TLDOCUMENT_ID)) {
|
||||||
store.put([TLDocument.create({ id: TLDOCUMENT_ID })])
|
store.put([TLDocument.create({ id: TLDOCUMENT_ID })])
|
||||||
return ensureStoreIsUsable()
|
return ensureStoreIsUsable()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!store.has(TLPOINTER_ID)) {
|
||||||
|
store.put([TLPointer.create({ id: TLPOINTER_ID })])
|
||||||
|
return ensureStoreIsUsable()
|
||||||
|
}
|
||||||
|
|
||||||
// make sure we have document state for the current user
|
// make sure we have document state for the current user
|
||||||
const userDocumentSettings = $userDocumentSettings.value
|
const userDocumentSettings = $userDocumentSettings.value
|
||||||
|
|
||||||
if (!userDocumentSettings) {
|
if (!userDocumentSettings) {
|
||||||
store.put([TLUserDocument.create({ userId })])
|
store.put([TLUserDocument.create({})])
|
||||||
return ensureStoreIsUsable()
|
return ensureStoreIsUsable()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,7 +129,6 @@ export function createIntegrityChecker(store: TLStore): () => void {
|
||||||
store.put([
|
store.put([
|
||||||
TLInstance.create({
|
TLInstance.create({
|
||||||
id: tabId,
|
id: tabId,
|
||||||
userId,
|
|
||||||
currentPageId,
|
currentPageId,
|
||||||
propsForNextShape,
|
propsForNextShape,
|
||||||
exportBackground: true,
|
exportBackground: true,
|
||||||
|
@ -169,22 +146,6 @@ export function createIntegrityChecker(store: TLStore): () => void {
|
||||||
return ensureStoreIsUsable()
|
return ensureStoreIsUsable()
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure we have a user state record for the current user
|
|
||||||
if (!$user.value) {
|
|
||||||
store.put([TLUser.create({ id: userId })])
|
|
||||||
return ensureStoreIsUsable()
|
|
||||||
}
|
|
||||||
|
|
||||||
const userPresences = $userPresences.value.filter((r) => r.userId === userId)
|
|
||||||
if (userPresences.length === 0) {
|
|
||||||
store.put([TLUserPresence.create({ userId, color: getRandomColor() })])
|
|
||||||
return ensureStoreIsUsable()
|
|
||||||
} else if (userPresences.length > 1) {
|
|
||||||
// make sure we don't duplicate user presences
|
|
||||||
store.remove(userPresences.slice(1).map((r) => r.id))
|
|
||||||
}
|
|
||||||
|
|
||||||
// make sure each page has a instancePageState and camera
|
|
||||||
for (const page of pages) {
|
for (const page of pages) {
|
||||||
const instancePageStates = $instancePageStates.value.filter(
|
const instancePageStates = $instancePageStates.value.filter(
|
||||||
(tps) => tps.pageId === page.id && tps.instanceId === tabId
|
(tps) => tps.pageId === page.id && tps.instanceId === tabId
|
||||||
|
|
58
packages/tlschema/src/createPresenceStateDerivation.ts
Normal file
58
packages/tlschema/src/createPresenceStateDerivation.ts
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import { Signal, computed } from 'signia'
|
||||||
|
import { TLStore } from './TLStore'
|
||||||
|
import { TLInstancePresence } from './records/TLInstancePresence'
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
export const createPresenceStateDerivation =
|
||||||
|
($user: Signal<{ id: string; color: string; name: string }>) =>
|
||||||
|
(store: TLStore): Signal<TLInstancePresence | null> => {
|
||||||
|
const $instance = store.query.record('instance', () => ({
|
||||||
|
id: { eq: store.props.instanceId },
|
||||||
|
}))
|
||||||
|
const $pageState = store.query.record('instance_page_state', () => ({
|
||||||
|
instanceId: { eq: store.props.instanceId },
|
||||||
|
pageId: { eq: $instance.value?.currentPageId ?? ('' as any) },
|
||||||
|
}))
|
||||||
|
const $camera = store.query.record('camera', () => ({
|
||||||
|
id: { eq: $pageState.value?.cameraId ?? ('' as any) },
|
||||||
|
}))
|
||||||
|
|
||||||
|
const $pointer = store.query.record('pointer')
|
||||||
|
|
||||||
|
return computed('instancePresence', () => {
|
||||||
|
const pageState = $pageState.value
|
||||||
|
const instance = $instance.value
|
||||||
|
const camera = $camera.value
|
||||||
|
const pointer = $pointer.value
|
||||||
|
const user = $user.value
|
||||||
|
if (!pageState || !instance || !camera || !pointer || !user) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return TLInstancePresence.create({
|
||||||
|
id: TLInstancePresence.createCustomId(store.props.instanceId),
|
||||||
|
instanceId: store.props.instanceId,
|
||||||
|
selectedIds: pageState.selectedIds,
|
||||||
|
brush: instance.brush,
|
||||||
|
scribble: instance.scribble,
|
||||||
|
userId: user.id,
|
||||||
|
userName: user.name,
|
||||||
|
followingUserId: instance.followingUserId,
|
||||||
|
camera: {
|
||||||
|
x: camera.x,
|
||||||
|
y: camera.y,
|
||||||
|
z: camera.z,
|
||||||
|
},
|
||||||
|
color: user.color,
|
||||||
|
currentPageId: instance.currentPageId,
|
||||||
|
cursor: {
|
||||||
|
x: pointer.x,
|
||||||
|
y: pointer.y,
|
||||||
|
rotation: instance.cursor.rotation,
|
||||||
|
type: instance.cursor.type,
|
||||||
|
},
|
||||||
|
lastActivityTimestamp: pointer.lastActivityTimestamp,
|
||||||
|
screenBounds: instance.screenBounds,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,9 +1,7 @@
|
||||||
import { Migrations, StoreSchema, createRecordType, defineMigrations } from '@tldraw/tlstore'
|
import { Migrations, StoreSchema, createRecordType, defineMigrations } from '@tldraw/tlstore'
|
||||||
import { T } from '@tldraw/tlvalidate'
|
import { T } from '@tldraw/tlvalidate'
|
||||||
import { Signal } from 'signia'
|
|
||||||
import { TLRecord } from './TLRecord'
|
import { TLRecord } from './TLRecord'
|
||||||
import { TLStore, TLStoreProps, createIntegrityChecker, onValidationFailure } from './TLStore'
|
import { TLStoreProps, createIntegrityChecker, onValidationFailure } from './TLStore'
|
||||||
import { defaultDerivePresenceState } from './defaultDerivePresenceState'
|
|
||||||
import { TLAsset } from './records/TLAsset'
|
import { TLAsset } from './records/TLAsset'
|
||||||
import { TLCamera } from './records/TLCamera'
|
import { TLCamera } from './records/TLCamera'
|
||||||
import { TLDocument } from './records/TLDocument'
|
import { TLDocument } from './records/TLDocument'
|
||||||
|
@ -11,10 +9,9 @@ import { TLInstance } from './records/TLInstance'
|
||||||
import { TLInstancePageState } from './records/TLInstancePageState'
|
import { TLInstancePageState } from './records/TLInstancePageState'
|
||||||
import { TLInstancePresence } from './records/TLInstancePresence'
|
import { TLInstancePresence } from './records/TLInstancePresence'
|
||||||
import { TLPage } from './records/TLPage'
|
import { TLPage } from './records/TLPage'
|
||||||
|
import { TLPointer } from './records/TLPointer'
|
||||||
import { TLShape, TLUnknownShape, rootShapeTypeMigrations } from './records/TLShape'
|
import { TLShape, TLUnknownShape, rootShapeTypeMigrations } from './records/TLShape'
|
||||||
import { TLUser } from './records/TLUser'
|
|
||||||
import { TLUserDocument } from './records/TLUserDocument'
|
import { TLUserDocument } from './records/TLUserDocument'
|
||||||
import { TLUserPresence } from './records/TLUserPresence'
|
|
||||||
import { storeMigrations } from './schema'
|
import { storeMigrations } from './schema'
|
||||||
import { arrowShapeTypeMigrations, arrowShapeTypeValidator } from './shapes/TLArrowShape'
|
import { arrowShapeTypeMigrations, arrowShapeTypeValidator } from './shapes/TLArrowShape'
|
||||||
import { bookmarkShapeTypeMigrations, bookmarkShapeTypeValidator } from './shapes/TLBookmarkShape'
|
import { bookmarkShapeTypeMigrations, bookmarkShapeTypeValidator } from './shapes/TLBookmarkShape'
|
||||||
|
@ -61,10 +58,9 @@ type CustomShapeInfo<T extends TLUnknownShape> = {
|
||||||
export function createTLSchema<T extends TLUnknownShape>(
|
export function createTLSchema<T extends TLUnknownShape>(
|
||||||
opts = {} as {
|
opts = {} as {
|
||||||
customShapes?: { [K in T['type']]: CustomShapeInfo<T> }
|
customShapes?: { [K in T['type']]: CustomShapeInfo<T> }
|
||||||
derivePresenceState?: (store: TLStore) => Signal<TLInstancePresence | null>
|
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const { customShapes = {}, derivePresenceState } = opts
|
const { customShapes = {} } = opts
|
||||||
|
|
||||||
const defaultShapeSubTypeEntries = Object.entries(DEFAULT_SHAPES) as [
|
const defaultShapeSubTypeEntries = Object.entries(DEFAULT_SHAPES) as [
|
||||||
TLShape['type'],
|
TLShape['type'],
|
||||||
|
@ -76,7 +72,7 @@ export function createTLSchema<T extends TLUnknownShape>(
|
||||||
CustomShapeInfo<T>
|
CustomShapeInfo<T>
|
||||||
][]
|
][]
|
||||||
|
|
||||||
// Create a shape record that incorporates the defeault shapes and any custom shapes
|
// Create a shape record that incorporates the default shapes and any custom shapes
|
||||||
// into its subtype migrations and validators, so that we can migrate any new custom
|
// into its subtype migrations and validators, so that we can migrate any new custom
|
||||||
// subtypes. Note that migrations AND validators for custom shapes are optional. If
|
// subtypes. Note that migrations AND validators for custom shapes are optional. If
|
||||||
// not provided, we use an empty migrations set and/or an "any" validator.
|
// not provided, we use an empty migrations set and/or an "any" validator.
|
||||||
|
@ -119,16 +115,14 @@ export function createTLSchema<T extends TLUnknownShape>(
|
||||||
instance_page_state: TLInstancePageState,
|
instance_page_state: TLInstancePageState,
|
||||||
page: TLPage,
|
page: TLPage,
|
||||||
shape: shapeRecord,
|
shape: shapeRecord,
|
||||||
user: TLUser,
|
|
||||||
user_document: TLUserDocument,
|
user_document: TLUserDocument,
|
||||||
user_presence: TLUserPresence,
|
|
||||||
instance_presence: TLInstancePresence,
|
instance_presence: TLInstancePresence,
|
||||||
|
pointer: TLPointer,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
snapshotMigrations: storeMigrations,
|
snapshotMigrations: storeMigrations,
|
||||||
onValidationFailure,
|
onValidationFailure,
|
||||||
createIntegrityChecker: createIntegrityChecker,
|
createIntegrityChecker: createIntegrityChecker,
|
||||||
derivePresenceState: derivePresenceState ?? defaultDerivePresenceState,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,57 +0,0 @@
|
||||||
import { Signal, computed } from 'signia'
|
|
||||||
import { TLStore } from './TLStore'
|
|
||||||
import { TLInstancePresence } from './records/TLInstancePresence'
|
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
export const defaultDerivePresenceState = (store: TLStore): Signal<TLInstancePresence | null> => {
|
|
||||||
const $instance = store.query.record('instance', () => ({
|
|
||||||
id: { eq: store.props.instanceId },
|
|
||||||
}))
|
|
||||||
const $user = store.query.record('user', () => ({ id: { eq: store.props.userId } }))
|
|
||||||
const $userPresence = store.query.record('user_presence', () => ({
|
|
||||||
userId: { eq: store.props.userId },
|
|
||||||
}))
|
|
||||||
const $pageState = store.query.record('instance_page_state', () => ({
|
|
||||||
instanceId: { eq: store.props.instanceId },
|
|
||||||
pageId: { eq: $instance.value?.currentPageId ?? ('' as any) },
|
|
||||||
}))
|
|
||||||
const $camera = store.query.record('camera', () => ({
|
|
||||||
id: { eq: $pageState.value?.cameraId ?? ('' as any) },
|
|
||||||
}))
|
|
||||||
return computed('instancePresence', () => {
|
|
||||||
const pageState = $pageState.value
|
|
||||||
const instance = $instance.value
|
|
||||||
const user = $user.value
|
|
||||||
const userPresence = $userPresence.value
|
|
||||||
const camera = $camera.value
|
|
||||||
if (!pageState || !instance || !user || !userPresence || !camera) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return TLInstancePresence.create({
|
|
||||||
id: TLInstancePresence.createCustomId(store.props.instanceId),
|
|
||||||
instanceId: store.props.instanceId,
|
|
||||||
selectedIds: pageState.selectedIds,
|
|
||||||
brush: instance.brush,
|
|
||||||
scribble: instance.scribble,
|
|
||||||
userId: store.props.userId,
|
|
||||||
userName: user.name,
|
|
||||||
followingUserId: instance.followingUserId,
|
|
||||||
camera: {
|
|
||||||
x: camera.x,
|
|
||||||
y: camera.y,
|
|
||||||
z: camera.z,
|
|
||||||
},
|
|
||||||
color: userPresence.color,
|
|
||||||
currentPageId: instance.currentPageId,
|
|
||||||
cursor: {
|
|
||||||
x: userPresence.cursor.x,
|
|
||||||
y: userPresence.cursor.y,
|
|
||||||
rotation: instance.cursor.rotation,
|
|
||||||
type: instance.cursor.type,
|
|
||||||
},
|
|
||||||
lastActivityTimestamp: userPresence.lastActivityTimestamp,
|
|
||||||
screenBounds: instance.screenBounds,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,6 +1,5 @@
|
||||||
export { type TLRecord } from './TLRecord'
|
export { type TLRecord } from './TLRecord'
|
||||||
export {
|
export {
|
||||||
USER_COLORS,
|
|
||||||
createIntegrityChecker,
|
createIntegrityChecker,
|
||||||
onValidationFailure,
|
onValidationFailure,
|
||||||
type TLStore,
|
type TLStore,
|
||||||
|
@ -24,8 +23,8 @@ export {
|
||||||
type TLVideoAsset,
|
type TLVideoAsset,
|
||||||
} from './assets/TLVideoAsset'
|
} from './assets/TLVideoAsset'
|
||||||
export { createAssetValidator, type TLBaseAsset } from './assets/asset-validation'
|
export { createAssetValidator, type TLBaseAsset } from './assets/asset-validation'
|
||||||
|
export { createPresenceStateDerivation } from './createPresenceStateDerivation'
|
||||||
export { createTLSchema } from './createTLSchema'
|
export { createTLSchema } from './createTLSchema'
|
||||||
export { defaultDerivePresenceState } from './defaultDerivePresenceState'
|
|
||||||
export { CLIENT_FIXUP_SCRIPT, fixupRecord } from './fixup'
|
export { CLIENT_FIXUP_SCRIPT, fixupRecord } from './fixup'
|
||||||
export { type Box2dModel, type Vec2dModel } from './geometry-types'
|
export { type Box2dModel, type Vec2dModel } from './geometry-types'
|
||||||
export {
|
export {
|
||||||
|
@ -53,6 +52,12 @@ export {
|
||||||
} from './records/TLInstancePageState'
|
} from './records/TLInstancePageState'
|
||||||
export { TLInstancePresence } from './records/TLInstancePresence'
|
export { TLInstancePresence } from './records/TLInstancePresence'
|
||||||
export { TLPage, pageTypeValidator, type TLPageId } from './records/TLPage'
|
export { TLPage, pageTypeValidator, type TLPageId } from './records/TLPage'
|
||||||
|
export {
|
||||||
|
TLPOINTER_ID,
|
||||||
|
TLPointer,
|
||||||
|
pointerTypeValidator,
|
||||||
|
type TLPointerId,
|
||||||
|
} from './records/TLPointer'
|
||||||
export {
|
export {
|
||||||
createCustomShapeId,
|
createCustomShapeId,
|
||||||
createShapeId,
|
createShapeId,
|
||||||
|
@ -69,19 +74,12 @@ export {
|
||||||
type TLShapeProps,
|
type TLShapeProps,
|
||||||
type TLUnknownShape,
|
type TLUnknownShape,
|
||||||
} from './records/TLShape'
|
} from './records/TLShape'
|
||||||
export { TLUser, userTypeValidator, type TLUserId } from './records/TLUser'
|
|
||||||
export {
|
export {
|
||||||
TLUserDocument,
|
TLUserDocument,
|
||||||
userDocumentTypeMigrations,
|
userDocumentTypeMigrations,
|
||||||
userDocumentTypeValidator,
|
userDocumentTypeValidator,
|
||||||
type TLUserDocumentId,
|
type TLUserDocumentId,
|
||||||
} from './records/TLUserDocument'
|
} from './records/TLUserDocument'
|
||||||
export {
|
|
||||||
TLUserPresence,
|
|
||||||
userPresenceTypeMigrations,
|
|
||||||
userPresenceTypeValidator,
|
|
||||||
type TLUserPresenceId,
|
|
||||||
} from './records/TLUserPresence'
|
|
||||||
export { storeMigrations } from './schema'
|
export { storeMigrations } from './schema'
|
||||||
export {
|
export {
|
||||||
TL_ARROW_TERMINAL_TYPE,
|
TL_ARROW_TERMINAL_TYPE,
|
||||||
|
@ -218,6 +216,7 @@ export {
|
||||||
type TLStyleType,
|
type TLStyleType,
|
||||||
type TLVerticalAlignType,
|
type TLVerticalAlignType,
|
||||||
} from './style-types'
|
} from './style-types'
|
||||||
|
export { getDefaultTranslationLocale } from './translations'
|
||||||
export {
|
export {
|
||||||
TL_CURSOR_TYPES,
|
TL_CURSOR_TYPES,
|
||||||
TL_HANDLE_TYPES,
|
TL_HANDLE_TYPES,
|
||||||
|
@ -255,5 +254,4 @@ export {
|
||||||
shapeIdValidator,
|
shapeIdValidator,
|
||||||
sizeValidator,
|
sizeValidator,
|
||||||
splineValidator,
|
splineValidator,
|
||||||
userIdValidator,
|
|
||||||
} from './validation'
|
} from './validation'
|
||||||
|
|
|
@ -3,13 +3,12 @@ import { structuredClone } from '@tldraw/utils'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import { imageAssetMigrations } from './assets/TLImageAsset'
|
import { imageAssetMigrations } from './assets/TLImageAsset'
|
||||||
import { videoAssetMigrations } from './assets/TLVideoAsset'
|
import { videoAssetMigrations } from './assets/TLVideoAsset'
|
||||||
import { instanceTypeMigrations } from './records/TLInstance'
|
import { instanceTypeMigrations, instanceTypeVersions } from './records/TLInstance'
|
||||||
import { instancePageStateMigrations } from './records/TLInstancePageState'
|
import { instancePageStateMigrations } from './records/TLInstancePageState'
|
||||||
import { instancePresenceTypeMigrations } from './records/TLInstancePresence'
|
import { instancePresenceTypeMigrations } from './records/TLInstancePresence'
|
||||||
import { rootShapeTypeMigrations, TLShape } from './records/TLShape'
|
import { rootShapeTypeMigrations, TLShape } from './records/TLShape'
|
||||||
import { userDocumentTypeMigrations, userDocumentVersions } from './records/TLUserDocument'
|
import { userDocumentTypeMigrations, userDocumentVersions } from './records/TLUserDocument'
|
||||||
import { userPresenceTypeMigrations } from './records/TLUserPresence'
|
import { storeMigrations, storeVersions } from './schema'
|
||||||
import { storeMigrations } from './schema'
|
|
||||||
import { arrowShapeTypeMigrations } from './shapes/TLArrowShape'
|
import { arrowShapeTypeMigrations } from './shapes/TLArrowShape'
|
||||||
import { bookmarkShapeTypeMigrations } from './shapes/TLBookmarkShape'
|
import { bookmarkShapeTypeMigrations } from './shapes/TLBookmarkShape'
|
||||||
import { drawShapeTypeMigrations } from './shapes/TLDrawShape'
|
import { drawShapeTypeMigrations } from './shapes/TLDrawShape'
|
||||||
|
@ -214,7 +213,7 @@ describe('Store removing Icon and Code shapes', () => {
|
||||||
} as any),
|
} as any),
|
||||||
].map((shape) => [shape.id, shape])
|
].map((shape) => [shape.id, shape])
|
||||||
)
|
)
|
||||||
const fixed = storeMigrations.migrators[1].up(snapshot)
|
const fixed = storeMigrations.migrators[storeVersions.RemoveCodeAndIconShapeTypes].up(snapshot)
|
||||||
expect(Object.entries(fixed)).toHaveLength(1)
|
expect(Object.entries(fixed)).toHaveLength(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -230,7 +229,7 @@ describe('Store removing Icon and Code shapes', () => {
|
||||||
].map((shape) => [shape.id, shape])
|
].map((shape) => [shape.id, shape])
|
||||||
)
|
)
|
||||||
|
|
||||||
storeMigrations.migrators[1].down(snapshot)
|
storeMigrations.migrators[storeVersions.RemoveCodeAndIconShapeTypes].down(snapshot)
|
||||||
expect(Object.entries(snapshot)).toHaveLength(1)
|
expect(Object.entries(snapshot)).toHaveLength(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -560,17 +559,6 @@ describe('Adding followingUserId prop to instance', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Adding viewportPageBounds prop to user presence', () => {
|
|
||||||
const { up, down } = userPresenceTypeMigrations.migrators[1]
|
|
||||||
test('up works as expected', () => {
|
|
||||||
expect(up({})).toEqual({ viewportPageBounds: { x: 0, y: 0, w: 1, h: 1 } })
|
|
||||||
})
|
|
||||||
|
|
||||||
test('down works as expected', () => {
|
|
||||||
expect(down({ viewportPageBounds: { x: 1, y: 2, w: 3, h: 4 } })).toEqual({})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Removing align=justify from propsForNextShape', () => {
|
describe('Removing align=justify from propsForNextShape', () => {
|
||||||
const { up, down } = instanceTypeMigrations.migrators[7]
|
const { up, down } = instanceTypeMigrations.migrators[7]
|
||||||
test('up works as expected', () => {
|
test('up works as expected', () => {
|
||||||
|
@ -639,7 +627,7 @@ describe('Add crop=null to image shapes', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Adding instance_presence to the schema', () => {
|
describe('Adding instance_presence to the schema', () => {
|
||||||
const { up, down } = storeMigrations.migrators[2]
|
const { up, down } = storeMigrations.migrators[storeVersions.AddInstancePresenceType]
|
||||||
|
|
||||||
test('up works as expected', () => {
|
test('up works as expected', () => {
|
||||||
expect(up({})).toEqual({})
|
expect(up({})).toEqual({})
|
||||||
|
@ -919,6 +907,103 @@ describe('Adds delay to scribble', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('user config refactor', () => {
|
||||||
|
test('removes user and user_presence types from snapshots', () => {
|
||||||
|
const { up, down } =
|
||||||
|
storeMigrations.migrators[storeVersions.RemoveTLUserAndPresenceAndAddPointer]
|
||||||
|
|
||||||
|
const prevSnapshot = {
|
||||||
|
'user:123': {
|
||||||
|
id: 'user:123',
|
||||||
|
typeName: 'user',
|
||||||
|
},
|
||||||
|
'user_presence:123': {
|
||||||
|
id: 'user_presence:123',
|
||||||
|
typeName: 'user_presence',
|
||||||
|
},
|
||||||
|
'instance:123': {
|
||||||
|
id: 'instance:123',
|
||||||
|
typeName: 'instance',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextSnapshot = {
|
||||||
|
'instance:123': {
|
||||||
|
id: 'instance:123',
|
||||||
|
typeName: 'instance',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// up removes the user and user_presence types
|
||||||
|
expect(up(prevSnapshot)).toEqual(nextSnapshot)
|
||||||
|
// down cannot add them back so it should be a no-op
|
||||||
|
expect(
|
||||||
|
down({
|
||||||
|
...nextSnapshot,
|
||||||
|
'pointer:134': {
|
||||||
|
id: 'pointer:134',
|
||||||
|
typeName: 'pointer',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).toEqual(nextSnapshot)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('removes userId from the instance state', () => {
|
||||||
|
const { up, down } = instanceTypeMigrations.migrators[instanceTypeVersions.RemoveUserId]
|
||||||
|
|
||||||
|
const prev = {
|
||||||
|
id: 'instance:123',
|
||||||
|
typeName: 'instance',
|
||||||
|
userId: 'user:123',
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = {
|
||||||
|
id: 'instance:123',
|
||||||
|
typeName: 'instance',
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(up(prev)).toEqual(next)
|
||||||
|
// it cannot be added back so it should add some meaningless id in there
|
||||||
|
// in practice, because we bumped the store version, this down migrator will never be used
|
||||||
|
expect(down(next)).toMatchInlineSnapshot(`
|
||||||
|
Object {
|
||||||
|
"id": "instance:123",
|
||||||
|
"typeName": "instance",
|
||||||
|
"userId": "user:none",
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('removes userId and isDarkMode from TLUserDocument', () => {
|
||||||
|
const { up, down } =
|
||||||
|
userDocumentTypeMigrations.migrators[userDocumentVersions.RemoveUserIdAndIsDarkMode]
|
||||||
|
|
||||||
|
const prev = {
|
||||||
|
id: 'user_document:123',
|
||||||
|
typeName: 'user_document',
|
||||||
|
userId: 'user:123',
|
||||||
|
isDarkMode: false,
|
||||||
|
isGridMode: false,
|
||||||
|
}
|
||||||
|
const next = {
|
||||||
|
id: 'user_document:123',
|
||||||
|
typeName: 'user_document',
|
||||||
|
isGridMode: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(up(prev)).toEqual(next)
|
||||||
|
expect(down(next)).toMatchInlineSnapshot(`
|
||||||
|
Object {
|
||||||
|
"id": "user_document:123",
|
||||||
|
"isDarkMode": false,
|
||||||
|
"isGridMode": false,
|
||||||
|
"typeName": "user_document",
|
||||||
|
"userId": "user:none",
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
/* --- PUT YOUR MIGRATIONS TESTS ABOVE HERE --- */
|
/* --- PUT YOUR MIGRATIONS TESTS ABOVE HERE --- */
|
||||||
|
|
||||||
for (const migrator of allMigrators) {
|
for (const migrator of allMigrators) {
|
||||||
|
|
|
@ -17,12 +17,10 @@ import {
|
||||||
pageIdValidator,
|
pageIdValidator,
|
||||||
sizeValidator,
|
sizeValidator,
|
||||||
splineValidator,
|
splineValidator,
|
||||||
userIdValidator,
|
|
||||||
verticalAlignValidator,
|
verticalAlignValidator,
|
||||||
} from '../validation'
|
} from '../validation'
|
||||||
import { TLPageId } from './TLPage'
|
import { TLPageId } from './TLPage'
|
||||||
import { TLShapeProps } from './TLShape'
|
import { TLShapeProps } from './TLShape'
|
||||||
import { TLUserId } from './TLUser'
|
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLInstancePropsForNextShape = Pick<TLShapeProps, TLStyleType>
|
export type TLInstancePropsForNextShape = Pick<TLShapeProps, TLStyleType>
|
||||||
|
@ -35,9 +33,8 @@ export type TLInstancePropsForNextShape = Pick<TLShapeProps, TLStyleType>
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export interface TLInstance extends BaseRecord<'instance', TLInstanceId> {
|
export interface TLInstance extends BaseRecord<'instance', TLInstanceId> {
|
||||||
userId: TLUserId
|
|
||||||
currentPageId: TLPageId
|
currentPageId: TLPageId
|
||||||
followingUserId: TLUserId | null
|
followingUserId: string | null
|
||||||
brush: Box2dModel | null
|
brush: Box2dModel | null
|
||||||
propsForNextShape: TLInstancePropsForNextShape
|
propsForNextShape: TLInstancePropsForNextShape
|
||||||
cursor: TLCursor
|
cursor: TLCursor
|
||||||
|
@ -59,9 +56,8 @@ export const instanceTypeValidator: T.Validator<TLInstance> = T.model(
|
||||||
T.object({
|
T.object({
|
||||||
typeName: T.literal('instance'),
|
typeName: T.literal('instance'),
|
||||||
id: idValidator<TLInstanceId>('instance'),
|
id: idValidator<TLInstanceId>('instance'),
|
||||||
userId: userIdValidator,
|
|
||||||
currentPageId: pageIdValidator,
|
currentPageId: pageIdValidator,
|
||||||
followingUserId: userIdValidator.nullable(),
|
followingUserId: T.string.nullable(),
|
||||||
brush: T.boxModel.nullable(),
|
brush: T.boxModel.nullable(),
|
||||||
propsForNextShape: T.object({
|
propsForNextShape: T.object({
|
||||||
color: colorValidator,
|
color: colorValidator,
|
||||||
|
@ -101,11 +97,14 @@ const Versions = {
|
||||||
AddZoom: 8,
|
AddZoom: 8,
|
||||||
AddVerticalAlign: 9,
|
AddVerticalAlign: 9,
|
||||||
AddScribbleDelay: 10,
|
AddScribbleDelay: 10,
|
||||||
|
RemoveUserId: 11,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
export { Versions as instanceTypeVersions }
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export const instanceTypeMigrations = defineMigrations({
|
export const instanceTypeMigrations = defineMigrations({
|
||||||
currentVersion: Versions.AddScribbleDelay,
|
currentVersion: Versions.RemoveUserId,
|
||||||
migrators: {
|
migrators: {
|
||||||
[Versions.AddTransparentExportBgs]: {
|
[Versions.AddTransparentExportBgs]: {
|
||||||
up: (instance: TLInstance) => {
|
up: (instance: TLInstance) => {
|
||||||
|
@ -235,6 +234,14 @@ export const instanceTypeMigrations = defineMigrations({
|
||||||
return { ...instance }
|
return { ...instance }
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
[Versions.RemoveUserId]: {
|
||||||
|
up: ({ userId: _, ...instance }: any) => {
|
||||||
|
return instance
|
||||||
|
},
|
||||||
|
down: (instance: TLInstance) => {
|
||||||
|
return { ...instance, userId: 'user:none' }
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -244,7 +251,7 @@ export const TLInstance = createRecordType<TLInstance>('instance', {
|
||||||
validator: instanceTypeValidator,
|
validator: instanceTypeValidator,
|
||||||
scope: 'instance',
|
scope: 'instance',
|
||||||
}).withDefaultProperties(
|
}).withDefaultProperties(
|
||||||
(): Omit<TLInstance, 'typeName' | 'id' | 'userId' | 'currentPageId'> => ({
|
(): Omit<TLInstance, 'typeName' | 'id' | 'currentPageId'> => ({
|
||||||
followingUserId: null,
|
followingUserId: null,
|
||||||
propsForNextShape: {
|
propsForNextShape: {
|
||||||
opacity: '1',
|
opacity: '1',
|
||||||
|
|
|
@ -2,16 +2,15 @@ import { BaseRecord, createRecordType, defineMigrations, ID } from '@tldraw/tlst
|
||||||
import { T } from '@tldraw/tlvalidate'
|
import { T } from '@tldraw/tlvalidate'
|
||||||
import { Box2dModel } from '../geometry-types'
|
import { Box2dModel } from '../geometry-types'
|
||||||
import { cursorTypeValidator, scribbleTypeValidator, TLCursor, TLScribble } from '../ui-types'
|
import { cursorTypeValidator, scribbleTypeValidator, TLCursor, TLScribble } from '../ui-types'
|
||||||
import { idValidator, userIdValidator } from '../validation'
|
import { idValidator } from '../validation'
|
||||||
import { TLInstanceId } from './TLInstance'
|
import { TLInstanceId } from './TLInstance'
|
||||||
import { TLPageId } from './TLPage'
|
import { TLPageId } from './TLPage'
|
||||||
import { TLShapeId } from './TLShape'
|
import { TLShapeId } from './TLShape'
|
||||||
import { TLUserId } from './TLUser'
|
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export interface TLInstancePresence extends BaseRecord<'instance_presence', TLInstancePresenceID> {
|
export interface TLInstancePresence extends BaseRecord<'instance_presence', TLInstancePresenceID> {
|
||||||
instanceId: TLInstanceId
|
instanceId: TLInstanceId
|
||||||
userId: TLUserId
|
userId: string
|
||||||
userName: string
|
userName: string
|
||||||
lastActivityTimestamp: number
|
lastActivityTimestamp: number
|
||||||
color: string // can be any hex color
|
color: string // can be any hex color
|
||||||
|
@ -21,7 +20,7 @@ export interface TLInstancePresence extends BaseRecord<'instance_presence', TLIn
|
||||||
brush: Box2dModel | null
|
brush: Box2dModel | null
|
||||||
scribble: TLScribble | null
|
scribble: TLScribble | null
|
||||||
screenBounds: Box2dModel
|
screenBounds: Box2dModel
|
||||||
followingUserId: TLUserId | null
|
followingUserId: string | null
|
||||||
cursor: {
|
cursor: {
|
||||||
x: number
|
x: number
|
||||||
y: number
|
y: number
|
||||||
|
@ -41,10 +40,10 @@ export const instancePresenceTypeValidator: T.Validator<TLInstancePresence> = T.
|
||||||
instanceId: idValidator<TLInstanceId>('instance'),
|
instanceId: idValidator<TLInstanceId>('instance'),
|
||||||
typeName: T.literal('instance_presence'),
|
typeName: T.literal('instance_presence'),
|
||||||
id: idValidator<TLInstancePresenceID>('instance_presence'),
|
id: idValidator<TLInstancePresenceID>('instance_presence'),
|
||||||
userId: userIdValidator,
|
userId: T.string,
|
||||||
userName: T.string,
|
userName: T.string,
|
||||||
lastActivityTimestamp: T.number,
|
lastActivityTimestamp: T.number,
|
||||||
followingUserId: userIdValidator.nullable(),
|
followingUserId: T.string.nullable(),
|
||||||
cursor: T.object({
|
cursor: T.object({
|
||||||
x: T.number,
|
x: T.number,
|
||||||
y: T.number,
|
y: T.number,
|
||||||
|
@ -95,4 +94,28 @@ export const TLInstancePresence = createRecordType<TLInstancePresence>('instance
|
||||||
migrations: instancePresenceTypeMigrations,
|
migrations: instancePresenceTypeMigrations,
|
||||||
validator: instancePresenceTypeValidator,
|
validator: instancePresenceTypeValidator,
|
||||||
scope: 'presence',
|
scope: 'presence',
|
||||||
})
|
}).withDefaultProperties(() => ({
|
||||||
|
lastActivityTimestamp: 0,
|
||||||
|
followingUserId: null,
|
||||||
|
color: '#FF0000',
|
||||||
|
camera: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
z: 1,
|
||||||
|
},
|
||||||
|
cursor: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
type: 'default',
|
||||||
|
rotation: 0,
|
||||||
|
},
|
||||||
|
screenBounds: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
w: 1,
|
||||||
|
h: 1,
|
||||||
|
},
|
||||||
|
selectedIds: [],
|
||||||
|
brush: null,
|
||||||
|
scribble: null,
|
||||||
|
}))
|
||||||
|
|
47
packages/tlschema/src/records/TLPointer.ts
Normal file
47
packages/tlschema/src/records/TLPointer.ts
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import { BaseRecord, createRecordType, defineMigrations, ID } from '@tldraw/tlstore'
|
||||||
|
import { T } from '@tldraw/tlvalidate'
|
||||||
|
import { idValidator } from '../validation'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TLPointer
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export interface TLPointer extends BaseRecord<'pointer', TLPointerId> {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
lastActivityTimestamp: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export type TLPointerId = ID<TLPointer>
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export const pointerTypeValidator: T.Validator<TLPointer> = T.model(
|
||||||
|
'pointer',
|
||||||
|
T.object({
|
||||||
|
typeName: T.literal('pointer'),
|
||||||
|
id: idValidator<TLPointerId>('pointer'),
|
||||||
|
x: T.number,
|
||||||
|
y: T.number,
|
||||||
|
lastActivityTimestamp: T.number,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export const TLPointer = createRecordType<TLPointer>('pointer', {
|
||||||
|
validator: pointerTypeValidator,
|
||||||
|
scope: 'instance',
|
||||||
|
}).withDefaultProperties(
|
||||||
|
(): Omit<TLPointer, 'id' | 'typeName'> => ({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
lastActivityTimestamp: 0,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export const TLPOINTER_ID = TLPointer.createCustomId('pointer')
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export const pointerTypeMigrations = defineMigrations({})
|
|
@ -1,45 +0,0 @@
|
||||||
import { BaseRecord, createRecordType, defineMigrations, ID } from '@tldraw/tlstore'
|
|
||||||
import { T } from '@tldraw/tlvalidate'
|
|
||||||
import { getDefaultTranslationLocale } from '../translations'
|
|
||||||
import { userIdValidator } from '../validation'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A user of tldraw
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
export interface TLUser extends BaseRecord<'user', TLUserId> {
|
|
||||||
name: string
|
|
||||||
locale: string
|
|
||||||
}
|
|
||||||
/** @public */
|
|
||||||
export type TLUserId = ID<TLUser>
|
|
||||||
|
|
||||||
/** @public */
|
|
||||||
export const userTypeValidator: T.Validator<TLUser> = T.model(
|
|
||||||
'user',
|
|
||||||
T.object({
|
|
||||||
typeName: T.literal('user'),
|
|
||||||
id: userIdValidator,
|
|
||||||
name: T.string,
|
|
||||||
locale: T.string,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
/** @public */
|
|
||||||
export const TLUser = createRecordType<TLUser>('user', {
|
|
||||||
validator: userTypeValidator,
|
|
||||||
scope: 'instance',
|
|
||||||
}).withDefaultProperties((): Omit<TLUser, 'id' | 'typeName'> => {
|
|
||||||
let locale = 'en'
|
|
||||||
if (typeof window !== 'undefined' && window.navigator) {
|
|
||||||
locale = getDefaultTranslationLocale(window.navigator.languages)
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
name: 'New User',
|
|
||||||
locale,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/** @public */
|
|
||||||
export const userTypeMigrations = defineMigrations({})
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { BaseRecord, createRecordType, defineMigrations, ID } from '@tldraw/tlstore'
|
import { BaseRecord, createRecordType, defineMigrations, ID } from '@tldraw/tlstore'
|
||||||
import { T } from '@tldraw/tlvalidate'
|
import { T } from '@tldraw/tlvalidate'
|
||||||
import { idValidator, instanceIdValidator, pageIdValidator, userIdValidator } from '../validation'
|
import { idValidator, instanceIdValidator, pageIdValidator } from '../validation'
|
||||||
import { TLInstance } from './TLInstance'
|
import { TLInstance } from './TLInstance'
|
||||||
import { TLPage } from './TLPage'
|
import { TLPage } from './TLPage'
|
||||||
import { TLUserId } from './TLUser'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TLUserDocument
|
* TLUserDocument
|
||||||
|
@ -13,10 +12,8 @@ import { TLUserId } from './TLUser'
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export interface TLUserDocument extends BaseRecord<'user_document', TLUserDocumentId> {
|
export interface TLUserDocument extends BaseRecord<'user_document', TLUserDocumentId> {
|
||||||
userId: TLUserId
|
|
||||||
isPenMode: boolean
|
isPenMode: boolean
|
||||||
isGridMode: boolean
|
isGridMode: boolean
|
||||||
isDarkMode: boolean
|
|
||||||
isMobileMode: boolean
|
isMobileMode: boolean
|
||||||
isSnapMode: boolean
|
isSnapMode: boolean
|
||||||
lastUpdatedPageId: ID<TLPage> | null
|
lastUpdatedPageId: ID<TLPage> | null
|
||||||
|
@ -32,10 +29,8 @@ export const userDocumentTypeValidator: T.Validator<TLUserDocument> = T.model(
|
||||||
T.object({
|
T.object({
|
||||||
typeName: T.literal('user_document'),
|
typeName: T.literal('user_document'),
|
||||||
id: idValidator<TLUserDocumentId>('user_document'),
|
id: idValidator<TLUserDocumentId>('user_document'),
|
||||||
userId: userIdValidator,
|
|
||||||
isPenMode: T.boolean,
|
isPenMode: T.boolean,
|
||||||
isGridMode: T.boolean,
|
isGridMode: T.boolean,
|
||||||
isDarkMode: T.boolean,
|
|
||||||
isMobileMode: T.boolean,
|
isMobileMode: T.boolean,
|
||||||
isSnapMode: T.boolean,
|
isSnapMode: T.boolean,
|
||||||
lastUpdatedPageId: pageIdValidator.nullable(),
|
lastUpdatedPageId: pageIdValidator.nullable(),
|
||||||
|
@ -47,11 +42,14 @@ export const Versions = {
|
||||||
AddSnapMode: 1,
|
AddSnapMode: 1,
|
||||||
AddMissingIsMobileMode: 2,
|
AddMissingIsMobileMode: 2,
|
||||||
RemoveIsReadOnly: 3,
|
RemoveIsReadOnly: 3,
|
||||||
|
RemoveUserIdAndIsDarkMode: 4,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
export { Versions as userDocumentVersions }
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export const userDocumentTypeMigrations = defineMigrations({
|
export const userDocumentTypeMigrations = defineMigrations({
|
||||||
currentVersion: Versions.RemoveIsReadOnly,
|
currentVersion: Versions.RemoveUserIdAndIsDarkMode,
|
||||||
migrators: {
|
migrators: {
|
||||||
[Versions.AddSnapMode]: {
|
[Versions.AddSnapMode]: {
|
||||||
up: (userDocument: TLUserDocument) => {
|
up: (userDocument: TLUserDocument) => {
|
||||||
|
@ -77,6 +75,18 @@ export const userDocumentTypeMigrations = defineMigrations({
|
||||||
return { ...userDocument, isReadOnly: false }
|
return { ...userDocument, isReadOnly: false }
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
[Versions.RemoveUserIdAndIsDarkMode]: {
|
||||||
|
up: ({
|
||||||
|
userId: _,
|
||||||
|
isDarkMode: __,
|
||||||
|
...userDocument
|
||||||
|
}: TLUserDocument & { userId: string; isDarkMode: boolean }) => {
|
||||||
|
return userDocument
|
||||||
|
},
|
||||||
|
down: (userDocument: TLUserDocument) => {
|
||||||
|
return { ...userDocument, userId: 'user:none', isDarkMode: false }
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
/* STEP 4: Add your changes to the record type */
|
/* STEP 4: Add your changes to the record type */
|
||||||
|
@ -92,12 +102,9 @@ export const TLUserDocument = createRecordType<TLUserDocument>('user_document',
|
||||||
/* STEP 6: Add any new default values for properties here */
|
/* STEP 6: Add any new default values for properties here */
|
||||||
isPenMode: false,
|
isPenMode: false,
|
||||||
isGridMode: false,
|
isGridMode: false,
|
||||||
isDarkMode: false,
|
|
||||||
isMobileMode: false,
|
isMobileMode: false,
|
||||||
isSnapMode: false,
|
isSnapMode: false,
|
||||||
lastUpdatedPageId: null,
|
lastUpdatedPageId: null,
|
||||||
lastUsedTabId: null,
|
lastUsedTabId: null,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
export { Versions as userDocumentVersions }
|
|
||||||
|
|
|
@ -1,69 +0,0 @@
|
||||||
import { BaseRecord, createRecordType, defineMigrations, ID } from '@tldraw/tlstore'
|
|
||||||
import { T } from '@tldraw/tlvalidate'
|
|
||||||
import { Box2dModel, Vec2dModel } from '../geometry-types'
|
|
||||||
import { idValidator, instanceIdValidator, userIdValidator } from '../validation'
|
|
||||||
import { TLInstanceId } from './TLInstance'
|
|
||||||
import { TLUserId } from './TLUser'
|
|
||||||
|
|
||||||
/** @public */
|
|
||||||
export interface TLUserPresence extends BaseRecord<'user_presence', TLUserPresenceId> {
|
|
||||||
userId: TLUserId
|
|
||||||
lastUsedInstanceId: TLInstanceId | null
|
|
||||||
lastActivityTimestamp: number
|
|
||||||
cursor: Vec2dModel
|
|
||||||
viewportPageBounds: Box2dModel
|
|
||||||
color: string // can be any hex color
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @public */
|
|
||||||
export type TLUserPresenceId = ID<TLUserPresence>
|
|
||||||
|
|
||||||
/** @public */
|
|
||||||
export const userPresenceTypeValidator: T.Validator<TLUserPresence> = T.model(
|
|
||||||
'user_presence',
|
|
||||||
T.object({
|
|
||||||
typeName: T.literal('user_presence'),
|
|
||||||
id: idValidator<TLUserPresenceId>('user_presence'),
|
|
||||||
userId: userIdValidator,
|
|
||||||
lastUsedInstanceId: instanceIdValidator.nullable(),
|
|
||||||
lastActivityTimestamp: T.number,
|
|
||||||
cursor: T.point,
|
|
||||||
viewportPageBounds: T.boxModel,
|
|
||||||
color: T.string,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
const Versions = {
|
|
||||||
AddViewportPageBounds: 1,
|
|
||||||
} as const
|
|
||||||
|
|
||||||
/** @public */
|
|
||||||
export const userPresenceTypeMigrations = defineMigrations({
|
|
||||||
currentVersion: Versions.AddViewportPageBounds,
|
|
||||||
migrators: {
|
|
||||||
[Versions.AddViewportPageBounds]: {
|
|
||||||
up: (record) => {
|
|
||||||
return {
|
|
||||||
...record,
|
|
||||||
viewportPageBounds: { x: 0, y: 0, w: 1, h: 1 },
|
|
||||||
}
|
|
||||||
},
|
|
||||||
down: ({ viewportPageBounds: _viewportPageBounds, ...rest }) => rest,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
/** @public */
|
|
||||||
export const TLUserPresence = createRecordType<TLUserPresence>('user_presence', {
|
|
||||||
migrations: userPresenceTypeMigrations,
|
|
||||||
validator: userPresenceTypeValidator,
|
|
||||||
scope: 'instance',
|
|
||||||
}).withDefaultProperties(
|
|
||||||
(): Omit<TLUserPresence, 'id' | 'typeName' | 'userId'> => ({
|
|
||||||
lastUsedInstanceId: null,
|
|
||||||
lastActivityTimestamp: 0,
|
|
||||||
cursor: { x: 0, y: 0 },
|
|
||||||
viewportPageBounds: { x: 0, y: 0, w: 1, h: 1 },
|
|
||||||
color: '#000000',
|
|
||||||
})
|
|
||||||
)
|
|
|
@ -4,11 +4,14 @@ import { TLRecord } from './TLRecord'
|
||||||
const Versions = {
|
const Versions = {
|
||||||
RemoveCodeAndIconShapeTypes: 1,
|
RemoveCodeAndIconShapeTypes: 1,
|
||||||
AddInstancePresenceType: 2,
|
AddInstancePresenceType: 2,
|
||||||
|
RemoveTLUserAndPresenceAndAddPointer: 3,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
export { Versions as storeVersions }
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export const storeMigrations = defineMigrations({
|
export const storeMigrations = defineMigrations({
|
||||||
currentVersion: Versions.AddInstancePresenceType,
|
currentVersion: Versions.RemoveTLUserAndPresenceAndAddPointer,
|
||||||
migrators: {
|
migrators: {
|
||||||
[Versions.RemoveCodeAndIconShapeTypes]: {
|
[Versions.RemoveCodeAndIconShapeTypes]: {
|
||||||
up: (store: StoreSnapshot<TLRecord>) => {
|
up: (store: StoreSnapshot<TLRecord>) => {
|
||||||
|
@ -33,5 +36,17 @@ export const storeMigrations = defineMigrations({
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
[Versions.RemoveTLUserAndPresenceAndAddPointer]: {
|
||||||
|
up: (store: StoreSnapshot<TLRecord>) => {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(store).filter(([_, v]) => !v.typeName.match(/^(user|user_presence)$/))
|
||||||
|
)
|
||||||
|
},
|
||||||
|
down: (store: StoreSnapshot<TLRecord>) => {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(store).filter(([_, v]) => v.typeName !== 'pointer')
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { getDefaultTranslationLocale } from './translations'
|
import { _getDefaultTranslationLocale } from './translations'
|
||||||
|
|
||||||
type DefaultLanguageTest = {
|
type DefaultLanguageTest = {
|
||||||
name: string
|
name: string
|
||||||
|
@ -37,7 +37,7 @@ describe('Choosing a sensible default translation locale', () => {
|
||||||
|
|
||||||
for (const test of tests) {
|
for (const test of tests) {
|
||||||
it(test.name, () => {
|
it(test.name, () => {
|
||||||
expect(getDefaultTranslationLocale(test.input)).toEqual(test.output)
|
expect(_getDefaultTranslationLocale(test.input)).toEqual(test.output)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -9,7 +9,13 @@ type TLListedTranslations = TLListedTranslation[]
|
||||||
type TLTranslationLocale = TLListedTranslations[number]['locale']
|
type TLTranslationLocale = TLListedTranslations[number]['locale']
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function getDefaultTranslationLocale(locales: readonly string[]): TLTranslationLocale {
|
export function getDefaultTranslationLocale(): TLTranslationLocale {
|
||||||
|
const locales = typeof window !== 'undefined' ? window.navigator.languages ?? ['en'] : ['en']
|
||||||
|
return _getDefaultTranslationLocale(locales)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
export function _getDefaultTranslationLocale(locales: readonly string[]): TLTranslationLocale {
|
||||||
for (const locale of locales) {
|
for (const locale of locales) {
|
||||||
const supportedLocale = getSupportedLocale(locale)
|
const supportedLocale = getSupportedLocale(locale)
|
||||||
if (supportedLocale) {
|
if (supportedLocale) {
|
||||||
|
|
|
@ -4,7 +4,6 @@ import type { TLAssetId } from './records/TLAsset'
|
||||||
import type { TLInstanceId } from './records/TLInstance'
|
import type { TLInstanceId } from './records/TLInstance'
|
||||||
import type { TLPageId } from './records/TLPage'
|
import type { TLPageId } from './records/TLPage'
|
||||||
import type { TLParentId, TLShapeId } from './records/TLShape'
|
import type { TLParentId, TLShapeId } from './records/TLShape'
|
||||||
import type { TLUserId } from './records/TLUser'
|
|
||||||
import {
|
import {
|
||||||
TLAlignType,
|
TLAlignType,
|
||||||
TL_ALIGN_TYPES_WITH_LEGACY_STUFF,
|
TL_ALIGN_TYPES_WITH_LEGACY_STUFF,
|
||||||
|
@ -33,8 +32,6 @@ export function idValidator<Id extends ID<UnknownRecord>>(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export const userIdValidator = idValidator<TLUserId>('user')
|
|
||||||
/** @internal */
|
|
||||||
export const assetIdValidator = idValidator<TLAssetId>('asset')
|
export const assetIdValidator = idValidator<TLAssetId>('asset')
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export const pageIdValidator = idValidator<TLPageId>('page')
|
export const pageIdValidator = idValidator<TLPageId>('page')
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
|
|
||||||
import { Atom } from 'signia';
|
import { Atom } from 'signia';
|
||||||
import { Computed } from 'signia';
|
import { Computed } from 'signia';
|
||||||
import { Signal } from 'signia';
|
|
||||||
|
|
||||||
// @public
|
// @public
|
||||||
export type AllRecords<T extends Store<any>> = ExtractR<ExtractRecordType<T>>;
|
export type AllRecords<T extends Store<any>> = ExtractR<ExtractRecordType<T>>;
|
||||||
|
@ -288,8 +287,6 @@ export class StoreSchema<R extends UnknownRecord, P = unknown> {
|
||||||
createIntegrityChecker(store: Store<R, P>): (() => void) | undefined;
|
createIntegrityChecker(store: Store<R, P>): (() => void) | undefined;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
get currentStoreVersion(): number;
|
get currentStoreVersion(): number;
|
||||||
// @internal (undocumented)
|
|
||||||
derivePresenceState(store: Store<R, P>): Signal<null | R> | undefined;
|
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
migratePersistedRecord(record: R, persistedSchema: SerializedSchema, direction?: 'down' | 'up'): MigrationResult<R>;
|
migratePersistedRecord(record: R, persistedSchema: SerializedSchema, direction?: 'down' | 'up'): MigrationResult<R>;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
@ -317,7 +314,6 @@ export type StoreSchemaOptions<R extends UnknownRecord, P> = {
|
||||||
recordBefore: null | R;
|
recordBefore: null | R;
|
||||||
}) => R;
|
}) => R;
|
||||||
createIntegrityChecker?: (store: Store<R, P>) => void;
|
createIntegrityChecker?: (store: Store<R, P>) => void;
|
||||||
derivePresenceState?: (store: Store<R, P>) => Signal<null | R>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// @public
|
// @public
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { getOwnProperty, objectMapValues } from '@tldraw/utils'
|
import { getOwnProperty, objectMapValues } from '@tldraw/utils'
|
||||||
import { Signal } from 'signia'
|
|
||||||
import { IdOf, UnknownRecord } from './BaseRecord'
|
import { IdOf, UnknownRecord } from './BaseRecord'
|
||||||
import { RecordType } from './RecordType'
|
import { RecordType } from './RecordType'
|
||||||
import { Store, StoreSnapshot } from './Store'
|
import { Store, StoreSnapshot } from './Store'
|
||||||
|
@ -49,8 +48,6 @@ export type StoreSchemaOptions<R extends UnknownRecord, P> = {
|
||||||
}) => R
|
}) => R
|
||||||
/** @internal */
|
/** @internal */
|
||||||
createIntegrityChecker?: (store: Store<R, P>) => void
|
createIntegrityChecker?: (store: Store<R, P>) => void
|
||||||
/** @internal */
|
|
||||||
derivePresenceState?: (store: Store<R, P>) => Signal<R | null>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
|
@ -244,11 +241,6 @@ export class StoreSchema<R extends UnknownRecord, P = unknown> {
|
||||||
return this.options.createIntegrityChecker?.(store) ?? undefined
|
return this.options.createIntegrityChecker?.(store) ?? undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
derivePresenceState(store: Store<R, P>): Signal<R | null> | undefined {
|
|
||||||
return this.options.derivePresenceState?.(store)
|
|
||||||
}
|
|
||||||
|
|
||||||
serialize(): SerializedSchema {
|
serialize(): SerializedSchema {
|
||||||
return {
|
return {
|
||||||
schemaVersion: 1,
|
schemaVersion: 1,
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
|
|
||||||
import { RecordsDiff } from '@tldraw/tlstore';
|
import { RecordsDiff } from '@tldraw/tlstore';
|
||||||
import { SerializedSchema } from '@tldraw/tlstore';
|
import { SerializedSchema } from '@tldraw/tlstore';
|
||||||
import { Store } from '@tldraw/tlstore';
|
|
||||||
import { StoreSnapshot } from '@tldraw/tlstore';
|
import { StoreSnapshot } from '@tldraw/tlstore';
|
||||||
import { SyncedStore } from '@tldraw/editor';
|
import { SyncedStore } from '@tldraw/editor';
|
||||||
import { TldrawEditorConfig } from '@tldraw/editor';
|
import { TldrawEditorConfig } from '@tldraw/editor';
|
||||||
|
@ -14,8 +13,6 @@ import { TLInstanceId } from '@tldraw/editor';
|
||||||
import { TLRecord } from '@tldraw/editor';
|
import { TLRecord } from '@tldraw/editor';
|
||||||
import { TLStore } from '@tldraw/editor';
|
import { TLStore } from '@tldraw/editor';
|
||||||
import { TLStoreSchema } from '@tldraw/editor';
|
import { TLStoreSchema } from '@tldraw/editor';
|
||||||
import { TLUser } from '@tldraw/editor';
|
|
||||||
import { TLUserId } from '@tldraw/editor';
|
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export function addDbName(name: string): void;
|
export function addDbName(name: string): void;
|
||||||
|
@ -40,9 +37,6 @@ export const DEFAULT_DOCUMENT_NAME: any;
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export function getAllIndexDbNames(): string[];
|
export function getAllIndexDbNames(): string[];
|
||||||
|
|
||||||
// @public (undocumented)
|
|
||||||
export function getUserData(): TLUser;
|
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export function hardReset({ shouldReload }?: {
|
export function hardReset({ shouldReload }?: {
|
||||||
shouldReload?: boolean | undefined;
|
shouldReload?: boolean | undefined;
|
||||||
|
@ -69,9 +63,6 @@ export function storeSnapshotInIndexedDb(universalPersistenceKey: string, schema
|
||||||
didCancel?: () => boolean;
|
didCancel?: () => boolean;
|
||||||
}): Promise<void>;
|
}): Promise<void>;
|
||||||
|
|
||||||
// @public (undocumented)
|
|
||||||
export function subscribeToUserData(store: Store<any>): () => void;
|
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const TAB_ID: TLInstanceId;
|
export const TAB_ID: TLInstanceId;
|
||||||
|
|
||||||
|
@ -97,10 +88,9 @@ export class TLLocalSyncClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
// @public
|
// @public
|
||||||
export function useLocalSyncClient({ universalPersistenceKey, instanceId, userId, config, }: {
|
export function useLocalSyncClient({ universalPersistenceKey, instanceId, config, }: {
|
||||||
universalPersistenceKey: string;
|
universalPersistenceKey: string;
|
||||||
instanceId: TLInstanceId;
|
instanceId: TLInstanceId;
|
||||||
userId: TLUserId;
|
|
||||||
config: TldrawEditorConfig;
|
config: TldrawEditorConfig;
|
||||||
}): SyncedStore;
|
}): SyncedStore;
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,4 @@ export {
|
||||||
TAB_ID,
|
TAB_ID,
|
||||||
addDbName,
|
addDbName,
|
||||||
getAllIndexDbNames,
|
getAllIndexDbNames,
|
||||||
getUserData,
|
|
||||||
subscribeToUserData,
|
|
||||||
} from './lib/persistence-constants'
|
} from './lib/persistence-constants'
|
||||||
|
|
|
@ -1,11 +1,4 @@
|
||||||
import {
|
import { TldrawEditorConfig, TLInstance, TLInstanceId, TLPage } from '@tldraw/editor'
|
||||||
TldrawEditorConfig,
|
|
||||||
TLInstance,
|
|
||||||
TLInstanceId,
|
|
||||||
TLPage,
|
|
||||||
TLUser,
|
|
||||||
TLUserId,
|
|
||||||
} from '@tldraw/editor'
|
|
||||||
import { promiseWithResolve } from '@tldraw/utils'
|
import { promiseWithResolve } from '@tldraw/utils'
|
||||||
import * as idb from './indexedDb'
|
import * as idb from './indexedDb'
|
||||||
import { TLLocalSyncClient } from './TLLocalSyncClient'
|
import { TLLocalSyncClient } from './TLLocalSyncClient'
|
||||||
|
@ -31,11 +24,9 @@ class BroadcastChannelMock {
|
||||||
|
|
||||||
function testClient(
|
function testClient(
|
||||||
instanceId: TLInstanceId = TLInstance.createCustomId('test'),
|
instanceId: TLInstanceId = TLInstance.createCustomId('test'),
|
||||||
userId: TLUserId = TLUser.createCustomId('test'),
|
|
||||||
channel = new BroadcastChannelMock('test')
|
channel = new BroadcastChannelMock('test')
|
||||||
) {
|
) {
|
||||||
const store = new TldrawEditorConfig().createStore({
|
const store = new TldrawEditorConfig().createStore({
|
||||||
userId,
|
|
||||||
instanceId,
|
instanceId,
|
||||||
})
|
})
|
||||||
const onLoad = jest.fn(() => {
|
const onLoad = jest.fn(() => {
|
||||||
|
@ -85,7 +76,7 @@ test('the client connects on instantiation, announcing its schema', async () =>
|
||||||
expect(msg).toMatchObject({ type: 'announce', schema: { recordVersions: {} } })
|
expect(msg).toMatchObject({ type: 'announce', schema: { recordVersions: {} } })
|
||||||
})
|
})
|
||||||
|
|
||||||
test('when a client receives an annouce with a newer schema version it reloads itself', async () => {
|
test('when a client receives an announce with a newer schema version it reloads itself', async () => {
|
||||||
const { client, channel, onLoadError } = testClient()
|
const { client, channel, onLoadError } = testClient()
|
||||||
await tick()
|
await tick()
|
||||||
jest.advanceTimersByTime(10000)
|
jest.advanceTimersByTime(10000)
|
||||||
|
@ -103,7 +94,7 @@ test('when a client receives an annouce with a newer schema version it reloads i
|
||||||
expect(onLoadError).not.toHaveBeenCalled()
|
expect(onLoadError).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('when a client receives an annouce with a newer schema version shortly after loading it does not reload but instead reports a loadError', async () => {
|
test('when a client receives an announce with a newer schema version shortly after loading it does not reload but instead reports a loadError', async () => {
|
||||||
const { client, channel, onLoadError } = testClient()
|
const { client, channel, onLoadError } = testClient()
|
||||||
await tick()
|
await tick()
|
||||||
jest.advanceTimersByTime(1000)
|
jest.advanceTimersByTime(1000)
|
||||||
|
|
|
@ -155,7 +155,12 @@ export class TLLocalSyncClient {
|
||||||
// 3. Merge the changes into the REAL STORE
|
// 3. Merge the changes into the REAL STORE
|
||||||
this.store.mergeRemoteChanges(() => {
|
this.store.mergeRemoteChanges(() => {
|
||||||
// Calling put will validate the records!
|
// Calling put will validate the records!
|
||||||
this.store.put(Object.values(migrationResult.value), 'initialize')
|
this.store.put(
|
||||||
|
Object.values(migrationResult.value).filter(
|
||||||
|
(r) => this.store.schema.types[r.typeName].scope !== 'presence'
|
||||||
|
),
|
||||||
|
'initialize'
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { SyncedStore, TldrawEditorConfig, TLInstanceId, TLUserId, uniqueId } from '@tldraw/editor'
|
import { SyncedStore, TldrawEditorConfig, TLInstanceId, uniqueId } from '@tldraw/editor'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import '../hardReset'
|
import '../hardReset'
|
||||||
import { subscribeToUserData } from '../persistence-constants'
|
|
||||||
import { TLLocalSyncClient } from '../TLLocalSyncClient'
|
import { TLLocalSyncClient } from '../TLLocalSyncClient'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -13,12 +12,10 @@ import { TLLocalSyncClient } from '../TLLocalSyncClient'
|
||||||
export function useLocalSyncClient({
|
export function useLocalSyncClient({
|
||||||
universalPersistenceKey,
|
universalPersistenceKey,
|
||||||
instanceId,
|
instanceId,
|
||||||
userId,
|
|
||||||
config,
|
config,
|
||||||
}: {
|
}: {
|
||||||
universalPersistenceKey: string
|
universalPersistenceKey: string
|
||||||
instanceId: TLInstanceId
|
instanceId: TLInstanceId
|
||||||
userId: TLUserId
|
|
||||||
config: TldrawEditorConfig
|
config: TldrawEditorConfig
|
||||||
}): SyncedStore {
|
}): SyncedStore {
|
||||||
const [state, setState] = useState<{ id: string; syncedStore: SyncedStore } | null>(null)
|
const [state, setState] = useState<{ id: string; syncedStore: SyncedStore } | null>(null)
|
||||||
|
@ -38,7 +35,7 @@ export function useLocalSyncClient({
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const store = config.createStore({ userId, instanceId })
|
const store = config.createStore({ instanceId })
|
||||||
|
|
||||||
const client = new TLLocalSyncClient(store, {
|
const client = new TLLocalSyncClient(store, {
|
||||||
universalPersistenceKey,
|
universalPersistenceKey,
|
||||||
|
@ -50,14 +47,11 @@ export function useLocalSyncClient({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const userDataUnsubcribe = subscribeToUserData(store)
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
setState((prevState) => (prevState?.id === id ? null : prevState))
|
setState((prevState) => (prevState?.id === id ? null : prevState))
|
||||||
userDataUnsubcribe()
|
|
||||||
client.close()
|
client.close()
|
||||||
}
|
}
|
||||||
}, [instanceId, universalPersistenceKey, config, userId])
|
}, [instanceId, universalPersistenceKey, config])
|
||||||
|
|
||||||
return state?.syncedStore ?? { status: 'loading' }
|
return state?.syncedStore ?? { status: 'loading' }
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
import { TLInstance, TLInstanceId, TLUser, uniqueId } from '@tldraw/editor'
|
import { TLInstance, TLInstanceId, uniqueId } from '@tldraw/editor'
|
||||||
import { Store } from '@tldraw/tlstore'
|
|
||||||
import { atom, react } from 'signia'
|
|
||||||
|
|
||||||
const tabIdKey = 'TLDRAW_TAB_ID_v2' as const
|
const tabIdKey = 'TLDRAW_TAB_ID_v2' as const
|
||||||
|
|
||||||
|
@ -26,42 +24,6 @@ function iOS() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// the id of the user, stored in localStorage so that it persists across sessions
|
|
||||||
const USER_DATA_KEY = 'TLDRAW_USER_DATA_v2'
|
|
||||||
|
|
||||||
const globalUserData = atom<TLUser>(
|
|
||||||
'globalUserData',
|
|
||||||
JSON.parse(window?.localStorage.getItem(USER_DATA_KEY) || 'null') ?? TLUser.create({})
|
|
||||||
)
|
|
||||||
|
|
||||||
react('set global user data', () => {
|
|
||||||
if (window) {
|
|
||||||
window.localStorage.setItem(USER_DATA_KEY, JSON.stringify(globalUserData.value))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/** @public */
|
|
||||||
export function getUserData() {
|
|
||||||
return globalUserData.value
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @public */
|
|
||||||
export function subscribeToUserData(store: Store<any>) {
|
|
||||||
const userId = globalUserData.value.id
|
|
||||||
return store.listen(({ changes }) => {
|
|
||||||
for (const record of Object.values(changes.added)) {
|
|
||||||
if (record.typeName === 'user' && userId === record.id) {
|
|
||||||
globalUserData.set(record)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const [_, record] of Object.values(changes.updated)) {
|
|
||||||
if (record.typeName === 'user' && userId === record.id) {
|
|
||||||
globalUserData.set(record)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// the id of the document that will be loaded if the URL doesn't contain a document id
|
// the id of the document that will be loaded if the URL doesn't contain a document id
|
||||||
// again, stored in localStorage
|
// again, stored in localStorage
|
||||||
const defaultDocumentKey = 'TLDRAW_DEFAULT_DOCUMENT_NAME_v2'
|
const defaultDocumentKey = 'TLDRAW_DEFAULT_DOCUMENT_NAME_v2'
|
||||||
|
|
|
@ -9,9 +9,7 @@ export function LanguageMenu() {
|
||||||
const { languages, currentLanguage } = useLanguages()
|
const { languages, currentLanguage } = useLanguages()
|
||||||
|
|
||||||
const handleLanguageSelect = useCallback(
|
const handleLanguageSelect = useCallback(
|
||||||
(locale: TLTranslationLocale) => {
|
(locale: TLTranslationLocale) => app.setLocale(locale),
|
||||||
app.updateUser({ locale })
|
|
||||||
},
|
|
||||||
[app]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@ export const Minimap = track(function Minimap({
|
||||||
|
|
||||||
const minimap = React.useMemo(() => new MinimapManager(app, app.devicePixelRatio), [app])
|
const minimap = React.useMemo(() => new MinimapManager(app, app.devicePixelRatio), [app])
|
||||||
|
|
||||||
const isDarkMode = app.userDocumentSettings.isDarkMode
|
const isDarkMode = app.isDarkMode
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
// Must check after render
|
// Must check after render
|
||||||
|
|
|
@ -49,7 +49,7 @@ export function MenuSchemaProvider({ overrides, children }: MenuSchemaProviderPr
|
||||||
const breakpoint = useBreakpoint()
|
const breakpoint = useBreakpoint()
|
||||||
const isMobile = breakpoint < 5
|
const isMobile = breakpoint < 5
|
||||||
|
|
||||||
const isDarkMode = useValue('isDarkMode', () => app.userDocumentSettings.isDarkMode, [app])
|
const isDarkMode = useValue('isDarkMode', () => app.isDarkMode, [app])
|
||||||
const isGridMode = useValue('isGridMode', () => app.userDocumentSettings.isGridMode, [app])
|
const isGridMode = useValue('isGridMode', () => app.userDocumentSettings.isGridMode, [app])
|
||||||
const isSnapMode = useValue('isSnapMode', () => app.userDocumentSettings.isSnapMode, [app])
|
const isSnapMode = useValue('isSnapMode', () => app.userDocumentSettings.isSnapMode, [app])
|
||||||
const isToolLock = useValue('isToolLock', () => app.instanceState.isToolLocked, [app])
|
const isToolLock = useValue('isToolLock', () => app.instanceState.isToolLocked, [app])
|
||||||
|
|
|
@ -4,5 +4,5 @@ import { LANGUAGES } from './languages'
|
||||||
/** @public */
|
/** @public */
|
||||||
export function useLanguages() {
|
export function useLanguages() {
|
||||||
const app = useApp()
|
const app = useApp()
|
||||||
return { languages: LANGUAGES, currentLanguage: app.user.locale }
|
return { languages: LANGUAGES, currentLanguage: app.locale }
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,7 @@ export const TranslationProvider = track(function TranslationProvider({
|
||||||
children,
|
children,
|
||||||
}: TranslationProviderProps) {
|
}: TranslationProviderProps) {
|
||||||
const app = useApp()
|
const app = useApp()
|
||||||
const locale = app.userSettings.locale
|
const locale = app.locale
|
||||||
const getAssetUrl = useAssetUrls()
|
const getAssetUrl = useAssetUrls()
|
||||||
|
|
||||||
const [currentTranslation, setCurrentTranslation] = React.useState<TLTranslation>(() => {
|
const [currentTranslation, setCurrentTranslation] = React.useState<TLTranslation>(() => {
|
||||||
|
|
Loading…
Reference in a new issue