Tldraw/packages/editor/src/lib/config/TLUserPreferences.ts

189 wiersze
4.2 KiB
TypeScript
Czysty Zwykły widok Historia

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
animationSpeed: number
}
interface UserDataSnapshot {
version: number
user: TLUserPreferences
}
interface UserChangeBroadcastMessage {
type: typeof broadcastEventKey
origin: string
data: UserDataSnapshot
}
const userTypeValidator: T.Validator<TLUserPreferences> = T.object<TLUserPreferences>({
id: T.string,
name: T.string,
locale: T.string,
color: T.string,
isDarkMode: T.boolean,
animationSpeed: T.number,
})
const Versions = {
AddAnimationSpeed: 1,
} as const
const userTypeMigrations = defineMigrations({
currentVersion: 1,
migrators: {
[Versions.AddAnimationSpeed]: {
up: (user) => {
return {
...user,
animationSpeed: 1,
}
},
down: ({ animationSpeed: _, ...user }) => {
return user
},
},
},
})
/** @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,
animationSpeed: 1,
}
}
function migrateUserPreferences(userData: unknown) {
if (userData === null || typeof userData !== 'object') {
return getFreshUserPreferences()
}
if (!('version' in userData) || !('user' in userData) || typeof userData.version !== 'number') {
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
}
function loadUserPreferences(): TLUserPreferences {
const userData =
typeof window === 'undefined'
? null
: ((JSON.parse(window?.localStorage?.getItem(USER_DATA_KEY) || 'null') ??
null) as null | UserDataSnapshot)
return migrateUserPreferences(userData)
}
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,
})
)
}
}
[refactor] User-facing APIs (#1478) This PR updates our user-facing APIs for the Tldraw and TldrawEditor components, as well as the Editor (App). It mainly incorporates surface changes from #1450 without any changes to validators or migrators, incorporating feedback / discussion with @SomeHats and @ds300. Here we: - remove the TldrawEditorConfig - bring back a loose version of shape definitions - make a separation between "core" shapes and "default" shapes - do not allow custom shapes, migrators or validators to overwrite core shapes - but _do_ allow new shapes ## `<Tldraw>` component In this PR, the `Tldraw` component wraps both the `TldrawEditor` component and our `TldrawUi` component. It accepts a union of props for both components. Previously, this component also added local syncing via a `useLocalSyncClient` hook call, however that has been pushed down to the `TldrawEditor` component. ## `<TldrawEditor>` component The `TldrawEditor` component now more neatly wraps up the different ways that the editor can be configured. ## The store prop (`TldrawEditorProps.store`) There are three main ways for the `TldrawEditor` component to be run: 1. with an externally defined store 2. with an externally defined syncing store (local or remote) 3. with an internally defined store 4. with an internally defined locally syncing store The `store` prop allows for these configurations. If the `store` prop is defined, it may be defined either as a `TLStore` or as a `SyncedStore`. If the store is a `TLStore`, then the Editor will assume that the store is ready to go; if it is defined as a SyncedStore, then the component will display the loading / error screens as needed, or the final editor once the store's status is "synced". When the store is left undefined, then the `TldrawEditor` will create its own internal store using the optional `instanceId`, `initialData`, or `shapes` props to define the store / store schema. If the `persistenceKey` prop is left undefined, then the store will not be synced. If the `persistenceKey` is defined, then the store will be synced locally. In the future, we may also here accept the API key / roomId / etc for creating a remotely synced store. The `SyncedStore` type has been expanded to also include types used for remote syncing, e.g. with `ConnectionStatus`. ## Tools By default, the App has two "baked-in" tools: the select tool and the zoom tool. These cannot (for now) be replaced or removed. The default tools are used by default, but may be replaced by other tools if provided. ## Shapes By default, the App has a set of "core" shapes: - group - embed - bookmark - image - video - text That cannot by overwritten because they're created by the app at different moments, such as when double clicking on the canvas or via a copy and paste event. In follow up PRs, we'll split these out so that users can replace parts of the code where these shapes are created. ### Change Type - [x] `major` — Breaking Change ### Test Plan - [x] Unit Tests
2023-06-01 15:47:34 +00:00
/** @public */
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(migrateUserPreferences(data.data))
}
})
const broadcastOrigin = uniqueId()
const broadcastEventKey = 'tldraw-user-preferences-change' as const
function broadcastUserPreferencesChange() {
channel?.postMessage({
type: broadcastEventKey,
origin: broadcastOrigin,
data: {
user: globalUserPreferences.value,
version: userTypeMigrations.currentVersion,
},
} satisfies UserChangeBroadcastMessage)
}
/** @public */
export function getUserPreferences() {
return globalUserPreferences.value
}