[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:
alex 2024-07-10 14:15:44 +01:00 committed by GitHub
parent 965bc10997
commit 627c84c2af
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 127 additions and 39 deletions

View file

@ -1,5 +1,5 @@
main = "src/worker.ts"
compatibility_date = "2024-06-25"
compatibility_date = "2024-06-20"
upload_source_maps = true
[dev]

View file

@ -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,

View file

@ -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} />
}

View file

@ -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[];

View file

@ -36,6 +36,7 @@ export {
ErrorScreen,
LoadingScreen,
TldrawEditor,
useOnMount,
type LoadingScreenProps,
type TLOnMountHandler,
type TldrawEditorBaseProps,

View file

@ -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) => {

View file

@ -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)
},
},
})
}

View file

@ -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'

View file

@ -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
}

View file

@ -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: {},
}
}
}

View file

@ -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()

View file

@ -1499,6 +1499,7 @@ export interface TLStoreProps {
assets: TLAssetStore;
// (undocumented)
defaultName: string;
onEditorMount: (editor: unknown) => (() => void) | void;
}
// @public (undocumented)

View file

@ -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 */