Improved rendering of voice messages (#1782)
This commit is contained in:
@@ -24,7 +24,6 @@ struct SwipeRightAction<Label: View>: ViewModifier {
|
||||
private let feedbackGenerator = UIImpactFeedbackGenerator(style: .heavy)
|
||||
|
||||
@State private var canStartAction = false
|
||||
@State private var animate = false
|
||||
@GestureState private var dragGestureActive = false
|
||||
|
||||
@State private var hasReachedActionThreshold = false
|
||||
@@ -40,7 +39,7 @@ struct SwipeRightAction<Label: View>: ViewModifier {
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.offset(x: xOffset, y: 0.0)
|
||||
.animation(.interactiveSpring().speed(0.5), value: animate)
|
||||
.animation(.interactiveSpring().speed(0.5), value: xOffset)
|
||||
.gesture(DragGesture()
|
||||
.updating($dragGestureActive) { _, state, _ in
|
||||
// Available actions should be computed on the fly so we use a gesture state change
|
||||
@@ -69,7 +68,6 @@ struct SwipeRightAction<Label: View>: ViewModifier {
|
||||
} else {
|
||||
hasReachedActionThreshold = false
|
||||
}
|
||||
animate = true
|
||||
}
|
||||
.onEnded { _ in
|
||||
if xOffset > actionThreshold {
|
||||
@@ -77,7 +75,6 @@ struct SwipeRightAction<Label: View>: ViewModifier {
|
||||
}
|
||||
|
||||
xOffset = 0.0
|
||||
animate = false
|
||||
}
|
||||
)
|
||||
.onChange(of: dragGestureActive, perform: { value in
|
||||
|
||||
@@ -33,11 +33,11 @@ struct VoiceRoomPlaybackView: View {
|
||||
|
||||
var onPlayPause: () -> Void = { }
|
||||
var onSeek: (Double) -> Void = { _ in }
|
||||
var onWaveformDragStateChanged: (Bool) -> Void = { _ in }
|
||||
var onScrubbing: (Bool) -> Void = { _ in }
|
||||
|
||||
private enum DragState: Equatable {
|
||||
case inactive
|
||||
case pressing
|
||||
case pressing(progress: Double)
|
||||
case dragging(progress: Double)
|
||||
|
||||
var progress: Double {
|
||||
@@ -69,6 +69,7 @@ struct VoiceRoomPlaybackView: View {
|
||||
}
|
||||
|
||||
@GestureState private var dragState = DragState.inactive
|
||||
@State private var tapProgress: Double = .zero
|
||||
|
||||
var timeLabelContent: String {
|
||||
// Display the duration if progress is 0.0
|
||||
@@ -76,6 +77,10 @@ struct VoiceRoomPlaybackView: View {
|
||||
return Self.elapsedTimeFormatter.string(from: Date(timeIntervalSinceReferenceDate: playbackViewState.duration * percent))
|
||||
}
|
||||
|
||||
var showWaveformCursor: Bool {
|
||||
playbackViewState.playing || dragState.isDragging
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
HStack {
|
||||
@@ -87,18 +92,25 @@ struct VoiceRoomPlaybackView: View {
|
||||
}
|
||||
.padding(.vertical, 6)
|
||||
GeometryReader { geometry in
|
||||
WaveformView(waveform: playbackViewState.waveform, progress: playbackViewState.progress)
|
||||
WaveformView(waveform: playbackViewState.waveform, progress: playbackViewState.progress, showCursor: showWaveformCursor)
|
||||
// Add a gesture to drag the waveform
|
||||
.gesture(LongPressGesture()
|
||||
.gesture(SpatialTapGesture()
|
||||
.simultaneously(with: LongPressGesture())
|
||||
.sequenced(before: DragGesture(coordinateSpace: .local))
|
||||
.updating($dragState) { value, state, _ in
|
||||
switch value {
|
||||
// Long press begins.
|
||||
case .first(true):
|
||||
state = .pressing
|
||||
// (SpatialTap, LongPress) begins.
|
||||
case .first(let spatialLongPress) where spatialLongPress.second == true:
|
||||
// Compute the progress with the spatialTap location
|
||||
let progress = (spatialLongPress.first?.location ?? .zero).x / geometry.size.width
|
||||
state = .pressing(progress: progress)
|
||||
// Long press confirmed, dragging may begin.
|
||||
case .second(true, let drag):
|
||||
let progress: Double = (drag?.location.x ?? .zero) / geometry.size.width
|
||||
case .second(let spatialLongPress, let drag) where spatialLongPress.second == true:
|
||||
var progress: Double = tapProgress
|
||||
// Compute the progress with drag location
|
||||
if let loc = drag?.location {
|
||||
progress = loc.x / geometry.size.width
|
||||
}
|
||||
state = .dragging(progress: progress)
|
||||
// Dragging ended or the long press cancelled.
|
||||
default:
|
||||
@@ -111,9 +123,10 @@ struct VoiceRoomPlaybackView: View {
|
||||
.onChange(of: dragState) { newDragState in
|
||||
switch newDragState {
|
||||
case .inactive:
|
||||
onWaveformDragStateChanged(false)
|
||||
case .pressing:
|
||||
onWaveformDragStateChanged(true)
|
||||
onScrubbing(false)
|
||||
case .pressing(let progress):
|
||||
tapProgress = progress
|
||||
onScrubbing(true)
|
||||
feedbackGenerator.prepare()
|
||||
sendFeedback = true
|
||||
case .dragging(let progress):
|
||||
|
||||
@@ -35,7 +35,7 @@ struct VoiceRoomTimelineView: View {
|
||||
VoiceRoomPlaybackView(playbackViewState: playbackViewState,
|
||||
onPlayPause: onPlaybackPlayPause,
|
||||
onSeek: onPlaybackSeek(_:),
|
||||
onWaveformDragStateChanged: onPlaybackDragStateChanged(_:))
|
||||
onScrubbing: onPlaybackScrubbing(_:))
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,7 @@ struct VoiceRoomTimelineView: View {
|
||||
context.send(viewAction: .seekAudio(itemID: timelineItem.id, progress: progress))
|
||||
}
|
||||
|
||||
private func onPlaybackDragStateChanged(_ dragging: Bool) {
|
||||
private func onPlaybackScrubbing(_ dragging: Bool) {
|
||||
if dragging {
|
||||
context.send(viewAction: .disableLongPress(itemID: timelineItem.id))
|
||||
} else {
|
||||
|
||||
@@ -40,11 +40,12 @@ extension Waveform {
|
||||
}
|
||||
|
||||
struct WaveformView: View {
|
||||
let lineWidth: CGFloat = 2
|
||||
let linePadding: CGFloat = 2
|
||||
private let lineWidth: CGFloat = 2
|
||||
private let linePadding: CGFloat = 2
|
||||
var waveform: Waveform
|
||||
let minimumGraphAmplitude: CGFloat = 1
|
||||
private let minimumGraphAmplitude: CGFloat = 1
|
||||
var progress: CGFloat = 0.0
|
||||
var showCursor = false
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
@@ -64,9 +65,9 @@ struct WaveformView: View {
|
||||
var xOffset: CGFloat = lineWidth / 2
|
||||
var index = 0
|
||||
|
||||
while xOffset < width - lineWidth {
|
||||
while xOffset <= width {
|
||||
let sample = CGFloat(index >= normalisedData.count ? 0 : normalisedData[index])
|
||||
let drawingAmplitude = max(minimumGraphAmplitude, sample * height)
|
||||
let drawingAmplitude = max(minimumGraphAmplitude, sample * (height - 2))
|
||||
|
||||
path.move(to: CGPoint(x: xOffset, y: centerY - drawingAmplitude / 2))
|
||||
path.addLine(to: CGPoint(x: xOffset, y: centerY + drawingAmplitude / 2))
|
||||
@@ -76,13 +77,28 @@ struct WaveformView: View {
|
||||
}
|
||||
.stroke(Color.compound.iconSecondary, style: StrokeStyle(lineWidth: lineWidth, lineCap: .round))
|
||||
}
|
||||
// Display a cursor
|
||||
.overlay(alignment: .leading) {
|
||||
RoundedRectangle(cornerRadius: 1).fill(Color.compound.iconAccentTertiary)
|
||||
.offset(CGSize(width: cursorPosition(progress: progress, width: geometry.size.width), height: 0.0))
|
||||
.frame(width: lineWidth, height: geometry.size.height)
|
||||
.opacity(showCursor ? 1 : 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func cursorPosition(progress: Double, width: Double) -> Double {
|
||||
guard progress > 0 else {
|
||||
return 0
|
||||
}
|
||||
let width = (width * progress)
|
||||
return width - width.truncatingRemainder(dividingBy: lineWidth + linePadding)
|
||||
}
|
||||
}
|
||||
|
||||
struct WaveformView_Previews: PreviewProvider, TestablePreview {
|
||||
static var previews: some View {
|
||||
WaveformView(waveform: Waveform.mockWaveform)
|
||||
WaveformView(waveform: Waveform.mockWaveform, progress: 0.5)
|
||||
.frame(width: 140, height: 50)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -461,7 +461,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
}
|
||||
|
||||
return AudioRoomTimelineItemContent(body: messageContent.body,
|
||||
duration: messageContent.info?.duration ?? 0,
|
||||
duration: (messageContent.audio?.duration ?? 0) / 1000.0,
|
||||
waveform: waveform,
|
||||
source: MediaSourceProxy(source: messageContent.source, mimeType: messageContent.info?.mimetype),
|
||||
contentType: UTType(mimeType: messageContent.info?.mimetype, fallbackFilename: messageContent.body))
|
||||
|
||||
@@ -283,7 +283,7 @@ class RoomScreenViewModelTests: XCTestCase {
|
||||
.store(in: &cancellables)
|
||||
|
||||
// Test
|
||||
let deferred = deferFulfillment(viewModel.context.$viewState.collect(2).first(),
|
||||
let deferred = deferFulfillment(viewModel.context.$viewState.collect(3).first(),
|
||||
message: "The existing view state plus one new one should be published.")
|
||||
viewModel.context.send(viewAction: .tappedOnUser(userID: "bob"))
|
||||
try await deferred.fulfill()
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:09dc1ab2bf9b5f9576a7f222ee84bd821ae1e608974a5b341cf2c41ce5bfab5e
|
||||
size 63967
|
||||
oid sha256:0e12529919d3829493278947168ddf881d9b7ae7929de5912a7a9c759c9be1ce
|
||||
size 62187
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:de7c4ad957c37ad1f46c965985ed4f8884f86a934595c1d4a2ece836b5d5b2c7
|
||||
size 72812
|
||||
oid sha256:5da10a2b12265a9e3e40ee408030ebc773c938d70a6cf14c2bf4249296bdf93e
|
||||
size 72786
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9de9d8875b3a9182562fe07f11777d3cb6132d2c97368f15b7f14ca14004a7af
|
||||
size 69146
|
||||
oid sha256:ccfa620baa535643770bd4db7a3c329493ac6bf0351847c03eaadc4831bc7f68
|
||||
size 69227
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9632b9c96b960ef1ee36ca7980253aaad8e21ded28d741f5d62b33cc4e8a0eba
|
||||
size 63913
|
||||
oid sha256:82130c80be62ed9d3ba0d54df5fec6803c3bed6ef0dc00d044696938d5c41598
|
||||
size 64238
|
||||
|
||||
Reference in New Issue
Block a user