[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/ui.css'
|
||||
import { useRef } from 'react'
|
||||
|
||||
const SHOW_MOVING_CURSOR = false
|
||||
const CURSOR_SPEED = 0.1
|
||||
const SHOW_MOVING_CURSOR = true
|
||||
const CURSOR_SPEED = 0.5
|
||||
const CIRCLE_RADIUS = 100
|
||||
const UPDATE_FPS = 60
|
||||
|
||||
|
@ -15,39 +15,19 @@ export default function UserPresenceExample() {
|
|||
<Tldraw
|
||||
persistenceKey="user-presence-example"
|
||||
onMount={(app) => {
|
||||
// There are several records related to user presence that must be
|
||||
// included for each user. These are created automatically by each
|
||||
// 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.
|
||||
// For every connected peer you should put a TLInstancePresence record in the
|
||||
// store with their cursor position etc.
|
||||
|
||||
const userId = TLUser.createCustomId('user-1')
|
||||
|
||||
const user = TLUser.create({
|
||||
id: userId,
|
||||
name: 'User 1',
|
||||
const peerPresence = TLInstancePresence.create({
|
||||
id: TLInstancePresence.createCustomId('peer-1-presence'),
|
||||
currentPageId: app.currentPageId,
|
||||
userId: 'peer-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.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])
|
||||
app.store.put([peerPresence])
|
||||
|
||||
// Make the fake user's cursor rotate in a circle
|
||||
if (rTimeout.current) {
|
||||
|
@ -62,24 +42,21 @@ export default function UserPresenceExample() {
|
|||
// rotate in a circle
|
||||
app.store.put([
|
||||
{
|
||||
...userPresence,
|
||||
...peerPresence,
|
||||
cursor: {
|
||||
x: Math.cos(t * Math.PI * 2) * CIRCLE_RADIUS,
|
||||
y: Math.sin(t * Math.PI * 2) * CIRCLE_RADIUS,
|
||||
...peerPresence.cursor,
|
||||
x: 150 + Math.cos(t * Math.PI * 2) * CIRCLE_RADIUS,
|
||||
y: 150 + Math.sin(t * Math.PI * 2) * CIRCLE_RADIUS,
|
||||
},
|
||||
lastActivityTimestamp: now,
|
||||
},
|
||||
])
|
||||
}, 1000 / UPDATE_FPS)
|
||||
} else {
|
||||
app.store.put([
|
||||
{ ...userPresence, cursor: { x: 0, y: 0 }, lastActivityTimestamp: Date.now() },
|
||||
])
|
||||
app.store.put([{ ...peerPresence, lastActivityTimestamp: Date.now() }])
|
||||
|
||||
rTimeout.current = setInterval(() => {
|
||||
app.store.put([
|
||||
{ ...userPresence, cursor: { x: 0, y: 0 }, lastActivityTimestamp: Date.now() },
|
||||
])
|
||||
app.store.put([{ ...peerPresence, lastActivityTimestamp: Date.now() }])
|
||||
}, 1000)
|
||||
}
|
||||
}}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import {
|
||||
Canvas,
|
||||
ContextMenu,
|
||||
getUserData,
|
||||
TldrawEditor,
|
||||
TldrawEditorConfig,
|
||||
TldrawUi,
|
||||
|
@ -13,28 +12,19 @@ import '@tldraw/tldraw/ui.css'
|
|||
|
||||
const instanceId = TLInstance.createCustomId('example')
|
||||
|
||||
// for custom config, see 3-custom-config
|
||||
const config = new TldrawEditorConfig()
|
||||
|
||||
export default function Example() {
|
||||
const userData = getUserData()
|
||||
|
||||
const syncedStore = useLocalSyncClient({
|
||||
config,
|
||||
instanceId,
|
||||
userId: userData.id,
|
||||
universalPersistenceKey: 'exploded-example',
|
||||
// config: myConfig // for custom config, see 3-custom-config
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="tldraw__editor">
|
||||
<TldrawEditor
|
||||
instanceId={instanceId}
|
||||
userId={userData.id}
|
||||
store={syncedStore}
|
||||
config={config}
|
||||
autoFocus
|
||||
>
|
||||
<TldrawEditor instanceId={instanceId} store={syncedStore} config={config} autoFocus>
|
||||
<TldrawUi>
|
||||
<ContextMenu>
|
||||
<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 { useDefaultHelpers } from '@tldraw/ui'
|
||||
import { debounce } from '@tldraw/utils'
|
||||
|
@ -11,11 +11,9 @@ import type { VscodeMessage } from '../../messages'
|
|||
|
||||
export const ChangeResponder = ({
|
||||
syncedStore,
|
||||
userId,
|
||||
instanceId,
|
||||
}: {
|
||||
syncedStore: SyncedStore
|
||||
userId: TLUserId
|
||||
instanceId: TLInstanceId
|
||||
}) => {
|
||||
const app = useApp()
|
||||
|
@ -46,7 +44,7 @@ export const ChangeResponder = ({
|
|||
clearToasts()
|
||||
window.removeEventListener('message', handleMessage)
|
||||
}
|
||||
}, [app, userId, instanceId, msg, addToast, clearToasts])
|
||||
}, [app, instanceId, msg, addToast, clearToasts])
|
||||
|
||||
React.useEffect(() => {
|
||||
// When the history changes, send the new file contents to VSCode
|
||||
|
@ -71,7 +69,7 @@ export const ChangeResponder = ({
|
|||
handleChange()
|
||||
app.off('change-history', handleChange)
|
||||
}
|
||||
}, [app, syncedStore, userId, instanceId])
|
||||
}, [app, syncedStore, instanceId])
|
||||
|
||||
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 { useDefaultHelpers } from '@tldraw/ui'
|
||||
import React from 'react'
|
||||
import { vscode } from './utils/vscode'
|
||||
|
||||
export function FileOpen({
|
||||
userId,
|
||||
fileContents,
|
||||
instanceId,
|
||||
forceDarkMode,
|
||||
}: {
|
||||
instanceId: TLInstanceId
|
||||
userId: TLUserId
|
||||
fileContents: string
|
||||
forceDarkMode: boolean
|
||||
}) {
|
||||
|
@ -44,17 +42,7 @@ export function FileOpen({
|
|||
return () => {
|
||||
clearToasts()
|
||||
}
|
||||
}, [
|
||||
fileContents,
|
||||
app,
|
||||
userId,
|
||||
instanceId,
|
||||
addToast,
|
||||
msg,
|
||||
clearToasts,
|
||||
forceDarkMode,
|
||||
isFileLoaded,
|
||||
])
|
||||
}, [fileContents, app, instanceId, addToast, msg, clearToasts, forceDarkMode, isFileLoaded])
|
||||
|
||||
return null
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ import {
|
|||
setRuntimeOverrides,
|
||||
TldrawEditor,
|
||||
TldrawEditorConfig,
|
||||
TLUserId,
|
||||
} from '@tldraw/editor'
|
||||
import { linksUiOverrides } from './utils/links'
|
||||
// eslint-disable-next-line import/no-internal-modules
|
||||
|
@ -97,7 +96,6 @@ export const TldrawWrapper = () => {
|
|||
assetSrc: message.data.assetSrc,
|
||||
fileContents: message.data.fileContents,
|
||||
uri: message.data.uri,
|
||||
userId: message.data.userId as TLUserId,
|
||||
isDarkMode: message.data.isDarkMode,
|
||||
config,
|
||||
})
|
||||
|
@ -128,24 +126,15 @@ export type TLDrawInnerProps = {
|
|||
assetSrc: string
|
||||
fileContents: string
|
||||
uri: string
|
||||
userId: TLUserId
|
||||
isDarkMode: boolean
|
||||
config: TldrawEditorConfig
|
||||
}
|
||||
|
||||
function TldrawInner({
|
||||
uri,
|
||||
config,
|
||||
assetSrc,
|
||||
userId,
|
||||
isDarkMode,
|
||||
fileContents,
|
||||
}: TLDrawInnerProps) {
|
||||
function TldrawInner({ uri, config, assetSrc, isDarkMode, fileContents }: TLDrawInnerProps) {
|
||||
const instanceId = TAB_ID
|
||||
const syncedStore = useLocalSyncClient({
|
||||
universalPersistenceKey: uri,
|
||||
instanceId,
|
||||
userId,
|
||||
config,
|
||||
})
|
||||
|
||||
|
@ -156,20 +145,14 @@ function TldrawInner({
|
|||
config={config}
|
||||
assetUrls={assetUrls}
|
||||
instanceId={TAB_ID}
|
||||
userId={userId}
|
||||
store={syncedStore}
|
||||
onCreateBookmarkFromUrl={onCreateBookmarkFromUrl}
|
||||
autoFocus
|
||||
>
|
||||
{/* <DarkModeHandler themeKind={themeKind} /> */}
|
||||
<TldrawUi assetUrls={assetUrls} overrides={[menuOverrides, linksUiOverrides]}>
|
||||
<FileOpen
|
||||
instanceId={instanceId}
|
||||
userId={userId}
|
||||
fileContents={fileContents}
|
||||
forceDarkMode={isDarkMode}
|
||||
/>
|
||||
<ChangeResponder syncedStore={syncedStore} userId={userId} instanceId={instanceId} />
|
||||
<FileOpen instanceId={instanceId} fileContents={fileContents} forceDarkMode={isDarkMode} />
|
||||
<ChangeResponder syncedStore={syncedStore} instanceId={instanceId} />
|
||||
<ContextMenu>
|
||||
<Canvas />
|
||||
</ContextMenu>
|
||||
|
|
|
@ -93,10 +93,7 @@ import { TLStyleType } from '@tldraw/tlschema';
|
|||
import { TLTextShape } from '@tldraw/tlschema';
|
||||
import { TLTextShapeProps } from '@tldraw/tlschema';
|
||||
import { TLUnknownShape } from '@tldraw/tlschema';
|
||||
import { TLUser } from '@tldraw/tlschema';
|
||||
import { TLUserDocument } from '@tldraw/tlschema';
|
||||
import { TLUserId } from '@tldraw/tlschema';
|
||||
import { TLUserPresence } from '@tldraw/tlschema';
|
||||
import { TLVideoAsset } from '@tldraw/tlschema';
|
||||
import { TLVideoShape } from '@tldraw/tlschema';
|
||||
import { UnknownRecord } from '@tldraw/tlstore';
|
||||
|
@ -368,6 +365,7 @@ export class App extends EventEmitter<TLEventMap> {
|
|||
// (undocumented)
|
||||
get isToolLocked(): boolean;
|
||||
isWithinSelection(id: TLShapeId): boolean;
|
||||
get locale(): string;
|
||||
// (undocumented)
|
||||
lockShapes(_ids?: TLShapeId[]): this;
|
||||
mark(reason?: string, onUndo?: boolean, onRedo?: boolean): string;
|
||||
|
@ -467,6 +465,7 @@ export class App extends EventEmitter<TLEventMap> {
|
|||
setHintingIds(ids: TLShapeId[]): this;
|
||||
setHoveredId(id?: null | TLShapeId): this;
|
||||
setInstancePageState(partial: Partial<TLInstancePageState>, ephemeral?: boolean): void;
|
||||
setLocale(locale: string): void;
|
||||
// (undocumented)
|
||||
setPenMode(isPenMode: 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;
|
||||
get sortedShapesArray(): TLShape[];
|
||||
stackShapes(operation: 'horizontal' | 'vertical', ids?: TLShapeId[], gap?: number): this;
|
||||
startFollowingUser: (userId: TLUserId) => this | undefined;
|
||||
startFollowingUser: (userId: string) => this | undefined;
|
||||
stopCameraAnimation(): this;
|
||||
stopFollowingUser: () => this;
|
||||
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;
|
||||
updatePage(partial: RequiredKeys<TLPage, 'id'>, squashing?: boolean): this;
|
||||
updateShapes(partials: (null | TLShapePartial | undefined)[], squashing?: boolean): this;
|
||||
updateUser(partial: Partial<TLUser>): void;
|
||||
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;
|
||||
get user(): TLUser;
|
||||
// @internal (undocumented)
|
||||
readonly user: UserPreferencesManager;
|
||||
// (undocumented)
|
||||
get userDocumentSettings(): TLUserDocument;
|
||||
get userId(): TLUserId;
|
||||
// (undocumented)
|
||||
get userPresence(): TLUserPresence | undefined;
|
||||
get userSettings(): TLUser;
|
||||
get viewportPageBounds(): Box2d;
|
||||
get viewportPageCenter(): Vec2d;
|
||||
get viewportScreenBounds(): Box2d;
|
||||
|
@ -1794,10 +1783,13 @@ export class TldrawEditorConfig {
|
|||
// (undocumented)
|
||||
createStore(config: {
|
||||
initialData?: StoreSnapshot<TLRecord>;
|
||||
userId: TLUserId;
|
||||
instanceId: TLInstanceId;
|
||||
}): TLStore;
|
||||
// (undocumented)
|
||||
readonly derivePresenceState: (store: TLStore) => Signal<null | TLInstancePresence>;
|
||||
// (undocumented)
|
||||
readonly setUserPreferences: (userPreferences: TLUserPreferences) => void;
|
||||
// (undocumented)
|
||||
readonly shapeUtils: Record<TLShape['type'], TLShapeUtilConstructor<any>>;
|
||||
// (undocumented)
|
||||
readonly storeSchema: StoreSchema<TLRecord, TLStoreProps>;
|
||||
|
@ -1805,6 +1797,8 @@ export class TldrawEditorConfig {
|
|||
readonly TLShape: RecordType<TLShape, 'index' | 'parentId' | 'props' | 'type'>;
|
||||
// (undocumented)
|
||||
readonly tools: readonly StateNodeConstructor[];
|
||||
// (undocumented)
|
||||
readonly userPreferences: Signal<TLUserPreferences>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
|
@ -1825,7 +1819,6 @@ export interface TldrawEditorProps {
|
|||
}>;
|
||||
onMount?: (app: App) => void;
|
||||
store?: SyncedStore | TLStore;
|
||||
userId?: TLUserId;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
|
@ -2658,17 +2651,20 @@ export const useApp: () => App;
|
|||
export function useContainer(): HTMLDivElement;
|
||||
|
||||
// @internal (undocumented)
|
||||
export function usePeerIds(): TLUserId[];
|
||||
export function usePeerIds(): string[];
|
||||
|
||||
// @public (undocumented)
|
||||
export function usePrefersReducedMotion(): boolean;
|
||||
|
||||
// @internal (undocumented)
|
||||
export function usePresence(userId: TLUserId): null | TLInstancePresence;
|
||||
export function usePresence(userId: string): null | TLInstancePresence;
|
||||
|
||||
// @public (undocumented)
|
||||
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)
|
||||
export function useReactor(name: string, reactFn: () => void, deps?: any[] | undefined): void;
|
||||
|
||||
|
|
|
@ -131,6 +131,7 @@ export {
|
|||
type ReadySyncedStore,
|
||||
type SyncedStore,
|
||||
} from './lib/config/SyncedStore'
|
||||
export { USER_COLORS } from './lib/config/TLUserPreferences'
|
||||
export { TldrawEditorConfig } from './lib/config/TldrawEditorConfig'
|
||||
export {
|
||||
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 { 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 { EditorAssetUrls, defaultEditorAssetUrls } from './assetUrls'
|
||||
import { OptionalErrorBoundary } from './components/ErrorBoundary'
|
||||
|
@ -87,8 +87,6 @@ export interface TldrawEditorProps {
|
|||
* from a server or database.
|
||||
*/
|
||||
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
|
||||
* tab). If not given, one will be generated.
|
||||
|
@ -132,38 +130,19 @@ export function TldrawEditor(props: TldrawEditorProps) {
|
|||
)
|
||||
}
|
||||
|
||||
function TldrawEditorBeforeLoading({
|
||||
config,
|
||||
userId,
|
||||
instanceId,
|
||||
store,
|
||||
...props
|
||||
}: TldrawEditorProps) {
|
||||
function TldrawEditorBeforeLoading({ config, instanceId, store, ...props }: TldrawEditorProps) {
|
||||
const { done: preloadingComplete, error: preloadingError } = usePreloadAssets(
|
||||
props.assetUrls ?? defaultEditorAssetUrls
|
||||
)
|
||||
|
||||
const [_store, _setStore] = useState<TLStore | SyncedStore>(() => {
|
||||
const _store = useMemo<TLStore | SyncedStore>(() => {
|
||||
return (
|
||||
store ??
|
||||
config.createStore({
|
||||
userId: userId ?? TLUser.createId(),
|
||||
instanceId: instanceId ?? TLInstance.createId(),
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
_setStore(() => {
|
||||
return (
|
||||
store ??
|
||||
config.createStore({
|
||||
userId: userId ?? TLUser.createId(),
|
||||
instanceId: instanceId ?? TLInstance.createId(),
|
||||
})
|
||||
)
|
||||
})
|
||||
}, [store, config, userId, instanceId])
|
||||
}, [store, config, instanceId])
|
||||
|
||||
let loadedStore: TLStore | SyncedStore
|
||||
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) {
|
||||
return <ErrorScreen>Could not load assets. Please refresh the page.</ErrorScreen>
|
||||
}
|
||||
|
@ -208,7 +181,6 @@ function TldrawEditorBeforeLoading({
|
|||
function TldrawEditorAfterLoading({
|
||||
onMount,
|
||||
config,
|
||||
isDarkMode,
|
||||
children,
|
||||
onCreateAssetFromFile,
|
||||
onCreateBookmarkFromUrl,
|
||||
|
@ -257,20 +229,15 @@ function TldrawEditorAfterLoading({
|
|||
const onMountEvent = useEvent((app: App) => {
|
||||
onMount?.(app)
|
||||
app.emit('mount')
|
||||
window.tldrawReady = true
|
||||
})
|
||||
|
||||
React.useEffect(() => {
|
||||
if (app) {
|
||||
// Set the initial theme state.
|
||||
if (isDarkMode !== undefined) {
|
||||
app.updateUserDocumentSettings({ isDarkMode })
|
||||
}
|
||||
|
||||
// Run onMount
|
||||
window.tldrawReady = true
|
||||
onMountEvent(app)
|
||||
}
|
||||
}, [app, onMountEvent, isDarkMode])
|
||||
}, [app, onMountEvent])
|
||||
|
||||
const crashingError = useSyncExternalStore(
|
||||
useCallback(
|
||||
|
|
|
@ -40,6 +40,7 @@ import {
|
|||
TLInstanceId,
|
||||
TLInstancePageState,
|
||||
TLNullableShapeProps,
|
||||
TLPOINTER_ID,
|
||||
TLPage,
|
||||
TLPageId,
|
||||
TLParentId,
|
||||
|
@ -52,9 +53,7 @@ import {
|
|||
TLSizeStyle,
|
||||
TLStore,
|
||||
TLUnknownShape,
|
||||
TLUser,
|
||||
TLUserDocument,
|
||||
TLUserId,
|
||||
TLVideoAsset,
|
||||
Vec2dModel,
|
||||
createCustomShapeId,
|
||||
|
@ -116,6 +115,7 @@ import { HistoryManager } from './managers/HistoryManager'
|
|||
import { SnapManager } from './managers/SnapManager'
|
||||
import { TextManager } from './managers/TextManager'
|
||||
import { TickManager } from './managers/TickManager'
|
||||
import { UserPreferencesManager } from './managers/UserPreferencesManager'
|
||||
import { TLArrowUtil } from './shapeutils/TLArrowUtil/TLArrowUtil'
|
||||
import { getCurvedArrowInfo } from './shapeutils/TLArrowUtil/arrow/curved-arrow'
|
||||
import {
|
||||
|
@ -185,6 +185,8 @@ export class App extends EventEmitter<TLEventMap> {
|
|||
|
||||
this.store = store
|
||||
|
||||
this.user = new UserPreferencesManager(this)
|
||||
|
||||
this.getContainer = getContainer ?? (() => document.body)
|
||||
|
||||
this.textMeasure = new TextManager(this)
|
||||
|
@ -355,6 +357,11 @@ export class App extends EventEmitter<TLEventMap> {
|
|||
*/
|
||||
readonly snaps = new SnapManager(this)
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
readonly user: UserPreferencesManager
|
||||
|
||||
/**
|
||||
* Whether the editor is running in Safari.
|
||||
*
|
||||
|
@ -424,21 +431,6 @@ export class App extends EventEmitter<TLEventMap> {
|
|||
*/
|
||||
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).
|
||||
*
|
||||
|
@ -1197,7 +1189,6 @@ export class App extends EventEmitter<TLEventMap> {
|
|||
}
|
||||
}
|
||||
|
||||
this.updateUserPresence()
|
||||
this.emit('update')
|
||||
}
|
||||
|
||||
|
@ -1500,16 +1491,6 @@ export class App extends EventEmitter<TLEventMap> {
|
|||
return this.documentSettings.gridSize
|
||||
}
|
||||
|
||||
/**
|
||||
* The user's global settings.
|
||||
*
|
||||
* @public
|
||||
* @readonly
|
||||
*/
|
||||
get userSettings(): TLUser {
|
||||
return this.store.get(this.userId)!
|
||||
}
|
||||
|
||||
get isSnapMode() {
|
||||
return this.userDocumentSettings.isSnapMode
|
||||
}
|
||||
|
@ -1522,12 +1503,12 @@ export class App extends EventEmitter<TLEventMap> {
|
|||
}
|
||||
|
||||
get isDarkMode() {
|
||||
return this.userDocumentSettings.isDarkMode
|
||||
return this.user.isDarkMode
|
||||
}
|
||||
|
||||
setDarkMode(isDarkMode: boolean) {
|
||||
if (isDarkMode !== this.isDarkMode) {
|
||||
this.updateUserDocumentSettings({ isDarkMode }, true)
|
||||
this.user.updateUserPreferences({ isDarkMode })
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
@ -1556,7 +1537,7 @@ export class App extends EventEmitter<TLEventMap> {
|
|||
|
||||
/** @internal */
|
||||
@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 {
|
||||
|
@ -1609,15 +1590,6 @@ export class App extends EventEmitter<TLEventMap> {
|
|||
|
||||
// User / User App State
|
||||
|
||||
/**
|
||||
* The current user state.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
get user(): TLUser {
|
||||
return this.store.get(this.userId)!
|
||||
}
|
||||
|
||||
/** The current tab state */
|
||||
get instanceState(): TLInstance {
|
||||
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
|
||||
this.updateUserPresence({ cursor: currentPagePoint.toJson() })
|
||||
this.store.put([
|
||||
{
|
||||
id: TLPOINTER_ID,
|
||||
typeName: 'pointer',
|
||||
x: currentPagePoint.x,
|
||||
y: currentPagePoint.y,
|
||||
lastActivityTimestamp: Date.now(),
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
/* --------------------- 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.
|
||||
*
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -5577,7 +5528,7 @@ export class App extends EventEmitter<TLEventMap> {
|
|||
scale = 1,
|
||||
background = false,
|
||||
padding = SVG_PADDING,
|
||||
darkMode = this.userDocumentSettings.isDarkMode,
|
||||
darkMode = this.isDarkMode,
|
||||
preserveAspectRatio = false,
|
||||
} = opts
|
||||
|
||||
|
@ -7246,17 +7197,11 @@ export class App extends EventEmitter<TLEventMap> {
|
|||
|
||||
this.store.put([{ ...this.instanceState, currentPageId: toId }])
|
||||
|
||||
this.updateUserPresence({
|
||||
viewportPageBounds: this.viewportPageBounds.toJson(),
|
||||
})
|
||||
this.updateCullingBounds()
|
||||
},
|
||||
undo: ({ fromId }) => {
|
||||
this.store.put([{ ...this.instanceState, currentPageId: fromId }])
|
||||
|
||||
this.updateUserPresence({
|
||||
viewportPageBounds: this.viewportPageBounds.toJson(),
|
||||
})
|
||||
this.updateCullingBounds()
|
||||
},
|
||||
squash: ({ fromId }, { toId }) => {
|
||||
|
@ -7890,10 +7835,6 @@ export class App extends EventEmitter<TLEventMap> {
|
|||
isPen: this.isPenMode ?? false,
|
||||
})
|
||||
|
||||
this.updateUserPresence({
|
||||
viewportPageBounds: this.viewportPageBounds.toJson(),
|
||||
})
|
||||
|
||||
this._cameraManager.tick()
|
||||
})
|
||||
|
||||
|
@ -8494,7 +8435,7 @@ export class App extends EventEmitter<TLEventMap> {
|
|||
* @param userId - The id of the user to follow.
|
||||
* @public
|
||||
*/
|
||||
startFollowingUser = (userId: TLUserId) => {
|
||||
startFollowingUser = (userId: string) => {
|
||||
// Currently, we get the leader's viewport page bounds from their user presence.
|
||||
// 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.
|
||||
|
@ -8502,8 +8443,14 @@ export class App extends EventEmitter<TLEventMap> {
|
|||
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 (leaderPresences.value.some((p) => p.followingUserId === this.userId)) {
|
||||
if (leaderPresences.value.some((p) => p.followingUserId === thisUserId)) {
|
||||
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.
|
||||
// If so, we can't try to contain their entire viewport
|
||||
// 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
|
||||
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 { TestApp } from '../../../test/TestApp'
|
||||
|
||||
let i = 0
|
||||
jest.mock('nanoid', () => ({ nanoid: () => 'id' + i++ }))
|
||||
jest.mock('nanoid', () => {
|
||||
let i = 0
|
||||
return { nanoid: () => 'id' + i++ }
|
||||
})
|
||||
|
||||
let app: TestApp
|
||||
const id = createCustomShapeId('line1')
|
||||
|
|
|
@ -5,7 +5,7 @@ Object {
|
|||
"id": "shape:line1",
|
||||
"index": "a1",
|
||||
"isLocked": false,
|
||||
"parentId": "page:id51",
|
||||
"parentId": "page:id50",
|
||||
"props": Object {
|
||||
"color": "black",
|
||||
"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 app = useApp()
|
||||
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 teenyTiny = app.zoomLevel <= 0.18
|
||||
|
|
|
@ -30,7 +30,7 @@ export const DefaultErrorFallback: TLErrorFallback = ({ error, app }) => {
|
|||
() => {
|
||||
try {
|
||||
if (app) {
|
||||
return app.userDocumentSettings.isDarkMode
|
||||
return app.isDarkMode
|
||||
}
|
||||
} catch {
|
||||
// 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 { useApp } from '../hooks/useApp'
|
||||
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 { 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,
|
||||
TLStore,
|
||||
TLStoreProps,
|
||||
TLUser,
|
||||
TLUserId,
|
||||
createTLSchema,
|
||||
} from '@tldraw/tlschema'
|
||||
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 { TLBookmarkUtil } from '../app/shapeutils/TLBookmarkUtil/TLBookmarkUtil'
|
||||
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 { TLVideoUtil } from '../app/shapeutils/TLVideoUtil/TLVideoUtil'
|
||||
import { StateNodeConstructor } from '../app/statechart/StateNode'
|
||||
import { TLUserPreferences, getUserPreferences, setUserPreferences } from './TLUserPreferences'
|
||||
|
||||
// Secret shape types that don't have a shape util yet
|
||||
type ShapeTypesNotImplemented = 'icon'
|
||||
|
@ -63,6 +62,8 @@ export type TldrawEditorConfigOptions = {
|
|||
>
|
||||
/** @internal */
|
||||
derivePresenceState?: (store: TLStore) => Signal<TLInstancePresence | null>
|
||||
userPreferences?: Signal<TLUserPreferences>
|
||||
setUserPreferences?: (userPreferences: TLUserPreferences) => void
|
||||
}
|
||||
|
||||
/** @public */
|
||||
|
@ -78,11 +79,18 @@ export class TldrawEditorConfig {
|
|||
|
||||
// The schema used for the store incorporating any custom shapes
|
||||
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) {
|
||||
const { shapes = {}, tools = [], derivePresenceState } = opts
|
||||
|
||||
this.tools = tools
|
||||
this.derivePresenceState = derivePresenceState ?? (() => computed('presence', () => null))
|
||||
this.userPreferences =
|
||||
opts.userPreferences ?? computed('userPreferences', () => getUserPreferences())
|
||||
this.setUserPreferences = opts.setUserPreferences ?? setUserPreferences
|
||||
|
||||
this.shapeUtils = {
|
||||
...DEFAULT_SHAPE_UTILS,
|
||||
|
@ -91,7 +99,6 @@ export class TldrawEditorConfig {
|
|||
|
||||
this.storeSchema = createTLSchema({
|
||||
customShapes: shapes,
|
||||
derivePresenceState: derivePresenceState,
|
||||
})
|
||||
|
||||
this.TLShape = this.storeSchema.types.shape as RecordType<
|
||||
|
@ -103,18 +110,17 @@ export class TldrawEditorConfig {
|
|||
createStore(config: {
|
||||
/** The store's initial data. */
|
||||
initialData?: StoreSnapshot<TLRecord>
|
||||
userId: TLUserId
|
||||
instanceId: TLInstanceId
|
||||
}): TLStore {
|
||||
let initialData = config.initialData
|
||||
if (initialData) {
|
||||
initialData = CLIENT_FIXUP_SCRIPT(initialData)
|
||||
}
|
||||
return new Store({
|
||||
|
||||
return new Store<TLRecord, TLStoreProps>({
|
||||
schema: this.storeSchema,
|
||||
initialData,
|
||||
props: {
|
||||
userId: config?.userId ?? TLUser.createId(),
|
||||
instanceId: config?.instanceId ?? TLInstance.createId(),
|
||||
documentId: TLDOCUMENT_ID,
|
||||
},
|
||||
|
|
|
@ -6,7 +6,7 @@ import { useContainer } from './useContainer'
|
|||
export function useDarkMode() {
|
||||
const app = useApp()
|
||||
const container = useContainer()
|
||||
const isDarkMode = useValue('isDarkMode', () => app.userDocumentSettings.isDarkMode, [app])
|
||||
const isDarkMode = useValue('isDarkMode', () => app.isDarkMode, [app])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isDarkMode) {
|
||||
|
|
|
@ -11,7 +11,7 @@ import { useApp } from './useApp'
|
|||
export function usePeerIds() {
|
||||
const app = useApp()
|
||||
const $presences = useMemo(() => {
|
||||
return app.store.query.records('instance_presence')
|
||||
return app.store.query.records('instance_presence', () => ({ userId: { neq: app.user.id } }))
|
||||
}, [app])
|
||||
|
||||
const $userIds = useComputed(
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { TLInstancePresence, TLUserId } from '@tldraw/tlschema'
|
||||
import { TLInstancePresence } from '@tldraw/tlschema'
|
||||
import { useMemo } from 'react'
|
||||
import { useValue } from 'signia-react'
|
||||
import { useApp } from './useApp'
|
||||
|
@ -8,7 +8,7 @@ import { useApp } from './useApp'
|
|||
* @returns The list of peer UserIDs
|
||||
* @internal
|
||||
*/
|
||||
export function usePresence(userId: TLUserId): TLInstancePresence | null {
|
||||
export function usePresence(userId: string): TLInstancePresence | null {
|
||||
const app = useApp()
|
||||
|
||||
const $presences = useMemo(() => {
|
||||
|
|
|
@ -13,7 +13,6 @@ import {
|
|||
TLPage,
|
||||
TLShapeId,
|
||||
TLShapePartial,
|
||||
TLUser,
|
||||
createCustomShapeId,
|
||||
createShapeId,
|
||||
} from '@tldraw/tlschema'
|
||||
|
@ -52,7 +51,6 @@ declare global {
|
|||
}
|
||||
}
|
||||
export const TEST_INSTANCE_ID = TLInstance.createCustomId('testInstance1')
|
||||
export const TEST_USER_ID = TLUser.createCustomId('testUser1')
|
||||
|
||||
export class TestApp extends App {
|
||||
constructor(options = {} as Partial<Omit<AppOptions, 'store'>>) {
|
||||
|
@ -62,7 +60,6 @@ export class TestApp extends App {
|
|||
super({
|
||||
config,
|
||||
store: config.createStore({
|
||||
userId: TEST_USER_ID,
|
||||
instanceId: TEST_INSTANCE_ID,
|
||||
}),
|
||||
getContainer: () => elm,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { render, screen } from '@testing-library/react'
|
||||
import { TLInstance, TLUser } from '@tldraw/tlschema'
|
||||
import { TLInstance } from '@tldraw/tlschema'
|
||||
import { TldrawEditor } from '../TldrawEditor'
|
||||
import { TldrawEditorConfig } from '../config/TldrawEditorConfig'
|
||||
|
||||
|
@ -25,7 +25,6 @@ describe('<Tldraw />', () => {
|
|||
|
||||
const initialStore = config.createStore({
|
||||
instanceId: TLInstance.createCustomId('test'),
|
||||
userId: TLUser.createCustomId('test'),
|
||||
})
|
||||
|
||||
const onMount = jest.fn()
|
||||
|
@ -54,7 +53,6 @@ describe('<Tldraw />', () => {
|
|||
// re-render with a new store:
|
||||
const newStore = config.createStore({
|
||||
instanceId: TLInstance.createCustomId('test'),
|
||||
userId: TLUser.createCustomId('test'),
|
||||
})
|
||||
rendered.rerender(
|
||||
<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 { TestApp } from '../TestApp'
|
||||
|
||||
let i = 0
|
||||
jest.mock('nanoid', () => ({ nanoid: () => 'id' + i++ }))
|
||||
jest.mock('nanoid', () => {
|
||||
let i = 0
|
||||
return { nanoid: () => 'id' + i++ }
|
||||
})
|
||||
|
||||
const ids = {
|
||||
boxA: createCustomShapeId('boxA'),
|
||||
|
|
|
@ -12,7 +12,6 @@ import { TldrawEditorConfig } from '@tldraw/editor';
|
|||
import { TLInstanceId } from '@tldraw/editor';
|
||||
import { TLStore } from '@tldraw/editor';
|
||||
import { TLTranslationKey } from '@tldraw/ui';
|
||||
import { TLUserId } from '@tldraw/editor';
|
||||
import { ToastsContextType } from '@tldraw/ui';
|
||||
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>;
|
||||
|
||||
// @public (undocumented)
|
||||
export function parseTldrawJsonFile({ config, json, userId, instanceId, }: {
|
||||
export function parseTldrawJsonFile({ config, json, instanceId, }: {
|
||||
config: TldrawEditorConfig;
|
||||
json: string;
|
||||
userId: TLUserId;
|
||||
instanceId: TLInstanceId;
|
||||
}): Result<TLStore, TldrawFileParseError>;
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@ import {
|
|||
TLInstanceId,
|
||||
TLRecord,
|
||||
TLStore,
|
||||
TLUserId,
|
||||
} from '@tldraw/editor'
|
||||
import {
|
||||
ID,
|
||||
|
@ -84,12 +83,10 @@ export type TldrawFileParseError =
|
|||
export function parseTldrawJsonFile({
|
||||
config,
|
||||
json,
|
||||
userId,
|
||||
instanceId,
|
||||
}: {
|
||||
config: TldrawEditorConfig
|
||||
json: string
|
||||
userId: TLUserId
|
||||
instanceId: TLInstanceId
|
||||
}): Result<TLStore, TldrawFileParseError> {
|
||||
// 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
|
||||
// are invalid, we don't open the file
|
||||
try {
|
||||
return Result.ok(config.createStore({ initialData: migrationResult.value, userId, instanceId }))
|
||||
return Result.ok(config.createStore({ initialData: migrationResult.value, instanceId }))
|
||||
} catch (e) {
|
||||
// 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
|
||||
|
@ -211,7 +208,6 @@ export async function parseAndLoadDocument(
|
|||
config: new TldrawEditorConfig(),
|
||||
json: document,
|
||||
instanceId: app.instanceId,
|
||||
userId: app.userId,
|
||||
})
|
||||
if (!parseFileResult.ok) {
|
||||
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 { assert } from '@tldraw/utils'
|
||||
import { parseTldrawJsonFile as _parseTldrawJsonFile, TldrawFile } from '../lib/file'
|
||||
|
@ -7,7 +7,6 @@ const parseTldrawJsonFile = (config: TldrawEditorConfig, json: string) =>
|
|||
_parseTldrawJsonFile({
|
||||
config,
|
||||
json,
|
||||
userId: TLUser.createCustomId('user'),
|
||||
instanceId: TLInstance.createCustomId('instance'),
|
||||
})
|
||||
|
||||
|
@ -22,7 +21,7 @@ describe('parseTldrawJsonFile', () => {
|
|||
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(
|
||||
new TldrawEditorConfig(),
|
||||
JSON.stringify({ not: 'a tldraw file' })
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
import { Canvas, TldrawEditor, TldrawEditorConfig, TldrawEditorProps } from '@tldraw/editor'
|
||||
import {
|
||||
DEFAULT_DOCUMENT_NAME,
|
||||
TAB_ID,
|
||||
getUserData,
|
||||
useLocalSyncClient,
|
||||
} from '@tldraw/tlsync-client'
|
||||
import { DEFAULT_DOCUMENT_NAME, TAB_ID, useLocalSyncClient } from '@tldraw/tlsync-client'
|
||||
import { ContextMenu, TldrawUi, TldrawUiContextProviderProps } from '@tldraw/ui'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
/** @public */
|
||||
export function Tldraw(
|
||||
|
@ -28,31 +23,16 @@ export function Tldraw(
|
|||
...rest
|
||||
} = props
|
||||
|
||||
const [_config, _setConfig] = useState(() => config ?? new TldrawEditorConfig())
|
||||
|
||||
useEffect(() => {
|
||||
_setConfig(config ?? new TldrawEditorConfig())
|
||||
}, [config])
|
||||
|
||||
const userData = getUserData()
|
||||
|
||||
const userId = props.userId ?? userData.id
|
||||
const _config = useMemo(() => config ?? new TldrawEditorConfig(), [config])
|
||||
|
||||
const syncedStore = useLocalSyncClient({
|
||||
instanceId,
|
||||
userId,
|
||||
config: _config,
|
||||
universalPersistenceKey: persistenceKey,
|
||||
})
|
||||
|
||||
return (
|
||||
<TldrawEditor
|
||||
{...rest}
|
||||
instanceId={instanceId}
|
||||
userId={userId}
|
||||
store={syncedStore}
|
||||
config={_config}
|
||||
>
|
||||
<TldrawEditor {...rest} instanceId={instanceId} store={syncedStore} config={_config}>
|
||||
<TldrawUi {...rest}>
|
||||
<ContextMenu>
|
||||
<Canvas />
|
||||
|
|
|
@ -87,6 +87,13 @@ export function createCustomShapeId(id: string): TLShapeId;
|
|||
// @internal (undocumented)
|
||||
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)
|
||||
export function createShapeId(): TLShapeId;
|
||||
|
||||
|
@ -107,7 +114,6 @@ export function createShapeValidator<Type extends string, Props extends object>(
|
|||
// @public
|
||||
export function createTLSchema<T extends TLUnknownShape>(opts?: {
|
||||
customShapes?: { [K in T["type"]]: CustomShapeInfo<T>; } | undefined;
|
||||
derivePresenceState?: ((store: TLStore) => Signal<null | TLInstancePresence>) | undefined;
|
||||
}): StoreSchema<TLRecord, TLStoreProps>;
|
||||
|
||||
// @public (undocumented)
|
||||
|
@ -119,9 +125,6 @@ export const cursorValidator: T.Validator<TLCursor>;
|
|||
// @internal (undocumented)
|
||||
export const dashValidator: T.Validator<"dashed" | "dotted" | "draw" | "solid">;
|
||||
|
||||
// @internal (undocumented)
|
||||
export const defaultDerivePresenceState: (store: TLStore) => Signal<null | TLInstancePresence>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const documentTypeValidator: T.Validator<TLDocument>;
|
||||
|
||||
|
@ -351,6 +354,9 @@ export const geoShapeTypeValidator: T.Validator<TLGeoShape>;
|
|||
// @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">;
|
||||
|
||||
// @public (undocumented)
|
||||
export function getDefaultTranslationLocale(): TLTranslationLocale;
|
||||
|
||||
// @public (undocumented)
|
||||
export const groupShapeTypeMigrations: Migrations;
|
||||
|
||||
|
@ -432,6 +438,9 @@ export const pageTypeValidator: T.Validator<TLPage>;
|
|||
// @internal (undocumented)
|
||||
export const parentIdValidator: T.Validator<TLParentId>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const pointerTypeValidator: T.Validator<TLPointer>;
|
||||
|
||||
// @internal (undocumented)
|
||||
export const rootShapeTypeMigrations: Migrations;
|
||||
|
||||
|
@ -970,7 +979,7 @@ export interface TLInstance extends BaseRecord<'instance', TLInstanceId> {
|
|||
// (undocumented)
|
||||
exportBackground: boolean;
|
||||
// (undocumented)
|
||||
followingUserId: null | TLUserId;
|
||||
followingUserId: null | string;
|
||||
// (undocumented)
|
||||
isDebugMode: boolean;
|
||||
// (undocumented)
|
||||
|
@ -984,13 +993,11 @@ export interface TLInstance extends BaseRecord<'instance', TLInstanceId> {
|
|||
// (undocumented)
|
||||
scribble: null | TLScribble;
|
||||
// (undocumented)
|
||||
userId: TLUserId;
|
||||
// (undocumented)
|
||||
zoomBrush: Box2dModel | null;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export const TLInstance: RecordType<TLInstance, "currentPageId" | "userId">;
|
||||
export const TLInstance: RecordType<TLInstance, "currentPageId">;
|
||||
|
||||
// @public (undocumented)
|
||||
export type TLInstanceId = ID<TLInstance>;
|
||||
|
@ -1047,7 +1054,7 @@ export interface TLInstancePresence extends BaseRecord<'instance_presence', TLIn
|
|||
rotation: number;
|
||||
};
|
||||
// (undocumented)
|
||||
followingUserId: null | TLUserId;
|
||||
followingUserId: null | string;
|
||||
// (undocumented)
|
||||
instanceId: TLInstanceId;
|
||||
// (undocumented)
|
||||
|
@ -1059,13 +1066,13 @@ export interface TLInstancePresence extends BaseRecord<'instance_presence', TLIn
|
|||
// (undocumented)
|
||||
selectedIds: TLShapeId[];
|
||||
// (undocumented)
|
||||
userId: TLUserId;
|
||||
userId: string;
|
||||
// (undocumented)
|
||||
userName: string;
|
||||
}
|
||||
|
||||
// @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)
|
||||
export type TLInstancePropsForNextShape = Pick<TLShapeProps, TLStyleType>;
|
||||
|
@ -1133,8 +1140,27 @@ export type TLPageId = ID<TLPage>;
|
|||
// @public (undocumented)
|
||||
export type TLParentId = TLPageId | TLShapeId;
|
||||
|
||||
// @public
|
||||
export interface TLPointer extends BaseRecord<'pointer', TLPointerId> {
|
||||
// (undocumented)
|
||||
lastActivityTimestamp: number;
|
||||
// (undocumented)
|
||||
x: number;
|
||||
// (undocumented)
|
||||
y: number;
|
||||
}
|
||||
|
||||
// @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)
|
||||
export type TLScribble = {
|
||||
|
@ -1192,7 +1218,6 @@ export type TLStore = Store<TLRecord, TLStoreProps>;
|
|||
|
||||
// @public (undocumented)
|
||||
export type TLStoreProps = {
|
||||
userId: TLUserId;
|
||||
instanceId: TLInstanceId;
|
||||
documentId: typeof TLDOCUMENT_ID;
|
||||
};
|
||||
|
@ -1262,21 +1287,8 @@ export type TLUiColorType = SetValue<typeof TL_UI_COLOR_TYPES>;
|
|||
// @public
|
||||
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
|
||||
export interface TLUserDocument extends BaseRecord<'user_document', TLUserDocumentId> {
|
||||
// (undocumented)
|
||||
isDarkMode: boolean;
|
||||
// (undocumented)
|
||||
isGridMode: boolean;
|
||||
// (undocumented)
|
||||
|
@ -1289,41 +1301,14 @@ export interface TLUserDocument extends BaseRecord<'user_document', TLUserDocume
|
|||
lastUpdatedPageId: ID<TLPage> | null;
|
||||
// (undocumented)
|
||||
lastUsedTabId: ID<TLInstance> | null;
|
||||
// (undocumented)
|
||||
userId: TLUserId;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export const TLUserDocument: RecordType<TLUserDocument, "userId">;
|
||||
export const TLUserDocument: RecordType<TLUserDocument, never>;
|
||||
|
||||
// @public (undocumented)
|
||||
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)
|
||||
export type TLVerticalAlignType = SetValue<typeof TL_VERTICAL_ALIGN_TYPES>;
|
||||
|
||||
|
@ -1354,27 +1339,12 @@ export type TLVideoShapeProps = {
|
|||
// @public (undocumented)
|
||||
export const uiColorTypeValidator: T.Validator<"accent" | "black" | "laser" | "muted-1" | "selection-fill" | "selection-stroke" | "white">;
|
||||
|
||||
// @internal (undocumented)
|
||||
export const USER_COLORS: string[];
|
||||
|
||||
// @public (undocumented)
|
||||
export const userDocumentTypeMigrations: Migrations;
|
||||
|
||||
// @public (undocumented)
|
||||
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)
|
||||
export interface Vec2dModel {
|
||||
// (undocumented)
|
||||
|
|
|
@ -5,10 +5,9 @@ import { TLInstance } from './records/TLInstance'
|
|||
import { TLInstancePageState } from './records/TLInstancePageState'
|
||||
import { TLInstancePresence } from './records/TLInstancePresence'
|
||||
import { TLPage } from './records/TLPage'
|
||||
import { TLPointer } from './records/TLPointer'
|
||||
import { TLShape } from './records/TLShape'
|
||||
import { TLUser } from './records/TLUser'
|
||||
import { TLUserDocument } from './records/TLUserDocument'
|
||||
import { TLUserPresence } from './records/TLUserPresence'
|
||||
|
||||
/** @public */
|
||||
export type TLRecord =
|
||||
|
@ -19,7 +18,6 @@ export type TLRecord =
|
|||
| TLInstancePageState
|
||||
| TLPage
|
||||
| TLShape
|
||||
| TLUser
|
||||
| TLUserDocument
|
||||
| TLUserPresence
|
||||
| TLInstancePresence
|
||||
| TLPointer
|
||||
|
|
|
@ -6,9 +6,8 @@ import { TLDOCUMENT_ID, TLDocument } from './records/TLDocument'
|
|||
import { TLInstance, TLInstanceId } from './records/TLInstance'
|
||||
import { TLInstancePageState } from './records/TLInstancePageState'
|
||||
import { TLPage } from './records/TLPage'
|
||||
import { TLUser, TLUserId } from './records/TLUser'
|
||||
import { TLPOINTER_ID, TLPointer } from './records/TLPointer'
|
||||
import { TLUserDocument } from './records/TLUserDocument'
|
||||
import { TLUserPresence } from './records/TLUserPresence'
|
||||
|
||||
function sortByIndex<T extends { index: string }>(a: T, b: T) {
|
||||
if (a.index < b.index) {
|
||||
|
@ -19,22 +18,6 @@ function sortByIndex<T extends { index: string }>(a: T, b: T) {
|
|||
return 0
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export const USER_COLORS = [
|
||||
'#FF802B',
|
||||
'#EC5E41',
|
||||
'#F2555A',
|
||||
'#F04F88',
|
||||
'#E34BA9',
|
||||
'#BD54C6',
|
||||
'#9D5BD2',
|
||||
'#7B66DC',
|
||||
'#02B1CC',
|
||||
'#11B3A3',
|
||||
'#39B178',
|
||||
'#55B467',
|
||||
]
|
||||
|
||||
function redactRecordForErrorReporting(record: any) {
|
||||
if (record.typeName === 'asset') {
|
||||
if ('src' in record) {
|
||||
|
@ -55,7 +38,6 @@ export type TLStoreSnapshot = StoreSnapshot<TLRecord>
|
|||
|
||||
/** @public */
|
||||
export type TLStoreProps = {
|
||||
userId: TLUserId
|
||||
instanceId: TLInstanceId
|
||||
documentId: typeof TLDOCUMENT_ID
|
||||
}
|
||||
|
@ -90,10 +72,6 @@ export const onValidationFailure: StoreSchemaOptions<
|
|||
throw error
|
||||
}
|
||||
|
||||
function getRandomColor() {
|
||||
return USER_COLORS[Math.floor(Math.random() * USER_COLORS.length)]
|
||||
}
|
||||
|
||||
function getDefaultPages() {
|
||||
return [TLPage.create({ name: 'Page 1', index: 'a1' })]
|
||||
}
|
||||
|
@ -101,32 +79,32 @@ function getDefaultPages() {
|
|||
/** @internal */
|
||||
export function createIntegrityChecker(store: TLStore): () => void {
|
||||
const $pages = store.query.records('page')
|
||||
const $userDocumentSettings = store.query.record('user_document', () => ({
|
||||
userId: { eq: store.props.userId },
|
||||
}))
|
||||
const $userDocumentSettings = store.query.record('user_document')
|
||||
|
||||
const $instanceState = store.query.record('instance', () => ({
|
||||
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 ensureStoreIsUsable = (): void => {
|
||||
const { userId, instanceId: tabId } = store.props
|
||||
const { instanceId: tabId } = store.props
|
||||
// make sure we have exactly one document
|
||||
if (!store.has(TLDOCUMENT_ID)) {
|
||||
store.put([TLDocument.create({ id: TLDOCUMENT_ID })])
|
||||
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
|
||||
const userDocumentSettings = $userDocumentSettings.value
|
||||
|
||||
if (!userDocumentSettings) {
|
||||
store.put([TLUserDocument.create({ userId })])
|
||||
store.put([TLUserDocument.create({})])
|
||||
return ensureStoreIsUsable()
|
||||
}
|
||||
|
||||
|
@ -151,7 +129,6 @@ export function createIntegrityChecker(store: TLStore): () => void {
|
|||
store.put([
|
||||
TLInstance.create({
|
||||
id: tabId,
|
||||
userId,
|
||||
currentPageId,
|
||||
propsForNextShape,
|
||||
exportBackground: true,
|
||||
|
@ -169,22 +146,6 @@ export function createIntegrityChecker(store: TLStore): () => void {
|
|||
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) {
|
||||
const instancePageStates = $instancePageStates.value.filter(
|
||||
(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 { T } from '@tldraw/tlvalidate'
|
||||
import { Signal } from 'signia'
|
||||
import { TLRecord } from './TLRecord'
|
||||
import { TLStore, TLStoreProps, createIntegrityChecker, onValidationFailure } from './TLStore'
|
||||
import { defaultDerivePresenceState } from './defaultDerivePresenceState'
|
||||
import { TLStoreProps, createIntegrityChecker, onValidationFailure } from './TLStore'
|
||||
import { TLAsset } from './records/TLAsset'
|
||||
import { TLCamera } from './records/TLCamera'
|
||||
import { TLDocument } from './records/TLDocument'
|
||||
|
@ -11,10 +9,9 @@ import { TLInstance } from './records/TLInstance'
|
|||
import { TLInstancePageState } from './records/TLInstancePageState'
|
||||
import { TLInstancePresence } from './records/TLInstancePresence'
|
||||
import { TLPage } from './records/TLPage'
|
||||
import { TLPointer } from './records/TLPointer'
|
||||
import { TLShape, TLUnknownShape, rootShapeTypeMigrations } from './records/TLShape'
|
||||
import { TLUser } from './records/TLUser'
|
||||
import { TLUserDocument } from './records/TLUserDocument'
|
||||
import { TLUserPresence } from './records/TLUserPresence'
|
||||
import { storeMigrations } from './schema'
|
||||
import { arrowShapeTypeMigrations, arrowShapeTypeValidator } from './shapes/TLArrowShape'
|
||||
import { bookmarkShapeTypeMigrations, bookmarkShapeTypeValidator } from './shapes/TLBookmarkShape'
|
||||
|
@ -61,10 +58,9 @@ type CustomShapeInfo<T extends TLUnknownShape> = {
|
|||
export function createTLSchema<T extends TLUnknownShape>(
|
||||
opts = {} as {
|
||||
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 [
|
||||
TLShape['type'],
|
||||
|
@ -76,7 +72,7 @@ export function createTLSchema<T extends TLUnknownShape>(
|
|||
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
|
||||
// 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.
|
||||
|
@ -119,16 +115,14 @@ export function createTLSchema<T extends TLUnknownShape>(
|
|||
instance_page_state: TLInstancePageState,
|
||||
page: TLPage,
|
||||
shape: shapeRecord,
|
||||
user: TLUser,
|
||||
user_document: TLUserDocument,
|
||||
user_presence: TLUserPresence,
|
||||
instance_presence: TLInstancePresence,
|
||||
pointer: TLPointer,
|
||||
},
|
||||
{
|
||||
snapshotMigrations: storeMigrations,
|
||||
onValidationFailure,
|
||||
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 {
|
||||
USER_COLORS,
|
||||
createIntegrityChecker,
|
||||
onValidationFailure,
|
||||
type TLStore,
|
||||
|
@ -24,8 +23,8 @@ export {
|
|||
type TLVideoAsset,
|
||||
} from './assets/TLVideoAsset'
|
||||
export { createAssetValidator, type TLBaseAsset } from './assets/asset-validation'
|
||||
export { createPresenceStateDerivation } from './createPresenceStateDerivation'
|
||||
export { createTLSchema } from './createTLSchema'
|
||||
export { defaultDerivePresenceState } from './defaultDerivePresenceState'
|
||||
export { CLIENT_FIXUP_SCRIPT, fixupRecord } from './fixup'
|
||||
export { type Box2dModel, type Vec2dModel } from './geometry-types'
|
||||
export {
|
||||
|
@ -53,6 +52,12 @@ export {
|
|||
} from './records/TLInstancePageState'
|
||||
export { TLInstancePresence } from './records/TLInstancePresence'
|
||||
export { TLPage, pageTypeValidator, type TLPageId } from './records/TLPage'
|
||||
export {
|
||||
TLPOINTER_ID,
|
||||
TLPointer,
|
||||
pointerTypeValidator,
|
||||
type TLPointerId,
|
||||
} from './records/TLPointer'
|
||||
export {
|
||||
createCustomShapeId,
|
||||
createShapeId,
|
||||
|
@ -69,19 +74,12 @@ export {
|
|||
type TLShapeProps,
|
||||
type TLUnknownShape,
|
||||
} from './records/TLShape'
|
||||
export { TLUser, userTypeValidator, type TLUserId } from './records/TLUser'
|
||||
export {
|
||||
TLUserDocument,
|
||||
userDocumentTypeMigrations,
|
||||
userDocumentTypeValidator,
|
||||
type TLUserDocumentId,
|
||||
} from './records/TLUserDocument'
|
||||
export {
|
||||
TLUserPresence,
|
||||
userPresenceTypeMigrations,
|
||||
userPresenceTypeValidator,
|
||||
type TLUserPresenceId,
|
||||
} from './records/TLUserPresence'
|
||||
export { storeMigrations } from './schema'
|
||||
export {
|
||||
TL_ARROW_TERMINAL_TYPE,
|
||||
|
@ -218,6 +216,7 @@ export {
|
|||
type TLStyleType,
|
||||
type TLVerticalAlignType,
|
||||
} from './style-types'
|
||||
export { getDefaultTranslationLocale } from './translations'
|
||||
export {
|
||||
TL_CURSOR_TYPES,
|
||||
TL_HANDLE_TYPES,
|
||||
|
@ -255,5 +254,4 @@ export {
|
|||
shapeIdValidator,
|
||||
sizeValidator,
|
||||
splineValidator,
|
||||
userIdValidator,
|
||||
} from './validation'
|
||||
|
|
|
@ -3,13 +3,12 @@ import { structuredClone } from '@tldraw/utils'
|
|||
import fs from 'fs'
|
||||
import { imageAssetMigrations } from './assets/TLImageAsset'
|
||||
import { videoAssetMigrations } from './assets/TLVideoAsset'
|
||||
import { instanceTypeMigrations } from './records/TLInstance'
|
||||
import { instanceTypeMigrations, instanceTypeVersions } from './records/TLInstance'
|
||||
import { instancePageStateMigrations } from './records/TLInstancePageState'
|
||||
import { instancePresenceTypeMigrations } from './records/TLInstancePresence'
|
||||
import { rootShapeTypeMigrations, TLShape } from './records/TLShape'
|
||||
import { userDocumentTypeMigrations, userDocumentVersions } from './records/TLUserDocument'
|
||||
import { userPresenceTypeMigrations } from './records/TLUserPresence'
|
||||
import { storeMigrations } from './schema'
|
||||
import { storeMigrations, storeVersions } from './schema'
|
||||
import { arrowShapeTypeMigrations } from './shapes/TLArrowShape'
|
||||
import { bookmarkShapeTypeMigrations } from './shapes/TLBookmarkShape'
|
||||
import { drawShapeTypeMigrations } from './shapes/TLDrawShape'
|
||||
|
@ -214,7 +213,7 @@ describe('Store removing Icon and Code shapes', () => {
|
|||
} as any),
|
||||
].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)
|
||||
})
|
||||
|
||||
|
@ -230,7 +229,7 @@ describe('Store removing Icon and Code shapes', () => {
|
|||
].map((shape) => [shape.id, shape])
|
||||
)
|
||||
|
||||
storeMigrations.migrators[1].down(snapshot)
|
||||
storeMigrations.migrators[storeVersions.RemoveCodeAndIconShapeTypes].down(snapshot)
|
||||
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', () => {
|
||||
const { up, down } = instanceTypeMigrations.migrators[7]
|
||||
test('up works as expected', () => {
|
||||
|
@ -639,7 +627,7 @@ describe('Add crop=null to image shapes', () => {
|
|||
})
|
||||
|
||||
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', () => {
|
||||
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 --- */
|
||||
|
||||
for (const migrator of allMigrators) {
|
||||
|
|
|
@ -17,12 +17,10 @@ import {
|
|||
pageIdValidator,
|
||||
sizeValidator,
|
||||
splineValidator,
|
||||
userIdValidator,
|
||||
verticalAlignValidator,
|
||||
} from '../validation'
|
||||
import { TLPageId } from './TLPage'
|
||||
import { TLShapeProps } from './TLShape'
|
||||
import { TLUserId } from './TLUser'
|
||||
|
||||
/** @public */
|
||||
export type TLInstancePropsForNextShape = Pick<TLShapeProps, TLStyleType>
|
||||
|
@ -35,9 +33,8 @@ export type TLInstancePropsForNextShape = Pick<TLShapeProps, TLStyleType>
|
|||
* @public
|
||||
*/
|
||||
export interface TLInstance extends BaseRecord<'instance', TLInstanceId> {
|
||||
userId: TLUserId
|
||||
currentPageId: TLPageId
|
||||
followingUserId: TLUserId | null
|
||||
followingUserId: string | null
|
||||
brush: Box2dModel | null
|
||||
propsForNextShape: TLInstancePropsForNextShape
|
||||
cursor: TLCursor
|
||||
|
@ -59,9 +56,8 @@ export const instanceTypeValidator: T.Validator<TLInstance> = T.model(
|
|||
T.object({
|
||||
typeName: T.literal('instance'),
|
||||
id: idValidator<TLInstanceId>('instance'),
|
||||
userId: userIdValidator,
|
||||
currentPageId: pageIdValidator,
|
||||
followingUserId: userIdValidator.nullable(),
|
||||
followingUserId: T.string.nullable(),
|
||||
brush: T.boxModel.nullable(),
|
||||
propsForNextShape: T.object({
|
||||
color: colorValidator,
|
||||
|
@ -101,11 +97,14 @@ const Versions = {
|
|||
AddZoom: 8,
|
||||
AddVerticalAlign: 9,
|
||||
AddScribbleDelay: 10,
|
||||
RemoveUserId: 11,
|
||||
} as const
|
||||
|
||||
export { Versions as instanceTypeVersions }
|
||||
|
||||
/** @public */
|
||||
export const instanceTypeMigrations = defineMigrations({
|
||||
currentVersion: Versions.AddScribbleDelay,
|
||||
currentVersion: Versions.RemoveUserId,
|
||||
migrators: {
|
||||
[Versions.AddTransparentExportBgs]: {
|
||||
up: (instance: TLInstance) => {
|
||||
|
@ -235,6 +234,14 @@ export const instanceTypeMigrations = defineMigrations({
|
|||
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,
|
||||
scope: 'instance',
|
||||
}).withDefaultProperties(
|
||||
(): Omit<TLInstance, 'typeName' | 'id' | 'userId' | 'currentPageId'> => ({
|
||||
(): Omit<TLInstance, 'typeName' | 'id' | 'currentPageId'> => ({
|
||||
followingUserId: null,
|
||||
propsForNextShape: {
|
||||
opacity: '1',
|
||||
|
|
|
@ -2,16 +2,15 @@ import { BaseRecord, createRecordType, defineMigrations, ID } from '@tldraw/tlst
|
|||
import { T } from '@tldraw/tlvalidate'
|
||||
import { Box2dModel } from '../geometry-types'
|
||||
import { cursorTypeValidator, scribbleTypeValidator, TLCursor, TLScribble } from '../ui-types'
|
||||
import { idValidator, userIdValidator } from '../validation'
|
||||
import { idValidator } from '../validation'
|
||||
import { TLInstanceId } from './TLInstance'
|
||||
import { TLPageId } from './TLPage'
|
||||
import { TLShapeId } from './TLShape'
|
||||
import { TLUserId } from './TLUser'
|
||||
|
||||
/** @public */
|
||||
export interface TLInstancePresence extends BaseRecord<'instance_presence', TLInstancePresenceID> {
|
||||
instanceId: TLInstanceId
|
||||
userId: TLUserId
|
||||
userId: string
|
||||
userName: string
|
||||
lastActivityTimestamp: number
|
||||
color: string // can be any hex color
|
||||
|
@ -21,7 +20,7 @@ export interface TLInstancePresence extends BaseRecord<'instance_presence', TLIn
|
|||
brush: Box2dModel | null
|
||||
scribble: TLScribble | null
|
||||
screenBounds: Box2dModel
|
||||
followingUserId: TLUserId | null
|
||||
followingUserId: string | null
|
||||
cursor: {
|
||||
x: number
|
||||
y: number
|
||||
|
@ -41,10 +40,10 @@ export const instancePresenceTypeValidator: T.Validator<TLInstancePresence> = T.
|
|||
instanceId: idValidator<TLInstanceId>('instance'),
|
||||
typeName: T.literal('instance_presence'),
|
||||
id: idValidator<TLInstancePresenceID>('instance_presence'),
|
||||
userId: userIdValidator,
|
||||
userId: T.string,
|
||||
userName: T.string,
|
||||
lastActivityTimestamp: T.number,
|
||||
followingUserId: userIdValidator.nullable(),
|
||||
followingUserId: T.string.nullable(),
|
||||
cursor: T.object({
|
||||
x: T.number,
|
||||
y: T.number,
|
||||
|
@ -95,4 +94,28 @@ export const TLInstancePresence = createRecordType<TLInstancePresence>('instance
|
|||
migrations: instancePresenceTypeMigrations,
|
||||
validator: instancePresenceTypeValidator,
|
||||
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 { T } from '@tldraw/tlvalidate'
|
||||
import { idValidator, instanceIdValidator, pageIdValidator, userIdValidator } from '../validation'
|
||||
import { idValidator, instanceIdValidator, pageIdValidator } from '../validation'
|
||||
import { TLInstance } from './TLInstance'
|
||||
import { TLPage } from './TLPage'
|
||||
import { TLUserId } from './TLUser'
|
||||
|
||||
/**
|
||||
* TLUserDocument
|
||||
|
@ -13,10 +12,8 @@ import { TLUserId } from './TLUser'
|
|||
* @public
|
||||
*/
|
||||
export interface TLUserDocument extends BaseRecord<'user_document', TLUserDocumentId> {
|
||||
userId: TLUserId
|
||||
isPenMode: boolean
|
||||
isGridMode: boolean
|
||||
isDarkMode: boolean
|
||||
isMobileMode: boolean
|
||||
isSnapMode: boolean
|
||||
lastUpdatedPageId: ID<TLPage> | null
|
||||
|
@ -32,10 +29,8 @@ export const userDocumentTypeValidator: T.Validator<TLUserDocument> = T.model(
|
|||
T.object({
|
||||
typeName: T.literal('user_document'),
|
||||
id: idValidator<TLUserDocumentId>('user_document'),
|
||||
userId: userIdValidator,
|
||||
isPenMode: T.boolean,
|
||||
isGridMode: T.boolean,
|
||||
isDarkMode: T.boolean,
|
||||
isMobileMode: T.boolean,
|
||||
isSnapMode: T.boolean,
|
||||
lastUpdatedPageId: pageIdValidator.nullable(),
|
||||
|
@ -47,11 +42,14 @@ export const Versions = {
|
|||
AddSnapMode: 1,
|
||||
AddMissingIsMobileMode: 2,
|
||||
RemoveIsReadOnly: 3,
|
||||
RemoveUserIdAndIsDarkMode: 4,
|
||||
} as const
|
||||
|
||||
export { Versions as userDocumentVersions }
|
||||
|
||||
/** @public */
|
||||
export const userDocumentTypeMigrations = defineMigrations({
|
||||
currentVersion: Versions.RemoveIsReadOnly,
|
||||
currentVersion: Versions.RemoveUserIdAndIsDarkMode,
|
||||
migrators: {
|
||||
[Versions.AddSnapMode]: {
|
||||
up: (userDocument: TLUserDocument) => {
|
||||
|
@ -77,6 +75,18 @@ export const userDocumentTypeMigrations = defineMigrations({
|
|||
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 */
|
||||
|
@ -92,12 +102,9 @@ export const TLUserDocument = createRecordType<TLUserDocument>('user_document',
|
|||
/* STEP 6: Add any new default values for properties here */
|
||||
isPenMode: false,
|
||||
isGridMode: false,
|
||||
isDarkMode: false,
|
||||
isMobileMode: false,
|
||||
isSnapMode: false,
|
||||
lastUpdatedPageId: 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 = {
|
||||
RemoveCodeAndIconShapeTypes: 1,
|
||||
AddInstancePresenceType: 2,
|
||||
RemoveTLUserAndPresenceAndAddPointer: 3,
|
||||
} as const
|
||||
|
||||
export { Versions as storeVersions }
|
||||
|
||||
/** @public */
|
||||
export const storeMigrations = defineMigrations({
|
||||
currentVersion: Versions.AddInstancePresenceType,
|
||||
currentVersion: Versions.RemoveTLUserAndPresenceAndAddPointer,
|
||||
migrators: {
|
||||
[Versions.RemoveCodeAndIconShapeTypes]: {
|
||||
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 = {
|
||||
name: string
|
||||
|
@ -37,7 +37,7 @@ describe('Choosing a sensible default translation locale', () => {
|
|||
|
||||
for (const test of tests) {
|
||||
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']
|
||||
|
||||
/** @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) {
|
||||
const supportedLocale = getSupportedLocale(locale)
|
||||
if (supportedLocale) {
|
||||
|
|
|
@ -4,7 +4,6 @@ import type { TLAssetId } from './records/TLAsset'
|
|||
import type { TLInstanceId } from './records/TLInstance'
|
||||
import type { TLPageId } from './records/TLPage'
|
||||
import type { TLParentId, TLShapeId } from './records/TLShape'
|
||||
import type { TLUserId } from './records/TLUser'
|
||||
import {
|
||||
TLAlignType,
|
||||
TL_ALIGN_TYPES_WITH_LEGACY_STUFF,
|
||||
|
@ -33,8 +32,6 @@ export function idValidator<Id extends ID<UnknownRecord>>(
|
|||
})
|
||||
}
|
||||
/** @internal */
|
||||
export const userIdValidator = idValidator<TLUserId>('user')
|
||||
/** @internal */
|
||||
export const assetIdValidator = idValidator<TLAssetId>('asset')
|
||||
/** @internal */
|
||||
export const pageIdValidator = idValidator<TLPageId>('page')
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
import { Atom } from 'signia';
|
||||
import { Computed } from 'signia';
|
||||
import { Signal } from 'signia';
|
||||
|
||||
// @public
|
||||
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;
|
||||
// (undocumented)
|
||||
get currentStoreVersion(): number;
|
||||
// @internal (undocumented)
|
||||
derivePresenceState(store: Store<R, P>): Signal<null | R> | undefined;
|
||||
// (undocumented)
|
||||
migratePersistedRecord(record: R, persistedSchema: SerializedSchema, direction?: 'down' | 'up'): MigrationResult<R>;
|
||||
// (undocumented)
|
||||
|
@ -317,7 +314,6 @@ export type StoreSchemaOptions<R extends UnknownRecord, P> = {
|
|||
recordBefore: null | R;
|
||||
}) => R;
|
||||
createIntegrityChecker?: (store: Store<R, P>) => void;
|
||||
derivePresenceState?: (store: Store<R, P>) => Signal<null | R>;
|
||||
};
|
||||
|
||||
// @public
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { getOwnProperty, objectMapValues } from '@tldraw/utils'
|
||||
import { Signal } from 'signia'
|
||||
import { IdOf, UnknownRecord } from './BaseRecord'
|
||||
import { RecordType } from './RecordType'
|
||||
import { Store, StoreSnapshot } from './Store'
|
||||
|
@ -49,8 +48,6 @@ export type StoreSchemaOptions<R extends UnknownRecord, P> = {
|
|||
}) => R
|
||||
/** @internal */
|
||||
createIntegrityChecker?: (store: Store<R, P>) => void
|
||||
/** @internal */
|
||||
derivePresenceState?: (store: Store<R, P>) => Signal<R | null>
|
||||
}
|
||||
|
||||
/** @public */
|
||||
|
@ -244,11 +241,6 @@ export class StoreSchema<R extends UnknownRecord, P = unknown> {
|
|||
return this.options.createIntegrityChecker?.(store) ?? undefined
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
derivePresenceState(store: Store<R, P>): Signal<R | null> | undefined {
|
||||
return this.options.derivePresenceState?.(store)
|
||||
}
|
||||
|
||||
serialize(): SerializedSchema {
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
import { RecordsDiff } from '@tldraw/tlstore';
|
||||
import { SerializedSchema } from '@tldraw/tlstore';
|
||||
import { Store } from '@tldraw/tlstore';
|
||||
import { StoreSnapshot } from '@tldraw/tlstore';
|
||||
import { SyncedStore } from '@tldraw/editor';
|
||||
import { TldrawEditorConfig } from '@tldraw/editor';
|
||||
|
@ -14,8 +13,6 @@ import { TLInstanceId } from '@tldraw/editor';
|
|||
import { TLRecord } from '@tldraw/editor';
|
||||
import { TLStore } from '@tldraw/editor';
|
||||
import { TLStoreSchema } from '@tldraw/editor';
|
||||
import { TLUser } from '@tldraw/editor';
|
||||
import { TLUserId } from '@tldraw/editor';
|
||||
|
||||
// @public (undocumented)
|
||||
export function addDbName(name: string): void;
|
||||
|
@ -40,9 +37,6 @@ export const DEFAULT_DOCUMENT_NAME: any;
|
|||
// @public (undocumented)
|
||||
export function getAllIndexDbNames(): string[];
|
||||
|
||||
// @public (undocumented)
|
||||
export function getUserData(): TLUser;
|
||||
|
||||
// @public (undocumented)
|
||||
export function hardReset({ shouldReload }?: {
|
||||
shouldReload?: boolean | undefined;
|
||||
|
@ -69,9 +63,6 @@ export function storeSnapshotInIndexedDb(universalPersistenceKey: string, schema
|
|||
didCancel?: () => boolean;
|
||||
}): Promise<void>;
|
||||
|
||||
// @public (undocumented)
|
||||
export function subscribeToUserData(store: Store<any>): () => void;
|
||||
|
||||
// @public (undocumented)
|
||||
export const TAB_ID: TLInstanceId;
|
||||
|
||||
|
@ -97,10 +88,9 @@ export class TLLocalSyncClient {
|
|||
}
|
||||
|
||||
// @public
|
||||
export function useLocalSyncClient({ universalPersistenceKey, instanceId, userId, config, }: {
|
||||
export function useLocalSyncClient({ universalPersistenceKey, instanceId, config, }: {
|
||||
universalPersistenceKey: string;
|
||||
instanceId: TLInstanceId;
|
||||
userId: TLUserId;
|
||||
config: TldrawEditorConfig;
|
||||
}): SyncedStore;
|
||||
|
||||
|
|
|
@ -13,6 +13,4 @@ export {
|
|||
TAB_ID,
|
||||
addDbName,
|
||||
getAllIndexDbNames,
|
||||
getUserData,
|
||||
subscribeToUserData,
|
||||
} from './lib/persistence-constants'
|
||||
|
|
|
@ -1,11 +1,4 @@
|
|||
import {
|
||||
TldrawEditorConfig,
|
||||
TLInstance,
|
||||
TLInstanceId,
|
||||
TLPage,
|
||||
TLUser,
|
||||
TLUserId,
|
||||
} from '@tldraw/editor'
|
||||
import { TldrawEditorConfig, TLInstance, TLInstanceId, TLPage } from '@tldraw/editor'
|
||||
import { promiseWithResolve } from '@tldraw/utils'
|
||||
import * as idb from './indexedDb'
|
||||
import { TLLocalSyncClient } from './TLLocalSyncClient'
|
||||
|
@ -31,11 +24,9 @@ class BroadcastChannelMock {
|
|||
|
||||
function testClient(
|
||||
instanceId: TLInstanceId = TLInstance.createCustomId('test'),
|
||||
userId: TLUserId = TLUser.createCustomId('test'),
|
||||
channel = new BroadcastChannelMock('test')
|
||||
) {
|
||||
const store = new TldrawEditorConfig().createStore({
|
||||
userId,
|
||||
instanceId,
|
||||
})
|
||||
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: {} } })
|
||||
})
|
||||
|
||||
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()
|
||||
await tick()
|
||||
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()
|
||||
})
|
||||
|
||||
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()
|
||||
await tick()
|
||||
jest.advanceTimersByTime(1000)
|
||||
|
|
|
@ -155,7 +155,12 @@ export class TLLocalSyncClient {
|
|||
// 3. Merge the changes into the REAL STORE
|
||||
this.store.mergeRemoteChanges(() => {
|
||||
// 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 '../hardReset'
|
||||
import { subscribeToUserData } from '../persistence-constants'
|
||||
import { TLLocalSyncClient } from '../TLLocalSyncClient'
|
||||
|
||||
/**
|
||||
|
@ -13,12 +12,10 @@ import { TLLocalSyncClient } from '../TLLocalSyncClient'
|
|||
export function useLocalSyncClient({
|
||||
universalPersistenceKey,
|
||||
instanceId,
|
||||
userId,
|
||||
config,
|
||||
}: {
|
||||
universalPersistenceKey: string
|
||||
instanceId: TLInstanceId
|
||||
userId: TLUserId
|
||||
config: TldrawEditorConfig
|
||||
}): SyncedStore {
|
||||
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, {
|
||||
universalPersistenceKey,
|
||||
|
@ -50,14 +47,11 @@ export function useLocalSyncClient({
|
|||
},
|
||||
})
|
||||
|
||||
const userDataUnsubcribe = subscribeToUserData(store)
|
||||
|
||||
return () => {
|
||||
setState((prevState) => (prevState?.id === id ? null : prevState))
|
||||
userDataUnsubcribe()
|
||||
client.close()
|
||||
}
|
||||
}, [instanceId, universalPersistenceKey, config, userId])
|
||||
}, [instanceId, universalPersistenceKey, config])
|
||||
|
||||
return state?.syncedStore ?? { status: 'loading' }
|
||||
}
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import { TLInstance, TLInstanceId, TLUser, uniqueId } from '@tldraw/editor'
|
||||
import { Store } from '@tldraw/tlstore'
|
||||
import { atom, react } from 'signia'
|
||||
import { TLInstance, TLInstanceId, uniqueId } from '@tldraw/editor'
|
||||
|
||||
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
|
||||
// again, stored in localStorage
|
||||
const defaultDocumentKey = 'TLDRAW_DEFAULT_DOCUMENT_NAME_v2'
|
||||
|
|
|
@ -9,9 +9,7 @@ export function LanguageMenu() {
|
|||
const { languages, currentLanguage } = useLanguages()
|
||||
|
||||
const handleLanguageSelect = useCallback(
|
||||
(locale: TLTranslationLocale) => {
|
||||
app.updateUser({ locale })
|
||||
},
|
||||
(locale: TLTranslationLocale) => app.setLocale(locale),
|
||||
[app]
|
||||
)
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ export const Minimap = track(function Minimap({
|
|||
|
||||
const minimap = React.useMemo(() => new MinimapManager(app, app.devicePixelRatio), [app])
|
||||
|
||||
const isDarkMode = app.userDocumentSettings.isDarkMode
|
||||
const isDarkMode = app.isDarkMode
|
||||
|
||||
React.useEffect(() => {
|
||||
// Must check after render
|
||||
|
|
|
@ -49,7 +49,7 @@ export function MenuSchemaProvider({ overrides, children }: MenuSchemaProviderPr
|
|||
const breakpoint = useBreakpoint()
|
||||
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 isSnapMode = useValue('isSnapMode', () => app.userDocumentSettings.isSnapMode, [app])
|
||||
const isToolLock = useValue('isToolLock', () => app.instanceState.isToolLocked, [app])
|
||||
|
|
|
@ -4,5 +4,5 @@ import { LANGUAGES } from './languages'
|
|||
/** @public */
|
||||
export function useLanguages() {
|
||||
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,
|
||||
}: TranslationProviderProps) {
|
||||
const app = useApp()
|
||||
const locale = app.userSettings.locale
|
||||
const locale = app.locale
|
||||
const getAssetUrl = useAssetUrls()
|
||||
|
||||
const [currentTranslation, setCurrentTranslation] = React.useState<TLTranslation>(() => {
|
||||
|
|
Loading…
Reference in a new issue