[2/4] Rename sync hooks, add bookmarks to demo (#4094)
Adds a new `onEditorMount` callback to the store, allowing store creators to do things like registering bookmark handlers. We use this in the new demo hook. This also renames `useRemoteSyncClient` to `useMultiplayerSync`, and `useRemoteSyncDemo` to `useMultiplayerDemo`. Closes TLD-2601 ### Change type - [x] `api`
This commit is contained in:
parent
965bc10997
commit
627c84c2af
13 changed files with 127 additions and 39 deletions
|
@ -1,5 +1,5 @@
|
|||
main = "src/worker.ts"
|
||||
compatibility_date = "2024-06-25"
|
||||
compatibility_date = "2024-06-20"
|
||||
upload_source_maps = true
|
||||
|
||||
[dev]
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { ROOM_OPEN_MODE, RoomOpenModeToPath, type RoomOpenMode } from '@tldraw/dotcom-shared'
|
||||
import { useRemoteSyncClient } from '@tldraw/sync-react'
|
||||
import { useMultiplayerSync } from '@tldraw/sync-react'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import {
|
||||
DefaultContextMenu,
|
||||
|
@ -112,7 +112,7 @@ export function MultiplayerEditor({
|
|||
}) {
|
||||
const handleUiEvent = useHandleUiEvents()
|
||||
|
||||
const storeWithStatus = useRemoteSyncClient({
|
||||
const storeWithStatus = useMultiplayerSync({
|
||||
uri: `${MULTIPLAYER_SERVER}/${RoomOpenModeToPath[roomOpenMode]}/${roomSlug}`,
|
||||
roomId: roomSlug,
|
||||
assets: multiplayerAssetStore,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useDemoRemoteSyncClient } from '@tldraw/sync-react'
|
||||
import { useMultiplayerDemo } from '@tldraw/sync-react'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { DefaultContextMenu, DefaultContextMenuContent, TLComponents, Tldraw, atom } from 'tldraw'
|
||||
import { UrlStateParams, useUrlState } from '../hooks/useUrlState'
|
||||
|
@ -37,7 +37,7 @@ const components: TLComponents = {
|
|||
export function TemporaryBemoDevEditor({ slug }: { slug: string }) {
|
||||
const handleUiEvent = useHandleUiEvents()
|
||||
|
||||
const storeWithStatus = useDemoRemoteSyncClient({ host: 'http://127.0.0.1:8989', roomId: slug })
|
||||
const storeWithStatus = useMultiplayerDemo({ host: 'http://127.0.0.1:8989', roomId: slug })
|
||||
|
||||
const isOffline =
|
||||
storeWithStatus.status === 'synced-remote' && storeWithStatus.connectionStatus === 'offline'
|
||||
|
@ -48,14 +48,6 @@ export function TemporaryBemoDevEditor({ slug }: { slug: string }) {
|
|||
const fileSystemUiOverrides = useFileSystem({ isMultiplayer: true })
|
||||
const cursorChatOverrides = useCursorChat()
|
||||
|
||||
// TODO: handle bookmarks
|
||||
// const handleMount = useCallback(
|
||||
// (editor: Editor) => {
|
||||
// editor.registerExternalAssetHandler('url', createAssetFromUrl)
|
||||
// },
|
||||
// []
|
||||
// )
|
||||
|
||||
if (storeWithStatus.error) {
|
||||
return <StoreErrorScreen error={storeWithStatus.error} />
|
||||
}
|
||||
|
|
|
@ -487,7 +487,7 @@ export function counterClockwiseAngleDist(a0: number, a1: number): number;
|
|||
export function createSessionStateSnapshotSignal(store: TLStore): Signal<null | TLSessionStateSnapshot>;
|
||||
|
||||
// @public
|
||||
export function createTLStore({ initialData, defaultName, id, assets, ...rest }?: TLStoreOptions): TLStore;
|
||||
export function createTLStore({ initialData, defaultName, id, assets, onEditorMount, ...rest }?: TLStoreOptions): TLStore;
|
||||
|
||||
// @public (undocumented)
|
||||
export function createTLUser(opts?: {
|
||||
|
@ -3246,6 +3246,7 @@ export interface TLStoreBaseOptions {
|
|||
assets?: Partial<TLAssetStore>;
|
||||
defaultName?: string;
|
||||
initialData?: SerializedStore<TLRecord>;
|
||||
onEditorMount?: (editor: Editor) => (() => void) | void;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
|
@ -3422,6 +3423,9 @@ export function useLocalStore(options: {
|
|||
snapshot?: TLEditorSnapshot | TLStoreSnapshot;
|
||||
} & TLStoreOptions): TLStoreWithStatus;
|
||||
|
||||
// @internal (undocumented)
|
||||
export function useOnMount(onMount?: TLOnMountHandler): void;
|
||||
|
||||
// @internal (undocumented)
|
||||
export function usePeerIds(): string[];
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@ export {
|
|||
ErrorScreen,
|
||||
LoadingScreen,
|
||||
TldrawEditor,
|
||||
useOnMount,
|
||||
type LoadingScreenProps,
|
||||
type TLOnMountHandler,
|
||||
type TldrawEditorBaseProps,
|
||||
|
|
|
@ -450,7 +450,15 @@ function Layout({ children, onMount }: { children: ReactNode; onMount?: TLOnMoun
|
|||
useCursor()
|
||||
useDarkMode()
|
||||
useForceUpdate()
|
||||
useOnMount(onMount)
|
||||
useOnMount((editor) => {
|
||||
const teardownStore = editor.store.props.onEditorMount(editor)
|
||||
const teardownCallback = onMount?.(editor)
|
||||
|
||||
return () => {
|
||||
teardownStore?.()
|
||||
teardownCallback?.()
|
||||
}
|
||||
})
|
||||
|
||||
return children
|
||||
}
|
||||
|
@ -474,7 +482,8 @@ export function ErrorScreen({ children }: LoadingScreenProps) {
|
|||
return <div className="tl-loading">{children}</div>
|
||||
}
|
||||
|
||||
function useOnMount(onMount?: TLOnMountHandler) {
|
||||
/** @internal */
|
||||
export function useOnMount(onMount?: TLOnMountHandler) {
|
||||
const editor = useEditor()
|
||||
|
||||
const onMountEvent = useEvent((editor: Editor) => {
|
||||
|
|
|
@ -7,7 +7,8 @@ import {
|
|||
TLStoreProps,
|
||||
createTLSchema,
|
||||
} from '@tldraw/tlschema'
|
||||
import { FileHelpers } from '@tldraw/utils'
|
||||
import { FileHelpers, assert } from '@tldraw/utils'
|
||||
import { Editor } from '../editor/Editor'
|
||||
import { TLAnyBindingUtilConstructor, checkBindings } from './defaultBindings'
|
||||
import { TLAnyShapeUtilConstructor, checkShapesAndAddCore } from './defaultShapes'
|
||||
|
||||
|
@ -21,6 +22,9 @@ export interface TLStoreBaseOptions {
|
|||
|
||||
/** How should this store upload & resolve assets? */
|
||||
assets?: Partial<TLAssetStore>
|
||||
|
||||
/** Called when the store is connected to an {@link Editor}. */
|
||||
onEditorMount?: (editor: Editor) => void | (() => void)
|
||||
}
|
||||
|
||||
/** @public */
|
||||
|
@ -58,6 +62,7 @@ export function createTLStore({
|
|||
defaultName = '',
|
||||
id,
|
||||
assets,
|
||||
onEditorMount,
|
||||
...rest
|
||||
}: TLStoreOptions = {}): TLStore {
|
||||
const schema =
|
||||
|
@ -87,6 +92,10 @@ export function createTLStore({
|
|||
...defaultAssetStore,
|
||||
...assets,
|
||||
},
|
||||
onEditorMount: (editor) => {
|
||||
assert(editor instanceof Editor)
|
||||
onEditorMount?.(editor)
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,2 +1,6 @@
|
|||
export { useDemoRemoteSyncClient, type UseDemoSyncClientConfig } from './useDemoSyncClient'
|
||||
export { useRemoteSyncClient, type RemoteTLStoreWithStatus } from './useRemoteSyncClient'
|
||||
export {
|
||||
useMultiplayerSync,
|
||||
type RemoteTLStoreWithStatus,
|
||||
type UseMultiplayerSyncOptions,
|
||||
} from './useMultiplayerSync'
|
||||
export { useMultiplayerDemo, type UseMultiplayerDemoOptions } from './useMutliplayerDemo'
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
} from '@tldraw/sync'
|
||||
import { useEffect, useState } from 'react'
|
||||
import {
|
||||
Editor,
|
||||
Signal,
|
||||
TAB_ID,
|
||||
TLAssetStore,
|
||||
|
@ -33,14 +34,14 @@ export type RemoteTLStoreWithStatus = Exclude<
|
|||
>
|
||||
|
||||
/** @public */
|
||||
export function useRemoteSyncClient(opts: UseSyncClientConfig): RemoteTLStoreWithStatus {
|
||||
export function useMultiplayerSync(opts: UseMultiplayerSyncOptions): RemoteTLStoreWithStatus {
|
||||
const [state, setState] = useState<{
|
||||
readyClient?: TLSyncClient<TLRecord, TLStore>
|
||||
error?: Error
|
||||
} | null>(null)
|
||||
const { uri, roomId = 'default', userPreferences: prefs, assets } = opts
|
||||
const { uri, roomId = 'default', userPreferences: prefs, assets, onEditorMount } = opts
|
||||
|
||||
const store = useTLStore({ schema, assets })
|
||||
const store = useTLStore({ schema, assets, onEditorMount })
|
||||
|
||||
const error: NonNullable<typeof state>['error'] = state?.error ?? undefined
|
||||
const track = opts.trackAnalyticsEvent
|
||||
|
@ -134,11 +135,12 @@ export function useRemoteSyncClient(opts: UseSyncClientConfig): RemoteTLStoreWit
|
|||
}
|
||||
|
||||
/** @public */
|
||||
export interface UseSyncClientConfig {
|
||||
export interface UseMultiplayerSyncOptions {
|
||||
uri: string
|
||||
roomId?: string
|
||||
userPreferences?: Signal<TLUserPreferences>
|
||||
/* @internal */
|
||||
trackAnalyticsEvent?(name: string, data: { [key: string]: any }): void
|
||||
assets?: Partial<TLAssetStore>
|
||||
onEditorMount?: (editor: Editor) => void
|
||||
}
|
|
@ -1,9 +1,19 @@
|
|||
import { useMemo } from 'react'
|
||||
import { MediaHelpers, Signal, TLAssetStore, TLUserPreferences, uniqueId } from 'tldraw'
|
||||
import { RemoteTLStoreWithStatus, useRemoteSyncClient } from './useRemoteSyncClient'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import {
|
||||
AssetRecordType,
|
||||
Editor,
|
||||
MediaHelpers,
|
||||
Signal,
|
||||
TLAsset,
|
||||
TLAssetStore,
|
||||
TLUserPreferences,
|
||||
getHashForString,
|
||||
uniqueId,
|
||||
} from 'tldraw'
|
||||
import { RemoteTLStoreWithStatus, useMultiplayerSync } from './useMultiplayerSync'
|
||||
|
||||
/** @public */
|
||||
export interface UseDemoSyncClientConfig {
|
||||
export interface UseMultiplayerDemoOptions {
|
||||
roomId: string
|
||||
userPreferences?: Signal<TLUserPreferences>
|
||||
/** @internal */
|
||||
|
@ -29,18 +39,26 @@ function getEnv(cb: () => string | undefined): string | undefined {
|
|||
const DEMO_WORKER = getEnv(() => process.env.DEMO_WORKER) ?? 'https://demo.tldraw.xyz'
|
||||
const IMAGE_WORKER = getEnv(() => process.env.IMAGE_WORKER) ?? 'https://images.tldraw.xyz'
|
||||
|
||||
export function useDemoRemoteSyncClient({
|
||||
export function useMultiplayerDemo({
|
||||
roomId,
|
||||
userPreferences,
|
||||
host = DEMO_WORKER,
|
||||
}: UseDemoSyncClientConfig): RemoteTLStoreWithStatus {
|
||||
}: UseMultiplayerDemoOptions): RemoteTLStoreWithStatus {
|
||||
const assets = useMemo(() => createDemoAssetStore(host), [host])
|
||||
|
||||
return useRemoteSyncClient({
|
||||
return useMultiplayerSync({
|
||||
uri: `${host}/connect/${roomId}`,
|
||||
roomId,
|
||||
userPreferences,
|
||||
assets,
|
||||
onEditorMount: useCallback(
|
||||
(editor: Editor) => {
|
||||
editor.registerExternalAssetHandler('url', async ({ url }) => {
|
||||
return await createAssetFromUrlUsingDemoServer(host, url)
|
||||
})
|
||||
},
|
||||
[host]
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -116,3 +134,49 @@ function createDemoAssetStore(host: string): TLAssetStore {
|
|||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function createAssetFromUrlUsingDemoServer(host: string, url: string): Promise<TLAsset> {
|
||||
const urlHash = getHashForString(url)
|
||||
try {
|
||||
// First, try to get the meta data from our endpoint
|
||||
const fetchUrl = new URL(`${host}/bookmarks/unfurl`)
|
||||
fetchUrl.searchParams.set('url', url)
|
||||
|
||||
const meta = (await (await fetch(fetchUrl)).json()) as {
|
||||
description?: string
|
||||
image?: string
|
||||
favicon?: string
|
||||
title?: string
|
||||
} | null
|
||||
|
||||
return {
|
||||
id: AssetRecordType.createId(urlHash),
|
||||
typeName: 'asset',
|
||||
type: 'bookmark',
|
||||
props: {
|
||||
src: url,
|
||||
description: meta?.description ?? '',
|
||||
image: meta?.image ?? '',
|
||||
favicon: meta?.favicon ?? '',
|
||||
title: meta?.title ?? '',
|
||||
},
|
||||
meta: {},
|
||||
}
|
||||
} catch (error) {
|
||||
// Otherwise, fallback to a blank bookmark
|
||||
console.error(error)
|
||||
return {
|
||||
id: AssetRecordType.createId(urlHash),
|
||||
typeName: 'asset',
|
||||
type: 'bookmark',
|
||||
props: {
|
||||
src: url,
|
||||
description: '',
|
||||
image: '',
|
||||
favicon: '',
|
||||
title: '',
|
||||
},
|
||||
meta: {},
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,7 +2,6 @@ import {
|
|||
DEFAULT_SUPPORTED_IMAGE_TYPES,
|
||||
DEFAULT_SUPPORT_VIDEO_TYPES,
|
||||
DefaultSpinner,
|
||||
Editor,
|
||||
ErrorScreen,
|
||||
LoadingScreen,
|
||||
TLEditorComponents,
|
||||
|
@ -12,11 +11,11 @@ import {
|
|||
TldrawEditorStoreProps,
|
||||
useEditor,
|
||||
useEditorComponents,
|
||||
useEvent,
|
||||
useOnMount,
|
||||
useShallowArrayIdentity,
|
||||
useShallowObjectIdentity,
|
||||
} from '@tldraw/editor'
|
||||
import { useLayoutEffect, useMemo } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { TldrawHandles } from './canvas/TldrawHandles'
|
||||
import { TldrawScribble } from './canvas/TldrawScribble'
|
||||
import { TldrawSelectionBackground } from './canvas/TldrawSelectionBackground'
|
||||
|
@ -148,7 +147,7 @@ function InsideOfEditorAndUiContext({
|
|||
const toasts = useToasts()
|
||||
const msg = useTranslation()
|
||||
|
||||
const onMountEvent = useEvent((editor: Editor) => {
|
||||
useOnMount(() => {
|
||||
const unsubs: (void | (() => void) | undefined)[] = []
|
||||
|
||||
unsubs.push(...registerDefaultSideEffects(editor))
|
||||
|
@ -168,7 +167,10 @@ function InsideOfEditorAndUiContext({
|
|||
}
|
||||
)
|
||||
|
||||
// ...then we run the onMount prop, which may override the above
|
||||
// ...then we call the store's on mount which may override them...
|
||||
unsubs.push(editor.store.props.onEditorMount(editor))
|
||||
|
||||
// ...then we run the user's onMount prop, which may override things again.
|
||||
unsubs.push(onMount?.(editor))
|
||||
|
||||
return () => {
|
||||
|
@ -176,10 +178,6 @@ function InsideOfEditorAndUiContext({
|
|||
}
|
||||
})
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (editor) return onMountEvent?.(editor)
|
||||
}, [editor, onMountEvent])
|
||||
|
||||
const { Canvas } = useEditorComponents()
|
||||
const { ContextMenu } = useTldrawUiComponents()
|
||||
|
||||
|
|
|
@ -1499,6 +1499,7 @@ export interface TLStoreProps {
|
|||
assets: TLAssetStore;
|
||||
// (undocumented)
|
||||
defaultName: string;
|
||||
onEditorMount: (editor: unknown) => (() => void) | void;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
|
|
|
@ -94,6 +94,10 @@ export interface TLAssetStore {
|
|||
export interface TLStoreProps {
|
||||
defaultName: string
|
||||
assets: TLAssetStore
|
||||
/**
|
||||
* Called an {@link @tldraw/editor#Editor} connected to this store is mounted.
|
||||
*/
|
||||
onEditorMount: (editor: unknown) => void | (() => void)
|
||||
}
|
||||
|
||||
/** @public */
|
||||
|
|
Loading…
Reference in a new issue