diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPlayer.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPlayer.kt index b166dcd78a..a0bca22fda 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPlayer.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPlayer.kt @@ -100,6 +100,10 @@ class VoiceMessageComposerPlayer @Inject constructor( * The progress of this player between 0 and 1. */ val progress: Float = - if (duration <= currentPosition) 0f else currentPosition.toFloat() / duration.toFloat() + if (duration == 0L) + 0f + else + (currentPosition.toFloat() / duration.toFloat()) + .coerceAtMost(1f) // Current position may exceed reported duration } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt index eeab0dc95c..9d1a9189dd 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt @@ -50,6 +50,7 @@ import kotlinx.coroutines.launch import timber.log.Timber import java.io.File import javax.inject.Inject +import kotlin.time.Duration.Companion.milliseconds @SingleIn(RoomScope::class) class VoiceMessageComposerPresenter @Inject constructor( @@ -191,6 +192,7 @@ class VoiceMessageComposerPresenter @Inject constructor( isSending = isSending, isPlaying = isPlaying, playbackProgress = playerState.progress, + time = playerState.currentPosition.milliseconds, waveform = waveform, ) else -> VoiceMessageState.Idle diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt index 9788797392..da33a70bcc 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt @@ -50,13 +50,14 @@ import io.element.android.libraries.textcomposer.model.VoiceMessageState import io.element.android.libraries.voicerecorder.test.FakeVoiceRecorder import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule -import kotlinx.collections.immutable.toPersistentList import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test +import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds class VoiceMessageComposerPresenterTest { @@ -195,7 +196,7 @@ class VoiceMessageComposerPresenterTest { awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd)) awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play)) val finalState = awaitItem().also { - assertThat(it.voiceMessageState).isEqualTo(aPreviewState(isPlaying = true, playbackProgress = 0.1f)) + assertThat(it.voiceMessageState).isEqualTo(aPreviewState(isPlaying = true, playbackProgress = 0.1f, time = RECORDING_DURATION)) } voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 0) @@ -214,7 +215,7 @@ class VoiceMessageComposerPresenterTest { awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play)) awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Pause)) val finalState = awaitItem().also { - assertThat(it.voiceMessageState).isEqualTo(aPreviewState(isPlaying = false, playbackProgress = 0.1f)) + assertThat(it.voiceMessageState).isEqualTo(aPreviewState(isPlaying = false, playbackProgress = 0.1f, time = RECORDING_DURATION)) } voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 0) @@ -251,7 +252,7 @@ class VoiceMessageComposerPresenterTest { awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play)) awaitItem().eventSink(VoiceMessageComposerEvents.DeleteVoiceMessage) awaitItem().apply { - assertThat(voiceMessageState).isEqualTo(aPreviewState(isPlaying = false, playbackProgress = 0.1f)) + assertThat(voiceMessageState).isEqualTo(aPreviewState(isPlaying = false, playbackProgress = 0.1f, time = RECORDING_DURATION)) } val finalState = awaitItem() @@ -321,9 +322,11 @@ class VoiceMessageComposerPresenterTest { awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd)) awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play)) awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage) - assertThat(awaitItem().voiceMessageState).isEqualTo(aPreviewState( - isSending = true, isPlaying = false, playbackProgress = 0.1f - )) + assertThat(awaitItem().voiceMessageState).isEqualTo( + aPreviewState( + isSending = true, isPlaying = false, playbackProgress = 0.1f, time = RECORDING_DURATION + ) + ) val finalState = awaitItem() assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) @@ -570,7 +573,7 @@ class VoiceMessageComposerPresenterTest { is VoiceMessageState.Preview -> when (state.isPlaying) { // If the preview was playing, it pauses true -> awaitItem().apply { - assertThat(voiceMessageState).isEqualTo(aPreviewState(playbackProgress = 0.1f)) + assertThat(voiceMessageState).isEqualTo(aPreviewState(playbackProgress = 0.1f, time = RECORDING_DURATION)) } false -> mostRecentState } @@ -624,11 +627,13 @@ class VoiceMessageComposerPresenterTest { isPlaying: Boolean = false, playbackProgress: Float = 0f, isSending: Boolean = false, + time: Duration = 0.seconds, waveform: List = voiceRecorder.waveform, ) = VoiceMessageState.Preview( isPlaying = isPlaying, playbackProgress = playbackProgress, isSending = isSending, + time = time, waveform = waveform.toImmutableList(), ) } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt index 513b5f6a3f..36e58405eb 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt @@ -85,7 +85,6 @@ import io.element.android.wysiwyg.compose.RichTextEditor import io.element.android.wysiwyg.compose.RichTextEditorState import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toPersistentList import uniffi.wysiwyg_composer.MenuAction import kotlin.time.Duration.Companion.seconds @@ -211,6 +210,7 @@ fun TextComposer( isPlaying = voiceMessageState.isPlaying, waveform = voiceMessageState.waveform, playbackProgress = voiceMessageState.playbackProgress, + time = voiceMessageState.time, onPlayClick = onPlayVoiceMessageClicked, onPauseClick = onPauseVoiceMessageClicked, onSeek = onSeekVoiceMessage, @@ -816,13 +816,14 @@ internal fun TextComposerVoicePreview() = ElementPreview { enableVoiceMessages = true, ) PreviewColumn(items = persistentListOf({ - VoicePreview(voiceMessageState = VoiceMessageState.Recording(61.seconds, List(100) { it.toFloat() / 100 }.toPersistentList())) + VoicePreview(voiceMessageState = VoiceMessageState.Recording(61.seconds, createFakeWaveform())) }, { VoicePreview( voiceMessageState = VoiceMessageState.Preview( isSending = false, isPlaying = false, waveform = createFakeWaveform(), + time = 0.seconds, playbackProgress = 0.0f ) ) @@ -832,6 +833,7 @@ internal fun TextComposerVoicePreview() = ElementPreview { isSending = false, isPlaying = true, waveform = createFakeWaveform(), + time = 3.seconds, playbackProgress = 0.2f ) ) @@ -841,6 +843,7 @@ internal fun TextComposerVoicePreview() = ElementPreview { isSending = true, isPlaying = false, waveform = createFakeWaveform(), + time = 61.seconds, playbackProgress = 0.0f ) ) diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt index efd7bf72e1..cc3ad967c9 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt @@ -34,6 +34,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import io.element.android.libraries.designsystem.components.media.WaveformPlaybackView import io.element.android.libraries.designsystem.components.media.createFakeWaveform @@ -42,16 +43,21 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.applyScaleUp import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.IconButton +import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.textcomposer.R import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.libraries.ui.utils.time.formatShort import kotlinx.collections.immutable.ImmutableList +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds @Composable internal fun VoiceMessagePreview( isInteractive: Boolean, isPlaying: Boolean, waveform: ImmutableList, + time: Duration, modifier: Modifier = Modifier, playbackProgress: Float = 0f, onPlayClick: () -> Unit = {}, @@ -85,7 +91,13 @@ internal fun VoiceMessagePreview( Spacer(modifier = Modifier.width(8.dp)) - // TODO Add timer UI + Text( + text = time.formatShort(), + color = ElementTheme.materialColors.secondary, + style = ElementTheme.typography.fontBodySmMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) Spacer(modifier = Modifier.width(12.dp)) @@ -151,8 +163,8 @@ internal fun VoiceMessagePreviewPreview() = ElementPreview { Column( verticalArrangement = Arrangement.spacedBy(8.dp) ) { - VoiceMessagePreview(isInteractive = true, isPlaying = true, waveform = createFakeWaveform()) - VoiceMessagePreview(isInteractive = true, isPlaying = false, waveform = createFakeWaveform()) - VoiceMessagePreview(isInteractive = false, isPlaying = false, waveform = createFakeWaveform()) + VoiceMessagePreview(isInteractive = true, isPlaying = true, time = 2.seconds, playbackProgress = 0.2f, waveform = createFakeWaveform()) + VoiceMessagePreview(isInteractive = true, isPlaying = false, time = 0.seconds, playbackProgress = 0.0f, waveform = createFakeWaveform()) + VoiceMessagePreview(isInteractive = false, isPlaying = false, time = 789.seconds, playbackProgress = 0.0f, waveform = createFakeWaveform()) } } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt index b3798be203..db8d75afdd 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt @@ -26,6 +26,7 @@ sealed class VoiceMessageState { val isSending: Boolean, val isPlaying: Boolean, val playbackProgress: Float, + val time: Duration, val waveform: ImmutableList, ): VoiceMessageState() diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-16_16_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-16_16_null,NEXUS_5,1.0,en].png index b2846a78f9..a1f501e591 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-16_16_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-16_16_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8521c3615973ebf8b5d09a596a6122866648f45fa2015b3261dcb64505ebc41d -size 22459 +oid sha256:1040e8767f3ce29d40842cb054dea64dc0d355b91ae86b1d25280b9308cdbda7 +size 24517 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-16_17_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-16_17_null,NEXUS_5,1.0,en].png index 33f750d708..9251738861 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-16_17_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-16_17_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:93a4628b19a5d8a5b1f064a0c5e6d8d90e2690481b10b34c1beca2cc84b3d34c -size 20848 +oid sha256:162a6b2a725ecfda754f8fe8bc06da0bd6b7efd7eb217a2e77b1ece57f89d5d6 +size 22936 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png index ba8791f9d8..e565325253 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4008401117a064b6eed9cf06ada8c15273a52dd729ae0fe989d075808c928cb5 -size 27376 +oid sha256:c692c6d70f847d37dce6c1cdbe630621cc6bdf37d48e24b4f95aa79d9422f15d +size 28039 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png index 7c287d4d9e..e9afc557c3 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b03f6a351e4f6a22ca78b1f7b703568cef9855920ae03089c300a60c0fc593fb -size 26401 +oid sha256:2f9b34504384622feed1348a00172d520123512c433973094634d159850a7653 +size 27124