Skip to content

Commit c3471a1

Browse files
jonnyandrewElementBot
andauthored
Show error dialog when voice message fails to send (#1796)
--------- Co-authored-by: ElementBot <[email protected]>
1 parent 6eb012a commit c3471a1

File tree

10 files changed

+128
-10
lines changed

10 files changed

+128
-10
lines changed

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import io.element.android.features.messages.impl.timeline.components.reactionsum
2727
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState
2828
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
2929
import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessageComposerState
30+
import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessagePreviewState
3031
import io.element.android.libraries.architecture.Async
3132
import io.element.android.libraries.designsystem.components.avatar.AvatarData
3233
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
@@ -65,7 +66,14 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
6566
),
6667
aMessagesState().copy(
6768
isCallOngoing = true,
68-
)
69+
),
70+
aMessagesState().copy(
71+
enableVoiceMessages = true,
72+
voiceMessageComposerState = aVoiceMessageComposerState(
73+
voiceMessageState = aVoiceMessagePreviewState(),
74+
showSendFailureDialog = true
75+
),
76+
),
6977
)
7078
}
7179

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ import io.element.android.features.messages.impl.timeline.components.retrysendme
7575
import io.element.android.features.messages.impl.timeline.model.TimelineItem
7676
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerEvents
7777
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessagePermissionRationaleDialog
78+
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageSendingFailedDialog
7879
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView
7980
import io.element.android.libraries.androidutils.ui.hideKeyboard
8081
import io.element.android.libraries.designsystem.atomic.molecules.IconTitlePlaceholdersRowMolecule
@@ -340,6 +341,11 @@ private fun MessagesViewContent(
340341
appName = state.appName
341342
)
342343
}
344+
if (state.enableVoiceMessages && state.voiceMessageComposerState.showSendFailureDialog) {
345+
VoiceMessageSendingFailedDialog(
346+
onDismiss = { state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvents.DismissSendFailureDialog) },
347+
)
348+
}
343349

