From f5dacb7b2fdd7ddf1f971dbaaba41c94993d7aa5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 26 Nov 2025 17:14:33 +0100 Subject: [PATCH 1/6] Fix crash when recording long voice message. --- .../composer/DefaultVoiceMessageComposerPresenter.kt | 5 ++++- .../element/android/libraries/textcomposer/TextComposer.kt | 5 ++++- .../libraries/textcomposer/components/LiveWaveformView.kt | 2 +- .../textcomposer/components/VoiceMessageRecording.kt | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/DefaultVoiceMessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/DefaultVoiceMessageComposerPresenter.kt index 1acaae2aae..5405124283 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/DefaultVoiceMessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/DefaultVoiceMessageComposerPresenter.kt @@ -192,7 +192,10 @@ class DefaultVoiceMessageComposerPresenter( voiceMessageState = when (val state = recorderState) { is VoiceRecorderState.Recording -> VoiceMessageState.Recording( duration = state.elapsedTime, - levels = state.levels.toImmutableList(), + levels = state.levels + // Keep only the last 128 samples for display, else we can have a crash + .takeLast(128) + .toImmutableList(), ) is VoiceRecorderState.Finished -> previewState( 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 5553fd0b01..f2070c9610 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 @@ -299,7 +299,10 @@ fun TextComposer( onSeek = onSeekVoiceMessage, ) is VoiceMessageState.Recording -> - VoiceMessageRecording(voiceMessageState.levels, voiceMessageState.duration) + VoiceMessageRecording( + levels = voiceMessageState.levels, + duration = voiceMessageState.duration, + ) VoiceMessageState.Idle -> {} } } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/LiveWaveformView.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/LiveWaveformView.kt index 48d7dcdc3e..5e4ad02bd6 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/LiveWaveformView.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/LiveWaveformView.kt @@ -54,7 +54,7 @@ fun LiveWaveformView( var parentWidth by remember { mutableIntStateOf(0) } - val waveformWidth by remember(levels, lineWidth, linePadding) { + val waveformWidth by remember(levels.size, lineWidth, linePadding) { derivedStateOf { levels.size * (lineWidth.value + linePadding.value) } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt index 17bdf3e7bf..e742372108 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt @@ -74,7 +74,7 @@ internal fun VoiceMessageRecording( modifier = Modifier .height(26.dp) .weight(1f), - levels = levels + levels = levels, ) } } From b6ee05ab24f4b766c121fef97660c8a0b0a927a7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 27 Nov 2025 07:16:13 +0100 Subject: [PATCH 2/6] Rename test class --- ...enterTest.kt => DefaultVoiceMessageComposerPresenterTest.kt} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/{VoiceMessageComposerPresenterTest.kt => DefaultVoiceMessageComposerPresenterTest.kt} (99%) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/DefaultVoiceMessageComposerPresenterTest.kt similarity index 99% rename from features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenterTest.kt rename to features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/DefaultVoiceMessageComposerPresenterTest.kt index 73688d2db4..c5ea1743a8 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/DefaultVoiceMessageComposerPresenterTest.kt @@ -57,7 +57,7 @@ import java.io.File import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds -class VoiceMessageComposerPresenterTest { +class DefaultVoiceMessageComposerPresenterTest { @get:Rule val warmUpRule = WarmUpRule() From 2cad307f6f6984423e74efd5a4bb678b18b40c29 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 27 Nov 2025 09:02:07 +0100 Subject: [PATCH 3/6] Use test extension --- ...efaultVoiceMessageComposerPresenterTest.kt | 96 +++++-------------- 1 file changed, 24 insertions(+), 72 deletions(-) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/DefaultVoiceMessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/DefaultVoiceMessageComposerPresenterTest.kt index c5ea1743a8..ff98a65a70 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/DefaultVoiceMessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/DefaultVoiceMessageComposerPresenterTest.kt @@ -12,10 +12,7 @@ package io.element.android.features.messages.impl.voicemessages.composer import android.Manifest import androidx.lifecycle.Lifecycle -import app.cash.molecule.RecompositionMode -import app.cash.molecule.moleculeFlow import app.cash.turbine.TurbineTestContext -import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.Composer import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerEvents @@ -46,6 +43,7 @@ import io.element.android.libraries.voicerecorder.test.FakeVoiceRecorder import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.test import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope @@ -91,9 +89,7 @@ class DefaultVoiceMessageComposerPresenterTest { @Test fun `present - initial state`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitItem() assertThat(initialState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) voiceRecorder.assertCalls(started = 0) @@ -105,9 +101,7 @@ class DefaultVoiceMessageComposerPresenterTest { @Test fun `present - recording state`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) val finalState = awaitItem() @@ -121,9 +115,7 @@ class DefaultVoiceMessageComposerPresenterTest { @Test fun `present - recording keeps screen on`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { awaitItem().apply { eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) assertThat(keepScreenOn).isFalse() @@ -145,9 +137,7 @@ class DefaultVoiceMessageComposerPresenterTest { @Test fun `present - abort recording`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Cancel)) val finalState = awaitItem() @@ -160,9 +150,7 @@ class DefaultVoiceMessageComposerPresenterTest { @Test fun `present - finish recording`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop)) @@ -177,9 +165,7 @@ class DefaultVoiceMessageComposerPresenterTest { @Test fun `present - play recording before it is ready`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) val finalState = awaitItem().apply { this.eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play)) @@ -196,9 +182,7 @@ class DefaultVoiceMessageComposerPresenterTest { @Test fun `present - play recording`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop)) awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play)) @@ -214,9 +198,7 @@ class DefaultVoiceMessageComposerPresenterTest { @Test fun `present - pause recording`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop)) awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play)) @@ -233,9 +215,7 @@ class DefaultVoiceMessageComposerPresenterTest { @Test fun `present - seek recording`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop)) awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Seek(0.5f))) @@ -260,9 +240,7 @@ class DefaultVoiceMessageComposerPresenterTest { @Test fun `present - delete recording`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop)) awaitItem().eventSink(VoiceMessageComposerEvents.DeleteVoiceMessage) @@ -278,9 +256,7 @@ class DefaultVoiceMessageComposerPresenterTest { @Test fun `present - delete while playing`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop)) awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play)) @@ -300,9 +276,7 @@ class DefaultVoiceMessageComposerPresenterTest { @Test fun `present - send recording`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop)) awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage) @@ -319,9 +293,7 @@ class DefaultVoiceMessageComposerPresenterTest { @Test fun `present - sending is tracked`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { // Send a normal voice message messageComposerContext.composerMode = MessageComposerMode.Normal awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) @@ -348,9 +320,7 @@ class DefaultVoiceMessageComposerPresenterTest { @Test fun `present - send while playing`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop)) awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play)) @@ -370,9 +340,7 @@ class DefaultVoiceMessageComposerPresenterTest { @Test fun `present - send recording before previous completed, waits`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop)) awaitItem().run { @@ -395,9 +363,7 @@ class DefaultVoiceMessageComposerPresenterTest { // Let sending fail due to media preprocessing error mediaPreProcessor.givenResult(Result.failure(Exception())) val presenter = createDefaultVoiceMessageComposerPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop)) awaitItem().apply { @@ -419,9 +385,7 @@ class DefaultVoiceMessageComposerPresenterTest { fun `present - send failures can be retried`() = runTest { // Let sending fail due to media preprocessing error val presenter = createDefaultVoiceMessageComposerPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { mediaPreProcessor.givenResult(Result.failure(Exception())) awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop)) @@ -448,9 +412,7 @@ class DefaultVoiceMessageComposerPresenterTest { @Test fun `present - send failures are displayed as an error dialog`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { // Let sending fail due to media preprocessing error mediaPreProcessor.givenResult(Result.failure(Exception())) awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) @@ -483,9 +445,7 @@ class DefaultVoiceMessageComposerPresenterTest { @Test fun `present - send error - missing recording is tracked`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitItem() // Send the message before recording anything initialState.eventSink(VoiceMessageComposerEvents.SendVoiceMessage) @@ -504,9 +464,7 @@ class DefaultVoiceMessageComposerPresenterTest { val exception = SecurityException("") voiceRecorder.givenThrowsSecurityException(exception) val presenter = createDefaultVoiceMessageComposerPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitItem() initialState.eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) @@ -528,9 +486,7 @@ class DefaultVoiceMessageComposerPresenterTest { val presenter = createDefaultVoiceMessageComposerPresenter( permissionsPresenter = permissionsPresenter, ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { val initialState = awaitItem() initialState.eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Idle) @@ -557,9 +513,7 @@ class DefaultVoiceMessageComposerPresenterTest { val presenter = createDefaultVoiceMessageComposerPresenter( permissionsPresenter = permissionsPresenter, ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) // See the dialog and accept it @@ -591,9 +545,7 @@ class DefaultVoiceMessageComposerPresenterTest { val presenter = createDefaultVoiceMessageComposerPresenter( permissionsPresenter = permissionsPresenter, ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.test { awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) // See the dialog and accept it From 88459b7a74373aff7b427a7e2c53fa103fea2a4b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 27 Nov 2025 09:24:43 +0100 Subject: [PATCH 4/6] Add unit test to ensure that number of levels is limited. --- ...efaultVoiceMessageComposerPresenterTest.kt | 26 +++++++++++++++++++ .../voicerecorder/test/FakeVoiceRecorder.kt | 2 ++ 2 files changed, 28 insertions(+) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/DefaultVoiceMessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/DefaultVoiceMessageComposerPresenterTest.kt index ff98a65a70..e478ccc726 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/DefaultVoiceMessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/DefaultVoiceMessageComposerPresenterTest.kt @@ -39,6 +39,7 @@ import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent import io.element.android.libraries.textcomposer.model.VoiceMessageRecorderEvent import io.element.android.libraries.textcomposer.model.VoiceMessageState import io.element.android.libraries.voiceplayer.api.VoiceMessageException +import io.element.android.libraries.voicerecorder.api.VoiceRecorder import io.element.android.libraries.voicerecorder.test.FakeVoiceRecorder import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule @@ -112,6 +113,30 @@ class DefaultVoiceMessageComposerPresenterTest { } } + @Test + fun `present - recording state - number of levels is limited`() = runTest { + val numberOfLevels = 200 + val levels = List(numberOfLevels) { it / numberOfLevels.toFloat() } + val voiceRecorder = FakeVoiceRecorder( + levels = levels, + recordingDuration = RECORDING_DURATION, + ) + val presenter = createDefaultVoiceMessageComposerPresenter( + voiceRecorder = voiceRecorder, + ) + presenter.test { + awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) + skipItems(numberOfLevels / 2 - 1) + val finalState = awaitItem() + assertThat(finalState.voiceMessageState).isInstanceOf(VoiceMessageState.Recording::class.java) + val recordingState = finalState.voiceMessageState as VoiceMessageState.Recording + // The number of levels should be limited to 128 items + assertThat(recordingState.levels.size).isEqualTo(128) + assertThat(recordingState.levels).isEqualTo(levels.takeLast(128)) + testPauseAndDestroy(finalState) + } + } + @Test fun `present - recording keeps screen on`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() @@ -614,6 +639,7 @@ class DefaultVoiceMessageComposerPresenterTest { private fun TestScope.createDefaultVoiceMessageComposerPresenter( permissionsPresenter: PermissionsPresenter = createFakePermissionsPresenter(), + voiceRecorder: VoiceRecorder = this@DefaultVoiceMessageComposerPresenterTest.voiceRecorder, ): DefaultVoiceMessageComposerPresenter { return DefaultVoiceMessageComposerPresenter( sessionCoroutineScope = backgroundScope, 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 a04f6d0ac9..ec62227857 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 @@ -14,6 +14,7 @@ import io.element.android.libraries.voicerecorder.api.VoiceRecorder import io.element.android.libraries.voicerecorder.api.VoiceRecorderState import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.yield import java.io.File import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds @@ -49,6 +50,7 @@ class FakeVoiceRecorder( timeSource += recordingDuration for (i in 1..levels.size) { _state.emit(VoiceRecorderState.Recording(startedAt.elapsedNow(), levels.take(i))) + yield() } } From 9d4e9fe2d2435f2ed6b9c99f3efb77edb1405771 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 27 Nov 2025 09:41:29 +0100 Subject: [PATCH 5/6] Rename VoiceMessageComposerEvents to VoiceMessageComposerEvent --- ...Events.kt => VoiceMessageComposerEvent.kt} | 18 +-- .../composer/VoiceMessageComposerState.kt | 2 +- .../features/messages/impl/MessagesView.kt | 10 +- .../messagecomposer/MessageComposerView.kt | 10 +- .../DefaultVoiceMessageComposerPresenter.kt | 20 +-- ...efaultVoiceMessageComposerPresenterTest.kt | 140 +++++++++--------- 6 files changed, 100 insertions(+), 100 deletions(-) rename features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/voicemessages/composer/{VoiceMessageComposerEvents.kt => VoiceMessageComposerEvent.kt} (76%) diff --git a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/voicemessages/composer/VoiceMessageComposerEvents.kt b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/voicemessages/composer/VoiceMessageComposerEvent.kt similarity index 76% rename from features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/voicemessages/composer/VoiceMessageComposerEvents.kt rename to features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/voicemessages/composer/VoiceMessageComposerEvent.kt index 5220c4a762..ef5fe93d08 100644 --- a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/voicemessages/composer/VoiceMessageComposerEvents.kt +++ b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/voicemessages/composer/VoiceMessageComposerEvent.kt @@ -12,17 +12,17 @@ import androidx.lifecycle.Lifecycle import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent import io.element.android.libraries.textcomposer.model.VoiceMessageRecorderEvent -sealed interface VoiceMessageComposerEvents { +sealed interface VoiceMessageComposerEvent { data class RecorderEvent( val recorderEvent: VoiceMessageRecorderEvent - ) : VoiceMessageComposerEvents + ) : VoiceMessageComposerEvent data class PlayerEvent( val playerEvent: VoiceMessagePlayerEvent, - ) : VoiceMessageComposerEvents - data object SendVoiceMessage : VoiceMessageComposerEvents - data object DeleteVoiceMessage : VoiceMessageComposerEvents - data object AcceptPermissionRationale : VoiceMessageComposerEvents - data object DismissPermissionsRationale : VoiceMessageComposerEvents - data class LifecycleEvent(val event: Lifecycle.Event) : VoiceMessageComposerEvents - data object DismissSendFailureDialog : VoiceMessageComposerEvents + ) : VoiceMessageComposerEvent + data object SendVoiceMessage : VoiceMessageComposerEvent + data object DeleteVoiceMessage : VoiceMessageComposerEvent + data object AcceptPermissionRationale : VoiceMessageComposerEvent + data object DismissPermissionsRationale : VoiceMessageComposerEvent + data class LifecycleEvent(val event: Lifecycle.Event) : VoiceMessageComposerEvent + data object DismissSendFailureDialog : VoiceMessageComposerEvent } diff --git a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/voicemessages/composer/VoiceMessageComposerState.kt b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/voicemessages/composer/VoiceMessageComposerState.kt index 5a213cbbbb..f324bb7612 100644 --- a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/voicemessages/composer/VoiceMessageComposerState.kt +++ b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/voicemessages/composer/VoiceMessageComposerState.kt @@ -17,5 +17,5 @@ data class VoiceMessageComposerState( val showPermissionRationaleDialog: Boolean, val showSendFailureDialog: Boolean, val keepScreenOn: Boolean, - val eventSink: (VoiceMessageComposerEvents) -> Unit, + val eventSink: (VoiceMessageComposerEvent) -> Unit, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index 1f62adeb23..03b04608d6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -44,7 +44,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme -import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerEvents +import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerEvent import io.element.android.features.messages.impl.actionlist.ActionListEvents import io.element.android.features.messages.impl.actionlist.ActionListView import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction @@ -120,7 +120,7 @@ fun MessagesView( knockRequestsBannerView: @Composable () -> Unit, ) { OnLifecycleEvent { _, event -> - state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvents.LifecycleEvent(event)) + state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvent.LifecycleEvent(event)) } KeepScreenOn(state.voiceMessageComposerState.keepScreenOn) @@ -399,17 +399,17 @@ private fun MessagesViewContent( if (state.voiceMessageComposerState.showPermissionRationaleDialog) { VoiceMessagePermissionRationaleDialog( onContinue = { - state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvents.AcceptPermissionRationale) + state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvent.AcceptPermissionRationale) }, onDismiss = { - state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvents.DismissPermissionsRationale) + state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvent.DismissPermissionsRationale) }, appName = state.appName ) } if (state.voiceMessageComposerState.showSendFailureDialog) { VoiceMessageSendingFailedDialog( - onDismiss = { state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvents.DismissSendFailureDialog) }, + onDismiss = { state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvent.DismissSendFailureDialog) }, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt index 292a6b39a8..4b346e0c15 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt @@ -18,7 +18,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalView import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp -import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerEvents +import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerEvent import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerState import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerStateProvider import io.element.android.features.messages.api.timeline.voicemessages.composer.aVoiceMessageComposerState @@ -78,19 +78,19 @@ internal fun MessageComposerView( } val onVoiceRecorderEvent = { press: VoiceMessageRecorderEvent -> - voiceMessageState.eventSink(VoiceMessageComposerEvents.RecorderEvent(press)) + voiceMessageState.eventSink(VoiceMessageComposerEvent.RecorderEvent(press)) } val onSendVoiceMessage = { - voiceMessageState.eventSink(VoiceMessageComposerEvents.SendVoiceMessage) + voiceMessageState.eventSink(VoiceMessageComposerEvent.SendVoiceMessage) } val onDeleteVoiceMessage = { - voiceMessageState.eventSink(VoiceMessageComposerEvents.DeleteVoiceMessage) + voiceMessageState.eventSink(VoiceMessageComposerEvent.DeleteVoiceMessage) } val onVoicePlayerEvent = { event: VoiceMessagePlayerEvent -> - voiceMessageState.eventSink(VoiceMessageComposerEvents.PlayerEvent(event)) + voiceMessageState.eventSink(VoiceMessageComposerEvent.PlayerEvent(event)) } TextComposer( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/DefaultVoiceMessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/DefaultVoiceMessageComposerPresenter.kt index 5405124283..051ded02ae 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/DefaultVoiceMessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/DefaultVoiceMessageComposerPresenter.kt @@ -26,7 +26,7 @@ import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.ContributesBinding import im.vector.app.features.analytics.plan.Composer import io.element.android.features.messages.api.MessageComposerContext -import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerEvents +import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerEvent import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerPresenter import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerState import io.element.android.libraries.di.RoomScope @@ -164,25 +164,25 @@ class DefaultVoiceMessageComposerPresenter( } } - fun handleEvent(event: VoiceMessageComposerEvents) { + fun handleEvent(event: VoiceMessageComposerEvent) { when (event) { - is VoiceMessageComposerEvents.RecorderEvent -> handleVoiceMessageRecorderEvent(event.recorderEvent) - is VoiceMessageComposerEvents.PlayerEvent -> handleVoiceMessagePlayerEvent(event.playerEvent) - is VoiceMessageComposerEvents.SendVoiceMessage -> localCoroutineScope.launch { + is VoiceMessageComposerEvent.RecorderEvent -> handleVoiceMessageRecorderEvent(event.recorderEvent) + is VoiceMessageComposerEvent.PlayerEvent -> handleVoiceMessagePlayerEvent(event.playerEvent) + is VoiceMessageComposerEvent.SendVoiceMessage -> localCoroutineScope.launch { sendVoiceMessage() } - VoiceMessageComposerEvents.DeleteVoiceMessage -> { + VoiceMessageComposerEvent.DeleteVoiceMessage -> { player.pause() localCoroutineScope.deleteRecording() } - VoiceMessageComposerEvents.DismissPermissionsRationale -> { + VoiceMessageComposerEvent.DismissPermissionsRationale -> { permissionState.eventSink(PermissionsEvents.CloseDialog) } - VoiceMessageComposerEvents.AcceptPermissionRationale -> { + VoiceMessageComposerEvent.AcceptPermissionRationale -> { permissionState.eventSink(PermissionsEvents.OpenSystemSettingAndCloseDialog) } - is VoiceMessageComposerEvents.LifecycleEvent -> handleLifecycleEvent(event.event) - VoiceMessageComposerEvents.DismissSendFailureDialog -> { + is VoiceMessageComposerEvent.LifecycleEvent -> handleLifecycleEvent(event.event) + VoiceMessageComposerEvent.DismissSendFailureDialog -> { showSendFailureDialog = false } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/DefaultVoiceMessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/DefaultVoiceMessageComposerPresenterTest.kt index e478ccc726..48348f5e9a 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/DefaultVoiceMessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/DefaultVoiceMessageComposerPresenterTest.kt @@ -15,7 +15,7 @@ import androidx.lifecycle.Lifecycle import app.cash.turbine.TurbineTestContext import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.Composer -import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerEvents +import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerEvent import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerState import io.element.android.features.messages.impl.messagecomposer.aReplyMode import io.element.android.features.messages.test.FakeMessageComposerContext @@ -103,7 +103,7 @@ class DefaultVoiceMessageComposerPresenterTest { fun `present - recording state`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() presenter.test { - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) + awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) val finalState = awaitItem() assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE) @@ -125,7 +125,7 @@ class DefaultVoiceMessageComposerPresenterTest { voiceRecorder = voiceRecorder, ) presenter.test { - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) + awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) skipItems(numberOfLevels / 2 - 1) val finalState = awaitItem() assertThat(finalState.voiceMessageState).isInstanceOf(VoiceMessageState.Recording::class.java) @@ -142,13 +142,13 @@ class DefaultVoiceMessageComposerPresenterTest { val presenter = createDefaultVoiceMessageComposerPresenter() presenter.test { awaitItem().apply { - eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) + eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) assertThat(keepScreenOn).isFalse() } awaitItem().apply { assertThat(keepScreenOn).isTrue() - eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop)) + eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop)) } val finalState = awaitItem().apply { @@ -163,8 +163,8 @@ class DefaultVoiceMessageComposerPresenterTest { fun `present - abort recording`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() presenter.test { - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Cancel)) + awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) + awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Cancel)) val finalState = awaitItem() assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 1) @@ -176,8 +176,8 @@ class DefaultVoiceMessageComposerPresenterTest { fun `present - finish recording`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() presenter.test { - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop)) + awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) + awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop)) val finalState = awaitItem() assertThat(finalState.voiceMessageState).isEqualTo(aPreviewState()) @@ -191,9 +191,9 @@ class DefaultVoiceMessageComposerPresenterTest { fun `present - play recording before it is ready`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() presenter.test { - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) + awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) val finalState = awaitItem().apply { - this.eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play)) + this.eventSink(VoiceMessageComposerEvent.PlayerEvent(VoiceMessagePlayerEvent.Play)) } // Nothing should happen @@ -208,9 +208,9 @@ class DefaultVoiceMessageComposerPresenterTest { fun `present - play recording`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() presenter.test { - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop)) - awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play)) + awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) + awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop)) + awaitItem().eventSink(VoiceMessageComposerEvent.PlayerEvent(VoiceMessagePlayerEvent.Play)) val finalState = awaitItem().also { assertThat(it.voiceMessageState).isEqualTo(aPlayingState()) } @@ -224,10 +224,10 @@ class DefaultVoiceMessageComposerPresenterTest { fun `present - pause recording`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() presenter.test { - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop)) - awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play)) - awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Pause)) + awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) + awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop)) + awaitItem().eventSink(VoiceMessageComposerEvent.PlayerEvent(VoiceMessagePlayerEvent.Play)) + awaitItem().eventSink(VoiceMessageComposerEvent.PlayerEvent(VoiceMessagePlayerEvent.Pause)) val finalState = awaitItem().also { assertThat(it.voiceMessageState).isEqualTo(aPausedState()) } @@ -241,15 +241,15 @@ class DefaultVoiceMessageComposerPresenterTest { fun `present - seek recording`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() presenter.test { - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop)) - awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Seek(0.5f))) + awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) + awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop)) + awaitItem().eventSink(VoiceMessageComposerEvent.PlayerEvent(VoiceMessagePlayerEvent.Seek(0.5f))) awaitItem().apply { assertThat(voiceMessageState).isEqualTo(aPreviewState(playbackProgress = 0.5f, time = 0.seconds, showCursor = true)) } awaitItem().apply { assertThat(voiceMessageState).isEqualTo(aPreviewState(playbackProgress = 0.5f, time = 5.seconds, showCursor = true)) - eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Seek(0.2f))) + eventSink(VoiceMessageComposerEvent.PlayerEvent(VoiceMessagePlayerEvent.Seek(0.2f))) } awaitItem().apply { assertThat(voiceMessageState).isEqualTo(aPreviewState(playbackProgress = 0.2f, time = 5.seconds, showCursor = true)) @@ -266,9 +266,9 @@ class DefaultVoiceMessageComposerPresenterTest { fun `present - delete recording`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() presenter.test { - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop)) - awaitItem().eventSink(VoiceMessageComposerEvents.DeleteVoiceMessage) + awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) + awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop)) + awaitItem().eventSink(VoiceMessageComposerEvent.DeleteVoiceMessage) val finalState = awaitItem() assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) @@ -282,10 +282,10 @@ class DefaultVoiceMessageComposerPresenterTest { fun `present - delete while playing`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() presenter.test { - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop)) - awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play)) - awaitItem().eventSink(VoiceMessageComposerEvents.DeleteVoiceMessage) + awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) + awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop)) + awaitItem().eventSink(VoiceMessageComposerEvent.PlayerEvent(VoiceMessagePlayerEvent.Play)) + awaitItem().eventSink(VoiceMessageComposerEvent.DeleteVoiceMessage) awaitItem().apply { assertThat(voiceMessageState).isEqualTo(aPausedState()) } @@ -302,9 +302,9 @@ class DefaultVoiceMessageComposerPresenterTest { fun `present - send recording`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() presenter.test { - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop)) - awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage) + awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) + awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop)) + awaitItem().eventSink(VoiceMessageComposerEvent.SendVoiceMessage) assertThat(awaitItem().voiceMessageState).isEqualTo(aPreviewState().toSendingState()) val finalState = awaitItem() assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) @@ -321,16 +321,16 @@ class DefaultVoiceMessageComposerPresenterTest { presenter.test { // Send a normal voice message messageComposerContext.composerMode = MessageComposerMode.Normal - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop)) - awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage) + awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) + awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop)) + awaitItem().eventSink(VoiceMessageComposerEvent.SendVoiceMessage) skipItems(1) // Sending state advanceUntilIdle() // Now reply with a voice message messageComposerContext.composerMode = aReplyMode() - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop)) - awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage) + awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) + awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop)) + awaitItem().eventSink(VoiceMessageComposerEvent.SendVoiceMessage) val finalState = awaitItem() // Sending state assertThat(analyticsService.capturedEvents).containsExactly( @@ -346,10 +346,10 @@ class DefaultVoiceMessageComposerPresenterTest { fun `present - send while playing`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() presenter.test { - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop)) - awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play)) - awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage) + awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) + awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop)) + awaitItem().eventSink(VoiceMessageComposerEvent.PlayerEvent(VoiceMessagePlayerEvent.Play)) + awaitItem().eventSink(VoiceMessageComposerEvent.SendVoiceMessage) assertThat(awaitItem().voiceMessageState).isEqualTo(aPlayingState().toSendingState()) skipItems(1) // Duplicate sending state @@ -366,11 +366,11 @@ class DefaultVoiceMessageComposerPresenterTest { fun `present - send recording before previous completed, waits`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() presenter.test { - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop)) + awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) + awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop)) awaitItem().run { - eventSink(VoiceMessageComposerEvents.SendVoiceMessage) - eventSink(VoiceMessageComposerEvents.SendVoiceMessage) + eventSink(VoiceMessageComposerEvent.SendVoiceMessage) + eventSink(VoiceMessageComposerEvent.SendVoiceMessage) } assertThat(awaitItem().voiceMessageState).isEqualTo(aPreviewState().toSendingState()) @@ -389,11 +389,11 @@ class DefaultVoiceMessageComposerPresenterTest { mediaPreProcessor.givenResult(Result.failure(Exception())) val presenter = createDefaultVoiceMessageComposerPresenter() presenter.test { - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop)) + awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) + awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop)) awaitItem().apply { assertThat(voiceMessageState).isEqualTo(aPreviewState()) - eventSink(VoiceMessageComposerEvents.SendVoiceMessage) + eventSink(VoiceMessageComposerEvent.SendVoiceMessage) } val finalState = awaitItem() @@ -412,11 +412,11 @@ class DefaultVoiceMessageComposerPresenterTest { val presenter = createDefaultVoiceMessageComposerPresenter() presenter.test { mediaPreProcessor.givenResult(Result.failure(Exception())) - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop)) + awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) + awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop)) val previewState = awaitItem() - previewState.eventSink(VoiceMessageComposerEvents.SendVoiceMessage) + previewState.eventSink(VoiceMessageComposerEvent.SendVoiceMessage) assertThat(awaitItem().voiceMessageState).isEqualTo(aPreviewState().toSendingState()) ensureAllEventsConsumed() @@ -424,7 +424,7 @@ class DefaultVoiceMessageComposerPresenterTest { sendVoiceMessageResult.assertions().isNeverCalled() mediaPreProcessor.givenAudioResult() - previewState.eventSink(VoiceMessageComposerEvents.SendVoiceMessage) + previewState.eventSink(VoiceMessageComposerEvent.SendVoiceMessage) val finalState = awaitItem() assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) sendVoiceMessageResult.assertions().isCalledOnce() @@ -440,9 +440,9 @@ class DefaultVoiceMessageComposerPresenterTest { presenter.test { // Let sending fail due to media preprocessing error mediaPreProcessor.givenResult(Result.failure(Exception())) - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop)) - awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage) + awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) + awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop)) + awaitItem().eventSink(VoiceMessageComposerEvent.SendVoiceMessage) assertThat(awaitItem().voiceMessageState).isEqualTo(aPreviewState().toSendingState()) @@ -454,7 +454,7 @@ class DefaultVoiceMessageComposerPresenterTest { awaitItem().apply { assertThat(voiceMessageState).isEqualTo(aPreviewState()) assertThat(showSendFailureDialog).isTrue() - eventSink(VoiceMessageComposerEvents.DismissSendFailureDialog) + eventSink(VoiceMessageComposerEvent.DismissSendFailureDialog) } val finalState = awaitItem().apply { @@ -473,7 +473,7 @@ class DefaultVoiceMessageComposerPresenterTest { presenter.test { val initialState = awaitItem() // Send the message before recording anything - initialState.eventSink(VoiceMessageComposerEvents.SendVoiceMessage) + initialState.eventSink(VoiceMessageComposerEvent.SendVoiceMessage) assertThat(initialState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) sendVoiceMessageResult.assertions().isNeverCalled() @@ -491,7 +491,7 @@ class DefaultVoiceMessageComposerPresenterTest { val presenter = createDefaultVoiceMessageComposerPresenter() presenter.test { val initialState = awaitItem() - initialState.eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) + initialState.eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) sendVoiceMessageResult.assertions().isNeverCalled() assertThat(analyticsService.trackedErrors).containsExactly( @@ -513,15 +513,15 @@ class DefaultVoiceMessageComposerPresenterTest { ) presenter.test { val initialState = awaitItem() - initialState.eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) + initialState.eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Idle) - initialState.eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop)) + initialState.eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop)) voiceRecorder.assertCalls(stopped = 1) permissionsPresenter.setPermissionGranted() - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) + awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) val finalState = awaitItem() assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE) voiceRecorder.assertCalls(stopped = 1, started = 1) @@ -539,13 +539,13 @@ class DefaultVoiceMessageComposerPresenterTest { permissionsPresenter = permissionsPresenter, ) presenter.test { - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) + awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) // See the dialog and accept it awaitItem().also { assertThat(it.voiceMessageState).isEqualTo(VoiceMessageState.Idle) assertThat(it.showPermissionRationaleDialog).isTrue() - it.eventSink(VoiceMessageComposerEvents.AcceptPermissionRationale) + it.eventSink(VoiceMessageComposerEvent.AcceptPermissionRationale) } // Dialog is hidden, user accepts permissions @@ -553,7 +553,7 @@ class DefaultVoiceMessageComposerPresenterTest { permissionsPresenter.setPermissionGranted() - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) + awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) val finalState = awaitItem() assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE) voiceRecorder.assertCalls(started = 1) @@ -571,19 +571,19 @@ class DefaultVoiceMessageComposerPresenterTest { permissionsPresenter = permissionsPresenter, ) presenter.test { - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) + awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) // See the dialog and accept it awaitItem().also { assertThat(it.voiceMessageState).isEqualTo(VoiceMessageState.Idle) assertThat(it.showPermissionRationaleDialog).isTrue() - it.eventSink(VoiceMessageComposerEvents.DismissPermissionsRationale) + it.eventSink(VoiceMessageComposerEvent.DismissPermissionsRationale) } // Dialog is hidden, user tries to record again awaitItem().also { assertThat(it.showPermissionRationaleDialog).isFalse() - it.eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) + it.eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) } // Dialog is shown once again @@ -601,7 +601,7 @@ class DefaultVoiceMessageComposerPresenterTest { mostRecentState: VoiceMessageComposerState, ) { mostRecentState.eventSink( - VoiceMessageComposerEvents.LifecycleEvent(event = Lifecycle.Event.ON_PAUSE) + VoiceMessageComposerEvent.LifecycleEvent(event = Lifecycle.Event.ON_PAUSE) ) val onPauseState = when (val state = mostRecentState.voiceMessageState) { @@ -622,7 +622,7 @@ class DefaultVoiceMessageComposerPresenterTest { } onPauseState.eventSink( - VoiceMessageComposerEvents.LifecycleEvent(event = Lifecycle.Event.ON_DESTROY) + VoiceMessageComposerEvent.LifecycleEvent(event = Lifecycle.Event.ON_DESTROY) ) when (val state = onPauseState.voiceMessageState) { From dca7bf0a7729c6254189dfee0fd731380a1eb5d2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 3 Dec 2025 09:52:10 +0100 Subject: [PATCH 6/6] Remove useless `derivedStateOf` --- .../components/LiveWaveformView.kt | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/LiveWaveformView.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/LiveWaveformView.kt index 5e4ad02bd6..d18b85c987 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/LiveWaveformView.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/LiveWaveformView.kt @@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -51,27 +50,23 @@ fun LiveWaveformView( linePadding: Dp = 2.dp, ) { var canvasSize by remember { mutableStateOf(DpSize(0.dp, 0.dp)) } - var parentWidth by remember { mutableIntStateOf(0) } - - val waveformWidth by remember(levels.size, lineWidth, linePadding) { - derivedStateOf { - levels.size * (lineWidth.value + linePadding.value) - } + val waveformWidth = remember(levels.size, lineWidth, linePadding) { + levels.size * (lineWidth.value + linePadding.value) } Box( contentAlignment = Alignment.CenterEnd, modifier = modifier - .fillMaxWidth() - .height(waveFormHeight) - .onSizeChanged { parentWidth = it.width } + .fillMaxWidth() + .height(waveFormHeight) + .onSizeChanged { parentWidth = it.width } ) { Canvas( modifier = Modifier - .width(Dp(waveformWidth)) - .graphicsLayer(alpha = DEFAULT_GRAPHICS_LAYER_ALPHA) - .then(modifier) + .width(Dp(waveformWidth)) + .graphicsLayer(alpha = DEFAULT_GRAPHICS_LAYER_ALPHA) + .then(modifier) ) { val width = min(waveformWidth, parentWidth.toFloat()) canvasSize = DpSize(width.dp, size.height.toDp())