[fix] yjs presence (#1603)
This PR: - updates the yjs example to include user presence - tweaks the `createPresenceStateDerivation` API - fix a "double update" bug caused by re-syncing local changes - fix connection bugs ### Change Type - [x] `minor` — New feature
This commit is contained in:
parent
bdd8913af3
commit
3f52c24fec
8 changed files with 228 additions and 184 deletions
|
@ -1,6 +1,7 @@
|
||||||
import { Tldraw } from '@tldraw/tldraw'
|
import { Tldraw, useEditor } from '@tldraw/tldraw'
|
||||||
import '@tldraw/tldraw/editor.css'
|
import '@tldraw/tldraw/editor.css'
|
||||||
import '@tldraw/tldraw/ui.css'
|
import '@tldraw/tldraw/ui.css'
|
||||||
|
import { track } from 'signia-react'
|
||||||
import { useYjsStore } from './useYjsStore'
|
import { useYjsStore } from './useYjsStore'
|
||||||
|
|
||||||
const HOST_URL =
|
const HOST_URL =
|
||||||
|
@ -8,14 +9,41 @@ const HOST_URL =
|
||||||
|
|
||||||
export default function YjsExample() {
|
export default function YjsExample() {
|
||||||
const store = useYjsStore({
|
const store = useYjsStore({
|
||||||
roomId: 'example',
|
roomId: 'example6',
|
||||||
hostUrl: HOST_URL,
|
hostUrl: HOST_URL,
|
||||||
version: 3,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tldraw__editor">
|
<div className="tldraw__editor">
|
||||||
<Tldraw autoFocus store={store} />
|
<Tldraw autoFocus store={store} shareZone={<NameEditor />} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const NameEditor = track(() => {
|
||||||
|
const editor = useEditor()
|
||||||
|
|
||||||
|
const { color, name } = editor.user
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ pointerEvents: 'all', display: 'flex' }}>
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={color}
|
||||||
|
onChange={(e) => {
|
||||||
|
editor.user.updateUserPreferences({
|
||||||
|
color: e.currentTarget.value,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => {
|
||||||
|
editor.user.updateUserPreferences({
|
||||||
|
name: e.currentTarget.value,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
|
@ -1,95 +1,133 @@
|
||||||
import {
|
import {
|
||||||
DocumentRecordType,
|
DocumentRecordType,
|
||||||
|
InstancePresenceRecordType,
|
||||||
PageRecordType,
|
PageRecordType,
|
||||||
TLDocument,
|
TLDocument,
|
||||||
TLInstancePresence,
|
TLInstancePresence,
|
||||||
TLPageId,
|
TLPageId,
|
||||||
TLRecord,
|
TLRecord,
|
||||||
TLStoreWithStatus,
|
TLStoreWithStatus,
|
||||||
TLUserPreferences,
|
|
||||||
createPresenceStateDerivation,
|
createPresenceStateDerivation,
|
||||||
createTLStore,
|
createTLStore,
|
||||||
defaultShapes,
|
defaultShapes,
|
||||||
getFreshUserPreferences,
|
getUserPreferences,
|
||||||
} from '@tldraw/tldraw'
|
} from '@tldraw/tldraw'
|
||||||
import { debounce } from '@tldraw/utils'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { useEffect, useState } from 'react'
|
import { computed, react, transact } from 'signia'
|
||||||
import { atom, react } from 'signia'
|
|
||||||
import { WebsocketProvider } from 'y-websocket'
|
import { WebsocketProvider } from 'y-websocket'
|
||||||
import * as Y from 'yjs'
|
import * as Y from 'yjs'
|
||||||
|
|
||||||
const doc = new Y.Doc({ gc: true })
|
|
||||||
|
|
||||||
export function useYjsStore({
|
export function useYjsStore({
|
||||||
roomId = 'example',
|
roomId = 'example',
|
||||||
version = 1,
|
|
||||||
hostUrl = process.env.NODE_ENV === 'development' ? 'ws://localhost:1234' : 'wss://demos.yjs.dev',
|
hostUrl = process.env.NODE_ENV === 'development' ? 'ws://localhost:1234' : 'wss://demos.yjs.dev',
|
||||||
}: Partial<{ hostUrl: string; roomId: string; version: number }>) {
|
}: Partial<{ hostUrl: string; roomId: string; version: number }>) {
|
||||||
|
const [store] = useState(() => createTLStore({ shapes: defaultShapes }))
|
||||||
const [storeWithStatus, setStoreWithStatus] = useState<TLStoreWithStatus>({ status: 'loading' })
|
const [storeWithStatus, setStoreWithStatus] = useState<TLStoreWithStatus>({ status: 'loading' })
|
||||||
|
|
||||||
|
const { doc, room, yRecords } = useMemo(() => {
|
||||||
|
const doc = new Y.Doc({ gc: true })
|
||||||
|
return {
|
||||||
|
doc,
|
||||||
|
room: new WebsocketProvider(hostUrl, roomId, doc, { connect: true }),
|
||||||
|
yRecords: doc.getMap<TLRecord>(`tl_${roomId}`),
|
||||||
|
}
|
||||||
|
}, [hostUrl, roomId])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const yRecords = doc.getMap<TLRecord>(`tl_${roomId}_${version}`)
|
|
||||||
|
|
||||||
const room = new WebsocketProvider(hostUrl, roomId, doc, { connect: true })
|
|
||||||
const userId = room.awareness.clientID.toString()
|
|
||||||
|
|
||||||
const unsubs: (() => void)[] = []
|
const unsubs: (() => void)[] = []
|
||||||
const store = createTLStore({ shapes: defaultShapes })
|
|
||||||
|
|
||||||
room.on('status', (connected: boolean) => {
|
// We'll use this flag to prevent repeating subscriptions if our connection drops and reconnects.
|
||||||
if (connected) {
|
let didConnect = false
|
||||||
/* ----------------- Initialization ----------------- */
|
|
||||||
|
|
||||||
if (yRecords.size === 0) {
|
room.on('status', ({ status }: { status: 'connecting' | 'disconnected' | 'connected' }) => {
|
||||||
doc.transact(() => {
|
// If we're disconnected, set the store status to 'synced-remote' and the connection status to 'offline'
|
||||||
store.clear()
|
if (status === 'connecting' || status === 'disconnected') {
|
||||||
store.put([
|
setStoreWithStatus({
|
||||||
DocumentRecordType.create({
|
store,
|
||||||
id: 'document:document' as TLDocument['id'],
|
status: 'synced-remote',
|
||||||
}),
|
connectionStatus: 'offline',
|
||||||
PageRecordType.create({
|
})
|
||||||
id: 'page:page' as TLPageId,
|
return
|
||||||
name: 'Page 1',
|
}
|
||||||
index: 'a1',
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
store.allRecords().forEach((record) => {
|
|
||||||
yRecords.set(record.id, record)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
store.put([...yRecords.values()], 'initialize')
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------- Document -------------------- */
|
if (status !== 'connected') return
|
||||||
|
|
||||||
// Sync store changes to the yjs doc
|
if (didConnect) {
|
||||||
unsubs.push(
|
setStoreWithStatus({
|
||||||
store.listen(
|
store,
|
||||||
({ changes }) => {
|
status: 'synced-remote',
|
||||||
doc.transact(() => {
|
connectionStatus: 'online',
|
||||||
Object.values(changes.added).forEach((record) => {
|
})
|
||||||
yRecords.set(record.id, record)
|
return
|
||||||
})
|
}
|
||||||
|
|
||||||
Object.values(changes.updated).forEach(([_, record]) => {
|
// Ok, we're connecting for the first time. Let's get started!
|
||||||
yRecords.set(record.id, record)
|
didConnect = true
|
||||||
})
|
|
||||||
|
|
||||||
Object.values(changes.removed).forEach((record) => {
|
// Initialize the store with the yjs doc records—or, if the yjs doc
|
||||||
yRecords.delete(record.id)
|
// is empty, initialize the yjs doc with the default store records.
|
||||||
})
|
if (yRecords.size === 0) {
|
||||||
|
// Create the initial store records
|
||||||
|
transact(() => {
|
||||||
|
store.clear()
|
||||||
|
store.put([
|
||||||
|
DocumentRecordType.create({
|
||||||
|
id: 'document:document' as TLDocument['id'],
|
||||||
|
}),
|
||||||
|
PageRecordType.create({
|
||||||
|
id: 'page:page' as TLPageId,
|
||||||
|
name: 'Page 1',
|
||||||
|
index: 'a1',
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Sync the store records to the yjs doc
|
||||||
|
doc.transact(() => {
|
||||||
|
for (const record of store.allRecords()) {
|
||||||
|
yRecords.set(record.id, record)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Replace the store records with the yjs doc records
|
||||||
|
transact(() => {
|
||||||
|
store.clear()
|
||||||
|
store.put([...yRecords.values()])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------- Document -------------------- */
|
||||||
|
|
||||||
|
// Sync store changes to the yjs doc
|
||||||
|
unsubs.push(
|
||||||
|
store.listen(
|
||||||
|
function syncStoreChangesToYjsDoc({ changes }) {
|
||||||
|
doc.transact(() => {
|
||||||
|
Object.values(changes.added).forEach((record) => {
|
||||||
|
yRecords.set(record.id, record)
|
||||||
})
|
})
|
||||||
},
|
|
||||||
{ source: 'user', scope: 'document' }
|
Object.values(changes.updated).forEach(([_, record]) => {
|
||||||
)
|
yRecords.set(record.id, record)
|
||||||
|
})
|
||||||
|
|
||||||
|
Object.values(changes.removed).forEach((record) => {
|
||||||
|
yRecords.delete(record.id)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ source: 'user', scope: 'document' } // only sync user's document changes
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
// Sync the yjs doc changes to the store
|
// Sync the yjs doc changes to the store
|
||||||
const handleChange = ([event]: Y.YEvent<any>[]) => {
|
const handleChange = (events: Y.YEvent<any>[], transaction: Y.Transaction) => {
|
||||||
const toDelete: TLRecord['id'][] = []
|
if (transaction.local) return
|
||||||
const toPut: TLRecord[] = []
|
|
||||||
|
|
||||||
|
const toRemove: TLRecord['id'][] = []
|
||||||
|
const toPut: TLRecord[] = []
|
||||||
|
|
||||||
|
events.forEach((event) => {
|
||||||
event.changes.keys.forEach((change, id) => {
|
event.changes.keys.forEach((change, id) => {
|
||||||
switch (change.action) {
|
switch (change.action) {
|
||||||
case 'add':
|
case 'add':
|
||||||
|
@ -98,136 +136,96 @@ export function useYjsStore({
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'delete': {
|
case 'delete': {
|
||||||
toDelete.push(id as TLRecord['id'])
|
toRemove.push(id as TLRecord['id'])
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
store.mergeRemoteChanges(() => {
|
|
||||||
store.remove(toDelete)
|
|
||||||
store.put(toPut)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
yRecords.observeDeep(handleChange)
|
|
||||||
unsubs.push(() => yRecords.unobserveDeep(handleChange))
|
|
||||||
|
|
||||||
/* -------------------- Awareness ------------------- */
|
|
||||||
|
|
||||||
// Get the persisted user preferences or use the defaults
|
|
||||||
|
|
||||||
let userPreferences: TLUserPreferences = {
|
|
||||||
...getFreshUserPreferences(),
|
|
||||||
id: userId,
|
|
||||||
}
|
|
||||||
|
|
||||||
const persistedUserPreferences = localStorage.getItem(`tldraw-presence-${version}`)
|
|
||||||
if (persistedUserPreferences !== null) {
|
|
||||||
try {
|
|
||||||
userPreferences = JSON.parse(persistedUserPreferences) as TLUserPreferences
|
|
||||||
} catch (e: any) {
|
|
||||||
// Something went wrong, persist the defaults instead
|
|
||||||
localStorage.setItem(`tldraw-presence-${version}`, JSON.stringify(userPreferences))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const debouncedPersist = debounce((presence: TLInstancePresence) => {
|
|
||||||
const preferences: TLUserPreferences = {
|
|
||||||
...userPreferences,
|
|
||||||
name: presence.userName,
|
|
||||||
color: presence.color,
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.setItem(`tldraw-presence-${version}`, JSON.stringify(preferences))
|
|
||||||
}, 1000)
|
|
||||||
|
|
||||||
// Create the instance presence derivation
|
|
||||||
const userPreferencesSignal = atom<TLUserPreferences>('user preferences', userPreferences)
|
|
||||||
const presenceDerivation = createPresenceStateDerivation(userPreferencesSignal)(store)
|
|
||||||
room.awareness.setLocalStateField('presence', presenceDerivation.value)
|
|
||||||
|
|
||||||
// Sync the instance presence changes to yjs awareness
|
|
||||||
unsubs.push(
|
|
||||||
react('when presence changes', () => {
|
|
||||||
const presence = presenceDerivation.value
|
|
||||||
if (presence && presence.userId === userId) {
|
|
||||||
room.awareness.setLocalStateField('presence', presence)
|
|
||||||
debouncedPersist(presence)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
// Sync yjs awareness changes to the store
|
|
||||||
const handleUpdate = ({
|
|
||||||
added,
|
|
||||||
updated,
|
|
||||||
removed,
|
|
||||||
}: {
|
|
||||||
added: number[]
|
|
||||||
updated: number[]
|
|
||||||
removed: number[]
|
|
||||||
}) => {
|
|
||||||
const states = room.awareness.getStates()
|
|
||||||
|
|
||||||
store.mergeRemoteChanges(() => {
|
|
||||||
added.forEach((id) => {
|
|
||||||
const state = states.get(id) as { presence: TLInstancePresence }
|
|
||||||
if (state.presence) {
|
|
||||||
if (state.presence.userId !== userId) {
|
|
||||||
store.put([state.presence])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
updated.forEach((id) => {
|
|
||||||
const state = states.get(id) as { presence: TLInstancePresence }
|
|
||||||
if (state.presence) {
|
|
||||||
if (state.presence.userId !== userId) {
|
|
||||||
store.put([state.presence])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (removed.length) {
|
|
||||||
const allRecords = store.allRecords()
|
|
||||||
|
|
||||||
removed.forEach((id) => {
|
|
||||||
const stringId = id.toString()
|
|
||||||
const recordsToRemove = allRecords
|
|
||||||
.filter((record) => 'userId' in record && record.userId === stringId)
|
|
||||||
.map((record) => record.id)
|
|
||||||
|
|
||||||
store.remove(recordsToRemove)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
room.awareness.on('update', handleUpdate)
|
|
||||||
unsubs.push(() => room.awareness.off('update', handleUpdate))
|
|
||||||
|
|
||||||
// And we're done!
|
|
||||||
|
|
||||||
setStoreWithStatus({
|
|
||||||
store,
|
|
||||||
status: 'synced-remote',
|
|
||||||
connectionStatus: 'online',
|
|
||||||
})
|
})
|
||||||
} else {
|
|
||||||
setStoreWithStatus({
|
// put / remove the records in the store
|
||||||
store,
|
store.mergeRemoteChanges(() => {
|
||||||
status: 'synced-remote',
|
if (toRemove.length) store.remove(toRemove)
|
||||||
connectionStatus: 'offline',
|
if (toPut.length) store.put(toPut)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
yRecords.observeDeep(handleChange)
|
||||||
|
unsubs.push(() => yRecords.unobserveDeep(handleChange))
|
||||||
|
|
||||||
|
/* -------------------- Awareness ------------------- */
|
||||||
|
|
||||||
|
// Create the instance presence derivation
|
||||||
|
const yClientId = room.awareness.clientID.toString()
|
||||||
|
const presenceId = InstancePresenceRecordType.createId(yClientId)
|
||||||
|
const userPreferencesComputed = computed('ok', () => getUserPreferences())
|
||||||
|
const presenceDerivation = createPresenceStateDerivation(
|
||||||
|
userPreferencesComputed,
|
||||||
|
presenceId
|
||||||
|
)(store)
|
||||||
|
|
||||||
|
// Set our initial presence from the derivation's current value
|
||||||
|
room.awareness.setLocalStateField('presence', presenceDerivation.value)
|
||||||
|
|
||||||
|
// When the derivation change, sync presence to to yjs awareness
|
||||||
|
unsubs.push(
|
||||||
|
react('when presence changes', () => {
|
||||||
|
const presence = presenceDerivation.value
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
room.awareness.setLocalStateField('presence', presence)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sync yjs awareness changes to the store
|
||||||
|
const handleUpdate = (update: { added: number[]; updated: number[]; removed: number[] }) => {
|
||||||
|
const states = room.awareness.getStates() as Map<number, { presence: TLInstancePresence }>
|
||||||
|
|
||||||
|
const toRemove: TLInstancePresence['id'][] = []
|
||||||
|
const toPut: TLInstancePresence[] = []
|
||||||
|
|
||||||
|
// Connect records to put / remove
|
||||||
|
for (const clientId of update.added) {
|
||||||
|
const state = states.get(clientId)
|
||||||
|
if (state?.presence && state.presence.id !== presenceId) {
|
||||||
|
toPut.push(state.presence)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const clientId of update.updated) {
|
||||||
|
const state = states.get(clientId)
|
||||||
|
if (state?.presence && state.presence.id !== presenceId) {
|
||||||
|
toPut.push(state.presence)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const clientId of update.removed) {
|
||||||
|
toRemove.push(InstancePresenceRecordType.createId(clientId.toString()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// put / remove the records in the store
|
||||||
|
store.mergeRemoteChanges(() => {
|
||||||
|
if (toRemove.length) store.remove(toRemove)
|
||||||
|
if (toPut.length) store.put(toPut)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
room.awareness.on('update', handleUpdate)
|
||||||
|
unsubs.push(() => room.awareness.off('update', handleUpdate))
|
||||||
|
|
||||||
|
// And we're done!
|
||||||
|
setStoreWithStatus({
|
||||||
|
store,
|
||||||
|
status: 'synced-remote',
|
||||||
|
connectionStatus: 'online',
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unsubs.forEach((fn) => fn())
|
unsubs.forEach((fn) => fn())
|
||||||
unsubs.length = 0
|
unsubs.length = 0
|
||||||
}
|
}
|
||||||
}, [hostUrl, roomId, version])
|
}, [room, doc, store, yRecords])
|
||||||
|
|
||||||
return storeWithStatus
|
return storeWithStatus
|
||||||
}
|
}
|
||||||
|
|
|
@ -243,6 +243,13 @@ export function createSessionStateSnapshotSignal(store: TLStore): Signal<null |
|
||||||
// @public
|
// @public
|
||||||
export function createTLStore({ initialData, defaultName, ...rest }: TLStoreOptions): TLStore;
|
export function createTLStore({ initialData, defaultName, ...rest }: TLStoreOptions): TLStore;
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export function createTLUser(opts?: {
|
||||||
|
derivePresenceState?: ((store: TLStore) => Signal<null | TLInstancePresence>) | undefined;
|
||||||
|
userPreferences?: Signal<TLUserPreferences, unknown> | undefined;
|
||||||
|
setUserPreferences?: ((userPreferences: TLUserPreferences) => void) | undefined;
|
||||||
|
}): TLUser;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export function dataTransferItemAsString(item: DataTransferItem): Promise<string>;
|
export function dataTransferItemAsString(item: DataTransferItem): Promise<string>;
|
||||||
|
|
||||||
|
|
|
@ -45,6 +45,7 @@ export {
|
||||||
type TLStoreEventInfo,
|
type TLStoreEventInfo,
|
||||||
type TLStoreOptions,
|
type TLStoreOptions,
|
||||||
} from './lib/config/createTLStore'
|
} from './lib/config/createTLStore'
|
||||||
|
export { createTLUser } from './lib/config/createTLUser'
|
||||||
export { coreShapes, defaultShapes } from './lib/config/defaultShapes'
|
export { coreShapes, defaultShapes } from './lib/config/defaultShapes'
|
||||||
export { defaultTools } from './lib/config/defaultTools'
|
export { defaultTools } from './lib/config/defaultTools'
|
||||||
export { defineShape, type TLShapeInfo } from './lib/config/defineShape'
|
export { defineShape, type TLShapeInfo } from './lib/config/defineShape'
|
||||||
|
|
|
@ -1674,7 +1674,10 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
get currentPage(): TLPage {
|
get currentPage(): TLPage {
|
||||||
return this.getPageById(this.currentPageId)!
|
const page = this.getPageById(this.currentPageId)
|
||||||
|
if (!page)
|
||||||
|
throw Error(`No current page (id ${this.currentPageId}, ${this.pages.length} pages))`)
|
||||||
|
return page
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -11,6 +11,10 @@ export class UserPreferencesManager {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get userPreferences() {
|
||||||
|
return this.user.userPreferences
|
||||||
|
}
|
||||||
|
|
||||||
get isDarkMode() {
|
get isDarkMode() {
|
||||||
return this.user.userPreferences.value.isDarkMode
|
return this.user.userPreferences.value.isDarkMode
|
||||||
}
|
}
|
||||||
|
|
|
@ -125,7 +125,7 @@ export const createPresenceStateDerivation: ($user: Signal<{
|
||||||
id: string;
|
id: string;
|
||||||
color: string;
|
color: string;
|
||||||
name: string;
|
name: string;
|
||||||
}>) => (store: TLStore) => Signal<null | TLInstancePresence>;
|
}>, instanceId?: TLInstancePresence['id']) => (store: TLStore) => Signal<null | TLInstancePresence>;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export function createShapeId(id?: string): TLShapeId;
|
export function createShapeId(id?: string): TLShapeId;
|
||||||
|
|
|
@ -8,7 +8,10 @@ import { InstancePresenceRecordType, TLInstancePresence } from './records/TLPres
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export const createPresenceStateDerivation =
|
export const createPresenceStateDerivation =
|
||||||
($user: Signal<{ id: string; color: string; name: string }>) =>
|
(
|
||||||
|
$user: Signal<{ id: string; color: string; name: string }>,
|
||||||
|
instanceId?: TLInstancePresence['id']
|
||||||
|
) =>
|
||||||
(store: TLStore): Signal<TLInstancePresence | null> => {
|
(store: TLStore): Signal<TLInstancePresence | null> => {
|
||||||
return computed('instancePresence', () => {
|
return computed('instancePresence', () => {
|
||||||
const instance = store.get(TLINSTANCE_ID)
|
const instance = store.get(TLINSTANCE_ID)
|
||||||
|
@ -21,7 +24,7 @@ export const createPresenceStateDerivation =
|
||||||
}
|
}
|
||||||
|
|
||||||
return InstancePresenceRecordType.create({
|
return InstancePresenceRecordType.create({
|
||||||
id: InstancePresenceRecordType.createId(store.id),
|
id: instanceId ?? InstancePresenceRecordType.createId(store.id),
|
||||||
selectedIds: pageState.selectedIds,
|
selectedIds: pageState.selectedIds,
|
||||||
brush: instance.brush,
|
brush: instance.brush,
|
||||||
scribble: instance.scribble,
|
scribble: instance.scribble,
|
||||||
|
|
Loading…
Reference in a new issue