Group together voice message and poll actions (#2054)

* Group together voice message and poll actions

* Extract nested switches into separate methods
This commit is contained in:
Stefan Ceriu
2023-11-09 19:16:42 +02:00
committed by GitHub
parent 4b26a798dc
commit 019ca625ab
8 changed files with 125 additions and 87 deletions
@@ -18,6 +18,18 @@ import SwiftUI
import UIKit import UIKit
import WysiwygComposer 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 { enum ComposerToolbarViewModelAction {
case sendMessage(plain: String, html: String?, mode: RoomScreenComposerMode, intentionalMentions: IntentionalMentions) case sendMessage(plain: String, html: String?, mode: RoomScreenComposerMode, intentionalMentions: IntentionalMentions)
case displayCameraPicker case displayCameraPicker
@@ -31,15 +43,7 @@ enum ComposerToolbarViewModelAction {
case composerModeChanged(mode: RoomScreenComposerMode) case composerModeChanged(mode: RoomScreenComposerMode)
case composerFocusedChanged(isFocused: Bool) case composerFocusedChanged(isFocused: Bool)
case startVoiceMessageRecording case voiceMessage(ComposerToolbarVoiceMessageAction)
case stopVoiceMessageRecording
case cancelVoiceMessageRecording
case deleteVoiceMessageRecording
case startVoiceMessagePlayback
case pauseVoiceMessagePlayback
case scrubVoiceMessagePlayback(scrubbing: Bool)
case seekVoiceMessagePlayback(progress: Double)
case sendVoiceMessage
} }
enum ComposerToolbarViewAction { enum ComposerToolbarViewAction {
@@ -56,14 +60,8 @@ enum ComposerToolbarViewAction {
case enableTextFormatting case enableTextFormatting
case composerAction(action: ComposerAction) case composerAction(action: ComposerAction)
case selectedSuggestion(_ suggestion: SuggestionItem) case selectedSuggestion(_ suggestion: SuggestionItem)
case startVoiceMessageRecording
case stopVoiceMessageRecording case voiceMessage(ComposerToolbarVoiceMessageAction)
case cancelVoiceMessageRecording
case deleteVoiceMessageRecording
case startVoiceMessagePlayback
case pauseVoiceMessagePlayback
case scrubVoiceMessagePlayback(scrubbing: Bool)
case seekVoiceMessagePlayback(progress: Double)
} }
struct ComposerToolbarViewState: BindableState { struct ComposerToolbarViewState: BindableState {
@@ -105,7 +105,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
switch state.composerMode { switch state.composerMode {
case .previewVoiceMessage: case .previewVoiceMessage:
actionsSubject.send(.sendVoiceMessage) actionsSubject.send(.voiceMessage(.send))
default: default:
let sendHTML = ServiceLocator.shared.settings.richTextEditorEnabled let sendHTML = ServiceLocator.shared.settings.richTextEditorEnabled
actionsSubject.send(.sendMessage(plain: wysiwygViewModel.content.markdown, actionsSubject.send(.sendMessage(plain: wysiwygViewModel.content.markdown,
@@ -143,23 +143,8 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
} }
case .selectedSuggestion(let suggestion): case .selectedSuggestion(let suggestion):
handleSuggestion(suggestion) handleSuggestion(suggestion)
case .startVoiceMessageRecording: case .voiceMessage(let voiceMessageAction):
state.bindings.composerActionsEnabled = false processVoiceMessageAction(voiceMessageAction)
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))
} }
} }
@@ -189,6 +174,30 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
// MARK: - Private // 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) { private func setupMentionsHandling(mentionDisplayHelper: MentionDisplayHelper) {
wysiwygViewModel.textView.mentionDisplayHelper = mentionDisplayHelper wysiwygViewModel.textView.mentionDisplayHelper = mentionDisplayHelper
@@ -250,12 +250,12 @@ struct ComposerToolbar: View {
private var voiceMessageRecordingButton: some View { private var voiceMessageRecordingButton: some View {
VoiceMessageRecordingButton { VoiceMessageRecordingButton {
context.send(viewAction: .startVoiceMessageRecording) context.send(viewAction: .voiceMessage(.startRecording))
} stopRecording: { minimumRecordTimeReached in } stopRecording: { minimumRecordTimeReached in
if minimumRecordTimeReached { if minimumRecordTimeReached {
context.send(viewAction: .stopVoiceMessageRecording) context.send(viewAction: .voiceMessage(.stopRecording))
} else { } else {
context.send(viewAction: .cancelVoiceMessageRecording) context.send(viewAction: .voiceMessage(.cancelRecording))
} }
} }
.padding(4) .padding(4)
@@ -263,7 +263,7 @@ struct ComposerToolbar: View {
private var voiceMessageTrashButton: some View { private var voiceMessageTrashButton: some View {
Button(role: .destructive) { Button(role: .destructive) {
context.send(viewAction: .deleteVoiceMessageRecording) context.send(viewAction: .voiceMessage(.deleteRecording))
} label: { } label: {
CompoundIcon(\.delete) CompoundIcon(\.delete)
.padding(EdgeInsets(top: 10, leading: 11, bottom: 10, trailing: 11)) .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 { private func voiceMessagePreviewComposer(audioPlayerState: AudioPlayerState, waveform: WaveformSource) -> some View {
VoiceMessagePreviewComposer(playerState: audioPlayerState, waveform: waveform) { VoiceMessagePreviewComposer(playerState: audioPlayerState, waveform: waveform) {
context.send(viewAction: .startVoiceMessagePlayback) context.send(viewAction: .voiceMessage(.startPlayback))
} onPause: { } onPause: {
context.send(viewAction: .pauseVoiceMessagePlayback) context.send(viewAction: .voiceMessage(.pausePlayback))
} onSeek: { progress in } onSeek: { progress in
context.send(viewAction: .seekVoiceMessagePlayback(progress: progress)) context.send(viewAction: .voiceMessage(.seekPlayback(progress: progress)))
} onScrubbing: { isScrubbing in } onScrubbing: { isScrubbing in
context.send(viewAction: .scrubVoiceMessagePlayback(scrubbing: isScrubbing)) context.send(viewAction: .voiceMessage(.scrubPlayback(scrubbing: isScrubbing)))
} }
} }
} }
@@ -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 { enum RoomScreenViewAction {
case displayRoomDetails case displayRoomDetails
case itemAppeared(itemID: TimelineItemIdentifier) case itemAppeared(itemID: TimelineItemIdentifier)
@@ -71,8 +81,6 @@ enum RoomScreenViewAction {
case toggleReaction(key: String, itemID: TimelineItemIdentifier) case toggleReaction(key: String, itemID: TimelineItemIdentifier)
case sendReadReceiptIfNeeded(TimelineItemIdentifier) case sendReadReceiptIfNeeded(TimelineItemIdentifier)
case paginateBackwards case paginateBackwards
case selectedPollOption(pollStartID: String, optionID: String)
case endPoll(pollStartID: String)
case timelineItemMenu(itemID: TimelineItemIdentifier) case timelineItemMenu(itemID: TimelineItemIdentifier)
case timelineItemMenuAction(itemID: TimelineItemIdentifier, action: TimelineItemMenuAction) case timelineItemMenuAction(itemID: TimelineItemIdentifier, action: TimelineItemMenuAction)
@@ -80,6 +88,7 @@ enum RoomScreenViewAction {
case displayEmojiPicker(itemID: TimelineItemIdentifier) case displayEmojiPicker(itemID: TimelineItemIdentifier)
case handlePasteOrDrop(provider: NSItemProvider) case handlePasteOrDrop(provider: NSItemProvider)
case tappedOnUser(userID: String) case tappedOnUser(userID: String)
case reactionSummary(itemID: TimelineItemIdentifier, key: String) case reactionSummary(itemID: TimelineItemIdentifier, key: String)
@@ -89,8 +98,9 @@ enum RoomScreenViewAction {
case scrolledToBottom case scrolledToBottom
case playPauseAudio(itemID: TimelineItemIdentifier) case poll(RoomScreenViewPollAction)
case seekAudio(itemID: TimelineItemIdentifier, progress: Double)
case audio(RoomScreenViewAudioAction)
case presentCall case presentCall
} }
@@ -160,18 +160,10 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
if state.swiftUITimelineEnabled { if state.swiftUITimelineEnabled {
renderPendingTimelineItems() renderPendingTimelineItems()
} }
case let .selectedPollOption(pollStartID, optionID): case .poll(let pollAction):
roomScreenInteractionHandler.sendPollResponse(pollStartID: pollStartID, optionID: optionID) processPollAction(pollAction)
case .playPauseAudio(let itemID): case .audio(let audioAction):
Task { await roomScreenInteractionHandler.playPauseAudio(for: itemID) } processAudioAction(audioAction)
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 .presentCall: case .presentCall:
actionsSubject.send(.displayCallScreen) actionsSubject.send(.displayCallScreen)
} }
@@ -202,31 +194,60 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
trackComposerMode(mode) trackComposerMode(mode)
case .composerFocusedChanged(isFocused: let isFocused): case .composerFocusedChanged(isFocused: let isFocused):
composerFocusedSubject.send(isFocused) composerFocusedSubject.send(isFocused)
case .startVoiceMessageRecording: case .voiceMessage(let voiceMessageAction):
Task { processVoiceMessageAction(voiceMessageAction)
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) }
} }
} }
// MARK: - Private // 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() { private func setupSubscriptions() {
appSettings.$swiftUITimelineEnabled appSettings.$swiftUITimelineEnabled
@@ -66,7 +66,7 @@ struct PollRoomTimelineView: View {
ForEach(poll.options, id: \.id) { option in ForEach(poll.options, id: \.id) { option in
Button { Button {
guard let eventID, !option.isSelected else { return } 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() feedbackGenerator.impactOccurred()
} label: { } label: {
PollOptionView(pollOption: option, PollOptionView(pollOption: option,
@@ -93,7 +93,7 @@ struct PollRoomTimelineView: View {
private var toolbarView: some View { private var toolbarView: some View {
if !poll.hasEnded, poll.createdByAccountOwner, let eventID { if !poll.hasEnded, poll.createdByAccountOwner, let eventID {
Button { Button {
context.send(viewAction: .endPoll(pollStartID: eventID)) context.send(viewAction: .poll(.end(pollStartID: eventID)))
} label: { } label: {
Text(L10n.actionEndPoll) Text(L10n.actionEndPoll)
.lineLimit(2, reservesSpace: false) .lineLimit(2, reservesSpace: false)
@@ -150,7 +150,7 @@ struct SecureBackupScreen_Previews: PreviewProvider, TestablePreview {
} }
.previewDisplayName("Recovery incomplete") .previewDisplayName("Recovery incomplete")
} }
.snapshot(delay: 0.5) .snapshot(delay: 1.0)
} }
static func viewModel(keyBackupState: SecureBackupKeyBackupState, static func viewModel(keyBackupState: SecureBackupKeyBackupState,
@@ -40,22 +40,22 @@ struct VoiceMessageRoomTimelineView: View {
} }
private func onPlaybackPlayPause() { private func onPlaybackPlayPause() {
context.send(viewAction: .playPauseAudio(itemID: timelineItem.id)) context.send(viewAction: .audio(.playPause(itemID: timelineItem.id)))
} }
private func onPlaybackSeek(_ progress: Double) { 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) { private func onPlaybackScrubbing(_ dragging: Bool) {
if dragging { if dragging {
if playerState.playbackState == .playing { if playerState.playbackState == .playing {
resumePlaybackAfterScrubbing = true resumePlaybackAfterScrubbing = true
context.send(viewAction: .playPauseAudio(itemID: timelineItem.id)) context.send(viewAction: .audio(.playPause(itemID: timelineItem.id)))
} }
} else { } else {
if resumePlaybackAfterScrubbing { if resumePlaybackAfterScrubbing {
context.send(viewAction: .playPauseAudio(itemID: timelineItem.id)) context.send(viewAction: .audio(.playPause(itemID: timelineItem.id)))
resumePlaybackAfterScrubbing = false resumePlaybackAfterScrubbing = false
} }
} }