Browse Source

Show error dialog when voice message fails to send (#1796)

---------

Co-authored-by: ElementBot <benoitm+elementbot@element.io>
pull/1804/head
jonnyandrew 10 months ago committed by GitHub
parent
commit
c3471a1d5d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 10
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt
  2. 6
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt
  3. 1
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerEvents.kt
  4. 32
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt
  5. 1
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerState.kt
  6. 12
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerStateProvider.kt
  7. 34
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageSendingFailedDialog.kt
  8. 36
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt
  9. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_11,NEXUS_5,1.0,en].png
  10. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_11,NEXUS_5,1.0,en].png

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

@ -27,6 +27,7 @@ import io.element.android.features.messages.impl.timeline.components.reactionsum @@ -27,6 +27,7 @@ import io.element.android.features.messages.impl.timeline.components.reactionsum
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessageComposerState
import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessagePreviewState
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
@ -65,7 +66,14 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> { @@ -65,7 +66,14 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
),
aMessagesState().copy(
isCallOngoing = true,
)
),
aMessagesState().copy(
enableVoiceMessages = true,
voiceMessageComposerState = aVoiceMessageComposerState(
voiceMessageState = aVoiceMessagePreviewState(),
showSendFailureDialog = true
),
),
)
}

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

@ -75,6 +75,7 @@ import io.element.android.features.messages.impl.timeline.components.retrysendme @@ -75,6 +75,7 @@ import io.element.android.features.messages.impl.timeline.components.retrysendme
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerEvents
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessagePermissionRationaleDialog
import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageSendingFailedDialog
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView
import io.element.android.libraries.androidutils.ui.hideKeyboard
import io.element.android.libraries.designsystem.atomic.molecules.IconTitlePlaceholdersRowMolecule
@ -340,6 +341,11 @@ private fun MessagesViewContent( @@ -340,6 +341,11 @@ private fun MessagesViewContent(
appName = state.appName
)
}
if (state.enableVoiceMessages && state.voiceMessageComposerState.showSendFailureDialog) {
VoiceMessageSendingFailedDialog(
onDismiss = { state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvents.DismissSendFailureDialog) },
)
}
// This key is used to force the sheet to be remeasured when the content changes.
// Any state change that should trigger a height size should be added to the list of remembered values here.

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

