#1658

Dragging on a playing voice note in a DM can leave the conversation

Openheyaimopened 2mo ago

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:

inputresult
tap on the waveformseek fires at the tapped fraction
40 pt rightward dragnothing fires
120 pt rightward dragthe leave action fires; the seek does not

With the ancestor detached, same hierarchy including the ScrollView:

inputresult
40 pt rightward dragseek fires
120 pt rightward dragseek 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

0

No activity yet.