UNPKG

@tldraw/tlschema

Version:

tldraw infinite canvas SDK (schema).

155 lines (147 loc) 5.51 kB
import { Signal, computed } from '@tldraw/state' import { CameraRecordType } from './records/TLCamera' import { TLINSTANCE_ID } from './records/TLInstance' import { InstancePageStateRecordType } from './records/TLPageState' import { TLPOINTER_ID } from './records/TLPointer' import { InstancePresenceRecordType, TLInstancePresence } from './records/TLPresence' import { TLUser } from './records/TLUser' import { TLStore } from './TLStore' /** @public */ export interface CreatePresenceStateDerivationOpts { /** Custom instance ID. If not provided, one is generated from the store ID. */ instanceId?: TLInstancePresence['id'] /** * Override how presence state is built from the store and current user. * Defaults to {@link getDefaultUserPresence}. */ getUserPresence?(store: TLStore, user: TLUser): TLPresenceStateInfo | null } /** * Creates a derivation that represents the current presence state of the current user. * * This function returns a derivation factory that, when given a store, creates a computed signal * containing the user's current presence state. The presence state includes information like cursor * position, selected shapes, camera position, and user metadata that gets synchronized in * multiplayer scenarios. * * @param $user - A reactive signal containing the user information, or `null` when anonymous * @param opts - Optional configuration for instance ID and presence derivation * @returns A function that takes a store and returns a computed signal of the user's presence state * * @example * ```ts * import { createPresenceStateDerivation } from '@tldraw/tlschema' * import { atom } from '@tldraw/state' * * const userSignal = atom('user', { id: 'user-123', name: 'Alice', color: '#ff0000', meta: {} }) * const presenceDerivation = createPresenceStateDerivation(userSignal) * * // Use with a store to get reactive presence state * const presenceState = presenceDerivation(store) * console.log(presenceState.get()) // Current user presence or null * ``` * * @public */ export function createPresenceStateDerivation( $user: Signal<TLUser | null>, opts?: CreatePresenceStateDerivationOpts ) { const { instanceId, getUserPresence: _getUserPresence } = opts ?? {} const getUserPresence = _getUserPresence ?? getDefaultUserPresence return (store: TLStore): Signal<TLInstancePresence | null> => { return computed('instancePresence', () => { const user = $user.get() if (!user) return null const state = getUserPresence(store, user) if (!state) return null return InstancePresenceRecordType.create({ ...state, id: instanceId ?? InstancePresenceRecordType.createId(store.id), }) }) } } /** * The shape of data used to create a presence record. * * This type represents all the properties needed to construct a TLInstancePresence record. * It includes user information, cursor state, camera position, selected shapes, and other * presence-related data that gets synchronized across multiplayer clients. * * @public */ export type TLPresenceStateInfo = Parameters<(typeof InstancePresenceRecordType)['create']>[0] /** * Creates default presence state information for a user based on the current store state. * * This function extracts the current state from various store records (instance, page state, * camera, pointer) and combines them with user information to create a complete presence * state object. This is commonly used as a starting point for custom presence implementations. * * @param store - The tldraw store containing the current editor state * @param user - The user information to include in the presence state * @returns The default presence state info, or null if required store records are missing * * @example * ```ts * import { getDefaultUserPresence } from '@tldraw/tlschema' * * const user = { id: 'user-123', name: 'Alice', color: '#ff0000', meta: {} } * const presenceInfo = getDefaultUserPresence(store, user) * * if (presenceInfo) { * console.log('Current cursor:', presenceInfo.cursor) * console.log('Selected shapes:', presenceInfo.selectedShapeIds) * console.log('Camera position:', presenceInfo.camera) * } * ``` * * @example * ```ts * // Common pattern: customize default presence * const customPresence = { * ...getDefaultUserPresence(store, user), * // Remove camera for privacy * camera: undefined, * // Add custom metadata * customField: 'my-data' * } * ``` * * @public */ export function getDefaultUserPresence(store: TLStore, user: TLUser) { const instance = store.get(TLINSTANCE_ID) const pageState = store.get(InstancePageStateRecordType.createId(instance?.currentPageId)) const camera = store.get(CameraRecordType.createId(instance?.currentPageId)) const pointer = store.get(TLPOINTER_ID) if (!pageState || !instance || !camera || !pointer) { return null } return { selectedShapeIds: pageState.selectedShapeIds, brush: instance.brush, scribbles: instance.scribbles, userId: user.id, userName: user.name, followingUserId: instance.followingUserId, camera: { x: camera.x, y: camera.y, z: camera.z, }, color: user.color || '#FF0000', currentPageId: instance.currentPageId, cursor: { x: pointer.x, y: pointer.y, rotation: instance.cursor.rotation, type: instance.cursor.type, }, lastActivityTimestamp: pointer.lastActivityTimestamp, screenBounds: instance.screenBounds, chatMessage: instance.chatMessage, meta: {}, } satisfies TLPresenceStateInfo }