Skip to content

Commit 7dd2c8d

Browse files
committed
Enhance canvas sync functionality in Tldraw components
- Refactored Tldraw components to incorporate a per-canvas sync mode, allowing users to toggle between 'sync' and 'local' modes. - Updated the main menu to include a checkbox for sync mode, with visual feedback for availability. - Integrated new utilities for managing sync mode state and ensured proper propagation of sync settings across components. - Improved user experience with tooltips and notifications related to sync mode changes.
1 parent f1d7d61 commit 7dd2c8d

3 files changed

Lines changed: 120 additions & 23 deletions

File tree

apps/roam/src/components/canvas/Tldraw.tsx

Lines changed: 86 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ import {
7373
createNodeShapeTools,
7474
createNodeShapeUtils,
7575
} from "./DiscourseNodeUtil";
76-
import { hasRoamPersistedCanvasData, useRoamStore } from "./useRoamStore";
76+
import { useRoamStore } from "./useRoamStore";
7777
import {
7878
TLDRAW_CLOUDFLARE_SYNC_ENABLED,
7979
TLDRAW_CLOUDFLARE_SYNC_WS_BASE_URL,
@@ -115,6 +115,12 @@ import { TLRecord } from "@tldraw/tlschema";
115115
import { WHITE_LOGO_SVG } from "~/icons";
116116
import { BLOCK_REF_REGEX } from "roamjs-components/dom";
117117
import { defaultHandleExternalTextContent } from "./defaultHandleExternalTextContent";
118+
import {
119+
CanvasSyncMode,
120+
ensureCanvasSyncMode,
121+
getEffectiveCanvasSyncMode,
122+
setCanvasSyncMode,
123+
} from "./canvasSyncMode";
118124

119125
declare global {
120126
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
@@ -153,16 +159,48 @@ const TldrawCanvas = ({ title }: { title: string }) => {
153159
// In Roam, canvas identity is currently keyed by the page UID.
154160
// Room sync is graph/page encoded as an opaque base64url token.
155161
const pageUid = useMemo(() => getPageUidByPageTitle(title), [title]);
156-
const useCloudflareSync =
162+
const [canvasSyncMode, setCanvasSyncModeState] = useState<CanvasSyncMode>(
163+
() => getEffectiveCanvasSyncMode({ pageUid }),
164+
);
165+
const isCloudflareSyncAvailable =
157166
TLDRAW_CLOUDFLARE_SYNC_ENABLED &&
158-
!!TLDRAW_CLOUDFLARE_SYNC_WS_BASE_URL &&
159-
!hasRoamPersistedCanvasData(pageUid);
167+
!!TLDRAW_CLOUDFLARE_SYNC_WS_BASE_URL;
168+
const useCloudflareSync =
169+
canvasSyncMode === "sync" && isCloudflareSyncAvailable;
170+
171+
useEffect(() => {
172+
setCanvasSyncModeState(ensureCanvasSyncMode({ pageUid }));
173+
}, [pageUid]);
174+
175+
const onCanvasSyncModeChange = useCallback(
176+
(mode: CanvasSyncMode) => {
177+
setCanvasSyncMode({ pageUid, mode });
178+
setCanvasSyncModeState(mode);
179+
},
180+
[pageUid],
181+
);
160182

161183
if (useCloudflareSync) {
162-
return <TldrawCanvasCloudflare title={title} pageUid={pageUid} />;
184+
return (
185+
<TldrawCanvasCloudflare
186+
title={title}
187+
pageUid={pageUid}
188+
canvasSyncMode={canvasSyncMode}
189+
isCloudflareSyncAvailable={isCloudflareSyncAvailable}
190+
onCanvasSyncModeChange={onCanvasSyncModeChange}
191+
/>
192+
);
163193
}
164194

165-
return <TldrawCanvasRoam title={title} pageUid={pageUid} />;
195+
return (
196+
<TldrawCanvasRoam
197+
title={title}
198+
pageUid={pageUid}
199+
canvasSyncMode={canvasSyncMode}
200+
isCloudflareSyncAvailable={isCloudflareSyncAvailable}
201+
onCanvasSyncModeChange={onCanvasSyncModeChange}
202+
/>
203+
);
166204
};
167205

168206
type CanvasStoreAdapterArgs = {
@@ -232,33 +270,51 @@ const useCloudflareCanvasStore = ({
232270
const TldrawCanvasRoam = ({
233271
title,
234272
pageUid,
273+
canvasSyncMode,
274+
isCloudflareSyncAvailable,
275+
onCanvasSyncModeChange,
235276
}: {
236277
title: string;
237278
pageUid: string;
279+
canvasSyncMode: CanvasSyncMode;
280+
isCloudflareSyncAvailable: boolean;
281+
onCanvasSyncModeChange: (mode: CanvasSyncMode) => void;
238282
}) => {
239283
return (
240284
<TldrawCanvasShared
241285
title={title}
242286
pageUid={pageUid}
243287
useStoreAdapter={useRoamCanvasStore}
244288
isCloudflareSync={false}
289+
canvasSyncMode={canvasSyncMode}
290+
isCloudflareSyncAvailable={isCloudflareSyncAvailable}
291+
onCanvasSyncModeChange={onCanvasSyncModeChange}
245292
/>
246293
);
247294
};
248295

249296
const TldrawCanvasCloudflare = ({
250297
title,
251298
pageUid,
299+
canvasSyncMode,
300+
isCloudflareSyncAvailable,
301+
onCanvasSyncModeChange,
252302
}: {
253303
title: string;
254304
pageUid: string;
305+
canvasSyncMode: CanvasSyncMode;
306+
isCloudflareSyncAvailable: boolean;
307+
onCanvasSyncModeChange: (mode: CanvasSyncMode) => void;
255308
}) => {
256309
return (
257310
<TldrawCanvasShared
258311
title={title}
259312
pageUid={pageUid}
260313
useStoreAdapter={useCloudflareCanvasStore}
261314
isCloudflareSync={true}
315+
canvasSyncMode={canvasSyncMode}
316+
isCloudflareSyncAvailable={isCloudflareSyncAvailable}
317+
onCanvasSyncModeChange={onCanvasSyncModeChange}
262318
/>
263319
);
264320
};
@@ -268,11 +324,17 @@ const TldrawCanvasShared = ({
268324
pageUid,
269325
useStoreAdapter,
270326
isCloudflareSync,
327+
canvasSyncMode,
328+
isCloudflareSyncAvailable,
329+
onCanvasSyncModeChange,
271330
}: {
272331
title: string;
273332
pageUid: string;
274333
useStoreAdapter: (args: CanvasStoreAdapterArgs) => CanvasStoreAdapterResult;
275334
isCloudflareSync: boolean;
335+
canvasSyncMode: CanvasSyncMode;
336+
isCloudflareSyncAvailable: boolean;
337+
onCanvasSyncModeChange: (mode: CanvasSyncMode) => void;
276338
}) => {
277339
const appRef = useRef<Editor | null>(null);
278340
const lastInsertRef = useRef<VecModel>();
@@ -591,8 +653,18 @@ const TldrawCanvasShared = ({
591653
allNodes,
592654
allRelationNames,
593655
allAddReferencedNodeActions,
656+
canvasSyncMode,
657+
isCloudflareSyncAvailable,
658+
onCanvasSyncModeChange,
594659
}),
595-
[allNodes, allRelationNames, allAddReferencedNodeActions],
660+
[
661+
allNodes,
662+
allRelationNames,
663+
allAddReferencedNodeActions,
664+
canvasSyncMode,
665+
isCloudflareSyncAvailable,
666+
onCanvasSyncModeChange,
667+
],
596668
);
597669

598670
// UTILS
@@ -859,12 +931,14 @@ const TldrawCanvasShared = ({
859931
onDrop={handleDrop}
860932
>
861933
{isCloudflareSync && (
862-
<div
863-
className="pointer-events-none absolute right-3 top-3 z-20 opacity-90"
864-
title="Cloudflare Sync enabled"
934+
<Tooltip
935+
content="Sync mode is enabled. Collaborators on older builds may still open this canvas in local mode."
936+
hoverOpenDelay={100}
865937
>
866-
<Icon icon="cloud" size={12} className="text-green-500" />
867-
</div>
938+
<div className="absolute right-3 top-3 z-20 opacity-90">
939+
<Icon icon="cloud" size={12} className="text-green-500" />
940+
</div>
941+
</Tooltip>
868942
)}
869943
<style>{tldrawStyles}</style>
870944

apps/roam/src/components/canvas/uiOverrides.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
DefaultKeyboardShortcutsDialogContent,
1515
DefaultToolbarContent,
1616
TldrawUiMenuItem,
17+
TldrawUiMenuCheckboxItem,
1718
DefaultMainMenu,
1819
TldrawUiMenuGroup,
1920
useActions,
@@ -46,6 +47,8 @@ import { DISCOURSE_TOOL_SHORTCUT_KEY } from "~/data/userSettings";
4647
import { getSetting } from "~/utils/extensionSettings";
4748
import { CustomDefaultToolbar } from "./CustomDefaultToolbar";
4849
import { renderModifyNodeDialog } from "~/components/ModifyNodeDialog";
50+
import { CanvasSyncMode } from "./canvasSyncMode";
51+
import renderToast from "roamjs-components/components/Toast";
4952

5053
export const getOnSelectForShape = ({
5154
shape,
@@ -185,10 +188,16 @@ export const createUiComponents = ({
185188
allNodes,
186189
allAddReferencedNodeActions,
187190
allRelationNames,
191+
canvasSyncMode,
192+
isCloudflareSyncAvailable,
193+
onCanvasSyncModeChange,
188194
}: {
189195
allNodes: DiscourseNode[];
190196
allRelationNames: string[];
191197
allAddReferencedNodeActions: string[];
198+
canvasSyncMode: CanvasSyncMode;
199+
isCloudflareSyncAvailable: boolean;
200+
onCanvasSyncModeChange: (mode: CanvasSyncMode) => void;
192201
}): TLUiComponents => {
193202
return {
194203
Toolbar: (props) => {
@@ -234,11 +243,36 @@ export const createUiComponents = ({
234243
</TldrawUiMenuSubmenu>
235244
);
236245
};
246+
const onToggleSyncMode = (): void => {
247+
const nextMode: CanvasSyncMode =
248+
canvasSyncMode === "sync" ? "local" : "sync";
249+
onCanvasSyncModeChange(nextMode);
250+
if (nextMode === "sync") {
251+
renderToast({
252+
id: "tldraw-sync-mode-enabled-v0",
253+
intent: "warning",
254+
content: "Sync mode starts from a new blank sync canvas.",
255+
});
256+
}
257+
};
258+
const syncModeLabel = isCloudflareSyncAvailable
259+
? "Sync mode"
260+
: "Sync mode (backend unavailable)";
237261

238262
return (
239263
<DefaultMainMenu>
240264
<EditSubmenu />
241265
<CustomViewMenu /> {/* Replaced <ViewSubmenu /> */}
266+
<TldrawUiMenuGroup id="sync-mode">
267+
<TldrawUiMenuCheckboxItem
268+
id="toggle-sync-mode"
269+
label={syncModeLabel}
270+
checked={canvasSyncMode === "sync"}
271+
readonlyOk
272+
disabled={!isCloudflareSyncAvailable}
273+
onSelect={onToggleSyncMode}
274+
/>
275+
</TldrawUiMenuGroup>
242276
<ExportFileContentSubMenu />
243277
<ExtrasGroup />
244278
<PreferencesGroup />

apps/roam/src/components/canvas/useRoamStore.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,6 @@ export const isTLStoreSnapshot = (value: unknown): value is TLStoreSnapshot => {
3838
);
3939
};
4040

41-
/** True if the canvas has existing tldraw state persisted in Roam (any format). */
42-
export const hasRoamPersistedCanvasData = (pageUid: string): boolean => {
43-
const props = getBlockProps(pageUid) as Record<string, unknown>;
44-
const rjsqb =
45-
typeof props?.["roamjs-query-builder"] === "object"
46-
? (props["roamjs-query-builder"] as Record<string, unknown>)
47-
: {};
48-
const tldraw = rjsqb?.tldraw;
49-
return tldraw !== undefined && tldraw !== null;
50-
};
51-
5241
const fixShapeIndices = (
5342
data: SerializedStore<TLRecord>,
5443
): SerializedStore<TLRecord> => {

0 commit comments

Comments
 (0)