kopia lustrzana https://github.com/Tldraw/Tldraw
271 wiersze
6.6 KiB
TypeScript
271 wiersze
6.6 KiB
TypeScript
import { atom } from '@tldraw/state'
|
|
import { defineMigrations, migrate } from '@tldraw/store'
|
|
import { getDefaultTranslationLocale } from '@tldraw/tlschema'
|
|
import { T } from '@tldraw/validate'
|
|
import { uniqueId } from '../utils/uniqueId'
|
|
|
|
const USER_DATA_KEY = 'TLDRAW_USER_DATA_v3'
|
|
|
|
/**
|
|
* A user of tldraw
|
|
*
|
|
* @public
|
|
*/
|
|
export interface TLUserPreferences {
|
|
id: string
|
|
name?: string | null
|
|
locale?: string | null
|
|
color?: string | null
|
|
animationSpeed?: number | null
|
|
edgeScrollSpeed?: number | null
|
|
isDarkMode?: boolean | null
|
|
isSnapMode?: boolean | null
|
|
isWrapMode?: boolean | null
|
|
}
|
|
|
|
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.nullable().optional(),
|
|
locale: T.string.nullable().optional(),
|
|
color: T.string.nullable().optional(),
|
|
isDarkMode: T.boolean.nullable().optional(),
|
|
animationSpeed: T.number.nullable().optional(),
|
|
edgeScrollSpeed: T.number.nullable().optional(),
|
|
isSnapMode: T.boolean.nullable().optional(),
|
|
isWrapMode: T.boolean.nullable().optional(),
|
|
})
|
|
|
|
const Versions = {
|
|
AddAnimationSpeed: 1,
|
|
AddIsSnapMode: 2,
|
|
MakeFieldsNullable: 3,
|
|
AddEdgeScrollSpeed: 4,
|
|
AddExcalidrawSelectMode: 5,
|
|
} as const
|
|
|
|
const userMigrations = defineMigrations({
|
|
currentVersion: Versions.AddExcalidrawSelectMode,
|
|
migrators: {
|
|
[Versions.AddAnimationSpeed]: {
|
|
up: (user) => {
|
|
return {
|
|
...user,
|
|
animationSpeed: 1,
|
|
}
|
|
},
|
|
down: ({ animationSpeed: _, ...user }) => {
|
|
return user
|
|
},
|
|
},
|
|
[Versions.AddIsSnapMode]: {
|
|
up: (user: TLUserPreferences) => {
|
|
return { ...user, isSnapMode: false }
|
|
},
|
|
down: ({ isSnapMode: _, ...user }: TLUserPreferences) => {
|
|
return user
|
|
},
|
|
},
|
|
[Versions.MakeFieldsNullable]: {
|
|
up: (user: TLUserPreferences) => {
|
|
return user
|
|
},
|
|
down: (user: TLUserPreferences) => {
|
|
return {
|
|
id: user.id,
|
|
name: user.name ?? defaultUserPreferences.name,
|
|
locale: user.locale ?? defaultUserPreferences.locale,
|
|
color: user.color ?? defaultUserPreferences.color,
|
|
animationSpeed: user.animationSpeed ?? defaultUserPreferences.animationSpeed,
|
|
isDarkMode: user.isDarkMode ?? defaultUserPreferences.isDarkMode,
|
|
isSnapMode: user.isSnapMode ?? defaultUserPreferences.isSnapMode,
|
|
isWrapMode: user.isWrapMode ?? defaultUserPreferences.isWrapMode,
|
|
}
|
|
},
|
|
},
|
|
[Versions.AddEdgeScrollSpeed]: {
|
|
up: (user: TLUserPreferences) => {
|
|
return {
|
|
...user,
|
|
edgeScrollSpeed: 1,
|
|
}
|
|
},
|
|
down: ({ edgeScrollSpeed: _, ...user }: TLUserPreferences) => {
|
|
return user
|
|
},
|
|
},
|
|
[Versions.AddExcalidrawSelectMode]: {
|
|
up: (user: TLUserPreferences) => {
|
|
return { ...user, isWrapMode: false }
|
|
},
|
|
down: ({ isWrapMode: _, ...user }: TLUserPreferences) => {
|
|
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)]
|
|
}
|
|
|
|
/** @internal */
|
|
export function userPrefersDarkUI() {
|
|
if (typeof window === 'undefined') {
|
|
return false
|
|
}
|
|
return window.matchMedia?.('(prefers-color-scheme: dark)')?.matches ?? false
|
|
}
|
|
|
|
/** @internal */
|
|
export function userPrefersReducedMotion() {
|
|
if (typeof window === 'undefined') {
|
|
return false
|
|
}
|
|
return window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches ?? false
|
|
}
|
|
|
|
/** @public */
|
|
export const defaultUserPreferences = Object.freeze({
|
|
name: 'New User',
|
|
locale: getDefaultTranslationLocale(),
|
|
color: getRandomColor(),
|
|
isDarkMode: false,
|
|
edgeScrollSpeed: 1,
|
|
animationSpeed: userPrefersReducedMotion() ? 0 : 1,
|
|
isSnapMode: false,
|
|
isWrapMode: false,
|
|
}) satisfies Readonly<Omit<TLUserPreferences, 'id'>>
|
|
|
|
/** @public */
|
|
export function getFreshUserPreferences(): TLUserPreferences {
|
|
return {
|
|
id: uniqueId(),
|
|
}
|
|
}
|
|
|
|
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: userMigrations.currentVersion ?? 0,
|
|
migrations: userMigrations,
|
|
})
|
|
|
|
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 | null>('globalUserData', null)
|
|
|
|
function storeUserPreferences() {
|
|
if (typeof window !== 'undefined' && window.localStorage) {
|
|
window.localStorage.setItem(
|
|
USER_DATA_KEY,
|
|
JSON.stringify({
|
|
version: userMigrations.currentVersion,
|
|
user: globalUserPreferences.get(),
|
|
})
|
|
)
|
|
}
|
|
}
|
|
|
|
/** @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: getUserPreferences(),
|
|
version: userMigrations.currentVersion,
|
|
},
|
|
} satisfies UserChangeBroadcastMessage)
|
|
}
|
|
|
|
/** @public */
|
|
export function getUserPreferences(): TLUserPreferences {
|
|
let prefs = globalUserPreferences.get()
|
|
if (!prefs) {
|
|
prefs = loadUserPreferences()
|
|
globalUserPreferences.set(prefs)
|
|
}
|
|
return prefs
|
|
}
|