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