Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions apps/roam/src/components/canvas/Tldraw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,6 @@ import ToastListener, { dispatchToastEvent } from "./ToastListener";
import { CanvasDrawerPanel } from "./CanvasDrawer";
import { ClipboardPanel, ClipboardProvider } from "./Clipboard";
import internalError from "~/utils/internalError";
import { syncCanvasNodeTitlesOnLoad } from "~/utils/syncCanvasNodeTitlesOnLoad";
import { AUTO_CANVAS_RELATIONS_KEY } from "~/data/userSettings";
import { getSetting } from "~/utils/extensionSettings";
import { isPluginTimerReady, waitForPluginTimer } from "~/utils/pluginTimer";
Expand All @@ -117,6 +116,7 @@ import {
} from "./useCanvasStoreAdapterArgs";
import posthog from "posthog-js";
import { json, normalizeProps } from "~/utils/getBlockProps";
import { syncCanvasNodesOnLoad } from "~/utils/syncCanvasNodesOnLoad";

declare global {
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
Expand Down Expand Up @@ -1003,14 +1003,15 @@ const TldrawCanvasShared = ({

appRef.current = app;

void syncCanvasNodeTitlesOnLoad(
app,
allNodes.map((n) => n.type),
allRelationIds,
).catch((error) => {
void syncCanvasNodesOnLoad({
editor: app,
nodeTypeIds: allNodes.map((n) => n.type),
relationShapeTypeIds: allRelationIds,
extensionAPI,
}).catch((error) => {
internalError({
error,
type: "Canvas: Sync node titles on load",
type: "Canvas: Sync nodes on load",
});
});

Expand Down
64 changes: 47 additions & 17 deletions apps/roam/src/utils/calcCanvasNodeSizeAndImg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,15 @@ const getFirstImageByUid = (uid: string): string | null => {
return findFirstImage(tree);
};

const calcCanvasNodeSizeAndImg = async ({
const getNodeCanvasSettings = (nodeType: string): Record<string, string> => {
const allNodes = getDiscourseNodes();
const canvasSettings = Object.fromEntries(
allNodes.map((n) => [n.type, { ...n.canvasSettings }]),
);
return canvasSettings[nodeType] || {};
};
Comment thread
trangdoan982 marked this conversation as resolved.

export const getCanvasNodeKeyImageUrl = async ({
nodeText,
uid,
nodeType,
Expand All @@ -86,26 +94,16 @@ const calcCanvasNodeSizeAndImg = async ({
uid: string;
nodeType: string;
extensionAPI: OnloadArgs["extensionAPI"];
}) => {
const allNodes = getDiscourseNodes();
const canvasSettings = Object.fromEntries(
allNodes.map((n) => [n.type, { ...n.canvasSettings }]),
);
}): Promise<string> => {
const {
"query-builder-alias": qbAlias = "",
"key-image": isKeyImage = "",
"key-image-option": keyImageOption = "",
} = canvasSettings[nodeType] || {};

const { w, h } = measureCanvasNodeText({
...DEFAULT_STYLE_PROPS,
maxWidth: MAX_WIDTH,
text: nodeText,
});
} = getNodeCanvasSettings(nodeType);

if (!isKeyImage) return { w, h, imageUrl: "" };
if (!isKeyImage) return "";

let imageUrl;
let imageUrl: string | null;
if (keyImageOption === "query-builder") {
const parentUid = resolveQueryBuilderRef({
queryRef: qbAlias,
Expand All @@ -122,14 +120,46 @@ const calcCanvasNodeSizeAndImg = async ({
} else {
imageUrl = getFirstImageByUid(uid);
}
return imageUrl ?? "";
};

const calcCanvasNodeSizeAndImg = async ({
nodeText,
uid,
nodeType,
extensionAPI,
}: {
nodeText: string;
uid: string;
nodeType: string;
extensionAPI: OnloadArgs["extensionAPI"];
}) => {
const { w, h } = measureCanvasNodeText({
...DEFAULT_STYLE_PROPS,
maxWidth: MAX_WIDTH,
text: nodeText,
});

const imageUrl = await getCanvasNodeKeyImageUrl({
nodeText,
uid,
nodeType,
extensionAPI,
});

if (!imageUrl) return { w, h, imageUrl: "" };

try {
const { width, height } = await loadImage(imageUrl);
if (!width || !height || !Number.isFinite(width) || !Number.isFinite(height)) {
if (
!width ||
!height ||
!Number.isFinite(width) ||
!Number.isFinite(height)
) {
return { w, h, imageUrl: "" };
}

const aspectRatio = width / height;
const nodeImageHeight = w / aspectRatio;
const newHeight = h + nodeImageHeight;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import type { Editor } from "tldraw";
import type { OnloadArgs } from "roamjs-components/types";
import type { DiscourseNodeShape } from "~/components/canvas/DiscourseNodeUtil";
import calcCanvasNodeSizeAndImg, {
getCanvasNodeKeyImageUrl,
} from "./calcCanvasNodeSizeAndImg";

/**
* Query Roam for current :node/title or :block/string for each uid.
Expand Down Expand Up @@ -56,11 +60,37 @@ const deleteNodeShapeAndRelations = (
* - Updates shapes whose title changed
* - Removes shapes whose uid no longer exists in the graph
*/
export const syncCanvasNodesOnLoad = async ({
editor,
nodeTypeIds,
relationShapeTypeIds,
extensionAPI,
}: {
editor: Editor;
nodeTypeIds: string[];
relationShapeTypeIds: string[];
extensionAPI: OnloadArgs["extensionAPI"];
}): Promise<void> => {
const { discourseNodeShapes, uidToTitle } = await syncCanvasNodeTitlesOnLoad(
editor,
nodeTypeIds,
relationShapeTypeIds,
);
await syncCanvasKeyImagesOnLoad({
editor,
discourseNodeShapes,
uidToTitle,
extensionAPI,
});
};
export const syncCanvasNodeTitlesOnLoad = async (
editor: Editor,
nodeTypeIds: string[],
relationShapeTypeIds: string[],
): Promise<void> => {
): Promise<{
discourseNodeShapes: DiscourseNodeShape[];
uidToTitle: Map<string, string>;
}> => {
const nodeTypeSet = new Set(nodeTypeIds);
const relationIds = new Set(relationShapeTypeIds);
const allRecords = editor.store.allRecords();
Expand All @@ -72,7 +102,8 @@ export const syncCanvasNodeTitlesOnLoad = async (
) as DiscourseNodeShape[];

const uids = [...new Set(discourseNodeShapes.map((s) => s.props.uid))];
if (uids.length === 0) return;
if (uids.length === 0)
return { discourseNodeShapes: [], uidToTitle: new Map() };

const uidToTitle = await queryTitlesByUids(uids);

Expand Down Expand Up @@ -104,4 +135,66 @@ export const syncCanvasNodeTitlesOnLoad = async (
})),
);
}

return { discourseNodeShapes, uidToTitle };
};

const syncCanvasKeyImagesOnLoad = async ({
editor,
discourseNodeShapes,
uidToTitle,
extensionAPI,
}: {
editor: Editor;
discourseNodeShapes: DiscourseNodeShape[];
uidToTitle: Map<string, string>;
extensionAPI: OnloadArgs["extensionAPI"];
}): Promise<void> => {
const survivingShapes = discourseNodeShapes.filter((s) =>
uidToTitle.has(s.props.uid),
);
const imageUpdates: {
id: DiscourseNodeShape["id"];
type: string;
props: { imageUrl: string; w: number; h: number };
}[] = [];

// First pass: cheaply fetch imageUrls (no image loading) to find which shapes changed.
const urlResults = await Promise.all(
survivingShapes.map(async (shape) => {
const title = uidToTitle.get(shape.props.uid) ?? shape.props.title ?? "";
const imageUrl = await getCanvasNodeKeyImageUrl({
Comment thread
trangdoan982 marked this conversation as resolved.
Outdated
nodeText: title,
uid: shape.props.uid,
nodeType: shape.type,
extensionAPI,
});
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also running an individual query for each shape. This could be hundreds of queries.

We'll have to write a custom query for any nodes that have the key-image option of First image on page. So we can run one single query rather than a query for each node. Those with Query builder reference will probably still be a Promise.all, unfortunately.

return { shape, title, imageUrl };
}),
);

const changedShapes = urlResults.filter(
({ shape, imageUrl }) => (shape.props.imageUrl ?? "") !== imageUrl,
);

// Second pass: load images only for shapes whose URL changed, to compute new dimensions.
await Promise.all(
changedShapes.map(async ({ shape, title }) => {
const { w, h, imageUrl } = await calcCanvasNodeSizeAndImg({
Comment thread
trangdoan982 marked this conversation as resolved.
Outdated
Comment thread
trangdoan982 marked this conversation as resolved.
Outdated
nodeText: title,
Comment thread
trangdoan982 marked this conversation as resolved.
Outdated
uid: shape.props.uid,
nodeType: shape.type,
extensionAPI,
});
imageUpdates.push({
id: shape.id,
type: shape.type,
props: { imageUrl, w, h },
});
}),
);
Comment thread
trangdoan982 marked this conversation as resolved.
Outdated

if (imageUpdates.length > 0) {
editor.updateShapes(imageUpdates);
}
};
Loading