mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-10-25 07:41:41 +00:00
fix: avoid swipe conflict when seeking voice notes
Fixes #1658. The private conversation attaches a high-priority right-swipe-to-leave DragGesture to the message list, while WaveformView used DragGesture(minimumDistance: 0) for seeking. A horizontal drag over a playing voice note could therefore close the conversation instead of seeking. Use SpatialTapGesture for deterministic tap-to-seek behavior, preserve bounded progress mapping, and cover normal mapping, bounds clamping, and zero-width geometry with regression tests.
This commit is contained in:
parent
9b84b36122
commit
2c3e8d4fcf
@ -8,6 +8,16 @@ struct WaveformView: View {
|
||||
let isInteractive: Bool
|
||||
@ThemedPalette private var palette
|
||||
|
||||
/// Converts a tap location into a bounded playback fraction.
|
||||
///
|
||||
/// Keep this separate from the gesture so the coordinate contract can be
|
||||
/// tested without mounting SwiftUI. A zero-sized geometry can occur while
|
||||
/// a row is being laid out and must not result in a bogus seek.
|
||||
static func seekFraction(forX x: CGFloat, inWidth width: CGFloat) -> Double? {
|
||||
guard width > 0 else { return nil }
|
||||
return max(0, min(1, Double(x / width)))
|
||||
}
|
||||
|
||||
private var clampedPlayback: Double {
|
||||
max(0, min(1, playbackProgress))
|
||||
}
|
||||
@ -52,11 +62,20 @@ struct WaveformView: View {
|
||||
if isInteractive, let onSeek = onSeek {
|
||||
Color.clear
|
||||
.contentShape(Rectangle())
|
||||
// The private conversation owns a high-priority
|
||||
// swipe-to-leave drag on the message list. A drag here
|
||||
// is therefore interpreted as navigation instead of
|
||||
// seeking and can close the conversation. Seeking is
|
||||
// intentionally a tap: it preserves the swipe gesture
|
||||
// while keeping the waveform's direct-manipulation
|
||||
// affordance deterministic.
|
||||
.gesture(
|
||||
DragGesture(minimumDistance: 0)
|
||||
SpatialTapGesture(count: 1, coordinateSpace: .local)
|
||||
.onEnded { value in
|
||||
guard geometry.size.width > 0 else { return }
|
||||
let fraction = max(0, min(1, value.location.x / geometry.size.width))
|
||||
guard let fraction = Self.seekFraction(
|
||||
forX: value.location.x,
|
||||
inWidth: geometry.size.width
|
||||
) else { return }
|
||||
onSeek(fraction)
|
||||
}
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user