Browse Source

Add progress indicator for sending voice messages (#1618)

---------

Co-authored-by: ElementBot <benoitm+elementbot@element.io>
pull/1632/head
jonnyandrew 11 months ago committed by GitHub
parent
commit
8c0d9cc6a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerPresenter.kt
  2. 15
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/VoiceMessageComposerPresenterTest.kt
  3. 48
      libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt
  4. 13
      libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt
  5. 1
      libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt
  6. 0
      tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_ComposerOptionsButton-D-8_8_null,NEXUS_5,1.0,en].png
  7. 0
      tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_ComposerOptionsButton-N-8_9_null,NEXUS_5,1.0,en].png
  8. 0
      tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_DismissTextFormattingButton-D-9_9_null,NEXUS_5,1.0,en].png
  9. 0
      tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_DismissTextFormattingButton-N-9_10_null,NEXUS_5,1.0,en].png
  10. 0
      tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_FormattingButton-D-10_10_null,NEXUS_5,1.0,en].png
  11. 0
      tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_FormattingButton-N-10_11_null,NEXUS_5,1.0,en].png
  12. 0
      tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_RecordButton-D-11_11_null,NEXUS_5,1.0,en].png
  13. 0
      tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_RecordButton-N-11_12_null,NEXUS_5,1.0,en].png
  14. 0
      tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-D-12_12_null,NEXUS_5,1.0,en].png
  15. 0
      tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-N-12_13_null,NEXUS_5,1.0,en].png
  16. 0
      tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-D-13_13_null,NEXUS_5,1.0,en].png
  17. 0
      tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-N-13_14_null,NEXUS_5,1.0,en].png
  18. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-13_13_null,NEXUS_5,1.0,en].png
  19. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-14_14_null,NEXUS_5,1.0,en].png
  20. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-13_14_null,NEXUS_5,1.0,en].png
  21. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-14_15_null,NEXUS_5,1.0,en].png
  22. 0
      tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-D-15_15_null,NEXUS_5,1.0,en].png
  23. 0
      tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-N-15_16_null,NEXUS_5,1.0,en].png
  24. 0
      tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerLinkDialogCreateLink-D-5_5_null,NEXUS_5,1.0,en].png
  25. 0
      tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerLinkDialogCreateLink-N-5_6_null,NEXUS_5,1.0,en].png
  26. 0
      tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerLinkDialogCreateLinkWithoutText-D-6_6_null,NEXUS_5,1.0,en].png
  27. 0
      tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerLinkDialogCreateLinkWithoutText-N-6_7_null,NEXUS_5,1.0,en].png
  28. 0
      tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerLinkDialogEditLink-D-7_7_null,NEXUS_5,1.0,en].png
  29. 0
      tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerLinkDialogEditLink-N-7_8_null,NEXUS_5,1.0,en].png
  30. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png
  31. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png

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

