mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 03:06:37 +00:00
Voice message loader & error handling when sending (#1945)
* Add loader & error handling * Refactor PlainButtonStyle() -> .plain
This commit is contained in:
@@ -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";
|
||||
@@ -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";
|
||||
|
||||
@@ -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))
|
||||
@@ -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") }
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?
|
||||
@@ -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 {
|
||||
@@ -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
|
||||
@@ -252,6 +265,7 @@ struct ComposerToolbar: View {
|
||||
.fixedSize()
|
||||
.accessibilityLabel(L10n.a11yDelete)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
private var voiceMessageRecordingButtonTooltipView: some View {
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -144,7 +144,7 @@ private struct CreatePollOptionView: View {
|
||||
.foregroundColor(.compound.iconCriticalPrimary)
|
||||
}
|
||||
.disabled(!canDeleteItem)
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(L10n.actionRemove)
|
||||
}
|
||||
TextField(text: $text) {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 {
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user