From d9f19025979d3b35fa3edb7932045b0b65375d37 Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Thu, 26 Oct 2023 16:22:14 +0100 Subject: [PATCH 1/4] Add waveform to preview UI --- .../impl/voicemessages/WaveformUtils.kt | 7 ++- .../composer/VoiceMessageComposerPresenter.kt | 23 ++++++---- .../VoiceMessageComposerPresenterTest.kt | 43 ++++++++++--------- .../components/media/FakeWaveformFactory.kt | 35 +++++++++++++++ .../components/media/WaveformPlaybackView.kt | 17 +++++++- .../libraries/textcomposer/TextComposer.kt | 40 ++++++++--------- .../components/VoiceMessagePreview.kt | 34 ++++++++++++--- .../textcomposer/model/VoiceMessageState.kt | 5 ++- .../voicerecorder/test/FakeVoiceRecorder.kt | 3 +- 9 files changed, 148 insertions(+), 59 deletions(-) create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/FakeWaveformFactory.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/WaveformUtils.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/WaveformUtils.kt index 354e2eba7d..83926f0f22 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/WaveformUtils.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/WaveformUtils.kt @@ -20,4 +20,9 @@ package io.element.android.features.messages.impl.voicemessages * Resizes the given [0;1024] int list as per unstable MSC3246 spec * to a [0;1] range float list to be used for waveform rendering. */ -fun List.fromMSC3246range(): List = map { it / 1024f } +internal fun List.fromMSC3246range(): List = map { it / 1024f } + +/** + * Resizes the given [0;1] float list to [0;1024] int list as per unstable MSC3246 spec. + */ +internal fun List.toMSC3246range(): List = map { (it * 1024).toInt() } 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 5e0d9bcada..f3f86a7bf9 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 @@ -28,6 +28,7 @@ import androidx.compose.runtime.setValue import androidx.core.net.toUri import androidx.lifecycle.Lifecycle import io.element.android.features.messages.impl.voicemessages.VoiceMessageException +import io.element.android.features.messages.impl.voicemessages.toMSC3246range import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.SingleIn @@ -40,6 +41,8 @@ import io.element.android.libraries.textcomposer.model.VoiceMessageState import io.element.android.libraries.voicerecorder.api.VoiceRecorder import io.element.android.libraries.voicerecorder.api.VoiceRecorderState import io.element.android.services.analytics.api.AnalyticsService +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import timber.log.Timber @@ -66,6 +69,7 @@ class VoiceMessageComposerPresenter @Inject constructor( var isSending by remember { mutableStateOf(false) } val playerState by player.state.collectAsState(initial = VoiceMessageComposerPlayer.State.NotPlaying) val isPlaying by remember(playerState.isPlaying) { derivedStateOf { playerState.isPlaying } } + val waveform by remember(recorderState) { derivedStateOf { recorderState.finishedWaveform() } } val onLifecycleEvent = { event: Lifecycle.Event -> when (event) { @@ -174,11 +178,11 @@ class VoiceMessageComposerPresenter @Inject constructor( duration = state.elapsedTime, level = state.level ) - is VoiceRecorderState.Finished -> if (isSending) { - VoiceMessageState.Sending - } else { - VoiceMessageState.Preview(isPlaying = isPlaying) - } + is VoiceRecorderState.Finished -> VoiceMessageState.Preview( + isSending = isSending, + isPlaying = isPlaying, + waveform = waveform, + ) else -> VoiceMessageState.Idle }, showPermissionRationaleDialog = permissionState.showDialog, @@ -227,7 +231,8 @@ class VoiceMessageComposerPresenter @Inject constructor( } } -/** - * Resizes the given [0;1] float list to [0;1024] int list as per unstable MSC3246 spec. - */ -private fun List.toMSC3246range(): List = map { (it * 1024).toInt() } +private fun VoiceRecorderState.finishedWaveform(): ImmutableList = + (this as? VoiceRecorderState.Finished) + ?.waveform + .orEmpty() + .toImmutableList() 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 276ea7a064..955536d9f9 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 @@ -25,11 +25,11 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.TurbineTestContext import app.cash.turbine.test import com.google.common.truth.Truth.assertThat +import io.element.android.features.messages.impl.voicemessages.VoiceMessageException import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerEvents +import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPlayer import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState -import io.element.android.features.messages.impl.voicemessages.VoiceMessageException -import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPlayer import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.mediaupload.api.MediaSender @@ -44,6 +44,7 @@ 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.toImmutableList import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest @@ -216,7 +217,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) + assertThat(awaitItem().voiceMessageState).isEqualTo(aPreviewState(isSending = true)) val finalState = awaitItem() assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) @@ -239,7 +240,7 @@ class VoiceMessageComposerPresenterTest { eventSink(VoiceMessageComposerEvents.SendVoiceMessage) eventSink(VoiceMessageComposerEvents.SendVoiceMessage) } - assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Sending) + assertThat(awaitItem().voiceMessageState).isEqualTo(aPreviewState(isSending = true)) val finalState = awaitItem() assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) @@ -266,7 +267,7 @@ class VoiceMessageComposerPresenterTest { } val finalState = awaitItem() - assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Sending) + assertThat(finalState.voiceMessageState).isEqualTo(aPreviewState(isSending = true)) assertThat(matrixRoom.sendMediaCount).isEqualTo(0) assertThat(analyticsService.trackedErrors).hasSize(0) voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 0) @@ -288,7 +289,7 @@ class VoiceMessageComposerPresenterTest { val previewState = awaitItem() previewState.eventSink(VoiceMessageComposerEvents.SendVoiceMessage) - assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Sending) + assertThat(awaitItem().voiceMessageState).isEqualTo(aPreviewState(isSending = true)) ensureAllEventsConsumed() assertThat(previewState.voiceMessageState).isEqualTo(aPreviewState()) @@ -452,18 +453,15 @@ class VoiceMessageComposerPresenterTest { VoiceMessageComposerEvents.LifecycleEvent(event = Lifecycle.Event.ON_PAUSE) ) - val onPauseState = when (val vmState = mostRecentState.voiceMessageState) { - VoiceMessageState.Idle, - VoiceMessageState.Sending -> { - mostRecentState - } + val onPauseState = when (val state = mostRecentState.voiceMessageState) { + VoiceMessageState.Idle -> mostRecentState is VoiceMessageState.Recording -> { // If recorder was active, it stops awaitItem().apply { assertThat(voiceMessageState).isEqualTo(aPreviewState()) } } - is VoiceMessageState.Preview -> when(vmState.isPlaying) { + is VoiceMessageState.Preview -> when (state.isPlaying) { // If the preview was playing, it pauses true -> awaitItem().apply { assertThat(voiceMessageState).isEqualTo(aPreviewState()) @@ -476,13 +474,15 @@ class VoiceMessageComposerPresenterTest { VoiceMessageComposerEvents.LifecycleEvent(event = Lifecycle.Event.ON_DESTROY) ) - when (onPauseState.voiceMessageState) { - VoiceMessageState.Idle, - VoiceMessageState.Sending -> + when (val state = onPauseState.voiceMessageState) { + VoiceMessageState.Idle -> ensureAllEventsConsumed() - is VoiceMessageState.Recording, - is VoiceMessageState.Preview -> + is VoiceMessageState.Recording -> assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Idle) + is VoiceMessageState.Preview -> when (state.isSending) { + true -> ensureAllEventsConsumed() + false -> assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Idle) + } } } @@ -514,9 +514,12 @@ class VoiceMessageComposerPresenterTest { } private fun aPreviewState( - isPlaying: Boolean = false + isPlaying: Boolean = false, + isSending: Boolean = false, + waveform: List = voiceRecorder.waveform, ) = VoiceMessageState.Preview( - isPlaying = isPlaying + isPlaying = isPlaying, + isSending = isSending, + waveform = waveform.toImmutableList(), ) - } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/FakeWaveformFactory.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/FakeWaveformFactory.kt new file mode 100644 index 0000000000..7fde282d58 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/FakeWaveformFactory.kt @@ -0,0 +1,35 @@ +/* + * 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.libraries.designsystem.components.media + +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toPersistentList +import kotlin.random.Random + +object FakeWaveformFactory { + private val random = Random(seed = 2) + /** + * Generate a waveform for testing purposes. + * + * The waveform is a list of floats between 0 and 1. + * + * @param length The length of the waveform. + */ + fun createFakeWaveform(length: Int = 1000): ImmutableList = + List(length) { random.nextFloat() } + .toPersistentList() +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt index b5d472ef69..273d8bf79b 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt @@ -54,6 +54,21 @@ import kotlin.math.roundToInt private const val DEFAULT_GRAPHICS_LAYER_ALPHA: Float = 0.99F +/** + * A view that displays a waveform and a cursor to indicate the current playback progress. + * + * @param playbackProgress The current playback progress, between 0 and 1. + * @param showCursor Whether to show the cursor or not. + * @param waveform The waveform to display. Use [FakeWaveformFactory] to generate a fake waveform. + * @param modifier The modifier to be applied to the view. + * @param onSeek Callback when the user seeks the waveform. Called with a value between 0 and 1. + * @param brush The brush to use to draw the waveform. + * @param progressBrush The brush to use to draw the progress. + * @param cursorBrush The brush to use to draw the cursor. + * @param lineWidth The width of the waveform lines. + * @param linePadding The padding between waveform lines. + * @param minimumGraphAmplitude The minimum amplitude to display, regardless of waveform data. + */ @OptIn(ExperimentalComposeUiApi::class) @Composable fun WaveformPlaybackView( @@ -78,7 +93,7 @@ fun WaveformPlaybackView( } } val progressAnimated = animateFloatAsState(targetValue = progress, label = "progressAnimation") - val amplitudeDisplayCount by remember(canvasSize) { + val amplitudeDisplayCount by remember(canvasSize, lineWidth, linePadding) { derivedStateOf { (canvasSize.width.value / (lineWidth.value + linePadding.value)).toInt() } 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 73c1764de2..9007dd23ad 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 @@ -47,6 +47,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.components.media.FakeWaveformFactory.createFakeWaveform import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.applyScaleUp @@ -118,6 +119,10 @@ fun TextComposer( onVoicePlayerEvent(VoiceMessagePlayerEvent.Pause) } + val onSeekVoiceMessage = { position: Float -> + onVoicePlayerEvent(VoiceMessagePlayerEvent.Seek(position)) + } + val layoutModifier = modifier .fillMaxSize() .height(IntrinsicSize.Min) @@ -180,8 +185,10 @@ fun TextComposer( when (voiceMessageState) { VoiceMessageState.Idle, is VoiceMessageState.Recording -> recordVoiceButton - is VoiceMessageState.Preview -> sendVoiceButton - is VoiceMessageState.Sending -> uploadVoiceProgress + is VoiceMessageState.Preview -> when(voiceMessageState.isSending) { + true -> uploadVoiceProgress + false -> sendVoiceButton + } } else -> sendButton @@ -191,17 +198,12 @@ fun TextComposer( when (voiceMessageState) { is VoiceMessageState.Preview -> VoiceMessagePreview( - isInteractive = true, + isInteractive = !voiceMessageState.isSending, isPlaying = voiceMessageState.isPlaying, + waveform = voiceMessageState.waveform, onPlayClick = onPlayVoiceMessageClicked, - onPauseClick = onPauseVoiceMessageClicked - ) - VoiceMessageState.Sending -> - VoiceMessagePreview( - isInteractive = false, - isPlaying = false, - onPlayClick = onPlayVoiceMessageClicked, - onPauseClick = onPauseVoiceMessageClicked + onPauseClick = onPauseVoiceMessageClicked, + onSeek = onSeekVoiceMessage, ) is VoiceMessageState.Recording -> VoiceMessageRecording(voiceMessageState.level, voiceMessageState.duration) @@ -210,13 +212,9 @@ fun TextComposer( } val voiceDeleteButton = @Composable { - val enabled = when (voiceMessageState) { - is VoiceMessageState.Preview -> true - VoiceMessageState.Sending, - is VoiceMessageState.Recording, - VoiceMessageState.Idle -> false + if(voiceMessageState is VoiceMessageState.Preview) { + VoiceMessageDeleteButton(enabled = !voiceMessageState.isSending, onClick = onDeleteVoiceMessage) } - VoiceMessageDeleteButton(enabled = enabled, onClick = onDeleteVoiceMessage) } if (showTextFormatting) { @@ -267,7 +265,7 @@ private fun StandardLayout( verticalAlignment = Alignment.Bottom, ) { if (enableVoiceMessages && voiceMessageState !is VoiceMessageState.Idle) { - if (voiceMessageState is VoiceMessageState.Preview || voiceMessageState is VoiceMessageState.Sending) { + if (voiceMessageState is VoiceMessageState.Preview) { Box( modifier = Modifier .padding(bottom = 5.dp, top = 5.dp, end = 3.dp, start = 3.dp) @@ -800,11 +798,11 @@ internal fun TextComposerVoicePreview() = ElementPreview { PreviewColumn(items = persistentListOf({ VoicePreview(voiceMessageState = VoiceMessageState.Recording(61.seconds, 0.5f)) }, { - VoicePreview(voiceMessageState = VoiceMessageState.Preview(isPlaying = false)) + VoicePreview(voiceMessageState = VoiceMessageState.Preview(isSending = false, isPlaying = false, waveform = createFakeWaveform())) }, { - VoicePreview(voiceMessageState = VoiceMessageState.Preview(isPlaying = true)) + VoicePreview(voiceMessageState = VoiceMessageState.Preview(isSending = false, isPlaying = true, waveform = createFakeWaveform())) }, { - VoicePreview(voiceMessageState = VoiceMessageState.Sending) + VoicePreview(voiceMessageState = VoiceMessageState.Preview(isSending = true, isPlaying = false, waveform = createFakeWaveform())) })) } 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 4d8521834c..be06f853f8 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 @@ -20,10 +20,13 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable @@ -31,6 +34,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.components.media.FakeWaveformFactory.createFakeWaveform +import io.element.android.libraries.designsystem.components.media.WaveformPlaybackView import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.applyScaleUp @@ -39,14 +44,18 @@ import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.textcomposer.R import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.ImmutableList @Composable internal fun VoiceMessagePreview( isInteractive: Boolean, isPlaying: Boolean, + waveform: ImmutableList, modifier: Modifier = Modifier, + playbackProgress: Float = 0f, onPlayClick: () -> Unit = {}, - onPauseClick: () -> Unit = {} + onPauseClick: () -> Unit = {}, + onSeek: (Float) -> Unit = {}, ) { Row( modifier = modifier @@ -72,7 +81,22 @@ internal fun VoiceMessagePreview( enabled = isInteractive ) } - // TODO Add recording preview UI + + Spacer(modifier = Modifier.width(8.dp)) + + // TODO Add timer UI + + Spacer(modifier = Modifier.width(12.dp)) + + WaveformPlaybackView( + modifier = Modifier + .weight(1f) + .height(26.dp.applyScaleUp()), + playbackProgress = playbackProgress, + showCursor = isInteractive, + waveform = waveform, + onSeek = onSeek, + ) } } @@ -119,8 +143,8 @@ internal fun VoiceMessagePreviewPreview() = ElementPreview { Column( verticalArrangement = Arrangement.spacedBy(8.dp) ) { - VoiceMessagePreview(isInteractive = true, isPlaying = true) - VoiceMessagePreview(isInteractive = true, isPlaying = false) - VoiceMessagePreview(isInteractive = false, isPlaying = false) + VoiceMessagePreview(isInteractive = true, isPlaying = true, waveform = createFakeWaveform()) + VoiceMessagePreview(isInteractive = true, isPlaying = false, waveform = createFakeWaveform()) + VoiceMessagePreview(isInteractive = false, isPlaying = false, 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 012f655ad2..53d5029e11 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 @@ -16,15 +16,18 @@ package io.element.android.libraries.textcomposer.model +import kotlinx.collections.immutable.ImmutableList import kotlin.time.Duration sealed class VoiceMessageState { data object Idle: VoiceMessageState() data class Preview( + val isSending: Boolean, val isPlaying: Boolean, + val waveform: ImmutableList, ): VoiceMessageState() - data object Sending: VoiceMessageState() + data class Recording( val duration: Duration, val level: Float, diff --git a/libraries/voicerecorder/test/src/main/kotlin/io/element/android/libraries/voicerecorder/test/FakeVoiceRecorder.kt b/libraries/voicerecorder/test/src/main/kotlin/io/element/android/libraries/voicerecorder/test/FakeVoiceRecorder.kt index 7d3f140529..2d3dcaf376 100644 --- a/libraries/voicerecorder/test/src/main/kotlin/io/element/android/libraries/voicerecorder/test/FakeVoiceRecorder.kt +++ b/libraries/voicerecorder/test/src/main/kotlin/io/element/android/libraries/voicerecorder/test/FakeVoiceRecorder.kt @@ -42,6 +42,7 @@ class FakeVoiceRecorder( private var stoppedCount = 0 private var deletedCount = 0 + var waveform: List = listOf(0f, 1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 8f, 7f, 6f, 5f, 4f, 3f, 2f, 1f, 0f) override suspend fun startRecord() { startedCount += 1 val startedAt = timeSource.markNow() @@ -73,7 +74,7 @@ class FakeVoiceRecorder( else -> VoiceRecorderState.Finished( file = curRecording!!, mimeType = "audio/ogg", - waveform = listOf(0f, 1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 8f, 7f, 6f, 5f, 4f, 3f, 2f, 1f, 0f), + waveform = waveform, ) } ) From 3d49e808c42c883748f0417c6c5c8bff4642ac28 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Fri, 27 Oct 2023 10:28:32 +0000 Subject: [PATCH 2/4] Update screenshots --- ..._null_VoiceMessagePreview-D-15_15_null,NEXUS_5,1.0,en].png | 4 ++-- ..._null_VoiceMessagePreview-N-15_16_null,NEXUS_5,1.0,en].png | 4 ++-- ...oser_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png | 4 ++-- ...oser_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-15_15_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-15_15_null,NEXUS_5,1.0,en].png index 7fcecb3e63..4a7b967966 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-15_15_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-15_15_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0ae485c12f93649418e661ed6f4a3f1393c3e1c703e8fce84e29ff9c4607ca7b -size 8699 +oid sha256:fa3ec43f945ee660432fab9fd003d1a694df9bff5af727304827fe55cba6bc01 +size 22571 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-15_16_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-15_16_null,NEXUS_5,1.0,en].png index 013ce61eb0..e84b887c08 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-15_16_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-15_16_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2afaaa054ed17809447352d245a287854afee0d05c0efd7b00341daf733dc07b -size 6623 +oid sha256:92d2f02daba03da92f54345005c8dc683f5a04050ea7771d151a31977780019b +size 19101 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 1fc392e02d..7bb132b638 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:8eed5b8511637961df60be133688df984560cb84b333a9377e859446dfac2e04 -size 18036 +oid sha256:f1d90f127b512d5dbba790e079ba8ad53895af3d6d244e407749e2c394b27df4 +size 25537 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 19603922a0..9eb3c6e199 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:14bcc3395316251b2e25a23f95b53030bf2e2ebe0623ef7e113330a5fd2f853d -size 15852 +oid sha256:789f9ae5f5738a8aa8010fdefadd3c77b992a2f1c2f9d1ff62b89cfe49ae016d +size 23557 From 00f4c2372dfcb328c51456f1fb5cc6291662e673 Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Fri, 27 Oct 2023 11:48:01 +0100 Subject: [PATCH 3/4] Make random waveform function deterministic --- .../designsystem/components/media/FakeWaveformFactory.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/FakeWaveformFactory.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/FakeWaveformFactory.kt index 7fde282d58..19b2d4e8b5 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/FakeWaveformFactory.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/FakeWaveformFactory.kt @@ -21,7 +21,6 @@ import kotlinx.collections.immutable.toPersistentList import kotlin.random.Random object FakeWaveformFactory { - private val random = Random(seed = 2) /** * Generate a waveform for testing purposes. * @@ -29,7 +28,9 @@ object FakeWaveformFactory { * * @param length The length of the waveform. */ - fun createFakeWaveform(length: Int = 1000): ImmutableList = - List(length) { random.nextFloat() } + fun createFakeWaveform(length: Int = 1000): ImmutableList { + val random = Random(seed = 2) + return List(length) { random.nextFloat() } .toPersistentList() + } } From 48ecaadfda992162d94fe57718f2e98d3395ded3 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Fri, 27 Oct 2023 11:09:00 +0000 Subject: [PATCH 4/4] Update screenshots --- ..._null_VoiceMessagePreview-D-15_15_null,NEXUS_5,1.0,en].png | 4 ++-- ..._null_VoiceMessagePreview-N-15_16_null,NEXUS_5,1.0,en].png | 4 ++-- ...oser_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png | 4 ++-- ...oser_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-15_15_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-15_15_null,NEXUS_5,1.0,en].png index 4a7b967966..37e1350a65 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-15_15_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-15_15_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fa3ec43f945ee660432fab9fd003d1a694df9bff5af727304827fe55cba6bc01 -size 22571 +oid sha256:ff4763716e5283a13f7028532cf22953f08d93512bd19ac98e774fd950173ad5 +size 22534 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-15_16_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-15_16_null,NEXUS_5,1.0,en].png index e84b887c08..32be676e9e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-15_16_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-15_16_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:92d2f02daba03da92f54345005c8dc683f5a04050ea7771d151a31977780019b -size 19101 +oid sha256:a88306846e0bf2958e235c5b0944376f4c4983ceb026b4da962deb10b249f315 +size 19326 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 7bb132b638..ccc7e6c9e1 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:f1d90f127b512d5dbba790e079ba8ad53895af3d6d244e407749e2c394b27df4 -size 25537 +oid sha256:8fbbbac71323947df0f09b8614b23206add7dbdd36a629668f3474be8b89b5a8 +size 25549 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 9eb3c6e199..ba316b75f6 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:789f9ae5f5738a8aa8010fdefadd3c77b992a2f1c2f9d1ff62b89cfe49ae016d -size 23557 +oid sha256:81863e643154025c515efb0070ceaad2e04b74517ddbd314d214d7d7dae2286d +size 23193