From 111be039074aed93b5062038f4ba2427c728866e Mon Sep 17 00:00:00 2001 From: Jeff Emmett <46964190+Jeff-Emmett@users.noreply.github.com> Date: Sat, 7 Dec 2024 15:23:56 -0500 Subject: [PATCH] swap persistentboard with Tldraw native sync --- src/components/Board copy.tsx | 123 ------------------------------- src/components/Board.tsx | 16 ++-- src/hooks/useGSetState.ts | 40 ---------- src/hooks/useLocalStorageRoom.ts | 20 ----- src/hooks/usePersistentBoard.ts | 92 ----------------------- worker/TldrawDurableObject.ts | 19 +++-- 6 files changed, 24 insertions(+), 286 deletions(-) delete mode 100644 src/components/Board copy.tsx delete mode 100644 src/hooks/useGSetState.ts delete mode 100644 src/hooks/useLocalStorageRoom.ts delete mode 100644 src/hooks/usePersistentBoard.ts diff --git a/src/components/Board copy.tsx b/src/components/Board copy.tsx deleted file mode 100644 index b0c851d..0000000 --- a/src/components/Board copy.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import { useSync } from '@tldraw/sync' -import { - AssetRecordType, - getHashForString, - TLBookmarkAsset, - Tldraw, - // useLocalStorageState, -} from 'tldraw' -import { useParams } from 'react-router-dom' -import useLocalStorageState from 'use-local-storage-state' -import { ChatBoxTool } from '@/tools/ChatBoxTool' -import { ChatBoxShape } from '@/shapes/ChatBoxShapeUtil' -import { VideoChatTool } from '@/tools/VideoChatTool' -import { VideoChatShape } from '@/shapes/VideoChatShapeUtil' -import { multiplayerAssetStore } from '../client/multiplayerAssetStore' -import { customSchema } from '../../worker/TldrawDurableObject' -import { EmbedShape } from '@/shapes/EmbedShapeUtil' -import { EmbedTool } from '@/tools/EmbedTool' - -import React, { useEffect, useState } from 'react'; -import { ChatBox } from '@/shapes/ChatBoxShapeUtil'; -import { components, uiOverrides } from '@/ui-overrides' - -const WORKER_URL = `https://jeffemmett-canvas.jeffemmett.workers.dev` - -const shapeUtils = [ChatBoxShape, VideoChatShape, EmbedShape] -const tools = [ChatBoxTool, VideoChatTool, EmbedTool]; // Array of tools - -export function Board() { - const { slug } = useParams<{ slug: string }>(); // Ensure this is inside the Board component - const roomId = slug || 'default-room'; // Declare roomId here - - const store = useSync({ - uri: `${WORKER_URL}/connect/${roomId}`, - assets: multiplayerAssetStore, - shapeUtils: shapeUtils, - schema: customSchema, - }); - - const [isChatBoxVisible, setChatBoxVisible] = useState(false); - const [userName, setUserName] = useState(''); - const [isVideoChatVisible, setVideoChatVisible] = useState(false); // Added state for video chat visibility - - const handleNameChange = (event: React.ChangeEvent) => { - setUserName(event.target.value); - }; - - const [persistedStore, setPersistedStore] = useLocalStorageState('board-store', { defaultValue: store } - ) - - useEffect(() => { - setPersistedStore(store); - }, [store]); - - return ( -
- { - editor.registerExternalAssetHandler('url', unfurlBookmarkUrl) - editor.setCurrentTool('hand') - }} - /> - {isChatBoxVisible && ( -
- - -
- )} - {isVideoChatVisible && ( // Render the button to join video chat - - )} -
- ) -} - -// How does our server handle bookmark unfurling? -async function unfurlBookmarkUrl({ url }: { url: string }): Promise { - const asset: TLBookmarkAsset = { - id: AssetRecordType.createId(getHashForString(url)), - typeName: 'asset', - type: 'bookmark', - meta: {}, - props: { - src: url, - description: '', - image: '', - favicon: '', - title: '', - }, - } - - try { - const response = await fetch(`${WORKER_URL}/unfurl?url=${encodeURIComponent(url)}`) - const data = await response.json() as { description: string, image: string, favicon: string, title: string } - - asset.props.description = data?.description ?? '' - asset.props.image = data?.image ?? '' - asset.props.favicon = data?.favicon ?? '' - asset.props.title = data?.title ?? '' - } catch (e) { - console.error(e) - } - - return asset -} diff --git a/src/components/Board.tsx b/src/components/Board.tsx index 669f970..dc2ee77 100644 --- a/src/components/Board.tsx +++ b/src/components/Board.tsx @@ -19,6 +19,7 @@ import { multiplayerAssetStore } from '../client/multiplayerAssetStore' import { customSchema } from '../../worker/TldrawDurableObject' import { EmbedShape } from '@/shapes/EmbedShapeUtil' import { EmbedTool } from '@/tools/EmbedTool' +import { defaultShapeUtils, defaultBindingUtils } from 'tldraw' import React, { useState, useEffect, useCallback } from 'react'; import { ChatBox } from '@/shapes/ChatBoxShapeUtil'; @@ -32,16 +33,19 @@ export const WORKER_URL = 'https://jeffemmett-canvas.jeffemmett.workers.dev'; const shapeUtils = [ChatBoxShape, VideoChatShape, EmbedShape] const tools = [ChatBoxTool, VideoChatTool, EmbedTool]; // Array of tools -// Add these imports -import { useGSetState } from '@/hooks/useGSetState'; -import { useLocalStorageRoom } from '@/hooks/useLocalStorageRoom'; -import { usePersistentBoard } from '@/hooks/usePersistentBoard'; - export function Board() { const { slug } = useParams<{ slug: string }>(); const roomId = slug || 'default-room'; - const store = usePersistentBoard(roomId); + + const store = useSync({ + uri: `${WORKER_URL}/connect/${roomId}`, + assets: multiplayerAssetStore, + shapeUtils: [...shapeUtils, ...defaultShapeUtils], + // Add default bindings if you're using them + bindingUtils: [...defaultBindingUtils], + }) + const [editor, setEditor] = useState(null) const { zoomToFrame, copyFrameLink, copyLocationLink, revertCamera } = useCameraControls(editor) diff --git a/src/hooks/useGSetState.ts b/src/hooks/useGSetState.ts deleted file mode 100644 index e825fa1..0000000 --- a/src/hooks/useGSetState.ts +++ /dev/null @@ -1,40 +0,0 @@ -import useLocalStorageState from 'use-local-storage-state'; -import GSet from 'crdts/src/G-Set'; -import { TLRecord } from 'tldraw'; -import { useRef, useCallback } from 'react'; - -export function useGSetState(roomId: string) { - const [localSet, setLocalSet] = useLocalStorageState(`gset-${roomId}`, { - defaultValue: [] - }); - - // Keep GSet instance in a ref to persist between renders - const gsetRef = useRef>(); - if (!gsetRef.current) { - gsetRef.current = new GSet(); - // Initialize G-Set with local data - if (localSet && Array.isArray(localSet)) { - localSet.forEach(record => gsetRef.current?.add(record)); - } - } - - const addRecord = useCallback((record: TLRecord) => { - if (!gsetRef.current) return; - gsetRef.current.add(record); - setLocalSet(Array.from(gsetRef.current.values())); - }, [setLocalSet]); - - const merge = useCallback((remoteSet: Set) => { - if (!gsetRef.current) return new Set(); - remoteSet.forEach(record => gsetRef.current?.add(record)); - setLocalSet(Array.from(gsetRef.current.values())); - return gsetRef.current.values(); - }, [setLocalSet]); - - return { - values: gsetRef.current.values(), - add: addRecord, - merge, - localSet - }; -} \ No newline at end of file diff --git a/src/hooks/useLocalStorageRoom.ts b/src/hooks/useLocalStorageRoom.ts deleted file mode 100644 index 946fef5..0000000 --- a/src/hooks/useLocalStorageRoom.ts +++ /dev/null @@ -1,20 +0,0 @@ -import useLocalStorageState from 'use-local-storage-state'; -import { TLRecord, createTLStore, SerializedStore } from 'tldraw'; -import { customSchema } from '../../worker/TldrawDurableObject'; - -export function useLocalStorageRoom(roomId: string) { - const [records, setRecords] = useLocalStorageState>(`tldraw-room-${roomId}`, { - defaultValue: createTLStore({ schema: customSchema }).serialize() - }); - - const store = createTLStore({ - schema: customSchema, - initialData: records, - }); - - return { - store, - records, - setRecords - }; -} \ No newline at end of file diff --git a/src/hooks/usePersistentBoard.ts b/src/hooks/usePersistentBoard.ts deleted file mode 100644 index 128bf51..0000000 --- a/src/hooks/usePersistentBoard.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { useSync } from '@tldraw/sync' -import { useState, useEffect, useCallback, useRef } from 'react' -import { customSchema } from '../../worker/TldrawDurableObject' -import { multiplayerAssetStore } from '../client/multiplayerAssetStore' -import { useGSetState } from './useGSetState' -import { useLocalStorageRoom } from './useLocalStorageRoom' -import { TLRecord } from 'tldraw' -import { WORKER_URL } from '../components/Board' - -export function usePersistentBoard(roomId: string) { - const [isOnline, setIsOnline] = useState(navigator.onLine) - const { store: localStore, records, setRecords } = useLocalStorageRoom(roomId) - const { values, add, merge } = useGSetState(roomId) - const initialSyncRef = useRef(false) - const mergeInProgressRef = useRef(false) - - const syncedStore = useSync({ - uri: `${WORKER_URL.replace('https://', 'wss://')}/connect/${roomId}`, - schema: customSchema, - assets: multiplayerAssetStore, - }) - - useEffect(() => { - const handleOnline = () => setIsOnline(true) - const handleOffline = () => setIsOnline(false) - - window.addEventListener('online', handleOnline) - window.addEventListener('offline', handleOffline) - - return () => { - window.removeEventListener('online', handleOnline) - window.removeEventListener('offline', handleOffline) - } - }, []) - - const mergeRecords = useCallback((records: Set) => { - if (mergeInProgressRef.current || records.size === 0) return - - try { - mergeInProgressRef.current = true - merge(records) - if (!isOnline && localStore) { - setRecords(localStore.serialize()) - } - } finally { - mergeInProgressRef.current = false - } - }, [isOnline, localStore, merge, setRecords]) - - useEffect(() => { - if (!syncedStore?.store || !localStore) return - - if (isOnline && !initialSyncRef.current) { - initialSyncRef.current = true - const serverRecords = Object.values(syncedStore.store.allRecords()) - if (serverRecords.length > 0) { - mergeRecords(new Set(serverRecords)) - } - - const unsubscribe = syncedStore.store.listen((event) => { - if ('changes' in event) { - const changedRecords = Object.values(event.changes) - if (changedRecords.length > 0) { - mergeRecords(new Set(changedRecords)) - } - } - }) - - return () => unsubscribe() - } else if (!isOnline) { - const currentRecords = Object.values(localStore.allRecords()) - if (currentRecords.length > 0) { - mergeRecords(new Set(currentRecords)) - } - } - }, [isOnline, syncedStore?.store, localStore, mergeRecords]) - - const addRecord = useCallback((record: TLRecord) => { - if (!record) return - add(record) - if (!isOnline && localStore) { - setRecords(localStore.serialize()) - } - }, [add, isOnline, localStore, setRecords]) - - return { - store: isOnline ? syncedStore?.store : localStore, - isOnline, - addRecord, - mergeRecords - } -} \ No newline at end of file diff --git a/worker/TldrawDurableObject.ts b/worker/TldrawDurableObject.ts index 7bf5ce0..2f97034 100644 --- a/worker/TldrawDurableObject.ts +++ b/worker/TldrawDurableObject.ts @@ -5,7 +5,7 @@ import { TLRecord, TLShape, createTLSchema, - // defaultBindingSchemas, + defaultBindingSchemas, defaultShapeSchemas, } from '@tldraw/tlschema' import { AutoRouter, IRequest, error } from 'itty-router' @@ -20,11 +20,20 @@ import GSet from 'crdts/src/G-Set' export const customSchema = createTLSchema({ shapes: { ...defaultShapeSchemas, - ChatBox: ChatBoxShape, - VideoChat: VideoChatShape, - Embed: EmbedShape + ChatBox: { + props: ChatBoxShape.props, + migrations: ChatBoxShape.migrations, + }, + VideoChat: { + props: VideoChatShape.props, + migrations: VideoChatShape.migrations, + }, + Embed: { + props: EmbedShape.props, + migrations: EmbedShape.migrations, + }, }, - // bindings: { ...defaultBindingSchemas }, + bindings: defaultBindingSchemas, }) // each whiteboard room is hosted in a DurableObject: