Skip to content

fix(attachments): send image dimensions so previews don't shift - #2262

Merged
kalvinnchau merged 1 commit into
block:mainfrom
kchung:chung/fix-layout-shifting-images
Nov 3, 2026
Merged

kalvinnchau merged 1 commit into
block:mainfrom
kchung:chung/fix-layout-shifting-images

Conversation

@kchung

@kchung kchung commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Why

Image attachments move the timeline when they load. This happens most with wide, short screenshots. Scroll up through a channel, and the content jumps as each image appears.

The relay measures each uploaded image and returns its size as dim. The app dropped that field when it sent the message. So images sent from the app had no imeta dim. Readers then reserved a 360×320 loading frame. When the image decoded, the frame shrank to the real size, and everything below it moved. Agent uploads already send dim, so their images did not move.

What

  • Images and videos sent from the app now carry their size.
  • Readers reserve the correct frame before the image loads, so the timeline does not move.
  • Readers on older app versions also get the correct frame, because the size travels with the message.

How

  • validateUploadResult keeps a well-formed dim from the relay's upload result. A malformed value is dropped and the upload continues.
  • attachmentMessage writes dim WxH into imeta. All app send paths use it: messages, replies, new DMs, Huddle and feedback.
  • The dev upload broker (browser-host/attachment-upload.mjs) forwards dim too, so broker and native uploads match.
  • The sender and the receiver (parseAttachments) share one IMETA_DIM pattern, so they cannot disagree about a valid value.

Before and after

before-after-labeled.mp4

Risk

Low. The change adds one optional, validated imeta field. The relay already accepts dim in imeta. Messages sent before this change still have no size, so they still move on first load.

Testing

  • New tests in attachments.test.ts send an upload result with dim through to imeta and back through the receiver. Malformed values are dropped. The test fails without the fix.
  • The broker test checks that dim is forwarded. It fails without the forward.
  • just desktop with the organization sign-in against the live community: sent wide screenshots, reloaded and scrolled past them. New images kept their frame. Older images still moved.

No browser journeys were added or removed.

🤖 Generated with Claude Code

The relay measures each uploaded image and returns its size as `dim`.
The app dropped that field, so sent images had no `imeta dim`.
Readers then reserved a 360x320 loading frame and resized it when the
image decoded, which moved the timeline while scrolling.

Keep a well-formed `dim` from the upload result and write it into
`imeta`. The dev upload broker forwards it too. Sender and receiver
share one `IMETA_DIM` pattern.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Kevin Chung <[email protected]>
@kchung
kchung marked this pull request as ready for review November 3, 2026 15:45
@kchung
kchung requested a review from a team as a code owner November 3, 2026 15:45

@kalvinnchau kalvinnchau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 Adversarial review at 299bf44: no inline findings.

Checked:

  • Every send path (channel/reply/DM/Huddle/Feedback, plus edits that copy imeta) goes through attachmentMessage → validateUploadResult, so dim is validated once against the same IMETA_DIM the receiver uses.
  • The native path (nativeAttachmentUpload) and broker path both return the relay descriptor verbatim to validateUploadResult; malformed dim is dropped without failing the upload.
  • The relay imeta allowlist accepts dim as a singleton key; the relay computes it from the decoded image (EXIF-bearing images are rejected upstream) or the transcoded mp4 track (ffmpeg autorotates during preparation), so the reserved frame matches the decoded media.

Ran at this head: vitest run src/features/relay/attachments.test.ts src/features/relay/fold.test.ts (123 passed), browser-host/attachment-upload.test.mjs (25 passed), biome check on the touched files (clean).

Residual risk, not blocking: for videos, MediaAttachment prefers attachment.dimensions over measured dimensions for the life of the element, and the relay reports coded avc1 width/height. A non-square-pixel (anamorphic) source would therefore keep a slightly wrong frame instead of correcting after load. Agent-sent videos already behave this way.

@kalvinnchau
kalvinnchau merged commit dbe7f24 into block:main Nov 3, 2026
41 checks passed
@kchung
kchung deleted the chung/fix-layout-shifting-images branch November 3, 2026 16:36
dr0uleau added a commit that referenced this pull request Nov 3, 2026
…xecution-support-for-native-buzz-33ea2bea

