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 1acaae2aae..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 } } @@ -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/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 71% 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..48348f5e9a 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 @@ -12,13 +12,10 @@ 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 +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 @@ -42,10 +39,12 @@ 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 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 @@ -57,7 +56,7 @@ import java.io.File import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds -class VoiceMessageComposerPresenterTest { +class DefaultVoiceMessageComposerPresenterTest { @get:Rule val warmUpRule = WarmUpRule() @@ -91,9 +90,7 @@ class VoiceMessageComposerPresenterTest { @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,10 +102,8 @@ class VoiceMessageComposerPresenterTest { @Test fun `present - recording state`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) + presenter.test { + awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) val finalState = awaitItem() assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE) @@ -118,20 +113,42 @@ class VoiceMessageComposerPresenterTest { } } + @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(VoiceMessageComposerEvent.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() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + 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 { @@ -145,11 +162,9 @@ class VoiceMessageComposerPresenterTest { @Test fun `present - abort recording`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Cancel)) + presenter.test { + 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) @@ -160,11 +175,9 @@ class VoiceMessageComposerPresenterTest { @Test fun `present - finish recording`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop)) + presenter.test { + awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) + awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop)) val finalState = awaitItem() assertThat(finalState.voiceMessageState).isEqualTo(aPreviewState()) @@ -177,12 +190,10 @@ class VoiceMessageComposerPresenterTest { @Test fun `present - play recording before it is ready`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) + presenter.test { + 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 @@ -196,12 +207,10 @@ class VoiceMessageComposerPresenterTest { @Test fun `present - play recording`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop)) - awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play)) + presenter.test { + 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()) } @@ -214,13 +223,11 @@ class VoiceMessageComposerPresenterTest { @Test fun `present - pause recording`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.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)) + presenter.test { + 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()) } @@ -233,18 +240,16 @@ class VoiceMessageComposerPresenterTest { @Test fun `present - seek recording`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop)) - awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Seek(0.5f))) + presenter.test { + 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)) @@ -260,12 +265,10 @@ class VoiceMessageComposerPresenterTest { @Test fun `present - delete recording`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop)) - awaitItem().eventSink(VoiceMessageComposerEvents.DeleteVoiceMessage) + presenter.test { + 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) @@ -278,13 +281,11 @@ class VoiceMessageComposerPresenterTest { @Test fun `present - delete while playing`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop)) - awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play)) - awaitItem().eventSink(VoiceMessageComposerEvents.DeleteVoiceMessage) + presenter.test { + 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()) } @@ -300,12 +301,10 @@ class VoiceMessageComposerPresenterTest { @Test fun `present - send recording`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop)) - awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage) + presenter.test { + 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) @@ -319,21 +318,19 @@ class VoiceMessageComposerPresenterTest { @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)) - 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( @@ -348,13 +345,11 @@ class VoiceMessageComposerPresenterTest { @Test fun `present - send while playing`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop)) - awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play)) - awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage) + presenter.test { + 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 @@ -370,14 +365,12 @@ class VoiceMessageComposerPresenterTest { @Test fun `present - send recording before previous completed, waits`() = runTest { val presenter = createDefaultVoiceMessageComposerPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop)) + presenter.test { + 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()) @@ -395,14 +388,12 @@ class VoiceMessageComposerPresenterTest { // Let sending fail due to media preprocessing error mediaPreProcessor.givenResult(Result.failure(Exception())) val presenter = createDefaultVoiceMessageComposerPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop)) + presenter.test { + 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() @@ -419,15 +410,13 @@ class VoiceMessageComposerPresenterTest { 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)) + 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() @@ -435,7 +424,7 @@ class VoiceMessageComposerPresenterTest { 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() @@ -448,14 +437,12 @@ class VoiceMessageComposerPresenterTest { @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)) - 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()) @@ -467,7 +454,7 @@ class VoiceMessageComposerPresenterTest { awaitItem().apply { assertThat(voiceMessageState).isEqualTo(aPreviewState()) assertThat(showSendFailureDialog).isTrue() - eventSink(VoiceMessageComposerEvents.DismissSendFailureDialog) + eventSink(VoiceMessageComposerEvent.DismissSendFailureDialog) } val finalState = awaitItem().apply { @@ -483,12 +470,10 @@ class VoiceMessageComposerPresenterTest { @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) + initialState.eventSink(VoiceMessageComposerEvent.SendVoiceMessage) assertThat(initialState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) sendVoiceMessageResult.assertions().isNeverCalled() @@ -504,11 +489,9 @@ class VoiceMessageComposerPresenterTest { 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)) + initialState.eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) sendVoiceMessageResult.assertions().isNeverCalled() assertThat(analyticsService.trackedErrors).containsExactly( @@ -528,19 +511,17 @@ class VoiceMessageComposerPresenterTest { val presenter = createDefaultVoiceMessageComposerPresenter( permissionsPresenter = permissionsPresenter, ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + 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) @@ -557,16 +538,14 @@ class VoiceMessageComposerPresenterTest { val presenter = createDefaultVoiceMessageComposerPresenter( permissionsPresenter = permissionsPresenter, ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) + presenter.test { + 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 @@ -574,7 +553,7 @@ class VoiceMessageComposerPresenterTest { 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) @@ -591,22 +570,20 @@ class VoiceMessageComposerPresenterTest { val presenter = createDefaultVoiceMessageComposerPresenter( permissionsPresenter = permissionsPresenter, ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start)) + presenter.test { + 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 @@ -624,7 +601,7 @@ class VoiceMessageComposerPresenterTest { 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) { @@ -645,7 +622,7 @@ class VoiceMessageComposerPresenterTest { } onPauseState.eventSink( - VoiceMessageComposerEvents.LifecycleEvent(event = Lifecycle.Event.ON_DESTROY) + VoiceMessageComposerEvent.LifecycleEvent(event = Lifecycle.Event.ON_DESTROY) ) when (val state = onPauseState.voiceMessageState) { @@ -662,6 +639,7 @@ class VoiceMessageComposerPresenterTest { private fun TestScope.createDefaultVoiceMessageComposerPresenter( permissionsPresenter: PermissionsPresenter = createFakePermissionsPresenter(), + voiceRecorder: VoiceRecorder = this@DefaultVoiceMessageComposerPresenterTest.voiceRecorder, ): DefaultVoiceMessageComposerPresenter { return DefaultVoiceMessageComposerPresenter( sessionCoroutineScope = backgroundScope, 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..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, 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()) 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, ) } } 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() } }