Repository navigation
fix(mobile): play short-delay animation frames at browser speed - #9432
Merged
Merged
Conversation
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]>
🔐 Codex Security Review
Review SummaryOverall Risk: NONE
FindingsNo concrete security, correctness, or reliability findings were identified. Notes
Generated by Codex Security Review | |
wesbillman
reviewed
Nov 13, 2026
wesbillman
left a comment
Collaborator
There was a problem hiding this comment.
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,repetitionCountanddispose, 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 throughImage.memory(mobile/lib/shared/widgets/avatar_image.dart), notMediaImageProvider, 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.
wesbillman
approved these changes
Nov 13, 2026
brow
approved these changes
Nov 13, 2026
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
MediaImageProvidernow 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.dartpasses (24/24), andflutter analyzereports 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