From 019ca625ab531b1c5e37d39b2b21770cc033569c Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 9 Nov 2023 19:16:42 +0200 Subject: [PATCH] Group together voice message and poll actions (#2054) * Group together voice message and poll actions * Extract nested switches into separate methods --- .../ComposerToolbarModels.swift | 32 ++++--- .../ComposerToolbarViewModel.swift | 45 ++++++---- .../View/ComposerToolbar.swift | 16 ++-- .../Screens/RoomScreen/RoomScreenModels.swift | 18 +++- .../RoomScreen/RoomScreenViewModel.swift | 87 ++++++++++++------- .../View/Timeline/PollRoomTimelineView.swift | 4 +- .../View/SecureBackupScreen.swift | 2 +- .../VoiceMessageRoomTimelineView.swift | 8 +- 8 files changed, 125 insertions(+), 87 deletions(-) diff --git a/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarModels.swift b/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarModels.swift index f33fa0ad0..dc47f48a2 100644 --- a/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarModels.swift +++ b/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarModels.swift @@ -18,6 +18,18 @@ import SwiftUI import UIKit import WysiwygComposer +enum ComposerToolbarVoiceMessageAction { + case startRecording + case stopRecording + case cancelRecording + case deleteRecording + case startPlayback + case pausePlayback + case scrubPlayback(scrubbing: Bool) + case seekPlayback(progress: Double) + case send +} + enum ComposerToolbarViewModelAction { case sendMessage(plain: String, html: String?, mode: RoomScreenComposerMode, intentionalMentions: IntentionalMentions) case displayCameraPicker @@ -31,15 +43,7 @@ enum ComposerToolbarViewModelAction { case composerModeChanged(mode: RoomScreenComposerMode) case composerFocusedChanged(isFocused: Bool) - case startVoiceMessageRecording - case stopVoiceMessageRecording - case cancelVoiceMessageRecording - case deleteVoiceMessageRecording - case startVoiceMessagePlayback - case pauseVoiceMessagePlayback - case scrubVoiceMessagePlayback(scrubbing: Bool) - case seekVoiceMessagePlayback(progress: Double) - case sendVoiceMessage + case voiceMessage(ComposerToolbarVoiceMessageAction) } enum ComposerToolbarViewAction { @@ -56,14 +60,8 @@ enum ComposerToolbarViewAction { case enableTextFormatting case composerAction(action: ComposerAction) case selectedSuggestion(_ suggestion: SuggestionItem) - case startVoiceMessageRecording - case stopVoiceMessageRecording - case cancelVoiceMessageRecording - case deleteVoiceMessageRecording - case startVoiceMessagePlayback - case pauseVoiceMessagePlayback - case scrubVoiceMessagePlayback(scrubbing: Bool) - case seekVoiceMessagePlayback(progress: Double) + + case voiceMessage(ComposerToolbarVoiceMessageAction) } struct ComposerToolbarViewState: BindableState { diff --git a/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarViewModel.swift b/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarViewModel.swift index 3faf6ad1a..778ccf2cb 100644 --- a/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarViewModel.swift +++ b/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarViewModel.swift @@ -105,7 +105,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool switch state.composerMode { case .previewVoiceMessage: - actionsSubject.send(.sendVoiceMessage) + actionsSubject.send(.voiceMessage(.send)) default: let sendHTML = ServiceLocator.shared.settings.richTextEditorEnabled actionsSubject.send(.sendMessage(plain: wysiwygViewModel.content.markdown, @@ -143,23 +143,8 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool } case .selectedSuggestion(let suggestion): handleSuggestion(suggestion) - case .startVoiceMessageRecording: - state.bindings.composerActionsEnabled = false - actionsSubject.send(.startVoiceMessageRecording) - case .stopVoiceMessageRecording: - actionsSubject.send(.stopVoiceMessageRecording) - case .cancelVoiceMessageRecording: - actionsSubject.send(.cancelVoiceMessageRecording) - case .deleteVoiceMessageRecording: - actionsSubject.send(.deleteVoiceMessageRecording) - case .startVoiceMessagePlayback: - actionsSubject.send(.startVoiceMessagePlayback) - case .pauseVoiceMessagePlayback: - actionsSubject.send(.pauseVoiceMessagePlayback) - case .seekVoiceMessagePlayback(let progress): - actionsSubject.send(.seekVoiceMessagePlayback(progress: progress)) - case .scrubVoiceMessagePlayback(let scrubbing): - actionsSubject.send(.scrubVoiceMessagePlayback(scrubbing: scrubbing)) + case .voiceMessage(let voiceMessageAction): + processVoiceMessageAction(voiceMessageAction) } } @@ -189,6 +174,30 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool // MARK: - Private + private func processVoiceMessageAction(_ action: ComposerToolbarVoiceMessageAction) { + switch action { + case .startRecording: + state.bindings.composerActionsEnabled = false + actionsSubject.send(.voiceMessage(.startRecording)) + case .stopRecording: + actionsSubject.send(.voiceMessage(.stopRecording)) + case .cancelRecording: + actionsSubject.send(.voiceMessage(.cancelRecording)) + case .deleteRecording: + actionsSubject.send(.voiceMessage(.deleteRecording)) + case .startPlayback: + actionsSubject.send(.voiceMessage(.startPlayback)) + case .pausePlayback: + actionsSubject.send(.voiceMessage(.pausePlayback)) + case .scrubPlayback(let scrubbing): + actionsSubject.send(.voiceMessage(.scrubPlayback(scrubbing: scrubbing))) + case .seekPlayback(let progress): + actionsSubject.send(.voiceMessage(.seekPlayback(progress: progress))) + case .send: + break + } + } + private func setupMentionsHandling(mentionDisplayHelper: MentionDisplayHelper) { wysiwygViewModel.textView.mentionDisplayHelper = mentionDisplayHelper diff --git a/ElementX/Sources/Screens/ComposerToolbar/View/ComposerToolbar.swift b/ElementX/Sources/Screens/ComposerToolbar/View/ComposerToolbar.swift index ea217f8a7..dd5774126 100644 --- a/ElementX/Sources/Screens/ComposerToolbar/View/ComposerToolbar.swift +++ b/ElementX/Sources/Screens/ComposerToolbar/View/ComposerToolbar.swift @@ -250,12 +250,12 @@ struct ComposerToolbar: View { private var voiceMessageRecordingButton: some View { VoiceMessageRecordingButton { - context.send(viewAction: .startVoiceMessageRecording) + context.send(viewAction: .voiceMessage(.startRecording)) } stopRecording: { minimumRecordTimeReached in if minimumRecordTimeReached { - context.send(viewAction: .stopVoiceMessageRecording) + context.send(viewAction: .voiceMessage(.stopRecording)) } else { - context.send(viewAction: .cancelVoiceMessageRecording) + context.send(viewAction: .voiceMessage(.cancelRecording)) } } .padding(4) @@ -263,7 +263,7 @@ struct ComposerToolbar: View { private var voiceMessageTrashButton: some View { Button(role: .destructive) { - context.send(viewAction: .deleteVoiceMessageRecording) + context.send(viewAction: .voiceMessage(.deleteRecording)) } label: { CompoundIcon(\.delete) .padding(EdgeInsets(top: 10, leading: 11, bottom: 10, trailing: 11)) @@ -274,13 +274,13 @@ struct ComposerToolbar: View { private func voiceMessagePreviewComposer(audioPlayerState: AudioPlayerState, waveform: WaveformSource) -> some View { VoiceMessagePreviewComposer(playerState: audioPlayerState, waveform: waveform) { - context.send(viewAction: .startVoiceMessagePlayback) + context.send(viewAction: .voiceMessage(.startPlayback)) } onPause: { - context.send(viewAction: .pauseVoiceMessagePlayback) + context.send(viewAction: .voiceMessage(.pausePlayback)) } onSeek: { progress in - context.send(viewAction: .seekVoiceMessagePlayback(progress: progress)) + context.send(viewAction: .voiceMessage(.seekPlayback(progress: progress))) } onScrubbing: { isScrubbing in - context.send(viewAction: .scrubVoiceMessagePlayback(scrubbing: isScrubbing)) + context.send(viewAction: .voiceMessage(.scrubPlayback(scrubbing: isScrubbing))) } } } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index 1a6e13268..658421978 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -63,6 +63,16 @@ enum RoomScreenComposerMode: Equatable { } } +enum RoomScreenViewPollAction { + case selectOption(pollStartID: String, optionID: String) + case end(pollStartID: String) +} + +enum RoomScreenViewAudioAction { + case playPause(itemID: TimelineItemIdentifier) + case seek(itemID: TimelineItemIdentifier, progress: Double) +} + enum RoomScreenViewAction { case displayRoomDetails case itemAppeared(itemID: TimelineItemIdentifier) @@ -71,8 +81,6 @@ enum RoomScreenViewAction { case toggleReaction(key: String, itemID: TimelineItemIdentifier) case sendReadReceiptIfNeeded(TimelineItemIdentifier) case paginateBackwards - case selectedPollOption(pollStartID: String, optionID: String) - case endPoll(pollStartID: String) case timelineItemMenu(itemID: TimelineItemIdentifier) case timelineItemMenuAction(itemID: TimelineItemIdentifier, action: TimelineItemMenuAction) @@ -80,6 +88,7 @@ enum RoomScreenViewAction { case displayEmojiPicker(itemID: TimelineItemIdentifier) case handlePasteOrDrop(provider: NSItemProvider) + case tappedOnUser(userID: String) case reactionSummary(itemID: TimelineItemIdentifier, key: String) @@ -89,8 +98,9 @@ enum RoomScreenViewAction { case scrolledToBottom - case playPauseAudio(itemID: TimelineItemIdentifier) - case seekAudio(itemID: TimelineItemIdentifier, progress: Double) + case poll(RoomScreenViewPollAction) + + case audio(RoomScreenViewAudioAction) case presentCall } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index 3fe915525..52a7106f5 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -160,18 +160,10 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol if state.swiftUITimelineEnabled { renderPendingTimelineItems() } - case let .selectedPollOption(pollStartID, optionID): - roomScreenInteractionHandler.sendPollResponse(pollStartID: pollStartID, optionID: optionID) - case .playPauseAudio(let itemID): - Task { await roomScreenInteractionHandler.playPauseAudio(for: itemID) } - case .seekAudio(let itemID, let progress): - Task { await roomScreenInteractionHandler.seekAudio(for: itemID, progress: progress) } - case let .endPoll(pollStartID): - state.bindings.confirmationAlertInfo = .init(id: .init(), - title: L10n.actionEndPoll, - message: L10n.commonPollEndConfirmation, - primaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil), - secondaryButton: .init(title: L10n.actionOk, action: { self.roomScreenInteractionHandler.endPoll(pollStartID: pollStartID) })) + case .poll(let pollAction): + processPollAction(pollAction) + case .audio(let audioAction): + processAudioAction(audioAction) case .presentCall: actionsSubject.send(.displayCallScreen) } @@ -202,31 +194,60 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol trackComposerMode(mode) case .composerFocusedChanged(isFocused: let isFocused): composerFocusedSubject.send(isFocused) - case .startVoiceMessageRecording: - Task { - await mediaPlayerProvider.detachAllStates(except: nil) - await roomScreenInteractionHandler.startRecordingVoiceMessage() - } - case .stopVoiceMessageRecording: - Task { await roomScreenInteractionHandler.stopRecordingVoiceMessage() } - case .cancelVoiceMessageRecording: - Task { await roomScreenInteractionHandler.cancelRecordingVoiceMessage() } - case .deleteVoiceMessageRecording: - Task { await roomScreenInteractionHandler.deleteCurrentVoiceMessage() } - case .sendVoiceMessage: - Task { await roomScreenInteractionHandler.sendCurrentVoiceMessage() } - case .startVoiceMessagePlayback: - Task { await roomScreenInteractionHandler.startPlayingRecordedVoiceMessage() } - case .pauseVoiceMessagePlayback: - roomScreenInteractionHandler.pausePlayingRecordedVoiceMessage() - case .seekVoiceMessagePlayback(let progress): - Task { await roomScreenInteractionHandler.seekRecordedVoiceMessage(to: progress) } - case .scrubVoiceMessagePlayback(let scrubbing): - Task { await roomScreenInteractionHandler.scrubVoiceMessagePlayback(scrubbing: scrubbing) } + case .voiceMessage(let voiceMessageAction): + processVoiceMessageAction(voiceMessageAction) } } // MARK: - Private + + private func processPollAction(_ action: RoomScreenViewPollAction) { + switch action { + case let .selectOption(pollStartID, optionID): + roomScreenInteractionHandler.sendPollResponse(pollStartID: pollStartID, optionID: optionID) + case let .end(pollStartID): + state.bindings.confirmationAlertInfo = .init(id: .init(), + title: L10n.actionEndPoll, + message: L10n.commonPollEndConfirmation, + primaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil), + secondaryButton: .init(title: L10n.actionOk, action: { self.roomScreenInteractionHandler.endPoll(pollStartID: pollStartID) })) + } + } + + private func processAudioAction(_ action: RoomScreenViewAudioAction) { + switch action { + case .playPause(let itemID): + Task { await roomScreenInteractionHandler.playPauseAudio(for: itemID) } + case .seek(let itemID, let progress): + Task { await roomScreenInteractionHandler.seekAudio(for: itemID, progress: progress) } + } + } + + private func processVoiceMessageAction(_ action: ComposerToolbarVoiceMessageAction) { + switch action { + case .startRecording: + Task { + await mediaPlayerProvider.detachAllStates(except: nil) + await roomScreenInteractionHandler.startRecordingVoiceMessage() + } + case .stopRecording: + Task { await roomScreenInteractionHandler.stopRecordingVoiceMessage() } + case .cancelRecording: + Task { await roomScreenInteractionHandler.cancelRecordingVoiceMessage() } + case .deleteRecording: + Task { await roomScreenInteractionHandler.deleteCurrentVoiceMessage() } + case .send: + Task { await roomScreenInteractionHandler.sendCurrentVoiceMessage() } + case .startPlayback: + Task { await roomScreenInteractionHandler.startPlayingRecordedVoiceMessage() } + case .pausePlayback: + roomScreenInteractionHandler.pausePlayingRecordedVoiceMessage() + case .seekPlayback(let progress): + Task { await roomScreenInteractionHandler.seekRecordedVoiceMessage(to: progress) } + case .scrubPlayback(let scrubbing): + Task { await roomScreenInteractionHandler.scrubVoiceMessagePlayback(scrubbing: scrubbing) } + } + } private func setupSubscriptions() { appSettings.$swiftUITimelineEnabled diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/PollRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/PollRoomTimelineView.swift index 98985a777..79547599a 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/PollRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/PollRoomTimelineView.swift @@ -66,7 +66,7 @@ struct PollRoomTimelineView: View { ForEach(poll.options, id: \.id) { option in Button { guard let eventID, !option.isSelected else { return } - context.send(viewAction: .selectedPollOption(pollStartID: eventID, optionID: option.id)) + context.send(viewAction: .poll(.selectOption(pollStartID: eventID, optionID: option.id))) feedbackGenerator.impactOccurred() } label: { PollOptionView(pollOption: option, @@ -93,7 +93,7 @@ struct PollRoomTimelineView: View { private var toolbarView: some View { if !poll.hasEnded, poll.createdByAccountOwner, let eventID { Button { - context.send(viewAction: .endPoll(pollStartID: eventID)) + context.send(viewAction: .poll(.end(pollStartID: eventID))) } label: { Text(L10n.actionEndPoll) .lineLimit(2, reservesSpace: false) diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/View/SecureBackupScreen.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/View/SecureBackupScreen.swift index 2c9bb30da..239f515b9 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/View/SecureBackupScreen.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/View/SecureBackupScreen.swift @@ -150,7 +150,7 @@ struct SecureBackupScreen_Previews: PreviewProvider, TestablePreview { } .previewDisplayName("Recovery incomplete") } - .snapshot(delay: 0.5) + .snapshot(delay: 1.0) } static func viewModel(keyBackupState: SecureBackupKeyBackupState, 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 ae337d2fe..a2434e1ab 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VoiceMessages/VoiceMessageRoomTimelineView.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VoiceMessages/VoiceMessageRoomTimelineView.swift @@ -40,22 +40,22 @@ struct VoiceMessageRoomTimelineView: View { } private func onPlaybackPlayPause() { - context.send(viewAction: .playPauseAudio(itemID: timelineItem.id)) + context.send(viewAction: .audio(.playPause(itemID: timelineItem.id))) } private func onPlaybackSeek(_ progress: Double) { - context.send(viewAction: .seekAudio(itemID: timelineItem.id, progress: progress)) + context.send(viewAction: .audio(.seek(itemID: timelineItem.id, progress: progress))) } private func onPlaybackScrubbing(_ dragging: Bool) { if dragging { if playerState.playbackState == .playing { resumePlaybackAfterScrubbing = true - context.send(viewAction: .playPauseAudio(itemID: timelineItem.id)) + context.send(viewAction: .audio(.playPause(itemID: timelineItem.id))) } } else { if resumePlaybackAfterScrubbing { - context.send(viewAction: .playPauseAudio(itemID: timelineItem.id)) + context.send(viewAction: .audio(.playPause(itemID: timelineItem.id))) resumePlaybackAfterScrubbing = false } }