@ -32,4 +32,5 @@ sealed interface VoiceMessageComposerEvents { @@ -32,4 +32,5 @@ sealed interface VoiceMessageComposerEvents {
data object AcceptPermissionRationale: VoiceMessageComposerEvents
data object DismissPermissionsRationale: VoiceMessageComposerEvents
data class LifecycleEvent(val event: Lifecycle.Event): VoiceMessageComposerEvents
data object DismissSendFailureDialog: VoiceMessageComposerEvents
}

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

@ -75,6 +75,7 @@ class VoiceMessageComposerPresenter @Inject constructor( @@ -75,6 +75,7 @@ class VoiceMessageComposerPresenter @Inject constructor(
val permissionState = permissionsPresenter.present()
var isSending by remember { mutableStateOf(false) }
var showSendFailureDialog by remember { mutableStateOf(false) }
LaunchedEffect(recorderState) {
val recording = recorderState as? VoiceRecorderState.Finished
@ -138,6 +139,10 @@ class VoiceMessageComposerPresenter @Inject constructor( @@ -138,6 +139,10 @@ class VoiceMessageComposerPresenter @Inject constructor(
permissionState.eventSink(PermissionsEvents.CloseDialog)
}
val onDismissSendFailureDialog = {
showSendFailureDialog = false
}
val onSendButtonPress = lambda@{
val finishedState = recorderState as? VoiceRecorderState.Finished
if (finishedState == null) {
@ -152,11 +157,16 @@ class VoiceMessageComposerPresenter @Inject constructor( @@ -152,11 +157,16 @@ class VoiceMessageComposerPresenter @Inject constructor(
isSending = true
player.pause()
analyticsService.captureComposerEvent()
appCoroutineScope.sendMessage(
file = finishedState.file,
mimeType = finishedState.mimeType,
waveform = finishedState.waveform,
).invokeOnCompletion {
appCoroutineScope.launch {
val result = sendMessage(
file = finishedState.file,
mimeType = finishedState.mimeType,
waveform = finishedState.waveform,
)
if (result.isFailure) {
showSendFailureDialog = true
}
}.invokeOnCompletion {
isSending = false
}
}
@ -175,6 +185,7 @@ class VoiceMessageComposerPresenter @Inject constructor( @@ -175,6 +185,7 @@ class VoiceMessageComposerPresenter @Inject constructor(
VoiceMessageComposerEvents.DismissPermissionsRationale -> onDismissPermissionsRationale()
VoiceMessageComposerEvents.AcceptPermissionRationale -> onAcceptPermissionsRationale()
is VoiceMessageComposerEvents.LifecycleEvent -> onLifecycleEvent(event.event)
VoiceMessageComposerEvents.DismissSendFailureDialog -> onDismissSendFailureDialog()
}
}
@ -193,6 +204,7 @@ class VoiceMessageComposerPresenter @Inject constructor( @@ -193,6 +204,7 @@ class VoiceMessageComposerPresenter @Inject constructor(
else -> VoiceMessageState.Idle
},
showPermissionRationaleDialog = permissionState.showDialog,
showSendFailureDialog = showSendFailureDialog,
keepScreenOn = keepScreenOn,
eventSink = handleEvents,
)
@ -239,11 +251,11 @@ class VoiceMessageComposerPresenter @Inject constructor( @@ -239,11 +251,11 @@ class VoiceMessageComposerPresenter @Inject constructor(
voiceRecorder.deleteRecording()
}
private fun CoroutineScope.sendMessage(
private suspend fun sendMessage(
file: File,
mimeType: String,
waveform: List<Float>
) = launch {
waveform: List<Float>,
): Result<Unit> {
val result = mediaSender.sendVoiceMessage(
uri = file.toUri(),
mimeType = mimeType,
@ -252,10 +264,12 @@ class VoiceMessageComposerPresenter @Inject constructor( @@ -252,10 +264,12 @@ class VoiceMessageComposerPresenter @Inject constructor(
if (result.isFailure) {
Timber.e(result.exceptionOrNull(), "Voice message error")
return@launch
return result
}
voiceRecorder.deleteRecording()
return result
}
private fun AnalyticsService.captureComposerEvent() =

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

@ -23,6 +23,7 @@ import io.element.android.libraries.textcomposer.model.VoiceMessageState @@ -23,6 +23,7 @@ import io.element.android.libraries.textcomposer.model.VoiceMessageState
data class VoiceMessageComposerState(
val voiceMessageState: VoiceMessageState,
val showPermissionRationaleDialog: Boolean,
val showSendFailureDialog: Boolean,
val keepScreenOn: Boolean,
val eventSink: (VoiceMessageComposerEvents) -> Unit,
)

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

@ -17,6 +17,7 @@ @@ -17,6 +17,7 @@
package io.element.android.features.messages.impl.voicemessages.composer
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.designsystem.components.media.createFakeWaveform
import io.element.android.libraries.textcomposer.model.VoiceMessageState
import kotlinx.collections.immutable.toPersistentList
import kotlin.time.Duration.Companion.seconds
@ -32,13 +33,24 @@ internal fun aVoiceMessageComposerState( @@ -32,13 +33,24 @@ internal fun aVoiceMessageComposerState(
voiceMessageState: VoiceMessageState = VoiceMessageState.Idle,
keepScreenOn: Boolean = false,
showPermissionRationaleDialog: Boolean = false,
showSendFailureDialog: Boolean = false,
) = VoiceMessageComposerState(
voiceMessageState = voiceMessageState,
showPermissionRationaleDialog = showPermissionRationaleDialog,
showSendFailureDialog = showSendFailureDialog,
keepScreenOn = keepScreenOn,
eventSink = {},
)
internal fun aVoiceMessagePreviewState() = VoiceMessageState.Preview(
isSending = false,
isPlaying = false,
showCursor = false,
playbackProgress = 0f,
time = 10.seconds,
waveform = createFakeWaveform(),
)
internal var aWaveformLevels = List(100) { it.toFloat() / 100 }.toPersistentList()

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

@ -0,0 +1,34 @@ @@ -0,0 +1,34 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl.voicemessages.composer
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
internal fun VoiceMessageSendingFailedDialog(
onDismiss: () -> Unit,
) {
ErrorDialog(
title = stringResource(CommonStrings.common_error),
content = stringResource(CommonStrings.error_failed_uploading_voice_message),
onDismiss = onDismiss,
submitText = stringResource(CommonStrings.action_ok),
)
}

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

@ -437,6 +437,42 @@ class VoiceMessageComposerPresenterTest { @@ -437,6 +437,42 @@ class VoiceMessageComposerPresenterTest {
}
}
@Test
fun `present - send failures are displayed as an error dialog`() = runTest {
val presenter = createVoiceMessageComposerPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
// Let sending fail due to media preprocessing error
mediaPreProcessor.givenResult(Result.failure(Exception()))
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart))
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd))
awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
assertThat(awaitItem().voiceMessageState).isEqualTo(aPreviewState().toSendingState())
awaitItem().apply {
assertThat(voiceMessageState).isEqualTo(aPreviewState().toSendingState())
assertThat(showSendFailureDialog).isTrue()
}
awaitItem().apply {
assertThat(voiceMessageState).isEqualTo(aPreviewState())
assertThat(showSendFailureDialog).isTrue()
eventSink(VoiceMessageComposerEvents.DismissSendFailureDialog)
}
val finalState = awaitItem().apply {
assertThat(voiceMessageState).isEqualTo(aPreviewState())
assertThat(showSendFailureDialog).isFalse()
}
assertThat(matrixRoom.sendMediaCount).isEqualTo(0)
testPauseAndDestroy(finalState)
}
}
@Test
fun `present - send error - missing recording is tracked`() = runTest {
val presenter = createVoiceMessageComposerPresenter()

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_11,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_11,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.
Loading…
Cancel
Save