#1792

Update bitchat for iPhone Duo

Openjackjackbitswants to merge intofrom

Summary

Updates bitchat for iPhone Duo, Apple's foldable (iOS 27.1). All measurements below come from the iPhone Duo simulator in Xcode 27.1, driven through DeviceHub's Closed / Partially Open / Open controls.

Before: a build made with the iOS 27.1 SDK was letterboxed on the open inner display. It ran in a ~469×669pt compact portrait window with black bars on both sides, because of UIRequiresFullScreen. The people list and DMs were modal sheets.

After:

  • Inner display (951×669, regular width): the public timeline and the people list or open DM sit side by side, split at the hinge.
  • Outer display (closed): works exactly as on any iPhone, with a single column and the people/DM sheet, laid out left of the Duo's vertical status-bar column.

What changes

Platform (Info.plist)

  • Remove UIRequiresFullScreen. It is what letterboxed the inner display. From the iOS 27 SDK it no longer prevents resizing (TN3192), and before iOS 27 it only applied to iPad. The app target is iPhone-only.
  • Pin one window (UIApplicationSupportsMultipleScenes = false). The Duo is the first iPhone that allows several, and DM selection, drafts, the privacy cover and the panic hook are single-window.
  • Portrait-only stays. The outer display honours it; the inner display ignores it by design.
  • AppInfoPlistContractTests pins these keys.

Two-column layout on wide displays (iOS only; macOS unchanged)

  • When it applies: regular width with room for two 340pt columns. The column shows the people list or the open DM, reusing the sheet's views inline.
  • Where it splits:
    • The gap is the fold's ReservedRegion(.division) (iOS 27.1; inactive regions included, so the columns don't jump when the hinge moves).
    • Before the system reports a region, the split falls on the window centre with the same 40pt gap.
    • 27.1 APIs are behind #if canImport(SwiftUI, _version: 8.0.85) plus #available.
  • Compact keeps today's sheet: the outer display, other iPhones, Split View halves and inner portrait.
  • Header corner: the header keeps its content off the inner display's 55pt top corner, using containerCornerInsets (iOS 26), paired by physical side so right-to-left layouts are handled.

Folding never loses the conversation

  • Unfolding moves an open DM from the sheet into the column; folding brings it back as a sheet. Drafts and selection survive.
  • The sheet's dismissal setter used to end the DM on any dismissal. Layout-driven dismissals are now ignored (PeopleSheetPresentationPolicy, with tests).
  • Presentations bound to the old host (image picker, preview, fingerprint, QR) close instead of racing a second presenter. The timeline stays pinned to the bottom across resizes.
  • With a DM open beside it, the public timeline no longer offers /clear, mention, hug or slap. Those act on the selected conversation, so they would have hit the DM. In-app sender and #geohash links are handled by the list that drew them.

Privacy fixes found along the way (some also affect every iPhone)

  • Media is bound to the conversation it was sent from. A voice note's destination was read ~0.5 s after the mic was released (the recorder's finalize padding), and a photo's after it finished processing. Leaving the DM in that window sent private media to the public mesh. The destination is now fixed at release or pick.
  • A held mic can't outlive its press:
    • Cancelled touches (fold, call, alert) now end the hold. Previously the first press raised the microphone prompt, and granting access started recording with nobody holding the button.
    • A conversation or layout change ends the hold.
    • The control disappearing ends the hold.
    • One press starts at most one hold. The press bookkeeping now lives in VoiceRecordingViewModel, where tests pin it.
  • A note released just before the next press is no longer lost. Pressing or tapping again during a classic note's 0.5 s finalize padding deleted the note just released. Now only a panic withholds a released note.
  • On the split layout, a screenshot of a geohash timeline beside a DM shows the location warning and forwards the DM notice.

