Merge pull request #5821 from element-hq/feature/bma/fixLongVoiceRecording
Fix long voice recording
This commit is contained in:
commit
2a73ce0f62
10 changed files with 169 additions and 188 deletions
|
|
@ -12,17 +12,17 @@ import androidx.lifecycle.Lifecycle
|
||||||
import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent
|
import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent
|
||||||
import io.element.android.libraries.textcomposer.model.VoiceMessageRecorderEvent
|
import io.element.android.libraries.textcomposer.model.VoiceMessageRecorderEvent
|
||||||
|
|
||||||
sealed interface VoiceMessageComposerEvents {
|
sealed interface VoiceMessageComposerEvent {
|
||||||
data class RecorderEvent(
|
data class RecorderEvent(
|
||||||
val recorderEvent: VoiceMessageRecorderEvent
|
val recorderEvent: VoiceMessageRecorderEvent
|
||||||
) : VoiceMessageComposerEvents
|
) : VoiceMessageComposerEvent
|
||||||
data class PlayerEvent(
|
data class PlayerEvent(
|
||||||
val playerEvent: VoiceMessagePlayerEvent,
|
val playerEvent: VoiceMessagePlayerEvent,
|
||||||
) : VoiceMessageComposerEvents
|
) : VoiceMessageComposerEvent
|
||||||
data object SendVoiceMessage : VoiceMessageComposerEvents
|
data object SendVoiceMessage : VoiceMessageComposerEvent
|
||||||
data object DeleteVoiceMessage : VoiceMessageComposerEvents
|
data object DeleteVoiceMessage : VoiceMessageComposerEvent
|
||||||
data object AcceptPermissionRationale : VoiceMessageComposerEvents
|
data object AcceptPermissionRationale : VoiceMessageComposerEvent
|
||||||
data object DismissPermissionsRationale : VoiceMessageComposerEvents
|
data object DismissPermissionsRationale : VoiceMessageComposerEvent
|
||||||
data class LifecycleEvent(val event: Lifecycle.Event) : VoiceMessageComposerEvents
|
data class LifecycleEvent(val event: Lifecycle.Event) : VoiceMessageComposerEvent
|
||||||
data object DismissSendFailureDialog : VoiceMessageComposerEvents
|
data object DismissSendFailureDialog : VoiceMessageComposerEvent
|
||||||
}
|
}
|
||||||
|
|
@ -17,5 +17,5 @@ data class VoiceMessageComposerState(
|
||||||
val showPermissionRationaleDialog: Boolean,
|
val showPermissionRationaleDialog: Boolean,
|
||||||
val showSendFailureDialog: Boolean,
|
val showSendFailureDialog: Boolean,
|
||||||
val keepScreenOn: Boolean,
|
val keepScreenOn: Boolean,
|
||||||
val eventSink: (VoiceMessageComposerEvents) -> Unit,
|
val eventSink: (VoiceMessageComposerEvent) -> Unit,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
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.ActionListEvents
|
||||||
import io.element.android.features.messages.impl.actionlist.ActionListView
|
import io.element.android.features.messages.impl.actionlist.ActionListView
|
||||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||||
|
|
@ -120,7 +120,7 @@ fun MessagesView(
|
||||||
knockRequestsBannerView: @Composable () -> Unit,
|
knockRequestsBannerView: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
OnLifecycleEvent { _, event ->
|
OnLifecycleEvent { _, event ->
|
||||||
state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvents.LifecycleEvent(event))
|
state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvent.LifecycleEvent(event))
|
||||||
}
|
}
|
||||||
|
|
||||||
KeepScreenOn(state.voiceMessageComposerState.keepScreenOn)
|
KeepScreenOn(state.voiceMessageComposerState.keepScreenOn)
|
||||||
|
|
@ -399,17 +399,17 @@ private fun MessagesViewContent(
|
||||||
if (state.voiceMessageComposerState.showPermissionRationaleDialog) {
|
if (state.voiceMessageComposerState.showPermissionRationaleDialog) {
|
||||||
VoiceMessagePermissionRationaleDialog(
|
VoiceMessagePermissionRationaleDialog(
|
||||||
onContinue = {
|
onContinue = {
|
||||||
state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvents.AcceptPermissionRationale)
|
state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvent.AcceptPermissionRationale)
|
||||||
},
|
},
|
||||||
onDismiss = {
|
onDismiss = {
|
||||||
state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvents.DismissPermissionsRationale)
|
state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvent.DismissPermissionsRationale)
|
||||||
},
|
},
|
||||||
appName = state.appName
|
appName = state.appName
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (state.voiceMessageComposerState.showSendFailureDialog) {
|
if (state.voiceMessageComposerState.showSendFailureDialog) {
|
||||||
VoiceMessageSendingFailedDialog(
|
VoiceMessageSendingFailedDialog(
|
||||||
onDismiss = { state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvents.DismissSendFailureDialog) },
|
onDismiss = { state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvent.DismissSendFailureDialog) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
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.VoiceMessageComposerState
|
||||||
import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerStateProvider
|
import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerStateProvider
|
||||||
import io.element.android.features.messages.api.timeline.voicemessages.composer.aVoiceMessageComposerState
|
import io.element.android.features.messages.api.timeline.voicemessages.composer.aVoiceMessageComposerState
|
||||||
|
|
@ -78,19 +78,19 @@ internal fun MessageComposerView(
|
||||||
}
|
}
|
||||||
|
|
||||||
val onVoiceRecorderEvent = { press: VoiceMessageRecorderEvent ->
|
val onVoiceRecorderEvent = { press: VoiceMessageRecorderEvent ->
|
||||||
voiceMessageState.eventSink(VoiceMessageComposerEvents.RecorderEvent(press))
|
voiceMessageState.eventSink(VoiceMessageComposerEvent.RecorderEvent(press))
|
||||||
}
|
}
|
||||||
|
|
||||||
val onSendVoiceMessage = {
|
val onSendVoiceMessage = {
|
||||||
voiceMessageState.eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
|
voiceMessageState.eventSink(VoiceMessageComposerEvent.SendVoiceMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
val onDeleteVoiceMessage = {
|
val onDeleteVoiceMessage = {
|
||||||
voiceMessageState.eventSink(VoiceMessageComposerEvents.DeleteVoiceMessage)
|
voiceMessageState.eventSink(VoiceMessageComposerEvent.DeleteVoiceMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
val onVoicePlayerEvent = { event: VoiceMessagePlayerEvent ->
|
val onVoicePlayerEvent = { event: VoiceMessagePlayerEvent ->
|
||||||
voiceMessageState.eventSink(VoiceMessageComposerEvents.PlayerEvent(event))
|
voiceMessageState.eventSink(VoiceMessageComposerEvent.PlayerEvent(event))
|
||||||
}
|
}
|
||||||
|
|
||||||
TextComposer(
|
TextComposer(
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ import dev.zacsweers.metro.AssistedInject
|
||||||
import dev.zacsweers.metro.ContributesBinding
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
import im.vector.app.features.analytics.plan.Composer
|
import im.vector.app.features.analytics.plan.Composer
|
||||||
import io.element.android.features.messages.api.MessageComposerContext
|
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.VoiceMessageComposerPresenter
|
||||||
import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerState
|
import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerState
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
|
|
@ -164,25 +164,25 @@ class DefaultVoiceMessageComposerPresenter(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleEvent(event: VoiceMessageComposerEvents) {
|
fun handleEvent(event: VoiceMessageComposerEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is VoiceMessageComposerEvents.RecorderEvent -> handleVoiceMessageRecorderEvent(event.recorderEvent)
|
is VoiceMessageComposerEvent.RecorderEvent -> handleVoiceMessageRecorderEvent(event.recorderEvent)
|
||||||
is VoiceMessageComposerEvents.PlayerEvent -> handleVoiceMessagePlayerEvent(event.playerEvent)
|
is VoiceMessageComposerEvent.PlayerEvent -> handleVoiceMessagePlayerEvent(event.playerEvent)
|
||||||
is VoiceMessageComposerEvents.SendVoiceMessage -> localCoroutineScope.launch {
|
is VoiceMessageComposerEvent.SendVoiceMessage -> localCoroutineScope.launch {
|
||||||
sendVoiceMessage()
|
sendVoiceMessage()
|
||||||
}
|
}
|
||||||
VoiceMessageComposerEvents.DeleteVoiceMessage -> {
|
VoiceMessageComposerEvent.DeleteVoiceMessage -> {
|
||||||
player.pause()
|
player.pause()
|
||||||
localCoroutineScope.deleteRecording()
|
localCoroutineScope.deleteRecording()
|
||||||
}
|
}
|
||||||
VoiceMessageComposerEvents.DismissPermissionsRationale -> {
|
VoiceMessageComposerEvent.DismissPermissionsRationale -> {
|
||||||
permissionState.eventSink(PermissionsEvents.CloseDialog)
|
permissionState.eventSink(PermissionsEvents.CloseDialog)
|
||||||
}
|
}
|
||||||
VoiceMessageComposerEvents.AcceptPermissionRationale -> {
|
VoiceMessageComposerEvent.AcceptPermissionRationale -> {
|
||||||
permissionState.eventSink(PermissionsEvents.OpenSystemSettingAndCloseDialog)
|
permissionState.eventSink(PermissionsEvents.OpenSystemSettingAndCloseDialog)
|
||||||
}
|
}
|
||||||
is VoiceMessageComposerEvents.LifecycleEvent -> handleLifecycleEvent(event.event)
|
is VoiceMessageComposerEvent.LifecycleEvent -> handleLifecycleEvent(event.event)
|
||||||
VoiceMessageComposerEvents.DismissSendFailureDialog -> {
|
VoiceMessageComposerEvent.DismissSendFailureDialog -> {
|
||||||
showSendFailureDialog = false
|
showSendFailureDialog = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -192,7 +192,10 @@ class DefaultVoiceMessageComposerPresenter(
|
||||||
voiceMessageState = when (val state = recorderState) {
|
voiceMessageState = when (val state = recorderState) {
|
||||||
is VoiceRecorderState.Recording -> VoiceMessageState.Recording(
|
is VoiceRecorderState.Recording -> VoiceMessageState.Recording(
|
||||||
duration = state.elapsedTime,
|
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 ->
|
is VoiceRecorderState.Finished ->
|
||||||
previewState(
|
previewState(
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,10 @@ package io.element.android.features.messages.impl.voicemessages.composer
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import app.cash.molecule.RecompositionMode
|
|
||||||
import app.cash.molecule.moleculeFlow
|
|
||||||
import app.cash.turbine.TurbineTestContext
|
import app.cash.turbine.TurbineTestContext
|
||||||
import app.cash.turbine.test
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import im.vector.app.features.analytics.plan.Composer
|
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.api.timeline.voicemessages.composer.VoiceMessageComposerState
|
||||||
import io.element.android.features.messages.impl.messagecomposer.aReplyMode
|
import io.element.android.features.messages.impl.messagecomposer.aReplyMode
|
||||||
import io.element.android.features.messages.test.FakeMessageComposerContext
|
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.VoiceMessageRecorderEvent
|
||||||
import io.element.android.libraries.textcomposer.model.VoiceMessageState
|
import io.element.android.libraries.textcomposer.model.VoiceMessageState
|
||||||
import io.element.android.libraries.voiceplayer.api.VoiceMessageException
|
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.libraries.voicerecorder.test.FakeVoiceRecorder
|
||||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
import io.element.android.tests.testutils.test
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
|
@ -57,7 +56,7 @@ import java.io.File
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
class VoiceMessageComposerPresenterTest {
|
class DefaultVoiceMessageComposerPresenterTest {
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val warmUpRule = WarmUpRule()
|
val warmUpRule = WarmUpRule()
|
||||||
|
|
||||||
|
|
@ -91,9 +90,7 @@ class VoiceMessageComposerPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state`() = runTest {
|
fun `present - initial state`() = runTest {
|
||||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
val presenter = createDefaultVoiceMessageComposerPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
assertThat(initialState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
||||||
voiceRecorder.assertCalls(started = 0)
|
voiceRecorder.assertCalls(started = 0)
|
||||||
|
|
@ -105,10 +102,8 @@ class VoiceMessageComposerPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - recording state`() = runTest {
|
fun `present - recording state`() = runTest {
|
||||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
val presenter = createDefaultVoiceMessageComposerPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||||
}.test {
|
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
|
||||||
|
|
||||||
val finalState = awaitItem()
|
val finalState = awaitItem()
|
||||||
assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE)
|
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
|
@Test
|
||||||
fun `present - recording keeps screen on`() = runTest {
|
fun `present - recording keeps screen on`() = runTest {
|
||||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
val presenter = createDefaultVoiceMessageComposerPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
awaitItem().apply {
|
awaitItem().apply {
|
||||||
eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||||
assertThat(keepScreenOn).isFalse()
|
assertThat(keepScreenOn).isFalse()
|
||||||
}
|
}
|
||||||
|
|
||||||
awaitItem().apply {
|
awaitItem().apply {
|
||||||
assertThat(keepScreenOn).isTrue()
|
assertThat(keepScreenOn).isTrue()
|
||||||
eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||||
}
|
}
|
||||||
|
|
||||||
val finalState = awaitItem().apply {
|
val finalState = awaitItem().apply {
|
||||||
|
|
@ -145,11 +162,9 @@ class VoiceMessageComposerPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - abort recording`() = runTest {
|
fun `present - abort recording`() = runTest {
|
||||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
val presenter = createDefaultVoiceMessageComposerPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||||
}.test {
|
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Cancel))
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Cancel))
|
|
||||||
val finalState = awaitItem()
|
val finalState = awaitItem()
|
||||||
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
||||||
voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 1)
|
voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 1)
|
||||||
|
|
@ -160,11 +175,9 @@ class VoiceMessageComposerPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - finish recording`() = runTest {
|
fun `present - finish recording`() = runTest {
|
||||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
val presenter = createDefaultVoiceMessageComposerPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||||
}.test {
|
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
|
||||||
|
|
||||||
val finalState = awaitItem()
|
val finalState = awaitItem()
|
||||||
assertThat(finalState.voiceMessageState).isEqualTo(aPreviewState())
|
assertThat(finalState.voiceMessageState).isEqualTo(aPreviewState())
|
||||||
|
|
@ -177,12 +190,10 @@ class VoiceMessageComposerPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - play recording before it is ready`() = runTest {
|
fun `present - play recording before it is ready`() = runTest {
|
||||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
val presenter = createDefaultVoiceMessageComposerPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||||
}.test {
|
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
|
||||||
val finalState = awaitItem().apply {
|
val finalState = awaitItem().apply {
|
||||||
this.eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play))
|
this.eventSink(VoiceMessageComposerEvent.PlayerEvent(VoiceMessagePlayerEvent.Play))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nothing should happen
|
// Nothing should happen
|
||||||
|
|
@ -196,12 +207,10 @@ class VoiceMessageComposerPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - play recording`() = runTest {
|
fun `present - play recording`() = runTest {
|
||||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
val presenter = createDefaultVoiceMessageComposerPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||||
}.test {
|
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
awaitItem().eventSink(VoiceMessageComposerEvent.PlayerEvent(VoiceMessagePlayerEvent.Play))
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play))
|
|
||||||
val finalState = awaitItem().also {
|
val finalState = awaitItem().also {
|
||||||
assertThat(it.voiceMessageState).isEqualTo(aPlayingState())
|
assertThat(it.voiceMessageState).isEqualTo(aPlayingState())
|
||||||
}
|
}
|
||||||
|
|
@ -214,13 +223,11 @@ class VoiceMessageComposerPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - pause recording`() = runTest {
|
fun `present - pause recording`() = runTest {
|
||||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
val presenter = createDefaultVoiceMessageComposerPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||||
}.test {
|
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
awaitItem().eventSink(VoiceMessageComposerEvent.PlayerEvent(VoiceMessagePlayerEvent.Play))
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
awaitItem().eventSink(VoiceMessageComposerEvent.PlayerEvent(VoiceMessagePlayerEvent.Pause))
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play))
|
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Pause))
|
|
||||||
val finalState = awaitItem().also {
|
val finalState = awaitItem().also {
|
||||||
assertThat(it.voiceMessageState).isEqualTo(aPausedState())
|
assertThat(it.voiceMessageState).isEqualTo(aPausedState())
|
||||||
}
|
}
|
||||||
|
|
@ -233,18 +240,16 @@ class VoiceMessageComposerPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - seek recording`() = runTest {
|
fun `present - seek recording`() = runTest {
|
||||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
val presenter = createDefaultVoiceMessageComposerPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||||
}.test {
|
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
awaitItem().eventSink(VoiceMessageComposerEvent.PlayerEvent(VoiceMessagePlayerEvent.Seek(0.5f)))
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Seek(0.5f)))
|
|
||||||
awaitItem().apply {
|
awaitItem().apply {
|
||||||
assertThat(voiceMessageState).isEqualTo(aPreviewState(playbackProgress = 0.5f, time = 0.seconds, showCursor = true))
|
assertThat(voiceMessageState).isEqualTo(aPreviewState(playbackProgress = 0.5f, time = 0.seconds, showCursor = true))
|
||||||
}
|
}
|
||||||
awaitItem().apply {
|
awaitItem().apply {
|
||||||
assertThat(voiceMessageState).isEqualTo(aPreviewState(playbackProgress = 0.5f, time = 5.seconds, showCursor = true))
|
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 {
|
awaitItem().apply {
|
||||||
assertThat(voiceMessageState).isEqualTo(aPreviewState(playbackProgress = 0.2f, time = 5.seconds, showCursor = true))
|
assertThat(voiceMessageState).isEqualTo(aPreviewState(playbackProgress = 0.2f, time = 5.seconds, showCursor = true))
|
||||||
|
|
@ -260,12 +265,10 @@ class VoiceMessageComposerPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - delete recording`() = runTest {
|
fun `present - delete recording`() = runTest {
|
||||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
val presenter = createDefaultVoiceMessageComposerPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||||
}.test {
|
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
awaitItem().eventSink(VoiceMessageComposerEvent.DeleteVoiceMessage)
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.DeleteVoiceMessage)
|
|
||||||
|
|
||||||
val finalState = awaitItem()
|
val finalState = awaitItem()
|
||||||
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
||||||
|
|
@ -278,13 +281,11 @@ class VoiceMessageComposerPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - delete while playing`() = runTest {
|
fun `present - delete while playing`() = runTest {
|
||||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
val presenter = createDefaultVoiceMessageComposerPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||||
}.test {
|
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
awaitItem().eventSink(VoiceMessageComposerEvent.PlayerEvent(VoiceMessagePlayerEvent.Play))
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
awaitItem().eventSink(VoiceMessageComposerEvent.DeleteVoiceMessage)
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play))
|
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.DeleteVoiceMessage)
|
|
||||||
awaitItem().apply {
|
awaitItem().apply {
|
||||||
assertThat(voiceMessageState).isEqualTo(aPausedState())
|
assertThat(voiceMessageState).isEqualTo(aPausedState())
|
||||||
}
|
}
|
||||||
|
|
@ -300,12 +301,10 @@ class VoiceMessageComposerPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - send recording`() = runTest {
|
fun `present - send recording`() = runTest {
|
||||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
val presenter = createDefaultVoiceMessageComposerPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||||
}.test {
|
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
awaitItem().eventSink(VoiceMessageComposerEvent.SendVoiceMessage)
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
|
|
||||||
assertThat(awaitItem().voiceMessageState).isEqualTo(aPreviewState().toSendingState())
|
assertThat(awaitItem().voiceMessageState).isEqualTo(aPreviewState().toSendingState())
|
||||||
val finalState = awaitItem()
|
val finalState = awaitItem()
|
||||||
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
||||||
|
|
@ -319,21 +318,19 @@ class VoiceMessageComposerPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - sending is tracked`() = runTest {
|
fun `present - sending is tracked`() = runTest {
|
||||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
val presenter = createDefaultVoiceMessageComposerPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
// Send a normal voice message
|
// Send a normal voice message
|
||||||
messageComposerContext.composerMode = MessageComposerMode.Normal
|
messageComposerContext.composerMode = MessageComposerMode.Normal
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
|
awaitItem().eventSink(VoiceMessageComposerEvent.SendVoiceMessage)
|
||||||
skipItems(1) // Sending state
|
skipItems(1) // Sending state
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
// Now reply with a voice message
|
// Now reply with a voice message
|
||||||
messageComposerContext.composerMode = aReplyMode()
|
messageComposerContext.composerMode = aReplyMode()
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
|
awaitItem().eventSink(VoiceMessageComposerEvent.SendVoiceMessage)
|
||||||
val finalState = awaitItem() // Sending state
|
val finalState = awaitItem() // Sending state
|
||||||
|
|
||||||
assertThat(analyticsService.capturedEvents).containsExactly(
|
assertThat(analyticsService.capturedEvents).containsExactly(
|
||||||
|
|
@ -348,13 +345,11 @@ class VoiceMessageComposerPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - send while playing`() = runTest {
|
fun `present - send while playing`() = runTest {
|
||||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
val presenter = createDefaultVoiceMessageComposerPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||||
}.test {
|
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
awaitItem().eventSink(VoiceMessageComposerEvent.PlayerEvent(VoiceMessagePlayerEvent.Play))
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
awaitItem().eventSink(VoiceMessageComposerEvent.SendVoiceMessage)
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play))
|
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
|
|
||||||
assertThat(awaitItem().voiceMessageState).isEqualTo(aPlayingState().toSendingState())
|
assertThat(awaitItem().voiceMessageState).isEqualTo(aPlayingState().toSendingState())
|
||||||
skipItems(1) // Duplicate sending state
|
skipItems(1) // Duplicate sending state
|
||||||
|
|
||||||
|
|
@ -370,14 +365,12 @@ class VoiceMessageComposerPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - send recording before previous completed, waits`() = runTest {
|
fun `present - send recording before previous completed, waits`() = runTest {
|
||||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
val presenter = createDefaultVoiceMessageComposerPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||||
}.test {
|
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
|
||||||
awaitItem().run {
|
awaitItem().run {
|
||||||
eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
|
eventSink(VoiceMessageComposerEvent.SendVoiceMessage)
|
||||||
eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
|
eventSink(VoiceMessageComposerEvent.SendVoiceMessage)
|
||||||
}
|
}
|
||||||
assertThat(awaitItem().voiceMessageState).isEqualTo(aPreviewState().toSendingState())
|
assertThat(awaitItem().voiceMessageState).isEqualTo(aPreviewState().toSendingState())
|
||||||
|
|
||||||
|
|
@ -395,14 +388,12 @@ class VoiceMessageComposerPresenterTest {
|
||||||
// Let sending fail due to media preprocessing error
|
// Let sending fail due to media preprocessing error
|
||||||
mediaPreProcessor.givenResult(Result.failure(Exception()))
|
mediaPreProcessor.givenResult(Result.failure(Exception()))
|
||||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
val presenter = createDefaultVoiceMessageComposerPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||||
}.test {
|
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
|
||||||
awaitItem().apply {
|
awaitItem().apply {
|
||||||
assertThat(voiceMessageState).isEqualTo(aPreviewState())
|
assertThat(voiceMessageState).isEqualTo(aPreviewState())
|
||||||
eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
|
eventSink(VoiceMessageComposerEvent.SendVoiceMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
val finalState = awaitItem()
|
val finalState = awaitItem()
|
||||||
|
|
@ -419,15 +410,13 @@ class VoiceMessageComposerPresenterTest {
|
||||||
fun `present - send failures can be retried`() = runTest {
|
fun `present - send failures can be retried`() = runTest {
|
||||||
// Let sending fail due to media preprocessing error
|
// Let sending fail due to media preprocessing error
|
||||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
val presenter = createDefaultVoiceMessageComposerPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
mediaPreProcessor.givenResult(Result.failure(Exception()))
|
mediaPreProcessor.givenResult(Result.failure(Exception()))
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||||
val previewState = awaitItem()
|
val previewState = awaitItem()
|
||||||
|
|
||||||
previewState.eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
|
previewState.eventSink(VoiceMessageComposerEvent.SendVoiceMessage)
|
||||||
assertThat(awaitItem().voiceMessageState).isEqualTo(aPreviewState().toSendingState())
|
assertThat(awaitItem().voiceMessageState).isEqualTo(aPreviewState().toSendingState())
|
||||||
|
|
||||||
ensureAllEventsConsumed()
|
ensureAllEventsConsumed()
|
||||||
|
|
@ -435,7 +424,7 @@ class VoiceMessageComposerPresenterTest {
|
||||||
sendVoiceMessageResult.assertions().isNeverCalled()
|
sendVoiceMessageResult.assertions().isNeverCalled()
|
||||||
|
|
||||||
mediaPreProcessor.givenAudioResult()
|
mediaPreProcessor.givenAudioResult()
|
||||||
previewState.eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
|
previewState.eventSink(VoiceMessageComposerEvent.SendVoiceMessage)
|
||||||
val finalState = awaitItem()
|
val finalState = awaitItem()
|
||||||
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
||||||
sendVoiceMessageResult.assertions().isCalledOnce()
|
sendVoiceMessageResult.assertions().isCalledOnce()
|
||||||
|
|
@ -448,14 +437,12 @@ class VoiceMessageComposerPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - send failures are displayed as an error dialog`() = runTest {
|
fun `present - send failures are displayed as an error dialog`() = runTest {
|
||||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
val presenter = createDefaultVoiceMessageComposerPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
// Let sending fail due to media preprocessing error
|
// Let sending fail due to media preprocessing error
|
||||||
mediaPreProcessor.givenResult(Result.failure(Exception()))
|
mediaPreProcessor.givenResult(Result.failure(Exception()))
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
|
awaitItem().eventSink(VoiceMessageComposerEvent.SendVoiceMessage)
|
||||||
|
|
||||||
assertThat(awaitItem().voiceMessageState).isEqualTo(aPreviewState().toSendingState())
|
assertThat(awaitItem().voiceMessageState).isEqualTo(aPreviewState().toSendingState())
|
||||||
|
|
||||||
|
|
@ -467,7 +454,7 @@ class VoiceMessageComposerPresenterTest {
|
||||||
awaitItem().apply {
|
awaitItem().apply {
|
||||||
assertThat(voiceMessageState).isEqualTo(aPreviewState())
|
assertThat(voiceMessageState).isEqualTo(aPreviewState())
|
||||||
assertThat(showSendFailureDialog).isTrue()
|
assertThat(showSendFailureDialog).isTrue()
|
||||||
eventSink(VoiceMessageComposerEvents.DismissSendFailureDialog)
|
eventSink(VoiceMessageComposerEvent.DismissSendFailureDialog)
|
||||||
}
|
}
|
||||||
|
|
||||||
val finalState = awaitItem().apply {
|
val finalState = awaitItem().apply {
|
||||||
|
|
@ -483,12 +470,10 @@ class VoiceMessageComposerPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - send error - missing recording is tracked`() = runTest {
|
fun `present - send error - missing recording is tracked`() = runTest {
|
||||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
val presenter = createDefaultVoiceMessageComposerPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
// Send the message before recording anything
|
// Send the message before recording anything
|
||||||
initialState.eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
|
initialState.eventSink(VoiceMessageComposerEvent.SendVoiceMessage)
|
||||||
|
|
||||||
assertThat(initialState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
assertThat(initialState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
||||||
sendVoiceMessageResult.assertions().isNeverCalled()
|
sendVoiceMessageResult.assertions().isNeverCalled()
|
||||||
|
|
@ -504,11 +489,9 @@ class VoiceMessageComposerPresenterTest {
|
||||||
val exception = SecurityException("")
|
val exception = SecurityException("")
|
||||||
voiceRecorder.givenThrowsSecurityException(exception)
|
voiceRecorder.givenThrowsSecurityException(exception)
|
||||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
val presenter = createDefaultVoiceMessageComposerPresenter()
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
initialState.eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||||
|
|
||||||
sendVoiceMessageResult.assertions().isNeverCalled()
|
sendVoiceMessageResult.assertions().isNeverCalled()
|
||||||
assertThat(analyticsService.trackedErrors).containsExactly(
|
assertThat(analyticsService.trackedErrors).containsExactly(
|
||||||
|
|
@ -528,19 +511,17 @@ class VoiceMessageComposerPresenterTest {
|
||||||
val presenter = createDefaultVoiceMessageComposerPresenter(
|
val presenter = createDefaultVoiceMessageComposerPresenter(
|
||||||
permissionsPresenter = permissionsPresenter,
|
permissionsPresenter = permissionsPresenter,
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
initialState.eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||||
assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
||||||
|
|
||||||
initialState.eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
initialState.eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||||
voiceRecorder.assertCalls(stopped = 1)
|
voiceRecorder.assertCalls(stopped = 1)
|
||||||
|
|
||||||
permissionsPresenter.setPermissionGranted()
|
permissionsPresenter.setPermissionGranted()
|
||||||
|
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||||
val finalState = awaitItem()
|
val finalState = awaitItem()
|
||||||
assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE)
|
assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE)
|
||||||
voiceRecorder.assertCalls(stopped = 1, started = 1)
|
voiceRecorder.assertCalls(stopped = 1, started = 1)
|
||||||
|
|
@ -557,16 +538,14 @@ class VoiceMessageComposerPresenterTest {
|
||||||
val presenter = createDefaultVoiceMessageComposerPresenter(
|
val presenter = createDefaultVoiceMessageComposerPresenter(
|
||||||
permissionsPresenter = permissionsPresenter,
|
permissionsPresenter = permissionsPresenter,
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||||
}.test {
|
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
|
||||||
|
|
||||||
// See the dialog and accept it
|
// See the dialog and accept it
|
||||||
awaitItem().also {
|
awaitItem().also {
|
||||||
assertThat(it.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
assertThat(it.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
||||||
assertThat(it.showPermissionRationaleDialog).isTrue()
|
assertThat(it.showPermissionRationaleDialog).isTrue()
|
||||||
it.eventSink(VoiceMessageComposerEvents.AcceptPermissionRationale)
|
it.eventSink(VoiceMessageComposerEvent.AcceptPermissionRationale)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dialog is hidden, user accepts permissions
|
// Dialog is hidden, user accepts permissions
|
||||||
|
|
@ -574,7 +553,7 @@ class VoiceMessageComposerPresenterTest {
|
||||||
|
|
||||||
permissionsPresenter.setPermissionGranted()
|
permissionsPresenter.setPermissionGranted()
|
||||||
|
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||||
val finalState = awaitItem()
|
val finalState = awaitItem()
|
||||||
assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE)
|
assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE)
|
||||||
voiceRecorder.assertCalls(started = 1)
|
voiceRecorder.assertCalls(started = 1)
|
||||||
|
|
@ -591,22 +570,20 @@ class VoiceMessageComposerPresenterTest {
|
||||||
val presenter = createDefaultVoiceMessageComposerPresenter(
|
val presenter = createDefaultVoiceMessageComposerPresenter(
|
||||||
permissionsPresenter = permissionsPresenter,
|
permissionsPresenter = permissionsPresenter,
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
presenter.test {
|
||||||
presenter.present()
|
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||||
}.test {
|
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
|
||||||
|
|
||||||
// See the dialog and accept it
|
// See the dialog and accept it
|
||||||
awaitItem().also {
|
awaitItem().also {
|
||||||
assertThat(it.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
assertThat(it.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
||||||
assertThat(it.showPermissionRationaleDialog).isTrue()
|
assertThat(it.showPermissionRationaleDialog).isTrue()
|
||||||
it.eventSink(VoiceMessageComposerEvents.DismissPermissionsRationale)
|
it.eventSink(VoiceMessageComposerEvent.DismissPermissionsRationale)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dialog is hidden, user tries to record again
|
// Dialog is hidden, user tries to record again
|
||||||
awaitItem().also {
|
awaitItem().also {
|
||||||
assertThat(it.showPermissionRationaleDialog).isFalse()
|
assertThat(it.showPermissionRationaleDialog).isFalse()
|
||||||
it.eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
it.eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dialog is shown once again
|
// Dialog is shown once again
|
||||||
|
|
@ -624,7 +601,7 @@ class VoiceMessageComposerPresenterTest {
|
||||||
mostRecentState: VoiceMessageComposerState,
|
mostRecentState: VoiceMessageComposerState,
|
||||||
) {
|
) {
|
||||||
mostRecentState.eventSink(
|
mostRecentState.eventSink(
|
||||||
VoiceMessageComposerEvents.LifecycleEvent(event = Lifecycle.Event.ON_PAUSE)
|
VoiceMessageComposerEvent.LifecycleEvent(event = Lifecycle.Event.ON_PAUSE)
|
||||||
)
|
)
|
||||||
|
|
||||||
val onPauseState = when (val state = mostRecentState.voiceMessageState) {
|
val onPauseState = when (val state = mostRecentState.voiceMessageState) {
|
||||||
|
|
@ -645,7 +622,7 @@ class VoiceMessageComposerPresenterTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
onPauseState.eventSink(
|
onPauseState.eventSink(
|
||||||
VoiceMessageComposerEvents.LifecycleEvent(event = Lifecycle.Event.ON_DESTROY)
|
VoiceMessageComposerEvent.LifecycleEvent(event = Lifecycle.Event.ON_DESTROY)
|
||||||
)
|
)
|
||||||
|
|
||||||
when (val state = onPauseState.voiceMessageState) {
|
when (val state = onPauseState.voiceMessageState) {
|
||||||
|
|
@ -662,6 +639,7 @@ class VoiceMessageComposerPresenterTest {
|
||||||
|
|
||||||
private fun TestScope.createDefaultVoiceMessageComposerPresenter(
|
private fun TestScope.createDefaultVoiceMessageComposerPresenter(
|
||||||
permissionsPresenter: PermissionsPresenter = createFakePermissionsPresenter(),
|
permissionsPresenter: PermissionsPresenter = createFakePermissionsPresenter(),
|
||||||
|
voiceRecorder: VoiceRecorder = this@DefaultVoiceMessageComposerPresenterTest.voiceRecorder,
|
||||||
): DefaultVoiceMessageComposerPresenter {
|
): DefaultVoiceMessageComposerPresenter {
|
||||||
return DefaultVoiceMessageComposerPresenter(
|
return DefaultVoiceMessageComposerPresenter(
|
||||||
sessionCoroutineScope = backgroundScope,
|
sessionCoroutineScope = backgroundScope,
|
||||||
|
|
@ -299,7 +299,10 @@ fun TextComposer(
|
||||||
onSeek = onSeekVoiceMessage,
|
onSeek = onSeekVoiceMessage,
|
||||||
)
|
)
|
||||||
is VoiceMessageState.Recording ->
|
is VoiceMessageState.Recording ->
|
||||||
VoiceMessageRecording(voiceMessageState.levels, voiceMessageState.duration)
|
VoiceMessageRecording(
|
||||||
|
levels = voiceMessageState.levels,
|
||||||
|
duration = voiceMessageState.duration,
|
||||||
|
)
|
||||||
VoiceMessageState.Idle -> {}
|
VoiceMessageState.Idle -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
|
@ -51,27 +50,23 @@ fun LiveWaveformView(
|
||||||
linePadding: Dp = 2.dp,
|
linePadding: Dp = 2.dp,
|
||||||
) {
|
) {
|
||||||
var canvasSize by remember { mutableStateOf(DpSize(0.dp, 0.dp)) }
|
var canvasSize by remember { mutableStateOf(DpSize(0.dp, 0.dp)) }
|
||||||
|
|
||||||
var parentWidth by remember { mutableIntStateOf(0) }
|
var parentWidth by remember { mutableIntStateOf(0) }
|
||||||
|
val waveformWidth = remember(levels.size, lineWidth, linePadding) {
|
||||||
val waveformWidth by remember(levels, lineWidth, linePadding) {
|
levels.size * (lineWidth.value + linePadding.value)
|
||||||
derivedStateOf {
|
|
||||||
levels.size * (lineWidth.value + linePadding.value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
contentAlignment = Alignment.CenterEnd,
|
contentAlignment = Alignment.CenterEnd,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(waveFormHeight)
|
.height(waveFormHeight)
|
||||||
.onSizeChanged { parentWidth = it.width }
|
.onSizeChanged { parentWidth = it.width }
|
||||||
) {
|
) {
|
||||||
Canvas(
|
Canvas(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(Dp(waveformWidth))
|
.width(Dp(waveformWidth))
|
||||||
.graphicsLayer(alpha = DEFAULT_GRAPHICS_LAYER_ALPHA)
|
.graphicsLayer(alpha = DEFAULT_GRAPHICS_LAYER_ALPHA)
|
||||||
.then(modifier)
|
.then(modifier)
|
||||||
) {
|
) {
|
||||||
val width = min(waveformWidth, parentWidth.toFloat())
|
val width = min(waveformWidth, parentWidth.toFloat())
|
||||||
canvasSize = DpSize(width.dp, size.height.toDp())
|
canvasSize = DpSize(width.dp, size.height.toDp())
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ internal fun VoiceMessageRecording(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.height(26.dp)
|
.height(26.dp)
|
||||||
.weight(1f),
|
.weight(1f),
|
||||||
levels = levels
|
levels = levels,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import io.element.android.libraries.voicerecorder.api.VoiceRecorder
|
||||||
import io.element.android.libraries.voicerecorder.api.VoiceRecorderState
|
import io.element.android.libraries.voicerecorder.api.VoiceRecorderState
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.yield
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
@ -49,6 +50,7 @@ class FakeVoiceRecorder(
|
||||||
timeSource += recordingDuration
|
timeSource += recordingDuration
|
||||||
for (i in 1..levels.size) {
|
for (i in 1..levels.size) {
|
||||||
_state.emit(VoiceRecorderState.Recording(startedAt.elapsedNow(), levels.take(i)))
|
_state.emit(VoiceRecorderState.Recording(startedAt.elapsedNow(), levels.take(i)))
|
||||||
|
yield()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue