[Voice messages] Add voice recording UI (#1546)

---------

Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
jonnyandrew 2023-10-12 16:17:18 +01:00 committed by GitHub
parent 7f65c137af
commit 12404fab78
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
293 changed files with 967 additions and 52 deletions

View file

@ -55,6 +55,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
import io.element.android.features.messages.impl.utils.messagesummary.MessageSummaryFormatter
import io.element.android.features.messages.impl.voicemessages.VoiceMessageComposerPresenter
import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.features.preferences.api.store.PreferencesStore
@ -67,6 +68,8 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
@ -75,7 +78,7 @@ import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
import io.element.android.libraries.matrix.ui.room.canRedactAsState
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
import io.element.android.libraries.textcomposer.MessageComposerMode
import io.element.android.libraries.textcomposer.model.MessageComposerMode
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -84,6 +87,7 @@ import timber.log.Timber
class MessagesPresenter @AssistedInject constructor(
private val room: MatrixRoom,
private val composerPresenter: MessageComposerPresenter,
private val voiceMessageComposerPresenter: VoiceMessageComposerPresenter,
private val timelinePresenter: TimelinePresenter,
private val actionListPresenter: ActionListPresenter,
private val customReactionPresenter: CustomReactionPresenter,
@ -95,6 +99,7 @@ class MessagesPresenter @AssistedInject constructor(
private val dispatchers: CoroutineDispatchers,
private val clipboardHelper: ClipboardHelper,
private val preferencesStore: PreferencesStore,
private val featureFlagsService: FeatureFlagService,
@Assisted private val navigator: MessagesNavigator,
) : Presenter<MessagesState> {
@ -107,6 +112,7 @@ class MessagesPresenter @AssistedInject constructor(
override fun present(): MessagesState {
val localCoroutineScope = rememberCoroutineScope()
val composerState = composerPresenter.present()
val voiceMessageComposerState = voiceMessageComposerPresenter.present()
val timelineState = timelinePresenter.present()
val actionListState = actionListPresenter.present()
val customReactionState = customReactionPresenter.present()
@ -145,6 +151,11 @@ class MessagesPresenter @AssistedInject constructor(
val enableTextFormatting by preferencesStore.isRichTextEditorEnabledFlow().collectAsState(initial = true)
var enableVoiceMessages by remember { mutableStateOf(false) }
LaunchedEffect(featureFlagsService) {
enableVoiceMessages = featureFlagsService.isFeatureEnabled(FeatureFlags.VoiceMessages)
}
fun handleEvents(event: MessagesEvents) {
when (event) {
is MessagesEvents.HandleAction -> {
@ -177,6 +188,7 @@ class MessagesPresenter @AssistedInject constructor(
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
userHasPermissionToRedact = userHasPermissionToRedact,
composerState = composerState,
voiceMessageComposerState = voiceMessageComposerState,
timelineState = timelineState,
actionListState = actionListState,
customReactionState = customReactionState,
@ -187,6 +199,7 @@ class MessagesPresenter @AssistedInject constructor(
showReinvitePrompt = showReinvitePrompt,
inviteProgress = inviteProgress.value,
enableTextFormatting = enableTextFormatting,
enableVoiceMessages = enableVoiceMessages,
eventSink = { handleEvents(it) }
)
}

View file

@ -23,6 +23,7 @@ import io.element.android.features.messages.impl.timeline.TimelineState
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState
import io.element.android.features.messages.impl.voicemessages.VoiceMessageComposerState
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
@ -36,6 +37,7 @@ data class MessagesState(
val userHasPermissionToSendMessage: Boolean,
val userHasPermissionToRedact: Boolean,
val composerState: MessageComposerState,
val voiceMessageComposerState: VoiceMessageComposerState,
val timelineState: TimelineState,
val actionListState: ActionListState,
val customReactionState: CustomReactionState,
@ -46,5 +48,6 @@ data class MessagesState(
val inviteProgress: Async<Unit>,
val showReinvitePrompt: Boolean,
val enableTextFormatting: Boolean,
val enableVoiceMessages: Boolean,
val eventSink: (MessagesEvents) -> Unit
)

View file

@ -25,11 +25,12 @@ import io.element.android.features.messages.impl.timeline.components.customreact
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
import io.element.android.features.messages.impl.voicemessages.aVoiceMessageComposerState
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.textcomposer.MessageComposerMode
import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.wysiwyg.compose.RichTextEditorState
import kotlinx.collections.immutable.persistentSetOf
@ -60,6 +61,7 @@ fun aMessagesState() = MessagesState(
isFullScreen = false,
mode = MessageComposerMode.Normal("Hello"),
),
voiceMessageComposerState = aVoiceMessageComposerState(),
timelineState = aTimelineState().copy(
timelineItems = aTimelineItemList(aTimelineItemTextContent()),
),
@ -82,5 +84,6 @@ fun aMessagesState() = MessagesState(
inviteProgress = Async.Uninitialized,
showReinvitePrompt = false,
enableTextFormatting = true,
enableVoiceMessages = true,
eventSink = {}
)

View file

@ -317,8 +317,10 @@ private fun MessagesViewContent(
if (state.userHasPermissionToSendMessage) {
MessageComposerView(
state = state.composerState,
voiceMessageState = state.voiceMessageComposerState,
subcomposing = subcomposing,
enableTextFormatting = state.enableTextFormatting,
enableVoiceMessages = state.enableVoiceMessages,
modifier = Modifier
.fillMaxWidth(),
)

View file

@ -23,7 +23,7 @@ import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.messages.api.MessageComposerContext
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.textcomposer.MessageComposerMode
import io.element.android.libraries.textcomposer.model.MessageComposerMode
import javax.inject.Inject
@SingleIn(RoomScope::class)

View file

@ -17,8 +17,8 @@
package io.element.android.features.messages.impl.messagecomposer
import androidx.compose.runtime.Immutable
import io.element.android.libraries.textcomposer.Message
import io.element.android.libraries.textcomposer.MessageComposerMode
import io.element.android.libraries.textcomposer.model.Message
import io.element.android.libraries.textcomposer.model.MessageComposerMode
@Immutable
sealed interface MessageComposerEvents {

View file

@ -47,8 +47,8 @@ import io.element.android.libraries.mediapickers.api.PickerProvider
import io.element.android.libraries.mediaupload.api.MediaSender
import io.element.android.libraries.permissions.api.PermissionsEvents
import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.libraries.textcomposer.Message
import io.element.android.libraries.textcomposer.MessageComposerMode
import io.element.android.libraries.textcomposer.model.Message
import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.wysiwyg.compose.RichTextEditorState
import kotlinx.collections.immutable.persistentListOf

View file

@ -19,7 +19,7 @@ package io.element.android.features.messages.impl.messagecomposer
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.libraries.textcomposer.MessageComposerMode
import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.wysiwyg.compose.RichTextEditorState
import kotlinx.collections.immutable.ImmutableList

View file

@ -17,7 +17,7 @@
package io.element.android.features.messages.impl.messagecomposer
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.textcomposer.MessageComposerMode
import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.wysiwyg.compose.RichTextEditorState
open class MessageComposerStateProvider : PreviewParameterProvider<MessageComposerState> {

View file

@ -24,17 +24,24 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.features.messages.impl.voicemessages.VoiceMessageComposerEvents
import io.element.android.features.messages.impl.voicemessages.VoiceMessageComposerState
import io.element.android.features.messages.impl.voicemessages.VoiceMessageComposerStateProvider
import io.element.android.features.messages.impl.voicemessages.aVoiceMessageComposerState
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.textcomposer.Message
import io.element.android.libraries.textcomposer.model.Message
import io.element.android.libraries.textcomposer.TextComposer
import io.element.android.libraries.textcomposer.model.PressEvent
import kotlinx.coroutines.launch
@Composable
fun MessageComposerView(
internal fun MessageComposerView(
state: MessageComposerState,
voiceMessageState: VoiceMessageComposerState,
subcomposing: Boolean,
enableTextFormatting: Boolean,
enableVoiceMessages: Boolean,
modifier: Modifier = Modifier,
) {
fun sendMessage(message: Message) {
@ -64,9 +71,14 @@ fun MessageComposerView(
}
}
fun onVoiceRecordButtonEvent(press: PressEvent) {
voiceMessageState.eventSink(VoiceMessageComposerEvents.RecordButtonEvent(press))
}
TextComposer(
modifier = modifier,
state = state.richTextEditorState,
voiceMessageState = voiceMessageState.voiceMessageState,
subcomposing = subcomposing,
onRequestFocus = ::onRequestFocus,
onSendMessage = ::sendMessage,
@ -76,24 +88,49 @@ fun MessageComposerView(
onAddAttachment = ::onAddAttachment,
onDismissTextFormatting = ::onDismissTextFormatting,
enableTextFormatting = enableTextFormatting,
enableVoiceMessages = enableVoiceMessages,
onVoiceRecordButtonEvent = ::onVoiceRecordButtonEvent,
onError = ::onError,
)
}
@PreviewsDayNight
@Composable
internal fun MessageComposerViewPreview(@PreviewParameter(MessageComposerStateProvider::class) state: MessageComposerState) = ElementPreview {
internal fun MessageComposerViewPreview(
@PreviewParameter(MessageComposerStateProvider::class) state: MessageComposerState,
) = ElementPreview {
Column {
MessageComposerView(
modifier = Modifier.height(IntrinsicSize.Min),
state = state,
voiceMessageState = aVoiceMessageComposerState(),
enableTextFormatting = true,
enableVoiceMessages = true,
subcomposing = false,
)
MessageComposerView(
modifier = Modifier.height(200.dp),
state = state,
voiceMessageState = aVoiceMessageComposerState(),
enableTextFormatting = true,
enableVoiceMessages = true,
subcomposing = false,
)
}
}
@PreviewsDayNight
@Composable
internal fun MessageComposerViewVoicePreview(
@PreviewParameter(VoiceMessageComposerStateProvider::class) state: VoiceMessageComposerState,
) = ElementPreview {
Column {
MessageComposerView(
modifier = Modifier.height(IntrinsicSize.Min),
state = aMessageComposerState(),
voiceMessageState = state,
enableTextFormatting = true,
enableVoiceMessages = true,
subcomposing = false,
)
}

View file

@ -0,0 +1,25 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl.voicemessages
import io.element.android.libraries.textcomposer.model.PressEvent
sealed class VoiceMessageComposerEvents {
data class RecordButtonEvent(
val pressEvent: PressEvent
): VoiceMessageComposerEvents()
}

View file

@ -0,0 +1,64 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl.voicemessages
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.textcomposer.model.PressEvent
import io.element.android.libraries.textcomposer.model.VoiceMessageState
import javax.inject.Inject
@SingleIn(RoomScope::class)
class VoiceMessageComposerPresenter @Inject constructor() : Presenter<VoiceMessageComposerState> {
@Composable
override fun present(): VoiceMessageComposerState {
var voiceMessageState by remember { mutableStateOf<VoiceMessageState>(VoiceMessageState.Idle) }
fun onRecordButtonPress(event: VoiceMessageComposerEvents.RecordButtonEvent) = when(event.pressEvent) {
PressEvent.PressStart -> {
// TODO start the recording
voiceMessageState = VoiceMessageState.Recording
}
PressEvent.LongPressEnd -> {
// TODO finish the recording
voiceMessageState = VoiceMessageState.Idle
}
PressEvent.Tapped -> {
// TODO discard the recording and show the 'hold to record' tooltip
voiceMessageState = VoiceMessageState.Idle
}
}
fun handleEvents(event: VoiceMessageComposerEvents) {
when (event) {
is VoiceMessageComposerEvents.RecordButtonEvent -> onRecordButtonPress(event)
}
}
return VoiceMessageComposerState(
voiceMessageState = voiceMessageState,
eventSink = { handleEvents(it) }
)
}
}

View file

@ -0,0 +1,27 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl.voicemessages
import androidx.compose.runtime.Stable
import io.element.android.libraries.textcomposer.model.VoiceMessageState
@Stable
data class VoiceMessageComposerState(
val voiceMessageState: VoiceMessageState,
val eventSink: (VoiceMessageComposerEvents) -> Unit,
)

View file

@ -0,0 +1,34 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl.voicemessages
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.textcomposer.model.VoiceMessageState
internal open class VoiceMessageComposerStateProvider : PreviewParameterProvider<VoiceMessageComposerState> {
override val values: Sequence<VoiceMessageComposerState>
get() = sequenceOf(
aVoiceMessageComposerState(voiceMessageState = VoiceMessageState.Recording),
)
}
internal fun aVoiceMessageComposerState(
voiceMessageState: VoiceMessageState = VoiceMessageState.Idle,
) = VoiceMessageComposerState(
voiceMessageState = voiceMessageState,
eventSink = {},
)

View file

@ -40,6 +40,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
import io.element.android.features.messages.impl.voicemessages.VoiceMessageComposerPresenter
import io.element.android.features.messages.media.FakeLocalMediaFactory
import io.element.android.features.messages.textcomposer.TestRichTextEditorStateFactory
import io.element.android.features.messages.timeline.components.customreaction.FakeEmojibaseProvider
@ -73,7 +74,7 @@ import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.libraries.permissions.test.FakePermissionsPresenter
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
import io.element.android.libraries.textcomposer.MessageComposerMode
import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.consumeItemsUntilPredicate
@ -619,6 +620,7 @@ class MessagesPresenterTest {
richTextEditorStateFactory = TestRichTextEditorStateFactory(),
permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter),
)
val voiceMessageComposerPresenter = VoiceMessageComposerPresenter()
val timelinePresenter = TimelinePresenter(
timelineItemsFactory = aTimelineItemsFactory(),
room = matrixRoom,
@ -634,6 +636,7 @@ class MessagesPresenterTest {
return MessagesPresenter(
room = matrixRoom,
composerPresenter = messageComposerPresenter,
voiceMessageComposerPresenter = voiceMessageComposerPresenter,
timelinePresenter = timelinePresenter,
actionListPresenter = actionListPresenter,
customReactionPresenter = customReactionPresenter,
@ -645,6 +648,7 @@ class MessagesPresenterTest {
navigator = navigator,
clipboardHelper = clipboardHelper,
preferencesStore = preferencesStore,
featureFlagsService = FakeFeatureFlagService(),
dispatchers = coroutineDispatchers,
)
}

View file

@ -58,8 +58,8 @@ import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.libraries.permissions.test.FakePermissionsPresenter
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
import io.element.android.libraries.textcomposer.Message
import io.element.android.libraries.textcomposer.MessageComposerMode
import io.element.android.libraries.textcomposer.model.Message
import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.waitForPredicate

View file

@ -0,0 +1,89 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:OptIn(ExperimentalCoroutinesApi::class)
package io.element.android.features.messages.voicemessages
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.messages.impl.voicemessages.VoiceMessageComposerEvents
import io.element.android.features.messages.impl.voicemessages.VoiceMessageComposerPresenter
import io.element.android.libraries.textcomposer.model.PressEvent
import io.element.android.libraries.textcomposer.model.VoiceMessageState
import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
class VoiceMessageComposerPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@Test
fun `present - initial state`() = runTest {
val presenter = createPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
}
}
@Test
fun `present - recording state`() = runTest {
val presenter = createPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart))
assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Recording)
}
}
@Test
fun `present - abort recording`() = runTest {
val presenter = createPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart))
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.Tapped))
assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Idle)
}
}
@Test
fun `present - finish recording`() = runTest {
val presenter = createPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart))
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd))
assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Idle)
}
}
private fun createPresenter() = VoiceMessageComposerPresenter()
}