Add progress indicator for sending voice messages (#1618)
--------- Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
parent
e61c7d8de0
commit
bdc52332bb
31 changed files with 83 additions and 18 deletions
|
|
@ -142,7 +142,11 @@ class VoiceMessageComposerPresenter @Inject constructor(
|
||||||
return VoiceMessageComposerState(
|
return VoiceMessageComposerState(
|
||||||
voiceMessageState = when (val state = recorderState) {
|
voiceMessageState = when (val state = recorderState) {
|
||||||
is VoiceRecorderState.Recording -> VoiceMessageState.Recording(level = state.level)
|
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
|
else -> VoiceMessageState.Idle
|
||||||
},
|
},
|
||||||
showPermissionRationaleDialog = permissionState.showDialog,
|
showPermissionRationaleDialog = permissionState.showDialog,
|
||||||
|
|
|
||||||
|
|
@ -127,6 +127,7 @@ class VoiceMessageComposerPresenterTest {
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart))
|
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart))
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd))
|
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd))
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
|
awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
|
||||||
|
assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Sending)
|
||||||
|
|
||||||
val finalState = awaitItem()
|
val finalState = awaitItem()
|
||||||
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
||||||
|
|
@ -148,6 +149,7 @@ class VoiceMessageComposerPresenterTest {
|
||||||
eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
|
eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
|
||||||
eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
|
eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
|
||||||
}
|
}
|
||||||
|
assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Sending)
|
||||||
|
|
||||||
val finalState = awaitItem()
|
val finalState = awaitItem()
|
||||||
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
||||||
|
|
@ -167,11 +169,13 @@ class VoiceMessageComposerPresenterTest {
|
||||||
}.test {
|
}.test {
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart))
|
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart))
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd))
|
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd))
|
||||||
|
awaitItem().apply {
|
||||||
val finalState = awaitItem().apply {
|
|
||||||
assertThat(voiceMessageState).isEqualTo(VoiceMessageState.Preview)
|
assertThat(voiceMessageState).isEqualTo(VoiceMessageState.Preview)
|
||||||
eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
|
eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val finalState = awaitItem()
|
||||||
|
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Sending)
|
||||||
assertThat(matrixRoom.sendMediaCount).isEqualTo(0)
|
assertThat(matrixRoom.sendMediaCount).isEqualTo(0)
|
||||||
assertThat(analyticsService.trackedErrors).hasSize(0)
|
assertThat(analyticsService.trackedErrors).hasSize(0)
|
||||||
|
|
||||||
|
|
@ -192,6 +196,7 @@ class VoiceMessageComposerPresenterTest {
|
||||||
val previewState = awaitItem()
|
val previewState = awaitItem()
|
||||||
|
|
||||||
previewState.eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
|
previewState.eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
|
||||||
|
assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Sending)
|
||||||
|
|
||||||
ensureAllEventsConsumed()
|
ensureAllEventsConsumed()
|
||||||
assertThat(previewState.voiceMessageState).isEqualTo(VoiceMessageState.Preview)
|
assertThat(previewState.voiceMessageState).isEqualTo(VoiceMessageState.Preview)
|
||||||
|
|
@ -349,7 +354,8 @@ class VoiceMessageComposerPresenterTest {
|
||||||
|
|
||||||
val onPauseState = when (mostRecentState.voiceMessageState) {
|
val onPauseState = when (mostRecentState.voiceMessageState) {
|
||||||
VoiceMessageState.Idle,
|
VoiceMessageState.Idle,
|
||||||
VoiceMessageState.Preview -> {
|
VoiceMessageState.Preview,
|
||||||
|
VoiceMessageState.Sending -> {
|
||||||
mostRecentState
|
mostRecentState
|
||||||
}
|
}
|
||||||
is VoiceMessageState.Recording -> {
|
is VoiceMessageState.Recording -> {
|
||||||
|
|
@ -364,7 +370,8 @@ class VoiceMessageComposerPresenterTest {
|
||||||
)
|
)
|
||||||
|
|
||||||
when (onPauseState.voiceMessageState) {
|
when (onPauseState.voiceMessageState) {
|
||||||
VoiceMessageState.Idle ->
|
VoiceMessageState.Idle,
|
||||||
|
VoiceMessageState.Sending ->
|
||||||
ensureAllEventsConsumed()
|
ensureAllEventsConsumed()
|
||||||
is VoiceMessageState.Recording,
|
is VoiceMessageState.Recording,
|
||||||
VoiceMessageState.Preview ->
|
VoiceMessageState.Preview ->
|
||||||
|
|
|
||||||
|
|
@ -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.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.text.applyScaleUp
|
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.Icon
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||||
|
|
@ -153,24 +154,35 @@ fun TextComposer(
|
||||||
composerMode = composerMode,
|
composerMode = composerMode,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
val uploadVoiceProgress = @Composable {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val textFormattingOptions = @Composable { TextFormatting(state = state) }
|
val textFormattingOptions = @Composable { TextFormatting(state = state) }
|
||||||
|
|
||||||
val sendOrRecordButton = when {
|
val sendOrRecordButton = when {
|
||||||
enableVoiceMessages && !canSendMessage ->
|
enableVoiceMessages && !canSendMessage ->
|
||||||
when (voiceMessageState) {
|
when (voiceMessageState) {
|
||||||
|
VoiceMessageState.Idle,
|
||||||
|
is VoiceMessageState.Recording -> recordVoiceButton
|
||||||
is VoiceMessageState.Preview -> sendVoiceButton
|
is VoiceMessageState.Preview -> sendVoiceButton
|
||||||
else -> recordVoiceButton
|
is VoiceMessageState.Sending -> uploadVoiceProgress
|
||||||
}
|
}
|
||||||
else ->
|
else ->
|
||||||
sendButton
|
sendButton
|
||||||
}
|
}
|
||||||
|
|
||||||
val voiceRecording = @Composable {
|
val voiceRecording = @Composable {
|
||||||
if (voiceMessageState is VoiceMessageState.Recording) {
|
when(voiceMessageState) {
|
||||||
VoiceMessageRecording(voiceMessageState.level)
|
VoiceMessageState.Preview ->
|
||||||
} else if (voiceMessageState is VoiceMessageState.Preview) {
|
VoiceMessagePreview(isInteractive = true)
|
||||||
VoiceMessagePreview()
|
VoiceMessageState.Sending ->
|
||||||
|
VoiceMessagePreview(isInteractive = false)
|
||||||
|
is VoiceMessageState.Recording ->
|
||||||
|
VoiceMessageRecording(voiceMessageState.level)
|
||||||
|
VoiceMessageState.Idle -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -245,6 +257,8 @@ private fun StandardLayout(
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.padding(bottom = 5.dp, top = 5.dp, end = 6.dp, start = 6.dp)
|
.padding(bottom = 5.dp, top = 5.dp, end = 6.dp, start = 6.dp)
|
||||||
|
.size(48.dp.applyScaleUp()),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
endButton()
|
endButton()
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
@Composable
|
||||||
private fun PreviewColumn(
|
private fun PreviewColumn(
|
||||||
items: ImmutableList<@Composable () -> Unit>,
|
items: ImmutableList<@Composable () -> Unit>,
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
package io.element.android.libraries.textcomposer.components
|
package io.element.android.libraries.textcomposer.components
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
|
@ -33,6 +34,7 @@ import io.element.android.libraries.theme.ElementTheme
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun VoiceMessagePreview(
|
internal fun VoiceMessagePreview(
|
||||||
|
isInteractive: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
|
|
@ -49,7 +51,11 @@ internal fun VoiceMessagePreview(
|
||||||
// TODO Replace with recording preview UI
|
// TODO Replace with recording preview UI
|
||||||
Text(
|
Text(
|
||||||
text = "Finished recording", // Not localized because it is a placeholder
|
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
|
style = ElementTheme.typography.fontBodySmMedium
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -58,5 +64,8 @@ internal fun VoiceMessagePreview(
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun VoiceMessagePreviewPreview() = ElementPreview {
|
internal fun VoiceMessagePreviewPreview() = ElementPreview {
|
||||||
VoiceMessagePreview()
|
Column {
|
||||||
|
VoiceMessagePreview(isInteractive = true)
|
||||||
|
VoiceMessagePreview(isInteractive = false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ sealed class VoiceMessageState {
|
||||||
data object Idle: VoiceMessageState()
|
data object Idle: VoiceMessageState()
|
||||||
|
|
||||||
data object Preview: VoiceMessageState()
|
data object Preview: VoiceMessageState()
|
||||||
|
data object Sending: VoiceMessageState()
|
||||||
data class Recording(
|
data class Recording(
|
||||||
val level: Double,
|
val level: Double,
|
||||||
): VoiceMessageState()
|
): VoiceMessageState()
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:1ae279216869b9a1a9fc8dd73ab9c5a6f769b3f275a8cfafddc51ab009868089
|
|
||||||
size 7829
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:256a21f9da287b5330fccf8de408137a39de1a73cefc5428e45cf391147fa249
|
||||||
|
size 10955
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:646d5522a31967921f2b1fbbf5716c8bd47d8c312e7778772e92618102526bc8
|
|
||||||
size 7521
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:9d92dac54bc06086757108838beb2e4f2067fdc0525a1a71c3f92b7787fce128
|
||||||
|
size 10261
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:c1bba507fdd9fc8526408d176f0519f42179a3618ff8e8a41f25b36a13a1a00f
|
||||||
|
size 18323
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:c268e0a5bbdabb2355cb44820f666d2c4f298e5eeda7720328ad08ac9ac7cdc5
|
||||||
|
size 17357
|
||||||
Loading…
Add table
Add a link
Reference in a new issue