* origin/main: (74 commits)
  feat(host): harden byte lifecycle and add protected image upload (#2199)
  Updated onboarding (#2183)
  perf(relay): read each signed roster once in large channels (#2244)
  AIDA-2346: Keep spoilers hidden in reminder previews (#2261)
  fix: AIDA-2371 Keep sidebar selection pill aligned during resize (#2182)
  fix(relay): skip background rereads of profiles a read found absent (#2147)
  fix: prevent startup logo selection (#1823)
  fix(agents): send sign-in evidence with agent Git credentials (#2240)
  perf(mentions): stop scanning every member on each mention keystroke (#2246)
  fix(attachments): send image dimensions so previews don't shift (#2262)
  feat(shortcuts): add Slack-style hotkeys (#2170)
  perf(plugins): share host request clients across calls (#2243)
  fix(identity): keep federated sessions and agent grants in one keychain item (#2225)
  fix(profiles): evict the least recently requested profile (#2255)
  fix(unread): repair unread from the recovery tick only every 15 minutes (#2252)
  AIDA-2374: Use readable labels in chat titles (#2185)
  perf(profiles): keep verified names checked through a planned renewal (#2234)
  feat(plugins): let a plugin ask Buzz to review an install (#2059)
  fix(identity): sign FI builds in at first run through the native session (FOUNDATION: App.tsx, services.ts) (#2249)
  Place profile verification beside the verified name (#2224)
  ...

Signed-off-by: Daniel Rouleau <[email protected]>
dr0uleau added a commit that referenced this pull request Nov 3, 2026
* commit 'c8e882165': (50 commits)
  Scope execution targets by harness session
  Read durable execution state before admission
  feat(host): harden byte lifecycle and add protected image upload (#2199)
  Updated onboarding (#2183)
  perf(relay): read each signed roster once in large channels (#2244)
  AIDA-2346: Keep spoilers hidden in reminder previews (#2261)
  fix: AIDA-2371 Keep sidebar selection pill aligned during resize (#2182)
  fix(relay): skip background rereads of profiles a read found absent (#2147)
  fix: prevent startup logo selection (#1823)
  fix(agents): send sign-in evidence with agent Git credentials (#2240)
  perf(mentions): stop scanning every member on each mention keystroke (#2246)
  fix(attachments): send image dimensions so previews don't shift (#2262)
  feat(shortcuts): add Slack-style hotkeys (#2170)
  perf(plugins): share host request clients across calls (#2243)
  fix(identity): keep federated sessions and agent grants in one keychain item (#2225)
  fix(profiles): evict the least recently requested profile (#2255)
  fix(unread): repair unread from the recovery tick only every 15 minutes (#2252)
  AIDA-2374: Use readable labels in chat titles (#2185)
  perf(profiles): keep verified names checked through a planned renewal (#2234)
  feat(plugins): let a plugin ask Buzz to review an install (#2059)
  ...

Signed-off-by: Daniel Rouleau <[email protected]>
Co-authored-by: Codex <[email protected]>
ljknight added a commit to ljknight/buzz-app that referenced this pull request Nov 3, 2026
…ection

* origin/main: (39 commits)
  Authorize local apps to use Buzz MCP tools (block#2211)
  feat(bestie): choose the private channels Bestie can read (block#2036)
  Polish message actions, spacing, and membership notices (block#2258)
  test(unread): prevent startup catch-up in receipt reserve journey (block#2274)
  test(search): wait for rejected join clearance to settle (block#2278)
  test(reactions): provide stable emoji media for icon rendering (block#2279)
  feat(profiles): reserve profile room for an open channel's roster (block#2269)
  feat(host): harden byte lifecycle and add protected image upload (block#2199)
  Updated onboarding (block#2183)
  perf(relay): read each signed roster once in large channels (block#2244)
  AIDA-2346: Keep spoilers hidden in reminder previews (block#2261)
  fix: AIDA-2371 Keep sidebar selection pill aligned during resize (block#2182)
  fix(relay): skip background rereads of profiles a read found absent (block#2147)
  fix: prevent startup logo selection (block#1823)
  fix(agents): send sign-in evidence with agent Git credentials (block#2240)
  perf(mentions): stop scanning every member on each mention keystroke (block#2246)
  fix(attachments): send image dimensions so previews don't shift (block#2262)
  feat(shortcuts): add Slack-style hotkeys (block#2170)
  perf(plugins): share host request clients across calls (block#2243)
  fix(identity): keep federated sessions and agent grants in one keychain item (block#2225)
  ...

# Conflicts:
#	src/features/messages/ThreadPanel.tsx
arividar pushed a commit to minniai/minni-buzz-app that referenced this pull request Nov 4, 2026
…k#2262)

## Why

Image attachments move the timeline when they load. This happens most
with wide, short screenshots. Scroll up through a channel, and the
content jumps as each image appears.

The relay measures each uploaded image and returns its size as `dim`.
The app dropped that field when it sent the message. So images sent from
the app had no `imeta dim`. Readers then reserved a 360×320 loading
frame. When the image decoded, the frame shrank to the real size, and
everything below it moved. Agent uploads already send `dim`, so their
images did not move.

## What

- Images and videos sent from the app now carry their size.
- Readers reserve the correct frame before the image loads, so the
timeline does not move.
- Readers on older app versions also get the correct frame, because the
size travels with the message.

## How

- `validateUploadResult` keeps a well-formed `dim` from the relay's
upload result. A malformed value is dropped and the upload continues.
- `attachmentMessage` writes `dim WxH` into `imeta`. All app send paths
use it: messages, replies, new DMs, Huddle and feedback.
- The dev upload broker (`browser-host/attachment-upload.mjs`) forwards
`dim` too, so broker and native uploads match.
- The sender and the receiver (`parseAttachments`) share one `IMETA_DIM`
pattern, so they cannot disagree about a valid value.

## Before and after


https://github.com/user-attachments/assets/7db7de64-59e6-4214-925c-8208ef714207


## Risk

Low. The change adds one optional, validated `imeta` field. The relay
already accepts `dim` in `imeta`. Messages sent before this change still
have no size, so they still move on first load.

## Testing

- New tests in `attachments.test.ts` send an upload result with `dim`
through to `imeta` and back through the receiver. Malformed values are
dropped. The test fails without the fix.
- The broker test checks that `dim` is forwarded. It fails without the
forward.
- `just desktop` with the organization sign-in against the live
community: sent wide screenshots, reloaded and scrolled past them. New
images kept their frame. Older images still moved.

No browser journeys were added or removed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: Kevin Chung <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants