From 01b292c5efa14f1b0c658f75e315a5e2a302ae00 Mon Sep 17 00:00:00 2001
From: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
<52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@sprout-oss.stage.blox.sqprod.co>
Date: Mon, 15 Jun 2026 22:05:54 -0700
Subject: [PATCH] fix(timeline): reserve inline image height from imeta dim to
stop load thrash
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Root cause of the first-load flicker tho saw: the inline message `
` had no
reserved height — it painted at height 0 and grew to its intrinsic size when the
bytes arrived. In the virtualized list every late image bumps the measured row
height, shifts its neighbors, and re-measures the virtualizer: a cascade of
reflows that flickers and thrashes while a media-heavy channel loads, and fights
the first-load stick-to-bottom because the document height keeps moving.
The stick-to-bottom settle-pin (6f6d2320) held the viewport THROUGH the growth
but did not stop the per-row reflow — this kills the growth at the source.
Fix: reserve the image's rendered box up front from the imeta `dim`
(WIDTHxHEIGHT), which is already parsed and was already used for video aspect
ratios. `reservedImageSize` scales dim to the display caps (max-w-sm/max-h-64 →
384×256, object-contain fit) and the `
` sets those as width/height
attributes, so the row's height is stable before load. `h-auto` keeps it
responsive under the width cap while the attributes establish the aspect ratio.
Falls back to natural load when `dim` is absent (can't reserve what we don't
know). The helper lives in markdownUtils.ts (pure, exported) so it's unit-tested
directly via the DOM lane.
Main timeline only; thread pane untouched. 5 DOM tests cover the dim→box math
(within caps, width-capped, height-capped, never-exceeds, malformed fallback).
Co-authored-by: Taylor Ho
Signed-off-by: Taylor Ho
---
desktop/src/shared/ui/markdown.tsx | 21 ++++++++-
desktop/src/shared/ui/markdownUtils.ts | 43 ++++++++++++++++++
.../shared/ui/reservedImageSize.dom.test.tsx | 45 +++++++++++++++++++
3 files changed, 107 insertions(+), 2 deletions(-)
create mode 100644 desktop/src/shared/ui/reservedImageSize.dom.test.tsx
diff --git a/desktop/src/shared/ui/markdown.tsx b/desktop/src/shared/ui/markdown.tsx
index 61e498f9e..2259d8f4c 100644
--- a/desktop/src/shared/ui/markdown.tsx
+++ b/desktop/src/shared/ui/markdown.tsx
@@ -51,6 +51,7 @@ import {
classifyChildren,
hasBlockMedia,
isImageOnlyParagraph,
+ reservedImageSize,
shallowArrayEqual,
} from "./markdownUtils";
import { resolveFileCard } from "./markdownFileCard";
@@ -246,10 +247,12 @@ type MarkdownVariant = "default" | "compact" | "tight";
*/
function ImageBlock({
alt,
+ dim,
resolvedSrc,
src,
}: {
alt: string | undefined;
+ dim?: string;
resolvedSrc: string | undefined;
src: string | undefined;
}) {
@@ -299,13 +302,22 @@ function ImageBlock({
});
};
+ const reserved = reservedImageSize(dim);
+
return (
<>
{/* biome-ignore lint/a11y/useKeyWithClickEvents: image opens lightbox on click; keyboard equivalent handled by lightbox close button */}
setLightboxOpen(true)}
onContextMenuCapture={handleContextMenu}
/>
@@ -911,7 +923,12 @@ function createMarkdownComponents(
}
return (
-
+
);
},
diff --git a/desktop/src/shared/ui/markdownUtils.ts b/desktop/src/shared/ui/markdownUtils.ts
index dab951285..886813102 100644
--- a/desktop/src/shared/ui/markdownUtils.ts
+++ b/desktop/src/shared/ui/markdownUtils.ts
@@ -65,3 +65,46 @@ export function shallowArrayEqual(a?: string[], b?: string[]): boolean {
}
return true;
}
+
+// Display caps for inline message images — must mirror the `max-h-64 max-w-sm`
+// (256×384px) Tailwind classes on the rendered
.
+export const INLINE_IMAGE_MAX_WIDTH = 384;
+export const INLINE_IMAGE_MAX_HEIGHT = 256;
+
+/**
+ * Compute the exact rendered box for an inline image from its imeta `dim`
+ * ("WIDTHxHEIGHT"), scaled to fit the display caps (same fit as `object-contain`
+ * within `max-w-sm`/`max-h-64`). Used to reserve the row's height via the
+ * width/height attributes BEFORE the bytes load — without it, image rows paint
+ * at height 0 then grow when the image arrives, thrashing the virtualized list
+ * (every late image re-measures and shifts its neighbors).
+ *
+ * Returns undefined when `dim` is missing/malformed/non-positive — we can't
+ * reserve what we don't know, so the natural load is the correct fallback.
+ */
+export function reservedImageSize(
+ dim: string | undefined,
+): { width: number; height: number } | undefined {
+ if (!dim) return undefined;
+ const match = dim.match(/^(\d+)x(\d+)$/i);
+ if (!match) return undefined;
+ const width = Number(match[1]);
+ const height = Number(match[2]);
+ if (
+ !Number.isFinite(width) ||
+ !Number.isFinite(height) ||
+ width <= 0 ||
+ height <= 0
+ ) {
+ return undefined;
+ }
+ const scale = Math.min(
+ 1,
+ INLINE_IMAGE_MAX_WIDTH / width,
+ INLINE_IMAGE_MAX_HEIGHT / height,
+ );
+ return {
+ width: Math.round(width * scale),
+ height: Math.round(height * scale),
+ };
+}
diff --git a/desktop/src/shared/ui/reservedImageSize.dom.test.tsx b/desktop/src/shared/ui/reservedImageSize.dom.test.tsx
new file mode 100644
index 000000000..2a021fb55
--- /dev/null
+++ b/desktop/src/shared/ui/reservedImageSize.dom.test.tsx
@@ -0,0 +1,45 @@
+import assert from "node:assert/strict";
+import { test } from "node:test";
+
+// Imported through the DOM lane (esbuild) so we can pull from markdownUtils.ts
+// directly even though it imports React — no stale inlined copy. Pure-math
+// helper, but it lives in a React module, hence the .dom lane.
+import {
+ INLINE_IMAGE_MAX_HEIGHT,
+ INLINE_IMAGE_MAX_WIDTH,
+ reservedImageSize,
+} from "./markdownUtils";
+
+test("returns undefined when dim is missing or malformed", () => {
+ assert.equal(reservedImageSize(undefined), undefined);
+ assert.equal(reservedImageSize(""), undefined);
+ assert.equal(reservedImageSize("800"), undefined);
+ assert.equal(reservedImageSize("800x"), undefined);
+ assert.equal(reservedImageSize("axb"), undefined);
+ assert.equal(reservedImageSize("0x600"), undefined);
+ assert.equal(reservedImageSize("800x0"), undefined);
+});
+
+test("small image within caps is reserved at its natural size", () => {
+ // 200x100 fits inside 384x256 → no scaling.
+ assert.deepEqual(reservedImageSize("200x100"), { width: 200, height: 100 });
+});
+
+test("wide image scales down to the width cap, preserving aspect ratio", () => {
+ // 800x400 (2:1). Width cap 384 → scale 0.48 → 384x192.
+ assert.deepEqual(reservedImageSize("800x400"), { width: 384, height: 192 });
+});
+
+test("tall image scales down to the height cap, preserving aspect ratio", () => {
+ // 400x800 (1:2). Height cap 256 → scale 0.32 → 128x256.
+ assert.deepEqual(reservedImageSize("400x800"), { width: 128, height: 256 });
+});
+
+test("reserved box never exceeds the display caps", () => {
+ const r = reservedImageSize("4000x3000");
+ assert.ok(r);
+ assert.ok(r.width <= INLINE_IMAGE_MAX_WIDTH);
+ assert.ok(r.height <= INLINE_IMAGE_MAX_HEIGHT);
+ // 4:3 → constrained by height (256) → 341x256.
+ assert.deepEqual(r, { width: 341, height: 256 });
+});