Skip to content

Commit

Permalink
Voice message loader & error handling when sending (#1945)
Browse files Browse the repository at this point in the history
* Add loader & error handling

* Refactor PlainButtonStyle() -> .plain
  • Loading branch information
alfogrillo authored Oct 24, 2023
1 parent 4c84878 commit 8ff1c75
Show file tree
Hide file tree
Showing 9 changed files with 68 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@
"error_failed_loading_map" = "%1$@ could not load the map. Please try again later.";
"error_failed_loading_messages" = "Failed loading messages";
"error_failed_locating_user" = "%1$@ could not access your location. Please try again later.";
"error_failed_uploading_voice_message" = "Failed to upload your voice message.";
"error_missing_location_auth_ios" = "%1$@ does not have permission to access your location. You can enable access in Settings > Location";
"error_no_compatible_app_found" = "No compatible app was found to handle this action.";
"error_some_messages_have_not_been_sent" = "Some messages have not been sent";
Expand Down Expand Up @@ -539,7 +540,7 @@
"screen_welcome_button" = "Let's go!";
"screen_welcome_subtitle" = "Here’s what you need to know:";
"screen_welcome_title" = "Welcome to %1$@!";
"session_verification_banner_message" = "Looks like you’re using a new device. Verify with another device to access your encrypted messages moving forwards.";
"session_verification_banner_message" = "Looks like you’re using a new device. Verify with another device to access your encrypted messages.";
"session_verification_banner_title" = "Verify it’s you";
"settings_rageshake" = "Rageshake";
"settings_rageshake_detection_threshold" = "Detection threshold";
Expand Down
4 changes: 3 additions & 1 deletion ElementX/Sources/Generated/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,8 @@ public enum L10n {
public static func errorFailedLocatingUser(_ p1: Any) -> String {
return L10n.tr("Localizable", "error_failed_locating_user", String(describing: p1))
}
/// Failed to upload your voice message.
public static var errorFailedUploadingVoiceMessage: String { return L10n.tr("Localizable", "error_failed_uploading_voice_message") }
/// %1$@ does not have permission to access your location. You can enable access in Settings > Location
public static func errorMissingLocationAuthIos(_ p1: Any) -> String {
return L10n.tr("Localizable", "error_missing_location_auth_ios", String(describing: p1))
Expand Down Expand Up @@ -1286,7 +1288,7 @@ public enum L10n {
public static func screenWelcomeTitle(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_welcome_title", String(describing: p1))
}
/// Looks like you’re using a new device. Verify with another device to access your encrypted messages moving forwards.
/// Looks like you’re using a new device. Verify with another device to access your encrypted messages.
public static var sessionVerificationBannerMessage: String { return L10n.tr("Localizable", "session_verification_banner_message") }
/// Verify it’s you
public static var sessionVerificationBannerTitle: String { return L10n.tr("Localizable", "session_verification_banner_title") }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@ struct ComposerToolbarViewState: BindableState {

var bindings: ComposerToolbarViewStateBindings

var isUploading: Bool {
switch composerMode {
case .previewVoiceMessage(_, _, let isUploading):
return isUploading
default:
return false
}
}

var showSendButton: Bool {
switch composerMode {
case .recordVoiceMessage:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
case .recordVoiceMessage(let audioRecorderState):
state.bindings.composerFocused = false
state.audioRecorderState = audioRecorderState
case .previewVoiceMessage(let audioPlayerState, _):
case .previewVoiceMessage(let audioPlayerState, _, _):
state.audioPlayerState = audioPlayerState
case .edit, .reply:
// Focus composer when switching to reply/edit
Expand Down
64 changes: 39 additions & 25 deletions ElementX/Sources/Screens/ComposerToolbar/View/ComposerToolbar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ struct ComposerToolbar: View {
@FocusState private var composerFocused: Bool
@ScaledMetric private var sendButtonIconSize = 16
@ScaledMetric private var trashButtonIconSize = 24
@ScaledMetric(relativeTo: .title) private var spinnerSize = 44
@ScaledMetric(relativeTo: .title) private var closeRTEButtonSize = 30

@State private var voiceMessageRecordingStartTime: Date?
Expand Down Expand Up @@ -71,29 +72,14 @@ struct ComposerToolbar: View {

private var topBar: some View {
HStack(alignment: .bottom, spacing: 5) {
switch context.viewState.composerMode {
case .recordVoiceMessage(let state) where context.viewState.enableVoiceMessageComposer:
VoiceMessageRecordingComposer(recorderState: state)
.padding(.leading, 12)
case .previewVoiceMessage(let state, let waveform) where context.viewState.enableVoiceMessageComposer:
voiceMessageTrashButton
voiceMessagePreviewComposer(audioPlayerState: state, waveform: waveform)
default:
if !context.composerActionsEnabled {
RoomAttachmentPicker(context: context)
}
messageComposer
.environmentObject(context)
.onTapGesture {
guard !composerFocused else { return }
composerFocused = true
}
.padding(.leading, context.composerActionsEnabled ? 7 : 0)
.padding(.trailing, context.composerActionsEnabled ? 4 : 0)
}
mainTopBarContent

if !context.composerActionsEnabled {
if context.viewState.showSendButton {
if context.viewState.isUploading {
ProgressView()
.frame(width: spinnerSize, height: spinnerSize)
.padding(.leading, 3)
} else if context.viewState.showSendButton {
sendButton
.padding(.leading, 3)
} else if context.viewState.enableVoiceMessageComposer {
Expand All @@ -117,7 +103,34 @@ struct ComposerToolbar: View {
.padding(.leading, 7)
}
}


@ViewBuilder
private var mainTopBarContent: some View {
switch context.viewState.composerMode {
case .recordVoiceMessage(let state) where context.viewState.enableVoiceMessageComposer:
VoiceMessageRecordingComposer(recorderState: state)
.padding(.leading, 12)
case .previewVoiceMessage(let state, let waveform, let isUploading) where context.viewState.enableVoiceMessageComposer:
Group {
voiceMessageTrashButton
voiceMessagePreviewComposer(audioPlayerState: state, waveform: waveform)
}
.disabled(isUploading)
default:
if !context.composerActionsEnabled {
RoomAttachmentPicker(context: context)
}
messageComposer
.environmentObject(context)
.onTapGesture {
guard !composerFocused else { return }
composerFocused = true
}
.padding(.leading, context.composerActionsEnabled ? 7 : 0)
.padding(.trailing, context.composerActionsEnabled ? 4 : 0)
}
}

private var closeRTEButton: some View {
Button {
context.composerActionsEnabled = false
Expand Down Expand Up @@ -252,6 +265,7 @@ struct ComposerToolbar: View {
.fixedSize()
.accessibilityLabel(L10n.a11yDelete)
}
.buttonStyle(.plain)
}

private var voiceMessageRecordingButtonTooltipView: some View {
Expand Down Expand Up @@ -303,7 +317,7 @@ struct ComposerToolbar_Previews: PreviewProvider, TestablePreview {
ComposerToolbar.textWithVoiceMessage(focused: false)
ComposerToolbar.textWithVoiceMessage(focused: true)
ComposerToolbar.voiceMessageRecordingMock(recording: true)
ComposerToolbar.voiceMessagePreviewMock(recording: false)
ComposerToolbar.voiceMessagePreviewMock(recording: false, uploading: false)
}
.previewDisplayName("Voice Message")
}
Expand Down Expand Up @@ -362,7 +376,7 @@ extension ComposerToolbar {
keyCommandHandler: { _ in false })
}

static func voiceMessagePreviewMock(recording: Bool) -> ComposerToolbar {
static func voiceMessagePreviewMock(recording: Bool, uploading: Bool) -> ComposerToolbar {
let wysiwygViewModel = WysiwygComposerViewModel()
let waveformData: [Float] = Array(repeating: 1.0, count: 1000)
var composerViewModel: ComposerToolbarViewModel {
Expand All @@ -371,7 +385,7 @@ extension ComposerToolbar {
mediaProvider: MockMediaProvider(),
appSettings: ServiceLocator.shared.settings,
mentionDisplayHelper: ComposerMentionDisplayHelper.mock)
model.state.composerMode = .previewVoiceMessage(state: AudioPlayerState(id: .recorderPreview, duration: 10.0), waveform: .data(waveformData))
model.state.composerMode = .previewVoiceMessage(state: AudioPlayerState(id: .recorderPreview, duration: 10.0), waveform: .data(waveformData), isUploading: uploading)
model.state.enableVoiceMessageComposer = true
return model
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,9 @@ struct VoiceMessagePreviewComposer: View {
}
}
}
.buttonStyle(.plain)
.disabled(playerState.playbackState == .loading)
.frame(width: playPauseButtonSize,
height: playPauseButtonSize)
.frame(width: playPauseButtonSize, height: playPauseButtonSize)
}

private func onPlayPause() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ private struct CreatePollOptionView: View {
.foregroundColor(.compound.iconCriticalPrimary)
}
.disabled(!canDeleteItem)
.buttonStyle(PlainButtonStyle())
.buttonStyle(.plain)
.accessibilityLabel(L10n.actionRemove)
}
TextField(text: $text) {
Expand Down
4 changes: 2 additions & 2 deletions ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ enum RoomScreenComposerMode: Equatable {
case reply(itemID: TimelineItemIdentifier, replyDetails: TimelineItemReplyDetails, isThread: Bool)
case edit(originalItemId: TimelineItemIdentifier)
case recordVoiceMessage(state: AudioRecorderState)
case previewVoiceMessage(state: AudioPlayerState, waveform: WaveformSource)
case previewVoiceMessage(state: AudioPlayerState, waveform: WaveformSource, isUploading: Bool)

var isEdit: Bool {
switch self {
case .edit:
Expand Down
10 changes: 9 additions & 1 deletion ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -980,7 +980,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
}

mediaPlayerProvider.register(audioPlayerState: audioPlayerState)
actionsSubject.send(.composer(action: .setMode(mode: .previewVoiceMessage(state: audioPlayerState, waveform: .url(recordingURL)))))
actionsSubject.send(.composer(action: .setMode(mode: .previewVoiceMessage(state: audioPlayerState, waveform: .url(recordingURL), isUploading: false))))
}

private func cancelRecordingVoiceMessage() async {
Expand All @@ -994,12 +994,20 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
}

private func sendCurrentVoiceMessage() async {
guard let audioPlayerState = voiceMessageRecorder.previewAudioPlayerState, let recordingURL = voiceMessageRecorder.recordingURL else {
displayError(.alert(L10n.errorFailedUploadingVoiceMessage))
return
}

actionsSubject.send(.composer(action: .setMode(mode: .previewVoiceMessage(state: audioPlayerState, waveform: .url(recordingURL), isUploading: true))))
await voiceMessageRecorder.stopPlayback()
switch await voiceMessageRecorder.sendVoiceMessage(inRoom: roomProxy, audioConverter: AudioConverter()) {
case .success:
await deleteCurrentVoiceMessage()
case .failure(let error):
MXLog.error("failed to send the voice message", context: error)
actionsSubject.send(.composer(action: .setMode(mode: .previewVoiceMessage(state: audioPlayerState, waveform: .url(recordingURL), isUploading: false))))
displayError(.alert(L10n.errorFailedUploadingVoiceMessage))
}
}

Expand Down

0 comments on commit 8ff1c75

Please sign in to comment.