A playing voice note's waveform accepts a seek: WaveformView overlays the bars with DragGesture(minimumDistance: 0) and seeks to the release point (WaveformView.swift:55), active while the note plays (VoiceNoteView.swift:71). In a private chat that waveform renders inside the message list, and the sheet attaches its swipe-right-to-leave gesture to the same list with .highPriorityGesture (ContentSheetViews.swift:575). A high-priority ancestor drag starves descendant gestures. A drag across the waveform never reaches the seek, and a rightward drag that ends past the leave thresholds (horizontal over 80, vertical under 60, ContentSheetViews.swift:613) leaves the conversation while the note is playing.
The same mechanism shows up in two merged PRs:
- #1405 fixed it as the cause: the composer mic's press-and-hold was cancelled 3 to 10 ms after touch-down, and the fix moved the swipe off the whole sheet and onto the message list.
- #1402 hardened against it: the grey-box root cause was a status default, and the PR moved the reveal tap onto a Button as defense against this same gesture; its description notes it starves descendant TapGestures while Button actions survive.
The comment at the attachment site records the mechanism:
// Swipe-right-to-leave lives on the message list only. On the
// whole sheet it preempted the composer's press-and-hold mic
// gesture (a high-priority ancestor drag cancels child gestures
// within milliseconds — same starvation as the image-reveal bug).After #1405 the app has two drag gestures attached with plain .gesture: the composer mic hold (ContentComposerView.swift:347), which that fix moved out of the swipe's reach, and this seek, which sits inside it.
Measurements
On the real DM screen, in the simulator, with an injected conversation. A mesh DM with a voice note normally needs two devices. To exercise the real view stack with one, I used a local debug build whose only change seeds a received voice note from a fake peer at launch, through the same handler the mesh receive path uses (ChatPrivateConversationCoordinator.handlePrivateMessage), and opens the conversation. Every view under test is stock. With the note playing, a rightward drag of about 130 points across the waveform closed the conversation. Simulator audio would not start on later runs; the drag-never-seeks half is covered by the harness below.
In a controlled harness. Voice notes also exist in the public mesh timeline, but only the DM sheet attaches swipe-to-leave, and geohash DMs cannot carry media (ConversationUIModel.swift:280). The harness rebuilds the nesting in the iPhone 17 Pro simulator, matching the real row stack: a ScrollView holding a LazyVStack of rows that each carry .contentShape(Rectangle()) and a context menu (MessageListView.swift:91-120), the voice row laid out as the app's play button plus waveform plus duration label (VoiceNoteView.swift), the seek gesture copied from WaveformView, and the container wrapped in the same high-priority DragGesture(minimumDistance: 25) with the same leave thresholds. A mode switch detaches the ancestor gesture, to check whether the seek works without it.
With the ancestor attached, matching the app:
| input | result |
|---|---|
| tap on the waveform | seek fires at the tapped fraction |
| 40 pt rightward drag | nothing fires |
| 120 pt rightward drag | the leave action fires; the seek does not |
With the ancestor detached, same hierarchy including the ScrollView:
| input | result |
|---|---|
| 40 pt rightward drag | seek fires |
| 120 pt rightward drag | seek fires |
Severity
Tap-to-seek still worked in the harness with the ancestor attached. The seek only acts when the finger lifts, and DragGesture(minimumDistance: 0) is a common way to read where a tap landed. Drag-to-seek may never have been intended as its own interaction. Either way, a rightward drag across a playing waveform leaves the conversation and returns to the sidebar, as in the run above.
On fixes
- Ruled out: giving the waveform's drag
.highPriorityGesture. The ancestor still won every drag in the harness. - Measured in the harness: the swipe attached as plain
.gesture. The leave still fired on message rows, the list still scrolled, and the waveform got its drags back. The debug build's plain-gesture swipe also left the conversation on the real screen. High priority may be there for a reason the harness does not model, and gesture feel needs real devices. - Waveform-side: make the seek an explicit tap, as #1402 did for the reveal. A drag then leaves the chat by design.
- Swipe-side: change where or how swipe-to-leave applies. #1405 moved that gesture once before.
If you decide this is worth changing, I can build and verify a PR for one of the directions with the harness and repro build above. If you would rather make the change, I can post the harness and the seed patch.
Activity
0No activity yet.