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.
AppInfoPlistContractTestspins 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.
- The gap is the fold's
- 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:
| Check | Result |
|---|---|
| Outer display | Unchanged from main (compared visually) |
| Inner display | Fills 867×635pt in two columns |
| Fold region | Applied (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 portrait | Falls back to the sheet |
| Keyboard | Correct in split |
| QR sheet on outer display | Scrolls with pinned controls |
Tests and CI-equivalent checks:
| Check | Head | Result |
|---|---|---|
| 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-testing | final head | Green |
Periphery --strict (macOS scheme) | final head | Same findings as main |
SwiftLint --strict | final head | Nothing 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/ArrangementViewrefactor.
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 throughmakeVoiceNoteSender(), which replaces the late-bindingsendVoiceNote(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
Codex Review Summary
This comment shows the latest Codex review activity on this pull request.
8fae836ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.