Skip to content

Support PNG and HEIC photos in the native iOS zoom gallery - #9314

Merged
brow merged 2 commits into
mainfrom
kennylopez-ios-photo-formats-20261110
Nov 10, 2026
Merged

brow merged 2 commits into
mainfrom
kennylopez-ios-photo-formats-20261110

Conversation

@klopez4212

Copy link
Copy Markdown
Contributor

Summary

The opt-in iOS zoom gallery rejected PNG and HEIC/HEIF photos, so photo stacks containing those files opened the Flutter fallback. Accept these formats by MIME type or file extension, including uppercase extensions and URLs with query strings.

Preserve animated PNG playback by forwarding continuing decoded frames to the selected native page. Keep the existing five-photo load window, coalesce frames while the bridge is busy, skip offscreen frame encoding, and release listeners on eviction or dismissal. Native updates reject stale sessions, offscreen pages, malformed frames, and closed viewers.

BUZZ_NATIVE_PHOTO_ZOOM remains opt-in. iOS below 18 and unsupported formats retain the existing gallery.

Related issue

Follow-up to #9259. No matching open issue or PR found; #7666 concerns desktop HEIC upload tiling, a separate path.

Testing

  • just mobile-install mobile-check mobile-test passed.
  • Full mobile suite with BUZZ_NATIVE_PHOTO_ZOOM=true: 4,623 passed, 68 skipped. Standard configuration: 4,599 passed, 92 skipped; push configuration and secp256k1 package tests also passed.
  • Real PNG and HEIC decoding through the production presentation bridge; real two-frame APNG decoding with continuing-frame and listener-cleanup assertions.
  • scripts/test-mobile-photo-zoom.sh on an iOS 27 simulator exercises the production native controller, including frame updates, stale-page rejection, malformed input and teardown.

No physical-device or end-to-end zoom recording captured. This extends accepted image formats and playback without changing the gallery layout.

Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
Signed-off-by: kenny lopez <[email protected]>
@klopez4212
klopez4212 requested a review from a team as a code owner November 10, 2026 03:36
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-10T03:39:37.776424Z 8531e29 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions github-actions Bot added the codex-security-review-current The posted Codex security review matches its recorded range. label Nov 10, 2026
@github-actions

github-actions Bot commented Nov 10, 2026 •

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: 6384c6cdfa764fe393ac5860e00f414cc9d85804...84e1ec24cc8f28504f95c2cfdeef76df69403d7d
  • Model: gpt-6.1-sol

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


Review Summary

Overall Risk: LOW

One animation correctness issue in the optional native iOS gallery.

Findings

[LOW] Preserve the latest animation frame when changing pages

  • Category: Reliability
  • Location: mobile/lib/features/channels/native_photo_zoom_preview.dart:231 (source)
  • Description: Full-image listeners remain attached for nearby pages, but this branch discards subsequent frames while their page is unselected. Open a finite APNG, switch to an adjacent photo before playback finishes, wait for its remaining frames, then return. The retained load has completed playback, select only changes the index, and the native image cache prevents another load. No remaining frame updates the obsolete image.
  • Impact: Finite APNGs can remain frozen on an intermediate frame until their load is evicted or the gallery is reopened. An animation prefetched before selection can remain on its first frame.
  • Recommendation: Retain the latest offscreen frame and deliver it when selected, or pause and resume playback with page selection. Add coverage for a finite APNG completing while another page is selected.

Notes

  • Review used read-only source inspection. Builds and tests were not executed, as instructed.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8531e295bf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +231 to +233
if (disposed || (first.isCompleted && !shouldForwardFrame())) {
info.dispose();
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Suspend offscreen animated image streams

When an animated PNG has produced its first frame and the user selects another nearby photo, this branch disposes each subsequent ImageInfo but leaves the ImageStreamListener attached. Flutter's multi-frame completer therefore continues decoding every frame for each previously viewed photo still inside the five-item window, wasting CPU and battery for content that cannot be displayed. Detach or pause these listeners on deselection and reattach only when their photo becomes selected again.

AGENTS.md reference: AGENTS.md:L249-L256

Useful? React with 👍 / 👎.

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.

[P2] Preserve the latest APNG frame across selection changes

This also affects displayed content: a finite APNG can finish while another nearby photo is selected, then remain on an outdated frame when reopened. Selection does not forward a retained frame or restart decoding, and the native image cache suppresses another load. Could we retain the latest frame for reselection or manage playback across selection changes?

Posted by Codex (gpt-6.1-sol) on behalf of kylez

Comment on lines +354 to +357
images[value] = image
for page in pager.children.compactMap({ $0 as? NativePhotoPage }) where page.index == value {
page.setImage(image)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep the dismissal anchor on the current animation frame

When an APNG advances, updateFrame refreshes only the page and cache. The zoom transition still targets the anchor UIImageView, whose image is updated only through syncSelection after the initial load or a page change, so dismissing after the animation advances zooms into the stale first frame and visibly snaps at the end. Propagate continuing frames to the anchor as well, without re-running the selection request.

Useful? React with 👍 / 👎.

@jedwards27 jedwards27 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.

🤖 :bot: Jude’s code review agent — changes recommended (GitHub COMMENT per repository agent-review policy).

Reviewed: 44b3c4823f0cf1f7715f85dbf5566cb64e848162..8531e295bf009418bb695a1cde706057698a2684 (exact head). Risk: high — native/Flutter async media playback and session lifetime.

Blocking P2 — an APNG can rewind on a delayed first-image reply. mobile/ios/Runner/NativePhotoZoomCoordinator.swift:335–347 installs the pending initial image response unconditionally, even if a continuing frame has already updated images[value] and the visible page at :352–357. The Dart bridge completes the first-image result and forwards later frames as a separate imageFrame method call (mobile/lib/features/channels/native_photo_zoom_preview.dart:278–287); there is no per-index frame-order fence in the native controller. If the frame method call is handled before the pending reply callback, the later callback replaces the newer frame with the older first frame, and it remains there until another frame arrives. Existing native coverage applies the later frame after draining the original image replies, so it misses this ordering (mobile/ios/NativeControlTests/NativePhotoZoomTests.swift:35–48). Author action: fence late first-image/preview completions against a newer frame (or otherwise guarantee their application order), and add a native regression delivering a blue continuing frame before completing the held red initial image request, asserting blue remains visible. The five-item window, stale-session checks, unsupported-format fallback and listener cleanup otherwise show no separate concrete defect in the assigned format/UI and systems reviews.

Verification: clean exact-head source trace and git diff --check reported by both assigned reviewers; I independently checked the callback and frame-update code against the immutable GitHub head. Exact-head GitHub Mobile Checks, three test shards, Android, Swift Native/Release and security review are green (run 42750553085 and security run 42750552936). The independent local Flutter/native fixture did not complete: Flutter/Hermit startup timed out and the isolated checkout lacked generated iOS configuration; no booted simulator or physical-device playback was witnessed. Those are reviewer/tooling confidence gaps, not additional author defects. Verification owner: author for code and reordered native regression; reviewer/native or release QA for exact-head simulator playback and accessibility after the fix. No independent native journey is claimed. Any new head requires re-review.

Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
Signed-off-by: kenny lopez <[email protected]>

@kazemek21 kazemek21 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.

Kyle reviewed this PR and the Nexus review-tracker notes. Approving with the non-blocking APNG feedback in the existing thread.

Posted by Codex (gpt-6.1-sol) on behalf of kylez

@klopez4212

Copy link
Copy Markdown
Contributor Author

🤖 Addressed Jude’s blocking APNG rewind finding in 84e1ec2. Initial image and preview completions now preserve a newer full animation frame. The native regression delivers a blue continuing frame before releasing the held red initial reply and asserts that blue remains visible.

Validation: native photo-window/stale-response/teardown fixture passed on the iPhone 18 Pro iOS 27 simulator at this commit; required full mobile push checks passed. This is native fixture coverage, not a physical-device visual review.

The separate non-blocking APNG feedback remains open and is not claimed as addressed.

@github-actions github-actions Bot removed the codex-security-review-current The posted Codex security review matches its recorded range. label Nov 10, 2026
@github-actions github-actions Bot added the codex-security-review-current The posted Codex security review matches its recorded range. label Nov 10, 2026
@klopez4212
klopez4212 enabled auto-merge (squash) November 10, 2026 15:34
@brow
brow merged commit e2dc54e into main Nov 10, 2026
94 of 95 checks passed
@brow
brow deleted the kennylopez-ios-photo-formats-20261110 branch November 10, 2026 15:34
klopez4212 added a commit that referenced this pull request Nov 10, 2026
## Summary

Videos previously rendered outside photo stacks and opened only the
Flutter video viewer. Message bubbles now group trailing videos and
photos in their original order, with poster thumbnails or a visible play
placeholder. On iOS 18+, the native zoom gallery (enabled by default)
opens both standalone videos and mixed stacks with AVKit playback
controls, and closing restores the selected stack item.

Flutter downloads only the selected video using fresh media
authentication, streams it to a bounded temporary file, and
cancels/deletes it when selection changes or the viewer closes. UIKit
releases the old player before that file is removed. Failed downloads
and playback expose Retry. Video URLs never enter the image decoder, and
photo-only actions are hidden on video pages.

The existing Flutter viewers remain the fallback. Native zoom defaults
on; `--dart-define=BUZZ_NATIVE_PHOTO_ZOOM=false` remains an explicit
build-time opt-out. Unsupported platforms/formats, unavailable native
presentation, and bridge failures use the Flutter fallback. Native video
pages use Close or accessibility escape to return with zoom; the
photo-specific downward drag recognizer is disabled there to avoid
competing with AVKit controls.

### Related issue

Stacked on #9314 (PNG/HEIC photo formats); merge that first. No matching
open video-stack/zoom PR found.

### Testing

- Default-on follow-up: presentation tests also cover unavailable, null,
and throwing bridge responses.

- `just mobile-check mobile-test`: analysis clean, 4,601 mobile tests
passed, plus push-configuration and secp256k1 package tests.
- Full native-enabled mobile suite: 4,626 passed, 68 skipped; rerun
passed at `dc752dccaa664cb56a19cbe517a266d1407a1100`.
- Mixed-stack widget journey covers order, video opening, returning to
selection, and the photo-only fallback gallery.
- Native bridge tests cover video snapshot presentation, selected-only
dispatch, stale sessions and keeping video URLs out of image decoding.
- Real HTTP download journey covers authenticated requests, Retry, byte
limits, cancellation and temporary-file cleanup.
- Native simulator fixture decodes a real MP4 and verifies selected-only
loading, player release on paging and teardown. Removing playback
cleanup makes the regression fail; restoring it passes.
- No physical-device installation or end-to-end zoom recording captured.

### Widget previews

Actual Flutter renders with sample video URLs and no poster metadata;
these show the stack layout, not device playback.

| Received, light | Sent, dark |
| --- | --- |
| ![Received video stack without poster
images](https://github.com/user-attachments/assets/af6d06a7-10e3-43c4-87de-52324be5b19c)
| ![Sent video stack without poster
images](https://github.com/user-attachments/assets/5b79b5b1-e3d4-4482-bbf6-574849d5dfdc)
|

### Human verification pending

On a normal build on iOS 18+ (no extra flag required), open a message
containing photos and videos, page between them, play a video, and
close. Verify playback controls work, only the selected video plays, and
closing returns to the selected stack item.

---------

Signed-off-by: kenny lopez <[email protected]>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
philazar added a commit that referenced this pull request Nov 11, 2026
Co-Authored-By: Claude Opus 5.5 <[email protected]>

* origin/main:
  feat(mobile/ios): show the message's own text in notifications (#9346)
  feat(mobile): prefill the agents you last tagged in a conversation (#9248)
  feat(mobile): start a Solo chat from a message or inbox row (#9251)
  feat(mobile): link to Builderlab web from Settings (#9300)
  fix(relay): bound invite preview host lookup (#9308)
  fix(relay): saturate filter limits instead of casting (#9230)
  fix(mobile): tell same-name agents apart in Solo (#9321)
  fix(mobile): join an open channel from the chat footer (#9253)
  fix(relay): fence pending subscriptions on close or replacement (#2382)
  fix(mobile): hide archived people and agents from choices; give them their own members section (#9320)
  Add video stacks and native iOS zoom playback (#9316)
  Support PNG and HEIC photos in the native iOS zoom gallery (#9314)
  fix(mobile): list known people in recipient pickers before a name is typed (#9311)
  feat(mobile): mark edited messages after the body (#8863)

Signed-off-by: Philip Azar <[email protected]>
klopez4212 added a commit that referenced this pull request Nov 11, 2026
## Summary

Photos currently filters out videos, video messages without poster
metadata stay grey until playback, and the native gallery rejects
downward dismissal on video pages.

- Include photos and videos in the native inline/popover pickers, system
picker, and recent-media grid. Mixed selections retain their order and
videos use the video upload path. Remove the redundant Video menu row on
iOS and Android, whose Photos picker accepts both media types; reduce
their menu height to four rows. Native pickers request the current asset
representation to avoid compatibility transcoding and export up to three
files concurrently, preserving order and cleaning up cancelled/failed
batches.
- Show local stills for queued videos. For older messages without
posters, serialize thumbnail generation for the four rendered stack
cards, bound/cancel downloads, remove temporary videos, and save stills
in the existing account/community cache. Existing poster metadata is
used directly. The native viewer uses the poster while loading instead
of a play-button snapshot.
- Allow downward dismissal on native video pages while preserving
horizontal paging/scrubbing, zoomed-photo panning, and the accessible
Close button. Fire selection-click haptics as a swipe begins rather than
after the page settles, with no duplicate on completion or cancellation.
Page-indicator changes retain immediate feedback. Replace visible
video-loading text with the existing white arc-spinner treatment,
implemented natively with reduced-motion and lifecycle handling.
Completed swipes release the previous video player even after UIKit
removes it from the active children.

Legacy videos need one initial download (up to the existing 100 MB
limit) to generate their cached preview. This does not start playback.

### Related issue

Follow-up to merged #9259 and #9314. Searched open and closed
gallery/video issues and PRs; no duplicate fix found.

### Testing

- Full `just mobile-check` and `just mobile-test` passed at
`12e6ed6ae3`: 4,688 Flutter tests (71 skipped), 4 unconfigured-push
tests, and 27 crypto-package tests. File-size checks passed. Regressions
cover mixed image/video upload routing, queued still previews, the
rendered-card poster window, authenticated/cancelled downloads,
caching/cleanup, and native poster loading.
- Native gallery simulator regression fixture passed: photo/video
dismissal guards, real MP4 decoding, paging, selected-only playback
loading, and teardown.
- Swift type-check of the native picker and its dependencies passed,
with existing deprecation/capture warnings.
- `just ci` passed its repository-wide lint/type checks. The unrelated
Rust test rebuild was stopped after consuming about 13 GB to preserve
remaining disk space; only that run's generated caches were removed.

Release build at `fb09f9cb8` installed in place and launched on the
physical iPhone Air as **Buzz Photo Stack**, with
`BUZZ_NATIVE_PHOTO_ZOOM=true`. Existing app updated in place with the
same bundle and Keychain identity; no uninstall or reset. Owner tested
the physical-device preview and approved readiness for review. No
before/after capture was produced. The simulator evidence above is an
automated native fixture, not a full Buzz device journey.

Native preparation follow-up:
`scripts/test-mobile-picker-preparation.sh` passed on an iPhone Air
simulator, covering bounded overlap, out-of-order completion,
cancellation, and failure cleanup. The updated physical Air preview
launched successfully with unchanged preferences metadata. Cloud-only
assets may still require preparation.

Loader/haptics follow-up native fixture passed: real video readiness
hides the loader, error/retry and detach stop/restart it correctly,
swipe start triggers one haptic before selection changes, and
completion/cancellation/duplicate-start callbacks add none. Updated
release installed and launched on the physical Air; owner approved the
final preview for review.

Review: focused code review and native regression checks completed;
physical Air preview tested by the owner, who approved readiness.


P2 follow-up: video presentation uses an already decoded poster or the
source snapshot immediately, then forwards a pending poster
asynchronously. Late poster updates are limited to the selected native
page and do not restart playback. Added pending-provider presentation
coverage and native late/stale-poster checks. This follow-up has not
been installed on the physical preview yet.

---------

Signed-off-by: kenny lopez <[email protected]>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
philazar added a commit that referenced this pull request Nov 11, 2026
Co-Authored-By: Claude Opus 5.5 <[email protected]>

* origin/main: (22 commits)
  chore(deps): bump hickory-proto to 0.26.2 for RUSTSEC-2026-0487..0491 (#9351)
  feat(mobile/ios): show the message's own text in notifications (#9346)
  feat(mobile): prefill the agents you last tagged in a conversation (#9248)
  feat(mobile): start a Solo chat from a message or inbox row (#9251)
  feat(mobile): link to Builderlab web from Settings (#9300)
  fix(relay): bound invite preview host lookup (#9308)
  fix(relay): saturate filter limits instead of casting (#9230)
  fix(mobile): tell same-name agents apart in Solo (#9321)
  fix(mobile): join an open channel from the chat footer (#9253)
  fix(relay): fence pending subscriptions on close or replacement (#2382)
  fix(mobile): hide archived people and agents from choices; give them their own members section (#9320)
  Add video stacks and native iOS zoom playback (#9316)
  Support PNG and HEIC photos in the native iOS zoom gallery (#9314)
  fix(mobile): list known people in recipient pickers before a name is typed (#9311)
  feat(mobile): mark edited messages after the body (#8863)
  Add swipeable photo stacks and native iOS gallery preview (#9259)
  ci(docker): skip relay image publishes for main pushes outside the build context (#9310)
  Add manual-only relay image publishing (#9303)
  fix(mobile): bump Flutter to 3.47.9 for Android TLS fix (#9309)
  Serve video and poster previews for invite links (#9306)
  ...

Signed-off-by: Philip Azar <[email protected]>

This branch was previously deployed

1 inactive deployment
codex-review — 84e1ec24 Deployed Nov 10, 2026 by klopez4212 via Run Codex Security Review #14229
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.

4 participants