Sheets, camera and theme

  • The fingerprint and QR verification sheets scroll. Their content (~640–890pt) overflowed the Duo's sheets (636pt outer, 629pt inner) and SE-class phones.
  • Image previews open full screen on iOS, including from inside the DM sheet, where the tap used to do nothing.
  • The QR scanner preview follows AVCaptureDevice.RotationCoordinator (iOS 17+) and names the back camera explicitly.
  • The matrix theme's backdrop fills under the Duo's 84pt status-bar column. Main's #1794 made the same code change for sheets, so this branch only adds the Duo case to its comment.
  • README.md: how to get the iOS 27.1 runtime and fold the simulator.

Verification

iPhone Duo simulator (iOS 27.1), both displays, matrix and glass, light and dark. Run at 510afe2a, before the review fixes:

CheckResult
Outer displayUnchanged from main (compared visually)
Inner displayFills 867×635pt in two columns
Fold regionApplied (455.5 | 40 | 371.5) in flat and book poses
DM open, fold → unfold → fold (×2)DM never lost, draft kept, sheet or column as expected, no stale "jump to latest" pill
Header people button (split)Leaves the DM for the list
Inner portraitFalls back to the sheet
KeyboardCorrect in split
QR sheet on outer displayScrolls with pinned controls

Tests and CI-equivalent checks:

CheckHeadResult
iOS simulator suite (iPhone 17 Pro, iOS 27.0, CI flags)8345e315 (final, main merged in)2426/2426 passed
SwiftPM (swift test, macOS, as CI runs it)8345e315 (final, main merged in)2200/2200 passed
iOS and macOS builds; macOS build-for-testingfinal headGreen
Periphery --strict (macOS scheme)final headSame findings as main
SwiftLint --strictfinal headNothing new (the 2 local errors are pre-existing in untouched ImagePreviewView.swift)
  • Fallback compile check: the 27.1 and iOS 26 gates were compiled in their fallback form, which is what CI's Xcode 26.6 builds.

  • Regular iPhone (iPhone 17 Pro): the matrix backdrop change left the main screen pixel-identical in both themes and appearances. The outer display, which runs the same compact path as other iPhones, looks unchanged.

  • Adversarial review, two rounds. Every finding was checked by independent skeptics, and mutation checks confirm the new tests fail if the fix is reverted.

    • Round 1: 15 confirmed and fixed, 5 refuted.
    • Round 2, on the round-1 fixes: 6 confirmed and handled, 2 refuted. One of the six was resolved by measurement: in right-to-left, the Duo's corner insets aren't physical in any reading (details in TopCornerClearance), so the square-corner rule stays, and it can only under-pad.
  • Duo pass on 8fae8360:

    • fallback and fold-region split identical (455.5 | 40 | 371.5)
    • glass title rows aligned
    • DM kept across two fold/unfold cycles, no stale pill
    • right-to-left (Arabic) correct on both displays

    The later commits change no layout. Their mic and media changes are covered by unit tests.

Needs hardware:

  • Camera rotation in each inner-display pose.
  • A real fold while holding the mic. The cancellation paths are unit-tested, but a physical fold needs real touch.

Not in this PR

  • Landscape on the outer display ("tent" pose). This is a product call that affects every iPhone.
  • Moving header actions into the Duo's vertical bar as system toolbar items.
  • Multiple windows on the inner display.
  • A NavigationSplitView / ArrangementView refactor.

Notes for reviewers

  • Main is merged in (#1782, #1788–#1791, #1793–#1796). The one conflict was MessageListView: #1790's single timeline-change handler is kept, and this branch's width re-pin sits on top of it. #1793's voice-note tests send through makeVoiceNoteSender(), which replaces the late-binding sendVoiceNote(at:).
  • Likely conflicts: with open PRs touching the same views (e.g. #1660, #1784). The changes are kept local to those files.
  • Behaviour changes on every iPhone, all intended:
    • Image previews are a full-screen cover, so swipe-down is gone and the close button stays.
    • A first-ever mic press that raises the permission prompt no longer records after "Allow".
    • Switching conversations ends an in-flight hold.

🤖 Generated with Claude Code

Activity

32
chatgpt-codex-connector[bot]7d ago

Codex Review Summary

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

ReviewStatusCommitReview trigger
📝 Code Review✅ Completed 2026-10-23T16:18:05.791573Z8fae836PR 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.