344350
// This key is used to force the sheet to be remeasured when the content changes.
345351
// Any state change that should trigger a height size should be added to the list of remembered values here.

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerEvents.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,5 @@ sealed interface VoiceMessageComposerEvents {
3232
data object AcceptPermissionRationale: VoiceMessageComposerEvents
3333
data object DismissPermissionsRationale: VoiceMessageComposerEvents
3434
data class LifecycleEvent(val event: Lifecycle.Event): VoiceMessageComposerEvents
35+
data object DismissSendFailureDialog: VoiceMessageComposerEvents
3536
}

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ class VoiceMessageComposerPresenter @Inject constructor(
7575

7676
val permissionState = permissionsPresenter.present()
7777
var isSending by remember { mutableStateOf(false) }
78+
var showSendFailureDialog by remember { mutableStateOf(false) }
7879

7980
LaunchedEffect(recorderState) {
8081
val recording = recorderState as? VoiceRecorderState.Finished
@@ -138,6 +139,10 @@ class VoiceMessageComposerPresenter @Inject constructor(
138139
permissionState.eventSink(PermissionsEvents.CloseDialog)
139140
}
140141

142+
val onDismissSendFailureDialog = {
143+
showSendFailureDialog = false
144+
}
145+
141146
val onSendButtonPress = lambda@{
142147
val finishedState = recorderState as? VoiceRecorderState.Finished
143148
if (finishedState == null) {
@@ -152,11 +157,16 @@ class VoiceMessageComposerPresenter @Inject constructor(
152157
isSending = true
153158
player.pause()
154159
analyticsService.captureComposerEvent()
155-
appCoroutineScope.sendMessage(
156-
file = finishedState.file,
157-
mimeType = finishedState.mimeType,
158-
waveform = finishedState.waveform,
159-
).invokeOnCompletion {
160+
appCoroutineScope.launch {
161+
val result = sendMessage(
162+
file = finishedState.file,
163+
mimeType = finishedState.mimeType,
164+
waveform = finishedState.waveform,
165+
)
166+
if (result.isFailure) {
167+
showSendFailureDialog = true
168+
}
169+
}.invokeOnCompletion {
160170
isSending = false
161171
}
162172
}
@@ -175,6 +185,7 @@ class VoiceMessageComposerPresenter @Inject constructor(
175185
VoiceMessageComposerEvents.DismissPermissionsRationale -> onDismissPermissionsRationale()
176186
VoiceMessageComposerEvents.AcceptPermissionRationale -> onAcceptPermissionsRationale()
177187
is VoiceMessageComposerEvents.LifecycleEvent -> onLifecycleEvent(event.event)
188+
VoiceMessageComposerEvents.DismissSendFailureDialog -> onDismissSendFailureDialog()
178189
}
179190
}
180191

@@ -193,6 +204,7 @@ class VoiceMessageComposerPresenter @Inject constructor(
193204
else -> VoiceMessageState.Idle
194205
},
195206
showPermissionRationaleDialog = permissionState.showDialog,
207+
showSendFailureDialog = showSendFailureDialog,
196208
keepScreenOn = keepScreenOn,
197209
eventSink = handleEvents,
198210
)
@@ -239,11 +251,11 @@ class VoiceMessageComposerPresenter @Inject constructor(
239251
voiceRecorder.deleteRecording()
240252
}
241253

242-
private fun CoroutineScope.sendMessage(
254+
private suspend fun sendMessage(
243255
file: File,
244256
mimeType: String,
245-
waveform: List<Float>
246-
) = launch {
257+
waveform: List<Float>,
258+
): Result<Unit> {
247259
val result = mediaSender.sendVoiceMessage(
248260
uri = file.toUri(),
249261
mimeType = mimeType,
@@ -252,10 +264,12 @@ class VoiceMessageComposerPresenter @Inject constructor(
252264

253265
if (result.isFailure) {
254266
Timber.e(result.exceptionOrNull(), "Voice message error")
255-
return@launch
267+
return result
256268
}
257269

258270
voiceRecorder.deleteRecording()
271+
272+
return result
259273
}
260274

261275
private fun AnalyticsService.captureComposerEvent() =

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerState.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import io.element.android.libraries.textcomposer.model.VoiceMessageState
2323
data class VoiceMessageComposerState(
2424
val voiceMessageState: VoiceMessageState,
2525
val showPermissionRationaleDialog: Boolean,
26+
val showSendFailureDialog: Boolean,
2627
val keepScreenOn: Boolean,
2728
val eventSink: (VoiceMessageComposerEvents) -> Unit,
2829
)

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerStateProvider.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package io.element.android.features.messages.impl.voicemessages.composer
1818

1919
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
20+
import io.element.android.libraries.designsystem.components.media.createFakeWaveform
2021
import io.element.android.libraries.textcomposer.model.VoiceMessageState
2122
import kotlinx.collections.immutable.toPersistentList
2223
import kotlin.time.Duration.Companion.seconds
@@ -32,13 +33,24 @@ internal fun aVoiceMessageComposerState(
3233
voiceMessageState: VoiceMessageState = VoiceMessageState.Idle,
3334
keepScreenOn: Boolean = false,
3435
showPermissionRationaleDialog: Boolean = false,
36+
showSendFailureDialog: Boolean = false,
3537
) = VoiceMessageComposerState(
3638
voiceMessageState = voiceMessageState,
3739
showPermissionRationaleDialog = showPermissionRationaleDialog,
40+
showSendFailureDialog = showSendFailureDialog,
3841
keepScreenOn = keepScreenOn,
3942
eventSink = {},
4043
)
4144

45+
internal fun aVoiceMessagePreviewState() = VoiceMessageState.Preview(
46+
isSending = false,
47+
isPlaying = false,
48+
showCursor = false,
49+
playbackProgress = 0f,
50+
time = 10.seconds,
51+
waveform = createFakeWaveform(),
52+
)
53+
4254
internal var aWaveformLevels = List(100) { it.toFloat() / 100 }.toPersistentList()
4355

4456

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright (c) 2023 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.element.android.features.messages.impl.voicemessages.composer
18+
19+
import androidx.compose.runtime.Composable
20+
import androidx.compose.ui.res.stringResource
21+
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
22+
import io.element.android.libraries.ui.strings.CommonStrings
23+
24+
@Composable
25+
internal fun VoiceMessageSendingFailedDialog(
26+
onDismiss: () -> Unit,
27+
) {
28+
ErrorDialog(
29+
title = stringResource(CommonStrings.common_error),
30+
content = stringResource(CommonStrings.error_failed_uploading_voice_message),
31+
onDismiss = onDismiss,
32+
submitText = stringResource(CommonStrings.action_ok),
33+
)
34+
}

features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,42 @@ class VoiceMessageComposerPresenterTest {
437437
}
438438
}
439439

440+
@Test
441+
fun `present - send failures are displayed as an error dialog`() = runTest {
442+
val presenter = createVoiceMessageComposerPresenter()
443+
moleculeFlow(RecompositionMode.Immediate) {
444+
presenter.present()
445+
}.test {
446+
// Let sending fail due to media preprocessing error
447+
mediaPreProcessor.givenResult(Result.failure(Exception()))
448+
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart))
449+
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd))
450+
awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
451+
452+
assertThat(awaitItem().voiceMessageState).isEqualTo(aPreviewState().toSendingState())
453+
454+
awaitItem().apply {
455+
assertThat(voiceMessageState).isEqualTo(aPreviewState().toSendingState())
456+
assertThat(showSendFailureDialog).isTrue()
457+
}
458+
459+
awaitItem().apply {
460+
assertThat(voiceMessageState).isEqualTo(aPreviewState())
461+
assertThat(showSendFailureDialog).isTrue()
462+
eventSink(VoiceMessageComposerEvents.DismissSendFailureDialog)
463+
}
464+
465+
val finalState = awaitItem().apply {
466+
assertThat(voiceMessageState).isEqualTo(aPreviewState())
467+
assertThat(showSendFailureDialog).isFalse()
468+
}
469+
470+
471+
assertThat(matrixRoom.sendMediaCount).isEqualTo(0)
472+
testPauseAndDestroy(finalState)
473+
}
474+
}
475+
440476
@Test
441477
fun `present - send error - missing recording is tracked`() = runTest {
442478
val presenter = createVoiceMessageComposerPresenter()
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)