swap persistentboard with Tldraw native sync
This commit is contained in:
parent
7eaec27041
commit
ae4fe5faf8
|
|
@ -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<HTMLInputElement>) => {
|
|
||||||
setUserName(event.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const [persistedStore, setPersistedStore] = useLocalStorageState('board-store', { defaultValue: store }
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setPersistedStore(store);
|
|
||||||
}, [store]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ position: 'fixed', inset: 0 }}>
|
|
||||||
<Tldraw
|
|
||||||
//store={persistedStore}
|
|
||||||
store={store}
|
|
||||||
shapeUtils={shapeUtils}
|
|
||||||
overrides={uiOverrides}
|
|
||||||
components={components}
|
|
||||||
tools={tools}
|
|
||||||
onMount={(editor) => {
|
|
||||||
editor.registerExternalAssetHandler('url', unfurlBookmarkUrl)
|
|
||||||
editor.setCurrentTool('hand')
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{isChatBoxVisible && (
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={userName}
|
|
||||||
onChange={handleNameChange}
|
|
||||||
placeholder="Enter your name"
|
|
||||||
/>
|
|
||||||
<ChatBox
|
|
||||||
userName={userName}
|
|
||||||
roomId={roomId} // Added roomId
|
|
||||||
w={200} // Set appropriate width
|
|
||||||
h={200} // Set appropriate height
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{isVideoChatVisible && ( // Render the button to join video chat
|
|
||||||
<button onClick={() => setVideoChatVisible(false)} className="bg-green-500 text-white px-4 py-2 rounded">
|
|
||||||
Join Video Call
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// How does our server handle bookmark unfurling?
|
|
||||||
async function unfurlBookmarkUrl({ url }: { url: string }): Promise<TLBookmarkAsset> {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
@ -19,6 +19,7 @@ import { multiplayerAssetStore } from '../client/multiplayerAssetStore'
|
||||||
import { customSchema } from '../../worker/TldrawDurableObject'
|
import { customSchema } from '../../worker/TldrawDurableObject'
|
||||||
import { EmbedShape } from '@/shapes/EmbedShapeUtil'
|
import { EmbedShape } from '@/shapes/EmbedShapeUtil'
|
||||||
import { EmbedTool } from '@/tools/EmbedTool'
|
import { EmbedTool } from '@/tools/EmbedTool'
|
||||||
|
import { defaultShapeUtils, defaultBindingUtils } from 'tldraw'
|
||||||
|
|
||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { ChatBox } from '@/shapes/ChatBoxShapeUtil';
|
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 shapeUtils = [ChatBoxShape, VideoChatShape, EmbedShape]
|
||||||
const tools = [ChatBoxTool, VideoChatTool, EmbedTool]; // Array of tools
|
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() {
|
export function Board() {
|
||||||
const { slug } = useParams<{ slug: string }>();
|
const { slug } = useParams<{ slug: string }>();
|
||||||
const roomId = slug || 'default-room';
|
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<Editor | null>(null)
|
const [editor, setEditor] = useState<Editor | null>(null)
|
||||||
const { zoomToFrame, copyFrameLink, copyLocationLink, revertCamera } = useCameraControls(editor)
|
const { zoomToFrame, copyFrameLink, copyLocationLink, revertCamera } = useCameraControls(editor)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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<TLRecord[]>(`gset-${roomId}`, {
|
|
||||||
defaultValue: []
|
|
||||||
});
|
|
||||||
|
|
||||||
// Keep GSet instance in a ref to persist between renders
|
|
||||||
const gsetRef = useRef<GSet<TLRecord>>();
|
|
||||||
if (!gsetRef.current) {
|
|
||||||
gsetRef.current = new GSet<TLRecord>();
|
|
||||||
// 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<TLRecord>) => {
|
|
||||||
if (!gsetRef.current) return new Set<TLRecord>();
|
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -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<SerializedStore<TLRecord>>(`tldraw-room-${roomId}`, {
|
|
||||||
defaultValue: createTLStore({ schema: customSchema }).serialize()
|
|
||||||
});
|
|
||||||
|
|
||||||
const store = createTLStore({
|
|
||||||
schema: customSchema,
|
|
||||||
initialData: records,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
store,
|
|
||||||
records,
|
|
||||||
setRecords
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -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<TLRecord>) => {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -5,7 +5,7 @@ import {
|
||||||
TLRecord,
|
TLRecord,
|
||||||
TLShape,
|
TLShape,
|
||||||
createTLSchema,
|
createTLSchema,
|
||||||
// defaultBindingSchemas,
|
defaultBindingSchemas,
|
||||||
defaultShapeSchemas,
|
defaultShapeSchemas,
|
||||||
} from '@tldraw/tlschema'
|
} from '@tldraw/tlschema'
|
||||||
import { AutoRouter, IRequest, error } from 'itty-router'
|
import { AutoRouter, IRequest, error } from 'itty-router'
|
||||||
|
|
@ -20,11 +20,20 @@ import GSet from 'crdts/src/G-Set'
|
||||||
export const customSchema = createTLSchema({
|
export const customSchema = createTLSchema({
|
||||||
shapes: {
|
shapes: {
|
||||||
...defaultShapeSchemas,
|
...defaultShapeSchemas,
|
||||||
ChatBox: ChatBoxShape,
|
ChatBox: {
|
||||||
VideoChat: VideoChatShape,
|
props: ChatBoxShape.props,
|
||||||
Embed: EmbedShape
|
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:
|
// each whiteboard room is hosted in a DurableObject:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue