diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 29a01eda9..16ecb3017 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -356,6 +356,7 @@ 62910B515BCB4B455E24D7C1 /* AdvancedSettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D086854995173E897F993C26 /* AdvancedSettingsScreenViewModelProtocol.swift */; }; 6298AB0906DDD3525CD78C6B /* LRUCache in Frameworks */ = {isa = PBXBuildFile; productRef = 1081D3630AAD3ACEDDEC3A98 /* LRUCache */; }; 62A7FC3A0191BC7181AA432B /* AudioRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 907FA4DE17DEA1A3738EFB83 /* AudioRecorder.swift */; }; + 63CDC201A5980F304F6D0A1C /* WaveformInteractionModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEE91FB8ABB5F5884B6D940 /* WaveformInteractionModifier.swift */; }; 63E46D18B91D08E15FC04125 /* ExpiringTaskRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B25F959A434BB9923A3223F /* ExpiringTaskRunner.swift */; }; 642DF13C49ED4121C148230E /* TestablePreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1E227F34BE43B08E098796E /* TestablePreview.swift */; }; 6448F8D1D3CA4CD27BB4CADD /* RoomMemberProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F36C5D9B37E50915ECBD3EE /* RoomMemberProxy.swift */; }; @@ -587,7 +588,6 @@ 9D2E03DB175A6AB14589076D /* AnalyticsEvents in Frameworks */ = {isa = PBXBuildFile; productRef = 2A3F7BCCB18C15B30CCA39A9 /* AnalyticsEvents */; }; 9D79B94493FB32249F7E472F /* PlaceholderAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C705E605EF57C19DBE86FFA1 /* PlaceholderAvatarImage.swift */; }; 9D9690D2FD4CD26FF670620F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C75EF87651B00A176AB08E97 /* AppDelegate.swift */; }; - 9D9EF9DD484E58A2E8877187 /* WaveformViewDragGestureModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BA7958A4BB9C22CA8884EF /* WaveformViewDragGestureModifier.swift */; }; 9DC5FB22B8F86C3B51E907C1 /* HomeScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D6E4C37E9F0E53D3DF951AC /* HomeScreenUITests.swift */; }; 9DD5AA10E85137140FEA86A3 /* MediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17EFA1D3D09FC2F9C5E1CB2 /* MediaProvider.swift */; }; 9DD84E014ADFB2DD813022D5 /* RoomDetailsEditScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00E5B2CBEF8F96424F095508 /* RoomDetailsEditScreenViewModelTests.swift */; }; @@ -1033,7 +1033,6 @@ 035177BCD8E8308B098AC3C2 /* WindowManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowManager.swift; sourceTree = ""; }; 0376C429FAB1687C3D905F3E /* MockCoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCoder.swift; sourceTree = ""; }; 0392E3FDE372C9B56FEEED8B /* test_voice_message.m4a */ = {isa = PBXFileReference; path = test_voice_message.m4a; sourceTree = ""; }; - 03BA7958A4BB9C22CA8884EF /* WaveformViewDragGestureModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaveformViewDragGestureModifier.swift; sourceTree = ""; }; 03DD998E523D4EC93C7ED703 /* RoomNotificationSettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsScreenViewModelProtocol.swift; sourceTree = ""; }; 03FABD73FD8086EFAB699F42 /* MediaUploadPreviewScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenViewModelTests.swift; sourceTree = ""; }; 045253F9967A535EE5B16691 /* Label.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Label.swift; sourceTree = ""; }; @@ -1681,6 +1680,7 @@ BFC9F57320EC80C7CE34FE4A /* VoiceMessagePreviewComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessagePreviewComposer.swift; sourceTree = ""; }; BFDCAC6CAAD65A2C24EA9C4B /* Dictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dictionary.swift; sourceTree = ""; }; BFEA446F8618DBA79A9239CC /* MessageForwardingScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreen.swift; sourceTree = ""; }; + BFEE91FB8ABB5F5884B6D940 /* WaveformInteractionModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaveformInteractionModifier.swift; sourceTree = ""; }; C024C151639C4E1B91FCC68B /* ElementXAttributeScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementXAttributeScope.swift; sourceTree = ""; }; C070FD43DC6BF4E50217965A /* LocalizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationTests.swift; sourceTree = ""; }; C08E9043618AE5B0BF7B07E1 /* TemplateScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenViewModelTests.swift; sourceTree = ""; }; @@ -3861,8 +3861,8 @@ AD0FF64B0E6470F66F42E182 /* EstimatedWaveformView.swift */, B8516302ACCA94A0E680AB3B /* VoiceMessageButton.swift */, FB9EABCA9348DFA27439A809 /* WaveformCursorView.swift */, + BFEE91FB8ABB5F5884B6D940 /* WaveformInteractionModifier.swift */, 94028A227645FA880B966211 /* WaveformSource.swift */, - 03BA7958A4BB9C22CA8884EF /* WaveformViewDragGestureModifier.swift */, ); path = VoiceMessage; sourceTree = ""; @@ -5898,8 +5898,8 @@ CF3827071B0BC9638BD44F5D /* WaitlistScreenViewModel.swift in Sources */, B717A820BE02C6FE2CB53F6E /* WaitlistScreenViewModelProtocol.swift in Sources */, CA12AE0DCD57D49CD96C699A /* WaveformCursorView.swift in Sources */, + 63CDC201A5980F304F6D0A1C /* WaveformInteractionModifier.swift in Sources */, B773ACD8881DB18E876D950C /* WaveformSource.swift in Sources */, - 9D9EF9DD484E58A2E8877187 /* WaveformViewDragGestureModifier.swift in Sources */, D871C8CF46950F959C9A62C3 /* WelcomeScreen.swift in Sources */, 383055C6ABE5BE058CEE1DDB /* WelcomeScreenScreenCoordinator.swift in Sources */, BD2BF1EC73FFB0C01552ECDA /* WelcomeScreenScreenModels.swift in Sources */, diff --git a/ElementX/Sources/Other/Progress/ProgressCursorModifier.swift b/ElementX/Sources/Other/Progress/ProgressCursorModifier.swift index 943873cf7..63a359861 100644 --- a/ElementX/Sources/Other/Progress/ProgressCursorModifier.swift +++ b/ElementX/Sources/Other/Progress/ProgressCursorModifier.swift @@ -19,7 +19,7 @@ import SwiftUI extension View { func progressCursor(progress: CGFloat, - cursorView: @escaping () -> CursorView) -> some View { + @ViewBuilder cursorView: @escaping () -> CursorView) -> some View { modifier(ProgressCursorModifier(progress: progress, cursorView: cursorView)) } diff --git a/ElementX/Sources/Other/VoiceMessage/WaveformInteractionModifier.swift b/ElementX/Sources/Other/VoiceMessage/WaveformInteractionModifier.swift new file mode 100644 index 000000000..d6149735b --- /dev/null +++ b/ElementX/Sources/Other/VoiceMessage/WaveformInteractionModifier.swift @@ -0,0 +1,65 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +extension View { + func waveformInteraction(isDragging: GestureState, progress: Double, showCursor: Bool, onSeek: @escaping (Double) -> Void) -> some View { + modifier(WaveformInteractionModifier(isDragging: isDragging, progress: progress, showCursor: showCursor, onSeek: onSeek)) + } +} + +private struct WaveformInteractionModifier: ViewModifier { + let isDragging: GestureState + let progress: Double + let showCursor: Bool + let onSeek: (Double) -> Void + + @ScaledMetric private var cursorVisibleWidth = 2.0 + @ScaledMetric private var cursorVisibleHeight = 24.0 + private let cursorInteractiveSize: CGFloat = 50 + + func body(content: Content) -> some View { + GeometryReader { geometry in + content + .gesture(SpatialTapGesture() + .onEnded { tapGesture in + let progress = tapGesture.location.x / geometry.size.width + onSeek(max(0, min(progress, 1.0))) + }) + .progressCursor(progress: progress) { + WaveformCursorView(color: .compound.iconAccentTertiary) + .frame(width: cursorVisibleWidth, height: cursorVisibleHeight) + .opacity(showCursor ? 1 : 0) + .frame(width: cursorInteractiveSize) + .frame(maxHeight: cursorInteractiveSize) + .contentShape(Rectangle()) + .gesture(DragGesture(coordinateSpace: .named(Self.namespaceName)) + .updating(isDragging) { dragGesture, isDragging, _ in + isDragging = true + let progress = dragGesture.location.x / geometry.size.width + onSeek(max(0, min(progress, 1.0))) + } + ) + .offset(x: -cursorInteractiveSize / 2, y: 0) + } + } + .coordinateSpace(name: Self.namespaceName) + .animation(nil, value: progress) + } + + private static let namespaceName = "voice-message-waveform" +} diff --git a/ElementX/Sources/Other/VoiceMessage/WaveformViewDragGestureModifier.swift b/ElementX/Sources/Other/VoiceMessage/WaveformViewDragGestureModifier.swift deleted file mode 100644 index 734bb2be3..000000000 --- a/ElementX/Sources/Other/VoiceMessage/WaveformViewDragGestureModifier.swift +++ /dev/null @@ -1,98 +0,0 @@ -// -// Copyright 2023 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import DSWaveformImageViews -import Foundation -import SwiftUI - -struct WaveformViewDragGestureModifier: ViewModifier { - @GestureState private var dragGestureState = WaveformViewDragState.inactive - @Binding var dragState: WaveformViewDragState - - let minimumDragDistance: Double - - func body(content: Content) -> some View { - GeometryReader { geometry in - content - .gesture(SpatialTapGesture() - .simultaneously(with: LongPressGesture()) - .sequenced(before: DragGesture(minimumDistance: minimumDragDistance, coordinateSpace: .local)) - .updating($dragGestureState) { value, state, _ in - switch value { - // (SpatialTap, LongPress) begins. - case .first(let spatialLongPress): - // 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(let spatialLongPress, let drag) where spatialLongPress.second ?? false: - var progress: Double = dragState.progress - // Compute the progress with drag location - if let location = drag?.location { - progress = location.x / geometry.size.width - } - state = .dragging(progress: progress) - // Dragging ended or the long press cancelled. - default: - state = .inactive - } - }) - } - .onChange(of: dragGestureState) { value in - dragState = value - } - } -} - -extension View { - func waveformDragGesture(_ dragState: Binding, minimumDragDistance: Double = 0) -> some View { - modifier(WaveformViewDragGestureModifier(dragState: dragState, - minimumDragDistance: minimumDragDistance)) - } -} - -enum WaveformViewDragState: Equatable { - case inactive - case pressing(progress: Double) - case dragging(progress: Double) - - var progress: Double { - switch self { - case .inactive: - return .zero - case .pressing(let progress), .dragging(let progress): - return progress - } - } - - var isActive: Bool { - switch self { - case .inactive: - return false - case .pressing, .dragging: - return true - } - } - - var isDragging: Bool { - switch self { - case .inactive, .pressing: - return false - case .dragging: - return true - } - } -} diff --git a/ElementX/Sources/Screens/ComposerToolbar/View/ComposerToolbar.swift b/ElementX/Sources/Screens/ComposerToolbar/View/ComposerToolbar.swift index 31d717eb3..ae2730014 100644 --- a/ElementX/Sources/Screens/ComposerToolbar/View/ComposerToolbar.swift +++ b/ElementX/Sources/Screens/ComposerToolbar/View/ComposerToolbar.swift @@ -116,7 +116,7 @@ struct ComposerToolbar: View { @ViewBuilder private var mainTopBarContent: some View { - ZStack { + ZStack(alignment: .bottom) { topBarLayout { if !context.composerActionsEnabled { RoomAttachmentPicker(context: context) diff --git a/ElementX/Sources/Screens/ComposerToolbar/View/VoiceMessagePreviewComposer.swift b/ElementX/Sources/Screens/ComposerToolbar/View/VoiceMessagePreviewComposer.swift index 1d27cab62..b8da88d25 100644 --- a/ElementX/Sources/Screens/ComposerToolbar/View/VoiceMessagePreviewComposer.swift +++ b/ElementX/Sources/Screens/ComposerToolbar/View/VoiceMessagePreviewComposer.swift @@ -25,29 +25,22 @@ struct VoiceMessagePreviewComposer: View { @ScaledMetric private var waveformLineWidth = 2.0 @ScaledMetric private var waveformLinePadding = 2.0 @State private var resumePlaybackAfterScrubbing = false + @GestureState var isDragging = false let onPlay: () -> Void let onPause: () -> Void let onSeek: (Double) -> Void - @State var dragState: WaveformViewDragState = .inactive - - private static let elapsedTimeFormatter: DateFormatter = { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "mm:ss" - return dateFormatter - }() - var timeLabelContent: String { // Display the duration if progress is 0.0 let percent = playerState.progress > 0.0 ? playerState.progress : 1.0 // If the duration is greater or equal 10 minutes, use the long format let elapsed = Date(timeIntervalSinceReferenceDate: playerState.duration * percent) - return Self.elapsedTimeFormatter.string(from: elapsed) + return DateFormatter.elapsedTimeFormatter.string(from: elapsed) } var showWaveformCursor: Bool { - playerState.playbackState == .playing || dragState.isActive + playerState.playbackState == .playing || isDragging } var body: some View { @@ -63,25 +56,12 @@ struct VoiceMessagePreviewComposer: View { .monospacedDigit() .fixedSize(horizontal: true, vertical: true) } + waveformView - .waveformDragGesture($dragState) - .progressCursor(progress: playerState.progress) { - WaveformCursorView(color: .compound.iconAccentTertiary) - .opacity(showWaveformCursor ? 1 : 0) - .frame(width: waveformLineWidth) - } - .onChange(of: dragState) { dragState in - switch dragState { - case .inactive: - onScrubbing(false) - case .pressing(let progress): - onScrubbing(true) - onSeek(max(0, min(progress, 1.0))) - case .dragging(let progress): - onSeek(max(0, min(progress, 1.0))) - } - self.dragState = dragState - } + .waveformInteraction(isDragging: $isDragging, + progress: playerState.progress, + showCursor: showWaveformCursor, + onSeek: onSeek) } .padding(.vertical, 4.0) .padding(.horizontal, 6.0) @@ -135,6 +115,14 @@ struct VoiceMessagePreviewComposer: View { } } +private extension DateFormatter { + static let elapsedTimeFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "mm:ss" + return dateFormatter + }() +} + struct VoiceMessagePreviewComposer_Previews: PreviewProvider, TestablePreview { static let playerState = AudioPlayerState(id: .recorderPreview, duration: 10.0, diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index d593578c7..da95cdd9c 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -80,9 +80,6 @@ enum RoomScreenViewAction { case scrolledToBottom - case enableLongPress(itemID: TimelineItemIdentifier) - case disableLongPress(itemID: TimelineItemIdentifier) - case playPauseAudio(itemID: TimelineItemIdentifier) case seekAudio(itemID: TimelineItemIdentifier, progress: Double) @@ -107,8 +104,7 @@ struct RoomScreenViewState: BindableState { var isEncryptedOneToOneRoom = false var timelineViewState = TimelineViewState() // check the doc before changing this var swiftUITimelineEnabled = false - - var longPressDisabledItemID: TimelineItemIdentifier? + var ownUserID: String var showCallButton = false diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index 2b7afb997..0cbe37220 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -158,11 +158,6 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol Task { await timelineController.playPauseAudio(for: itemID) } case .seekAudio(let itemID, let progress): Task { await timelineController.seekAudio(for: itemID, progress: progress) } - case .enableLongPress(let itemID): - guard state.longPressDisabledItemID == itemID else { return } - state.longPressDisabledItemID = nil - case .disableLongPress(let itemID): - state.longPressDisabledItemID = itemID case let .endPoll(pollStartID): state.bindings.confirmationAlertInfo = .init(id: .init(), title: L10n.actionEndPoll, diff --git a/ElementX/Sources/Screens/RoomScreen/View/Style/LongPressWithFeedback.swift b/ElementX/Sources/Screens/RoomScreen/View/Style/LongPressWithFeedback.swift index 8a676a1ec..680b6ea51 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Style/LongPressWithFeedback.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Style/LongPressWithFeedback.swift @@ -18,7 +18,6 @@ import SwiftUI struct LongPressWithFeedback: ViewModifier { let action: () -> Void - let disabled: () -> Bool @State private var triggerTask: Task? @State private var isLongPressing = false @@ -50,8 +49,6 @@ struct LongPressWithFeedback: ViewModifier { if Task.isCancelled { return } - guard !disabled() else { return } - action() feedbackGenerator.impactOccurred() } @@ -60,8 +57,8 @@ struct LongPressWithFeedback: ViewModifier { } extension View { - func longPressWithFeedback(disabled: @escaping @autoclosure () -> Bool = false, action: @escaping () -> Void) -> some View { - modifier(LongPressWithFeedback(action: action, disabled: disabled)) + func longPressWithFeedback(action: @escaping () -> Void) -> some View { + modifier(LongPressWithFeedback(action: action)) } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift index feebf5f3e..27d924d35 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift @@ -128,7 +128,7 @@ struct TimelineItemBubbledStylerView: View { } // We need a tap gesture before this long one so that it doesn't // steal away the gestures from the scroll view - .longPressWithFeedback(disabled: context.viewState.longPressDisabledItemID == timelineItem.id) { + .longPressWithFeedback { context.send(viewAction: .timelineItemMenu(itemID: timelineItem.id)) } .swipeRightAction { diff --git a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemPlainStylerView.swift b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemPlainStylerView.swift index 0cf444f6f..4b662db58 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemPlainStylerView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemPlainStylerView.swift @@ -73,7 +73,7 @@ struct TimelineItemPlainStylerView: View { } // We need a tap gesture before this long one so that it doesn't // steal away the gestures from the scroll view - .longPressWithFeedback(disabled: context.viewState.longPressDisabledItemID == timelineItem.id) { + .longPressWithFeedback { context.send(viewAction: .timelineItemMenu(itemID: timelineItem.id)) } .swipeRightAction { diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VoiceMessages/VoiceMessageRoomPlaybackView.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VoiceMessages/VoiceMessageRoomPlaybackView.swift index 74b47e33e..f389f5ac9 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VoiceMessages/VoiceMessageRoomPlaybackView.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VoiceMessages/VoiceMessageRoomPlaybackView.swift @@ -22,42 +22,26 @@ struct VoiceMessageRoomPlaybackView: View { @ObservedObject var playerState: AudioPlayerState @ScaledMetric private var waveformLineWidth = 2.0 @ScaledMetric private var waveformLinePadding = 2.0 + @GestureState var isDragging = false let onPlayPause: () -> Void let onSeek: (Double) -> Void let onScrubbing: (Bool) -> Void - private let feedbackGenerator = UIImpactFeedbackGenerator(style: .heavy) - @State private var sendFeedback = false - - private static let elapsedTimeFormatter: DateFormatter = { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "m:ss" - return dateFormatter - }() - - private static let longElapsedTimeFormatter: DateFormatter = { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "mm:ss" - return dateFormatter - }() - - @State var dragState: WaveformViewDragState = .inactive - var timeLabelContent: String { // Display the duration if progress is 0.0 let percent = playerState.progress > 0.0 ? playerState.progress : 1.0 // If the duration is greater or equal 10 minutes, use the long format let elapsed = Date(timeIntervalSinceReferenceDate: playerState.duration * percent) if playerState.duration >= 600 { - return Self.longElapsedTimeFormatter.string(from: elapsed) + return DateFormatter.longElapsedTimeFormatter.string(from: elapsed) } else { - return Self.elapsedTimeFormatter.string(from: elapsed) + return DateFormatter.elapsedTimeFormatter.string(from: elapsed) } } var showWaveformCursor: Bool { - playerState.playbackState == .playing || dragState.isDragging + playerState.playbackState == .playing || isDragging } var body: some View { @@ -73,32 +57,18 @@ struct VoiceMessageRoomPlaybackView: View { .monospacedDigit() .fixedSize(horizontal: true, vertical: true) } + waveformView - .waveformDragGesture($dragState) - .progressCursor(progress: playerState.progress) { - WaveformCursorView(color: .compound.iconAccentTertiary) - .opacity(showWaveformCursor ? 1 : 0) - .frame(width: waveformLineWidth) - } - } - .onChange(of: dragState) { newDragState in - switch newDragState { - case .inactive: - onScrubbing(false) - case .pressing: - onScrubbing(true) - feedbackGenerator.prepare() - sendFeedback = true - case .dragging(let progress): - if sendFeedback { - feedbackGenerator.impactOccurred() - sendFeedback = false - } - onSeek(max(0, min(progress, 1.0))) - } + .waveformInteraction(isDragging: $isDragging, + progress: playerState.progress, + showCursor: showWaveformCursor, + onSeek: onSeek) } .padding(.leading, 2) .padding(.trailing, 8) + .onChange(of: isDragging) { isDragging in + onScrubbing(isDragging) + } } @ViewBuilder @@ -122,37 +92,18 @@ struct VoiceMessageRoomPlaybackView: View { } } -private enum DragState: Equatable { - case inactive - case pressing(progress: Double) - case dragging(progress: Double) - - var progress: Double { - switch self { - case .inactive, .pressing: - return .zero - case .dragging(let progress): - return progress - } - } - - var isActive: Bool { - switch self { - case .inactive: - return false - case .pressing, .dragging: - return true - } - } - - var isDragging: Bool { - switch self { - case .inactive, .pressing: - return false - case .dragging: - return true - } - } +private extension DateFormatter { + static let elapsedTimeFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "m:ss" + return dateFormatter + }() + + static let longElapsedTimeFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "mm:ss" + return dateFormatter + }() } struct VoiceMessageRoomPlaybackView_Previews: PreviewProvider, TestablePreview { diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VoiceMessages/VoiceMessageRoomTimelineView.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VoiceMessages/VoiceMessageRoomTimelineView.swift index 61a702be7..82f395596 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VoiceMessages/VoiceMessageRoomTimelineView.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VoiceMessages/VoiceMessageRoomTimelineView.swift @@ -52,9 +52,7 @@ struct VoiceMessageRoomTimelineView: View { resumePlaybackAfterScrubbing = true context.send(viewAction: .playPauseAudio(itemID: timelineItem.id)) } - context.send(viewAction: .disableLongPress(itemID: timelineItem.id)) } else { - context.send(viewAction: .enableLongPress(itemID: timelineItem.id)) if resumePlaybackAfterScrubbing { context.send(viewAction: .playPauseAudio(itemID: timelineItem.id)) resumePlaybackAfterScrubbing = false diff --git a/UnitTests/__Snapshots__/PreviewTests/test_composerToolbar.Voice-Message.png b/UnitTests/__Snapshots__/PreviewTests/test_composerToolbar.Voice-Message.png index bcb569293..793475ae6 100644 --- a/UnitTests/__Snapshots__/PreviewTests/test_composerToolbar.Voice-Message.png +++ b/UnitTests/__Snapshots__/PreviewTests/test_composerToolbar.Voice-Message.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:09e3015e6bc13ff47decd46943fdee10c9c4b79d1361143f6d35e48c0822e5f8 -size 93714 +oid sha256:6387ef1c9d6ba711f20718a3bf9c3f32d5af688dbdb078481301d4a5ccc88a69 +size 93719 diff --git a/project.yml b/project.yml index 9a51accd5..69cd46d59 100644 --- a/project.yml +++ b/project.yml @@ -120,6 +120,3 @@ packages: DSWaveformImage: url: https://github.com/dmrschmidt/DSWaveformImage exactVersion: 14.1.1 - - -