diff --git a/apps/bemo-worker/wrangler.toml b/apps/bemo-worker/wrangler.toml index 2f80f6cf6..b7ab477dd 100644 --- a/apps/bemo-worker/wrangler.toml +++ b/apps/bemo-worker/wrangler.toml @@ -1,5 +1,5 @@ main = "src/worker.ts" -compatibility_date = "2024-06-25" +compatibility_date = "2024-06-20" upload_source_maps = true [dev] diff --git a/apps/dotcom/src/components/MultiplayerEditor.tsx b/apps/dotcom/src/components/MultiplayerEditor.tsx index b768721d4..09e911377 100644 --- a/apps/dotcom/src/components/MultiplayerEditor.tsx +++ b/apps/dotcom/src/components/MultiplayerEditor.tsx @@ -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, diff --git a/apps/dotcom/src/components/TemporaryBemoDevEditor.tsx b/apps/dotcom/src/components/TemporaryBemoDevEditor.tsx index f2a8cb973..6cda934f8 100644 --- a/apps/dotcom/src/components/TemporaryBemoDevEditor.tsx +++ b/apps/dotcom/src/components/TemporaryBemoDevEditor.tsx @@ -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 } diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index 13a44273a..1b3a8135c 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -487,7 +487,7 @@ export function counterClockwiseAngleDist(a0: number, a1: number): number; export function createSessionStateSnapshotSignal(store: TLStore): Signal; // @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; defaultName?: string; initialData?: SerializedStore; + 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[]; diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index 033f345d0..bb6fd7722 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -36,6 +36,7 @@ export { ErrorScreen, LoadingScreen, TldrawEditor, + useOnMount, type LoadingScreenProps, type TLOnMountHandler, type TldrawEditorBaseProps, diff --git a/packages/editor/src/lib/TldrawEditor.tsx b/packages/editor/src/lib/TldrawEditor.tsx index b2138d1d7..8cd406627 100644 --- a/packages/editor/src/lib/TldrawEditor.tsx +++ b/packages/editor/src/lib/TldrawEditor.tsx @@ -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
{children}
} -function useOnMount(onMount?: TLOnMountHandler) { +/** @internal */ +export function useOnMount(onMount?: TLOnMountHandler) { const editor = useEditor() const onMountEvent = useEvent((editor: Editor) => { diff --git a/packages/editor/src/lib/config/createTLStore.ts b/packages/editor/src/lib/config/createTLStore.ts index 2461eb606..c3f3fa758 100644 --- a/packages/editor/src/lib/config/createTLStore.ts +++ b/packages/editor/src/lib/config/createTLStore.ts @@ -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 + + /** 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) + }, }, }) } diff --git a/packages/sync-react/src/index.ts b/packages/sync-react/src/index.ts index 1c4c9fe87..080a1208b 100644 --- a/packages/sync-react/src/index.ts +++ b/packages/sync-react/src/index.ts @@ -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' diff --git a/packages/sync-react/src/useRemoteSyncClient.ts b/packages/sync-react/src/useMultiplayerSync.ts similarity index 93% rename from packages/sync-react/src/useRemoteSyncClient.ts rename to packages/sync-react/src/useMultiplayerSync.ts index 1482381c9..ee27db439 100644 --- a/packages/sync-react/src/useRemoteSyncClient.ts +++ b/packages/sync-react/src/useMultiplayerSync.ts @@ -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 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['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 /* @internal */ trackAnalyticsEvent?(name: string, data: { [key: string]: any }): void assets?: Partial + onEditorMount?: (editor: Editor) => void } diff --git a/packages/sync-react/src/useDemoSyncClient.ts b/packages/sync-react/src/useMutliplayerDemo.ts similarity index 67% rename from packages/sync-react/src/useDemoSyncClient.ts rename to packages/sync-react/src/useMutliplayerDemo.ts index 62c601723..d16bf3f7e 100644 --- a/packages/sync-react/src/useDemoSyncClient.ts +++ b/packages/sync-react/src/useMutliplayerDemo.ts @@ -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 /** @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 { + 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: {}, + } + } +} diff --git a/packages/tldraw/src/lib/Tldraw.tsx b/packages/tldraw/src/lib/Tldraw.tsx index 0f2ffd0cd..4a7dc3ca2 100644 --- a/packages/tldraw/src/lib/Tldraw.tsx +++ b/packages/tldraw/src/lib/Tldraw.tsx @@ -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() diff --git a/packages/tlschema/api-report.md b/packages/tlschema/api-report.md index acf42c0a0..d3bd9fb49 100644 --- a/packages/tlschema/api-report.md +++ b/packages/tlschema/api-report.md @@ -1499,6 +1499,7 @@ export interface TLStoreProps { assets: TLAssetStore; // (undocumented) defaultName: string; + onEditorMount: (editor: unknown) => (() => void) | void; } // @public (undocumented) diff --git a/packages/tlschema/src/TLStore.ts b/packages/tlschema/src/TLStore.ts index 6b2681c68..dfe8c6709 100644 --- a/packages/tlschema/src/TLStore.ts +++ b/packages/tlschema/src/TLStore.ts @@ -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 */