@ -142,7 +142,11 @@ class VoiceMessageComposerPresenter @Inject constructor( @@ -142,7 +142,11 @@ class VoiceMessageComposerPresenter @Inject constructor(
return VoiceMessageComposerState(
voiceMessageState = when (val state = recorderState) {
is VoiceRecorderState.Recording -> VoiceMessageState.Recording(level = state.level)
is VoiceRecorderState.Finished -> VoiceMessageState.Preview
is VoiceRecorderState.Finished -> if (isSending) {
VoiceMessageState.Sending
} else {
VoiceMessageState.Preview
}
else -> VoiceMessageState.Idle
},
showPermissionRationaleDialog = permissionState.showDialog,

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

@ -127,6 +127,7 @@ class VoiceMessageComposerPresenterTest { @@ -127,6 +127,7 @@ class VoiceMessageComposerPresenterTest {
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart))
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd))
awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Sending)
val finalState = awaitItem()
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
@ -148,6 +149,7 @@ class VoiceMessageComposerPresenterTest { @@ -148,6 +149,7 @@ class VoiceMessageComposerPresenterTest {
eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
}
assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Sending)
val finalState = awaitItem()
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
@ -167,11 +169,13 @@ class VoiceMessageComposerPresenterTest { @@ -167,11 +169,13 @@ class VoiceMessageComposerPresenterTest {
}.test {
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart))
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd))
val finalState = awaitItem().apply {
awaitItem().apply {
assertThat(voiceMessageState).isEqualTo(VoiceMessageState.Preview)
eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
}
val finalState = awaitItem()
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Sending)
assertThat(matrixRoom.sendMediaCount).isEqualTo(0)
assertThat(analyticsService.trackedErrors).hasSize(0)
@ -192,6 +196,7 @@ class VoiceMessageComposerPresenterTest { @@ -192,6 +196,7 @@ class VoiceMessageComposerPresenterTest {
val previewState = awaitItem()
previewState.eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Sending)
ensureAllEventsConsumed()
assertThat(previewState.voiceMessageState).isEqualTo(VoiceMessageState.Preview)
@ -349,7 +354,8 @@ class VoiceMessageComposerPresenterTest { @@ -349,7 +354,8 @@ class VoiceMessageComposerPresenterTest {
val onPauseState = when (mostRecentState.voiceMessageState) {
VoiceMessageState.Idle,
VoiceMessageState.Preview -> {
VoiceMessageState.Preview,
VoiceMessageState.Sending -> {
mostRecentState
}
is VoiceMessageState.Recording -> {
@ -364,7 +370,8 @@ class VoiceMessageComposerPresenterTest { @@ -364,7 +370,8 @@ class VoiceMessageComposerPresenterTest {
)
when (onPauseState.voiceMessageState) {
VoiceMessageState.Idle ->
VoiceMessageState.Idle,
VoiceMessageState.Sending ->
ensureAllEventsConsumed()
is VoiceMessageState.Recording,
VoiceMessageState.Preview ->

48
libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt

@ -50,6 +50,7 @@ import androidx.compose.ui.unit.dp @@ -50,6 +50,7 @@ import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.text.applyScaleUp
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.CommonDrawables
@ -153,24 +154,35 @@ fun TextComposer( @@ -153,24 +154,35 @@ fun TextComposer(
composerMode = composerMode,
)
}
val uploadVoiceProgress = @Composable {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
)
}
val textFormattingOptions = @Composable { TextFormatting(state = state) }
val sendOrRecordButton = when {
enableVoiceMessages && !canSendMessage ->
when (voiceMessageState) {
VoiceMessageState.Idle,
is VoiceMessageState.Recording -> recordVoiceButton
is VoiceMessageState.Preview -> sendVoiceButton
else -> recordVoiceButton
is VoiceMessageState.Sending -> uploadVoiceProgress
}
else ->
sendButton
}
val voiceRecording = @Composable {
if (voiceMessageState is VoiceMessageState.Recording) {
VoiceMessageRecording(voiceMessageState.level)
} else if (voiceMessageState is VoiceMessageState.Preview) {
VoiceMessagePreview()
when(voiceMessageState) {
VoiceMessageState.Preview ->
VoiceMessagePreview(isInteractive = true)
VoiceMessageState.Sending ->
VoiceMessagePreview(isInteractive = false)
is VoiceMessageState.Recording ->
VoiceMessageRecording(voiceMessageState.level)
VoiceMessageState.Idle -> {}
}
}
@ -245,6 +257,8 @@ private fun StandardLayout( @@ -245,6 +257,8 @@ private fun StandardLayout(
Box(
Modifier
.padding(bottom = 5.dp, top = 5.dp, end = 6.dp, start = 6.dp)
.size(48.dp.applyScaleUp()),
contentAlignment = Alignment.Center,
) {
endButton()
}
@ -721,6 +735,30 @@ internal fun TextComposerReplyPreview() = ElementPreview { @@ -721,6 +735,30 @@ internal fun TextComposerReplyPreview() = ElementPreview {
)
}
@PreviewsDayNight
@Composable
internal fun TextComposerVoicePreview() = ElementPreview {
@Composable
fun VoicePreview(
voiceMessageState: VoiceMessageState
) = TextComposer(
RichTextEditorState("", initialFocus = true),
voiceMessageState = voiceMessageState,
onSendMessage = {},
composerMode = MessageComposerMode.Normal,
onResetComposerMode = {},
enableTextFormatting = true,
enableVoiceMessages = true,
)
PreviewColumn(items = persistentListOf({
VoicePreview(voiceMessageState = VoiceMessageState.Recording(0.5))
}, {
VoicePreview(voiceMessageState = VoiceMessageState.Preview)
}, {
VoicePreview(voiceMessageState = VoiceMessageState.Sending)
}))
}
@Composable
private fun PreviewColumn(
items: ImmutableList<@Composable () -> Unit>,

13
libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt

@ -17,6 +17,7 @@ @@ -17,6 +17,7 @@
package io.element.android.libraries.textcomposer.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
@ -33,6 +34,7 @@ import io.element.android.libraries.theme.ElementTheme @@ -33,6 +34,7 @@ import io.element.android.libraries.theme.ElementTheme
@Composable
internal fun VoiceMessagePreview(
isInteractive: Boolean,
modifier: Modifier = Modifier,
) {
Row(
@ -49,7 +51,11 @@ internal fun VoiceMessagePreview( @@ -49,7 +51,11 @@ internal fun VoiceMessagePreview(
// TODO Replace with recording preview UI
Text(
text = "Finished recording", // Not localized because it is a placeholder
color = ElementTheme.colors.textSecondary,
color = if (isInteractive) {
ElementTheme.colors.textSecondary
} else {
ElementTheme.colors.textDisabled
},
style = ElementTheme.typography.fontBodySmMedium
)
}
@ -58,5 +64,8 @@ internal fun VoiceMessagePreview( @@ -58,5 +64,8 @@ internal fun VoiceMessagePreview(
@PreviewsDayNight
@Composable
internal fun VoiceMessagePreviewPreview() = ElementPreview {
VoiceMessagePreview()
Column {
VoiceMessagePreview(isInteractive = true)
VoiceMessagePreview(isInteractive = false)
}
}

1
libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt

@ -20,6 +20,7 @@ sealed class VoiceMessageState { @@ -20,6 +20,7 @@ sealed class VoiceMessageState {
data object Idle: VoiceMessageState()
data object Preview: VoiceMessageState()
data object Sending: VoiceMessageState()
data class Recording(
val level: Double,
): VoiceMessageState()

0
tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_ComposerOptionsButton-D-7_7_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_ComposerOptionsButton-D-8_8_null,NEXUS_5,1.0,en].png

0
tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_ComposerOptionsButton-N-7_8_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_ComposerOptionsButton-N-8_9_null,NEXUS_5,1.0,en].png

0
tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_DismissTextFormattingButton-D-8_8_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_DismissTextFormattingButton-D-9_9_null,NEXUS_5,1.0,en].png

0
tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_DismissTextFormattingButton-N-8_9_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_DismissTextFormattingButton-N-9_10_null,NEXUS_5,1.0,en].png

0
tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_FormattingButton-D-9_9_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_FormattingButton-D-10_10_null,NEXUS_5,1.0,en].png

0
tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_FormattingButton-N-9_10_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_FormattingButton-N-10_11_null,NEXUS_5,1.0,en].png

0
tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_RecordButton-D-10_10_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_RecordButton-D-11_11_null,NEXUS_5,1.0,en].png

0
tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_RecordButton-N-10_11_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_RecordButton-N-11_12_null,NEXUS_5,1.0,en].png

0
tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-D-11_11_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-D-12_12_null,NEXUS_5,1.0,en].png

0
tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-N-11_12_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-N-12_13_null,NEXUS_5,1.0,en].png

0
tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-D-12_12_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-D-13_13_null,NEXUS_5,1.0,en].png

0
tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-N-12_13_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-N-13_14_null,NEXUS_5,1.0,en].png

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-13_13_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-14_14_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-13_14_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-14_15_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

0
tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-D-14_14_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-D-15_15_null,NEXUS_5,1.0,en].png

0
tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-N-14_15_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-N-15_16_null,NEXUS_5,1.0,en].png

0
tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerLinkDialogCreateLink-D-4_4_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerLinkDialogCreateLink-D-5_5_null,NEXUS_5,1.0,en].png

0
tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerLinkDialogCreateLink-N-4_5_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerLinkDialogCreateLink-N-5_6_null,NEXUS_5,1.0,en].png

0
tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerLinkDialogCreateLinkWithoutText-D-5_5_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerLinkDialogCreateLinkWithoutText-D-6_6_null,NEXUS_5,1.0,en].png

0
tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerLinkDialogCreateLinkWithoutText-N-5_6_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerLinkDialogCreateLinkWithoutText-N-6_7_null,NEXUS_5,1.0,en].png

0
tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerLinkDialogEditLink-D-6_6_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerLinkDialogEditLink-D-7_7_null,NEXUS_5,1.0,en].png

0
tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerLinkDialogEditLink-N-6_7_null,NEXUS_5,1.0,en].png → tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerLinkDialogEditLink-N-7_8_null,NEXUS_5,1.0,en].png

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.
Loading…
Cancel
Save