Skip to content

fix(mobile): play short-delay animation frames at browser speed - #9432

Merged
kalvinnchau merged 1 commit into
mainfrom
peon/mobile-gif-min-frame-delay
Nov 13, 2026
Merged

kalvinnchau merged 1 commit into
mainfrom
peon/mobile-gif-min-frame-delay

Conversation

@kalvinnchau

Copy link
Copy Markdown
Contributor

Some animated custom emoji play far faster on mobile than on desktop.

Browsers show any animation frame with a delay of 10 ms or less for 100 ms. Firefox started this (bug 207059). Chromium copied it (decoder comment: "use a duration of 100 ms for any frames that specify a duration of <= 10 ms"), and so did WebKit (changeset 73295). Flutter uses the delay as written (flutter/flutter#130684), so a GIF with 0 or 1 centisecond delays runs at the display refresh rate on mobile and at 10 fps on desktop.

MediaImageProvider now wraps multi-frame codecs so frames of 10 ms or less get 100 ms. Every other frame keeps its own delay. Still images are passed through unchanged. Custom emoji, avatars and attachments all render through this provider.

Tests: flutter test test/shared/relay/media_image_test.dart passes (24/24), and flutter analyze reports no issues on the changed files. The new test decodes a GIF with delays of 0, 1, 2 and 5 cs and expects 100, 100, 20 and 50 ms. It fails when the wrapper is removed.

🤖 Generated with Claude Code

browsers show an animation frame that asks for 10 ms or less for 100 ms
(firefox, chromium, webkit). flutter uses the delay as written, so gifs
with 0 or 1 centisecond delays raced at the display refresh rate on
mobile while desktop played them at 10 fps. wrap multi-frame codecs from
MediaImageProvider to apply the same rule.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Kalvin Chau <[email protected]>
@kalvinnchau
kalvinnchau requested a review from a team as a code owner November 13, 2026 00:14
@github-actions github-actions Bot added the codex-security-review-current The posted Codex security review matches its recorded range. label Nov 13, 2026
@github-actions

Copy link
Copy Markdown

🔐 Codex Security Review

Note: This is an automated, security-focused review generated by Codex.
Use it as a supplement to human review; false positives are possible.

Scope

  • Exact PR diff: 5bfbda4c3355771601b921f90d8941a7c80c9899...d4a165954dc8cac659f53362e9ba302a5d84db4f
  • Model: gpt-6.1-sol

💡 Click "edited" above to see earlier reviews for this PR.


Review Summary

Overall Risk: NONE

No concrete security, correctness, or reliability findings in the reviewed PR range.

Findings

No concrete security, correctness, or reliability findings were identified.

Notes

  • Review used read-only inspection. Builds and tests were not run, as requested.

Generated by Codex Security Review |
Requested by: @kalvinnchau |
Workflow run

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Carl, an automated reviewer, commenting via Wes's GitHub account.

Verdict: no blockers. Reviewed head d4a1659 against base 5bfbda4.

  • The ≤10 ms → 100 ms rule matches the browser behavior desktop gets from its webview. The wrapper only touches multi-frame codecs and forwards frameCount, repetitionCount and dispose, so caching, settle/eviction and the still-image path are unchanged.
  • Validation: Mobile checks, tests and Android CI are green on this head. The new GIF test covers both sides of the threshold.
  • Non-blocking: one avatar path skips the wrapper. Raster data:image/... avatars render through Image.memory (mobile/lib/shared/widgets/avatar_image.dart), not MediaImageProvider, so an animated data-URL avatar keeps raw timing. So does the local compose preview of an attached GIF. Both are rare, and neither needs to change here, but "avatars all render through this provider" is slightly broader than the code.

@kalvinnchau
kalvinnchau merged commit 0bbcf3e into main Nov 13, 2026
53 checks passed
@kalvinnchau
kalvinnchau deleted the peon/mobile-gif-min-frame-delay branch November 13, 2026 01:43
brow added a commit that referenced this pull request Nov 13, 2026
The two follow-ups from the pre-review of [#9432 GIF frame
timing](#9432), rebased onto main now
that it has landed.

**iOS reaction details sheet.** The sheet decodes custom emoji natively
(`NativeMessageImageLoader.decode`) and timed each frame as `max(0.02,
delay)`, so with [#9432 GIF frame
timing](#9432) a GIF with 0 or 1 cs
delays plays at 100 ms per frame in the timeline chip but 20 ms per
frame in the sheet opened from it. The decoder now shows frames of 10 ms
or less for 100 ms, as browsers and `MediaImageProvider` do, and keeps
longer frames as written. The 20 ms floor goes with it: browsers keep 11
to 19 ms frames as written too.

**Provider-level test.** The existing tests call
`withBrowserFrameTiming` directly, so removing the provider's call to it
left the suite green. A new test resolves a GIF through
`MediaImageProvider` with a mock HTTP client and pumps the scheduler's
app frames at explicit timestamps: the 10 ms first frame is still
showing at 50 ms and is replaced at 100 ms.

### Regressions

None expected. The sheet already summed the frame delays into one
`UIImage.animatedImage` duration, so only the per-frame value changes,
and only for frames of 10 ms or less, which ran at 20 ms before. Still
images and longer frames decode as before. The Dart test drives frame
callbacks with explicit timestamps rather than waiting on wall-clock
time.

### Validation

Added tests: `NativeMessageImageLoaderDecodeTests` in RunnerTests (a 0,
1, 2, 5 cs GIF decodes to a 0.27 s animation; a one-frame GIF stays
still) and `withBrowserFrameTiming is applied to the frames
MediaImageProvider plays` in `media_image_test.dart`, which fails when
the provider's call is removed (the second frame shows at 50 ms).

Full mobile suite: 4,780 tests passed. `flutter analyze`: no issues.
RunnerTests `NativeMessageImageLoaderDecodeTests` and
`NativeReactionDetailsTests`: all five pass on an iPhone 17 simulator
(Xcode build-for-testing of the Debug simulator app). No full-app run:
the change is one delay value in a decoder and is covered by the unit
tests above.

Signed-off-by: Tom Brow <[email protected]>
Co-authored-by: Tom Brow <[email protected]>
Co-authored-by: judge <7615ec4b7ddf99f0bb48e913297f99f8f772d289a1c9ef54e1d906dd55a93ba8@sprout-oss.stage.blox.sqprod.co>
bostonaholic added a commit that referenced this pull request Nov 13, 2026
Co-authored-by: mboston.Engineer <932d3b6ebea6cc83e091551ae46d1c7e7f8f7d0fa8e9cef5e6facc8544af413b@buzz.block.builderlab.xyz>

* origin/main:
  fix(mobile): require Builderlab consent for existing identities (#8823)
  feat(mobile): use HTTP/2 on iOS through app-wide URLSessions (#8897)
  fix(relay): publish a git push only when refs changed; check the protocol by refinement (#9425)
  fix(mobile): show shared chats under Solo (#9348)
  chore(release): release Buzz Desktop version 0.5.29 (#9456)
  feat(mobile): render messages with gpt_markdown 1.3.4 plusparse (#8945)
  fix(acp): bound observer retention by bytes (#7987) (#9436)
  Tune release codegen for faster/smaller binaries (#9434)
  feat(desktop): add dismissible banner pointing to the new buzz app (#9379)
  fix(mobile): keep publishing presence while the app is open (#9441)
  feat(mobile): share into Buzz from the iOS share sheet (#9433)
  fix(mobile): qualify every namesake mention and keep authored mention casing (#9325)
  fix(mobile): match browser GIF timing in the iOS reaction sheet (#9437)
  fix(mobile): play short-delay animation frames at browser speed (#9432)
  buzz-search: read profile names without the profile_name_fields function (#9397)
  buzz-db: build the community deletion lock key in Rust (#9396)
  fix(cli): pair for the active community, skip stopped desktops, accept a bare relay host (#9417)
  test(mobile): run push tests without a gateway dart-define (#9423)
  fix(mobile): never pop Home when archiving from channel settings (#9387)
  perf(acp): reduce observer CPU and lock overhead (#9381)

Signed-off-by: Matthew Boston <[email protected]>
bostonaholic added a commit that referenced this pull request Nov 13, 2026
…count-backfill

Co-authored-by: mboston.Engineer <932d3b6ebea6cc83e091551ae46d1c7e7f8f7d0fa8e9cef5e6facc8544af413b@buzz.block.builderlab.xyz>

* bostonaholic--agent-reply-count:
  fix(mobile): require Builderlab consent for existing identities (#8823)
  feat(mobile): use HTTP/2 on iOS through app-wide URLSessions (#8897)
  fix(relay): publish a git push only when refs changed; check the protocol by refinement (#9425)
  fix(mobile): show shared chats under Solo (#9348)
  chore(release): release Buzz Desktop version 0.5.29 (#9456)
  feat(mobile): render messages with gpt_markdown 1.3.4 plusparse (#8945)
  fix(acp): bound observer retention by bytes (#7987) (#9436)
  Tune release codegen for faster/smaller binaries (#9434)
  feat(desktop): add dismissible banner pointing to the new buzz app (#9379)
  fix(mobile): keep publishing presence while the app is open (#9441)
  feat(mobile): share into Buzz from the iOS share sheet (#9433)
  fix(mobile): qualify every namesake mention and keep authored mention casing (#9325)
  fix(mobile): match browser GIF timing in the iOS reaction sheet (#9437)
  fix(mobile): play short-delay animation frames at browser speed (#9432)
  buzz-search: read profile names without the profile_name_fields function (#9397)
  buzz-db: build the community deletion lock key in Rust (#9396)
  fix(cli): pair for the active community, skip stopped desktops, accept a bare relay host (#9417)
  test(mobile): run push tests without a gateway dart-define (#9423)
  fix(mobile): never pop Home when archiving from channel settings (#9387)
  perf(acp): reduce observer CPU and lock overhead (#9381)

Signed-off-by: Matthew Boston <[email protected]>

This branch was successfully deployed

1 active deployment
codex-review — d4a16595 Deployed Nov 13, 2026 by kalvinnchau via Run Codex Security Review #14761
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

codex-security-review-current The posted Codex security review matches its recorded range.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants