From d744e075d58a3dc53c3f51299e5a54022675dfc2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 7 Oct 2024 11:38:12 +0200 Subject: [PATCH] Remove dependencies to other presenters in MessagesPresenter. --- .../messages/impl/MessagesPresenter.kt | 19 +- .../messages/impl/di/MessagesModule.kt | 25 ++ .../MessageComposerStateProvider.kt | 3 +- .../messages/impl/MessagesPresenterTest.kt | 378 ++++++++---------- .../actionlist/FakeActionListPresenter.kt | 8 +- .../list/PinnedMessagesListPresenterTest.kt | 2 +- .../impl/timeline/TimelinePresenterTest.kt | 58 +-- 7 files changed, 237 insertions(+), 256 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index 388780d384..236ea211fe 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -32,22 +32,21 @@ import io.element.android.features.messages.impl.actionlist.ActionListPresenter import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents -import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter import io.element.android.features.messages.impl.messagecomposer.MessageComposerState import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState import io.element.android.features.messages.impl.timeline.TimelineController import io.element.android.features.messages.impl.timeline.TimelineEvents import io.element.android.features.messages.impl.timeline.TimelinePresenter import io.element.android.features.messages.impl.timeline.TimelineState -import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter -import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryPresenter -import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetPresenter +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.receipt.bottomsheet.ReadReceiptBottomSheetState import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState -import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter +import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.libraries.androidutils.clipboard.ClipboardHelper @@ -88,14 +87,14 @@ import timber.log.Timber class MessagesPresenter @AssistedInject constructor( @Assisted private val navigator: MessagesNavigator, private val room: MatrixRoom, - private val composerPresenter: MessageComposerPresenter, - private val voiceMessageComposerPresenter: VoiceMessageComposerPresenter, + private val composerPresenter: Presenter, + private val voiceMessageComposerPresenter: Presenter, timelinePresenterFactory: TimelinePresenter.Factory, private val timelineProtectionPresenter: Presenter, private val actionListPresenterFactory: ActionListPresenter.Factory, - private val customReactionPresenter: CustomReactionPresenter, - private val reactionSummaryPresenter: ReactionSummaryPresenter, - private val readReceiptBottomSheetPresenter: ReadReceiptBottomSheetPresenter, + private val customReactionPresenter: Presenter, + private val reactionSummaryPresenter: Presenter, + private val readReceiptBottomSheetPresenter: Presenter, private val pinnedMessagesBannerPresenter: Presenter, private val networkMonitor: NetworkMonitor, private val snackbarDispatcher: SnackbarDispatcher, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesModule.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesModule.kt index 1a5aa1c7d1..31c1d6a758 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesModule.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesModule.kt @@ -12,12 +12,22 @@ import dagger.Binds import dagger.Module import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailurePresenter import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureState +import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter +import io.element.android.features.messages.impl.messagecomposer.MessageComposerState import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerPresenter import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState +import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter +import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState +import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryPresenter +import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState +import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetPresenter +import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionPresenter import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState import io.element.android.features.messages.impl.typing.TypingNotificationPresenter import io.element.android.features.messages.impl.typing.TypingNotificationState +import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter +import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.di.RoomScope @@ -35,4 +45,19 @@ interface MessagesModule { @Binds fun bindTimelineProtectionPresenter(presenter: TimelineProtectionPresenter): Presenter + + @Binds + fun bindMessageComposerPresenter(presenter: MessageComposerPresenter): Presenter + + @Binds + fun bindVoiceMessageComposerPresenter(presenter: VoiceMessageComposerPresenter): Presenter + + @Binds + fun bindCustomReactionPresenter(presenter: CustomReactionPresenter): Presenter + + @Binds + fun bindReactionSummaryPresenter(presenter: ReactionSummaryPresenter): Presenter + + @Binds + fun bindReadReceiptBottomSheetPresenter(presenter: ReadReceiptBottomSheetPresenter): Presenter } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt index 63a5eeb8c2..2730872072 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt @@ -33,6 +33,7 @@ fun aMessageComposerState( canCreatePoll: Boolean = true, attachmentsState: AttachmentsState = AttachmentsState.None, suggestions: ImmutableList = persistentListOf(), + eventSink: (MessageComposerEvents) -> Unit = {}, ) = MessageComposerState( textEditorState = textEditorState, isFullScreen = isFullScreen, @@ -44,5 +45,5 @@ fun aMessageComposerState( attachmentsState = attachmentsState, suggestions = suggestions, resolveMentionDisplay = { _, _ -> TextDisplay.Plain }, - eventSink = {}, + eventSink = eventSink, ) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt index 209d719b82..730206e696 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt @@ -7,33 +7,23 @@ package io.element.android.features.messages.impl -import android.net.Uri import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow -import app.cash.turbine.ReceiveTurbine import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.PinUnpinAction +import io.element.android.features.messages.impl.actionlist.ActionListEvents import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.features.messages.impl.actionlist.FakeActionListPresenter import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction -import io.element.android.features.messages.impl.crypto.sendfailure.resolve.aResolveVerifiedUserSendFailureState -import io.element.android.features.messages.impl.draft.FakeComposerDraftService import io.element.android.features.messages.impl.fixtures.aMessageEvent -import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactoryCreator -import io.element.android.features.messages.impl.messagecomposer.DefaultMessageComposerContext -import io.element.android.features.messages.impl.messagecomposer.FakeRoomAliasSuggestionsDataSource -import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter -import io.element.android.features.messages.impl.messagecomposer.TestRichTextEditorStateFactory -import io.element.android.features.messages.impl.messagecomposer.suggestions.SuggestionsProcessor +import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents +import io.element.android.features.messages.impl.messagecomposer.MessageComposerState +import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState import io.element.android.features.messages.impl.pinned.banner.aLoadedPinnedMessagesBannerState import io.element.android.features.messages.impl.timeline.TimelineController -import io.element.android.features.messages.impl.timeline.TimelineItemIndexer import io.element.android.features.messages.impl.timeline.TimelinePresenter -import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter -import io.element.android.features.messages.impl.timeline.components.customreaction.FakeEmojibaseProvider -import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryPresenter -import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetPresenter +import io.element.android.features.messages.impl.timeline.createTimelinePresenter import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent @@ -41,25 +31,19 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent import io.element.android.features.messages.impl.timeline.protection.aTimelineProtectionState -import io.element.android.features.messages.impl.typing.aTypingNotificationState -import io.element.android.features.messages.impl.utils.FakeTextPillificationHelper -import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPlayer -import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter -import io.element.android.features.messages.impl.voicemessages.timeline.FakeRedactedVoiceMessageManager -import io.element.android.features.messages.test.FakeMessageComposerContext +import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessageComposerState import io.element.android.features.messages.test.timeline.FakeHtmlConverterProvider import io.element.android.features.networkmonitor.test.FakeNetworkMonitor import io.element.android.features.poll.api.actions.EndPollAction import io.element.android.features.poll.test.actions.FakeEndPollAction -import io.element.android.features.poll.test.actions.FakeSendPollResponseAction import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher -import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.TransactionId @@ -72,33 +56,24 @@ import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.MessageEventType import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.test.AN_AVATAR_URL +import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID_2 import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.A_UNIQUE_ID import io.element.android.libraries.matrix.test.core.aBuildMeta -import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.matrix.test.timeline.FakeTimeline -import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails -import io.element.android.libraries.mediapickers.test.FakePickerProvider -import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer -import io.element.android.libraries.mediaupload.api.MediaSender -import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor -import io.element.android.libraries.mediaviewer.test.FakeLocalMediaFactory -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.preferences.test.InMemorySessionPreferencesStore -import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider +import io.element.android.libraries.textcomposer.model.MarkdownTextEditorState import io.element.android.libraries.textcomposer.model.MessageComposerMode -import io.element.android.libraries.voicerecorder.test.FakeVoiceRecorder +import io.element.android.libraries.textcomposer.model.TextEditorState import io.element.android.services.analytics.test.FakeAnalyticsService +import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.consumeItemsUntilPredicate import io.element.android.tests.testutils.consumeItemsUntilTimeout @@ -107,7 +82,6 @@ import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.testCoroutineDispatchers -import io.mockk.mockk import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay @@ -123,8 +97,6 @@ class MessagesPresenterTest { @get:Rule val warmUpRule = WarmUpRule() - private val mockMediaUrl: Uri = mockk("localMediaUri") - @Test fun `present - initial state`() = runTest { val presenter = createMessagesPresenter() @@ -212,15 +184,13 @@ class MessagesPresenterTest { }.test { skipItems(1) val initialState = awaitItem() - initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", A_UNIQUE_ID)) - // No crashes when sending a reaction failed - timeline.apply { toggleReactionLambda = toggleReactionFailure } - initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", A_UNIQUE_ID)) - assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) - + initialState.eventSink(MessagesEvents.ToggleReaction("👍", A_UNIQUE_ID)) assert(toggleReactionSuccess) .isCalledOnce() .with(value("👍"), value(A_UNIQUE_ID)) + // No crashes when sending a reaction failed + timeline.apply { toggleReactionLambda = toggleReactionFailure } + initialState.eventSink(MessagesEvents.ToggleReaction("👍", A_UNIQUE_ID)) assert(toggleReactionFailure) .isCalledOnce() .with(value("👍"), value(A_UNIQUE_ID)) @@ -248,15 +218,16 @@ class MessagesPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitFirstItem() - initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", A_UNIQUE_ID)) - initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", A_UNIQUE_ID)) + val initialState = awaitItem() + initialState.eventSink(MessagesEvents.ToggleReaction("👍", A_UNIQUE_ID)) + initialState.eventSink(MessagesEvents.ToggleReaction("👍", A_UNIQUE_ID)) assert(toggleReactionSuccess) .isCalledExactly(2) .withSequence( listOf(value("👍"), value(A_UNIQUE_ID)), listOf(value("👍"), value(A_UNIQUE_ID)), ) + skipItems(1) } } @@ -267,9 +238,8 @@ class MessagesPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(1) val initialState = awaitItem() - initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Forward, aMessageEvent())) + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Forward, aMessageEvent())) assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) assertThat(navigator.onForwardEventClickedCount).isEqualTo(1) } @@ -283,9 +253,9 @@ class MessagesPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitFirstItem() - initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Copy, event)) - assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) + val initialState = awaitItem() + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Copy, event)) + skipItems(2) assertThat(clipboardHelper.clipboardContents).isEqualTo((event.content as TimelineItemTextContent).body) } } @@ -310,24 +280,33 @@ class MessagesPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitFirstItem() - initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.CopyLink, event)) - assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) + val initialState = awaitItem() + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.CopyLink, event)) + skipItems(2) assertThat(clipboardHelper.clipboardContents).isEqualTo("a link") } } @Test fun `present - handle action reply`() = runTest { - val presenter = createMessagesPresenter() + val composerRecorder = EventsRecorder() + val presenter = createMessagesPresenter( + messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) }, + ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitFirstItem() - initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, aMessageEvent())) - val finalState = awaitItem() - assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java) - assertThat(finalState.actionListState.target).isEqualTo(ActionListState.Target.None) + val initialState = awaitItem() + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, aMessageEvent())) + awaitItem() + composerRecorder.assertSingle( + MessageComposerEvents.SetMode( + composerMode = MessageComposerMode.Reply( + replyToDetails = InReplyToDetails.Loading(AN_EVENT_ID), + hideImage = false, + ) + ) + ) } } @@ -337,21 +316,22 @@ class MessagesPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitFirstItem() - initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, aMessageEvent(eventId = null))) - assertThat(initialState.actionListState.target).isEqualTo(ActionListState.Target.None) - // Otherwise we would have some extra items here - ensureAllEventsConsumed() + val initialState = awaitItem() + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, aMessageEvent(eventId = null))) + skipItems(1) } } @Test fun `present - handle action reply to an image media message`() = runTest { - val presenter = createMessagesPresenter() + val composerRecorder = EventsRecorder() + val presenter = createMessagesPresenter( + messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) }, + ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitFirstItem() + val initialState = awaitItem() val mediaMessage = aMessageEvent( content = TimelineItemImageContent( body = "image.jpg", @@ -368,22 +348,29 @@ class MessagesPresenterTest { formattedFileSize = "4MB" ) ) - initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, mediaMessage)) - val finalState = awaitItem() - assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java) - val replyMode = finalState.composerState.mode as MessageComposerMode.Reply - assertThat(replyMode.replyToDetails).isInstanceOf(InReplyToDetails.Loading::class.java) - assertThat(finalState.actionListState.target).isEqualTo(ActionListState.Target.None) + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, mediaMessage)) + awaitItem() + composerRecorder.assertSingle( + MessageComposerEvents.SetMode( + composerMode = MessageComposerMode.Reply( + replyToDetails = InReplyToDetails.Loading(AN_EVENT_ID), + hideImage = false, + ) + ) + ) } } @Test fun `present - handle action reply to a video media message`() = runTest { - val presenter = createMessagesPresenter() + val composerRecorder = EventsRecorder() + val presenter = createMessagesPresenter( + messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) }, + ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitFirstItem() + val initialState = awaitItem() val mediaMessage = aMessageEvent( content = TimelineItemVideoContent( body = "video.mp4", @@ -401,22 +388,29 @@ class MessagesPresenterTest { formattedFileSize = "50MB" ) ) - initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, mediaMessage)) - val finalState = awaitItem() - assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java) - val replyMode = finalState.composerState.mode as MessageComposerMode.Reply - assertThat(replyMode.replyToDetails).isInstanceOf(InReplyToDetails.Loading::class.java) - assertThat(finalState.actionListState.target).isEqualTo(ActionListState.Target.None) + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, mediaMessage)) + awaitItem() + composerRecorder.assertSingle( + MessageComposerEvents.SetMode( + composerMode = MessageComposerMode.Reply( + replyToDetails = InReplyToDetails.Loading(AN_EVENT_ID), + hideImage = false, + ) + ) + ) } } @Test fun `present - handle action reply to a file media message`() = runTest { - val presenter = createMessagesPresenter() + val composerRecorder = EventsRecorder() + val presenter = createMessagesPresenter( + messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) }, + ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitFirstItem() + val initialState = awaitItem() val mediaMessage = aMessageEvent( content = TimelineItemFileContent( body = "file.pdf", @@ -427,26 +421,40 @@ class MessagesPresenterTest { fileExtension = "pdf", ) ) - initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, mediaMessage)) - val finalState = awaitItem() - assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java) - val replyMode = finalState.composerState.mode as MessageComposerMode.Reply - assertThat(replyMode.replyToDetails).isInstanceOf(InReplyToDetails.Loading::class.java) - assertThat(finalState.actionListState.target).isEqualTo(ActionListState.Target.None) + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, mediaMessage)) + awaitItem() + composerRecorder.assertSingle( + MessageComposerEvents.SetMode( + composerMode = MessageComposerMode.Reply( + replyToDetails = InReplyToDetails.Loading(AN_EVENT_ID), + hideImage = false, + ) + ) + ) } } @Test fun `present - handle action edit`() = runTest { - val presenter = createMessagesPresenter() + val composerRecorder = EventsRecorder() + val presenter = createMessagesPresenter( + messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) }, + ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitFirstItem() - initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Edit, aMessageEvent())) - val finalState = awaitItem() - assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Edit::class.java) - assertThat(finalState.actionListState.target).isEqualTo(ActionListState.Target.None) + val initialState = awaitItem() + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Edit, aMessageEvent())) + awaitItem() + composerRecorder.assertSingle( + MessageComposerEvents.SetMode( + composerMode = MessageComposerMode.Edit( + eventId = AN_EVENT_ID, + transactionId = null, + content = (aMessageEvent().content as TimelineItemTextContent).body + ) + ) + ) } } @@ -457,8 +465,9 @@ class MessagesPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitFirstItem() - initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Edit, aMessageEvent(content = aTimelineItemPollContent()))) + val initialState = awaitItem() + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Edit, aMessageEvent(content = aTimelineItemPollContent()))) + awaitItem() assertThat(navigator.onEditPollClickedCount).isEqualTo(1) } } @@ -470,9 +479,9 @@ class MessagesPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitFirstItem() + val initialState = awaitItem() endPollAction.verifyExecutionCount(0) - initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.EndPoll, aMessageEvent(content = aTimelineItemPollContent()))) + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.EndPoll, aMessageEvent(content = aTimelineItemPollContent()))) delay(1) endPollAction.verifyExecutionCount(1) cancelAndIgnoreRemainingEvents() @@ -496,16 +505,17 @@ class MessagesPresenterTest { val redactEventLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String? -> Result.success(Unit) } liveTimeline.redactEventLambda = redactEventLambda - - val presenter = createMessagesPresenter(matrixRoom = matrixRoom, coroutineDispatchers = coroutineDispatchers) + val presenter = createMessagesPresenter( + matrixRoom = matrixRoom, + coroutineDispatchers = coroutineDispatchers, + ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(1) val initialState = awaitItem() val messageEvent = aMessageEvent() - initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Redact, messageEvent)) - assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Redact, messageEvent)) + awaitItem() assert(redactEventLambda) .isCalledOnce() .with(value(messageEvent.eventId), value(messageEvent.transactionId), value(null)) @@ -519,9 +529,8 @@ class MessagesPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(1) val initialState = awaitItem() - initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.ReportContent, aMessageEvent())) + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.ReportContent, aMessageEvent())) assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) assertThat(navigator.onReportContentClickedCount).isEqualTo(1) } @@ -533,9 +542,8 @@ class MessagesPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(1) val initialState = awaitItem() - initialState.eventSink.invoke(MessagesEvents.Dismiss) + initialState.eventSink(MessagesEvents.Dismiss) assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) } } @@ -547,9 +555,8 @@ class MessagesPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(1) val initialState = awaitItem() - initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.ViewSource, aMessageEvent())) + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.ViewSource, aMessageEvent())) assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) assertThat(navigator.onShowEventDebugInfoClickedCount).isEqualTo(1) } @@ -572,21 +579,18 @@ class MessagesPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(1) val initialState = awaitItem() // Initially the composer doesn't have focus, so we don't show the alert assertThat(initialState.showReinvitePrompt).isFalse() // When the input field is focused we show the alert - initialState.composerState.textEditorState.requestFocus() - val focusedState = consumeItemsUntilPredicate(timeout = 250.milliseconds) { state -> - state.showReinvitePrompt - }.last() + (initialState.composerState.textEditorState as TextEditorState.Markdown).state.hasFocus = true + skipItems(1) + val focusedState = awaitItem() assertThat(focusedState.showReinvitePrompt).isTrue() // If it's dismissed then we stop showing the alert initialState.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Cancel)) - val dismissedState = consumeItemsUntilPredicate(timeout = 250.milliseconds) { state -> - !state.showReinvitePrompt - }.last() + skipItems(1) + val dismissedState = awaitItem() assertThat(dismissedState.showReinvitePrompt).isFalse() } } @@ -608,9 +612,9 @@ class MessagesPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitFirstItem() + val initialState = awaitItem() assertThat(initialState.showReinvitePrompt).isFalse() - initialState.composerState.textEditorState.requestFocus() + (initialState.composerState.textEditorState as TextEditorState.Markdown).state.hasFocus = true val focusedState = awaitItem() assertThat(focusedState.showReinvitePrompt).isFalse() } @@ -633,9 +637,9 @@ class MessagesPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitFirstItem() + val initialState = awaitItem() assertThat(initialState.showReinvitePrompt).isFalse() - initialState.composerState.textEditorState.requestFocus() + (initialState.composerState.textEditorState as TextEditorState.Markdown).state.hasFocus = true val focusedState = awaitItem() assertThat(focusedState.showReinvitePrompt).isFalse() } @@ -799,7 +803,8 @@ class MessagesPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val state = awaitFirstItem() + skipItems(1) + val state = awaitItem() assertThat(state.userEventPermissions.canSendMessage).isTrue() } } @@ -826,9 +831,7 @@ class MessagesPresenterTest { }.test { // Default value assertThat(awaitItem().userEventPermissions.canSendMessage).isTrue() - skipItems(1) assertThat(awaitItem().userEventPermissions.canSendMessage).isFalse() - cancelAndIgnoreRemainingEvents() } } @@ -876,21 +879,27 @@ class MessagesPresenterTest { @Test fun `present - handle action reply to a poll`() = runTest { - val presenter = createMessagesPresenter() + val composerRecorder = EventsRecorder() + val presenter = createMessagesPresenter( + messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) }, + ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitFirstItem() + val initialState = awaitItem() val poll = aMessageEvent( content = aTimelineItemPollContent() ) - initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, poll)) + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, poll)) val finalState = awaitItem() - assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java) - val replyMode = finalState.composerState.mode as MessageComposerMode.Reply - - assertThat(replyMode.replyToDetails).isInstanceOf(InReplyToDetails.Loading::class.java) - assertThat(finalState.actionListState.target).isEqualTo(ActionListState.Target.None) + composerRecorder.assertSingle( + MessageComposerEvents.SetMode( + composerMode = MessageComposerMode.Reply( + replyToDetails = InReplyToDetails.Loading(AN_EVENT_ID), + hideImage = false, + ) + ) + ) } } @@ -916,15 +925,16 @@ class MessagesPresenterTest { val messageEvent = aMessageEvent( content = aTimelineItemTextContent() ) - val initialState = awaitFirstItem() + val initialState = awaitItem() timeline.pinEventLambda = successPinEventLambda - initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Pin, messageEvent)) + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Pin, messageEvent)) assert(successPinEventLambda).isCalledOnce().with(value(messageEvent.eventId)) timeline.pinEventLambda = failurePinEventLambda - initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Pin, messageEvent)) + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Pin, messageEvent)) assert(failurePinEventLambda).isCalledOnce().with(value(messageEvent.eventId)) + skipItems(1) assertThat(awaitItem().snackbarMessage).isNotNull() assertThat(analyticsService.capturedEvents).containsExactly( PinUnpinAction(kind = PinUnpinAction.Kind.Pin, from = PinUnpinAction.From.Timeline), @@ -955,15 +965,16 @@ class MessagesPresenterTest { val messageEvent = aMessageEvent( content = aTimelineItemTextContent() ) - val initialState = awaitFirstItem() + val initialState = awaitItem() timeline.unpinEventLambda = successUnpinEventLambda - initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Unpin, messageEvent)) + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Unpin, messageEvent)) assert(successUnpinEventLambda).isCalledOnce().with(value(messageEvent.eventId)) timeline.unpinEventLambda = failureUnpinEventLambda - initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Unpin, messageEvent)) + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Unpin, messageEvent)) assert(failureUnpinEventLambda).isCalledOnce().with(value(messageEvent.eventId)) + skipItems(1) assertThat(awaitItem().snackbarMessage).isNotNull() assertThat(analyticsService.capturedEvents).containsExactly( PinUnpinAction(kind = PinUnpinAction.Kind.Unpin, from = PinUnpinAction.From.Timeline), @@ -972,12 +983,6 @@ class MessagesPresenterTest { } } - private suspend fun ReceiveTurbine.awaitFirstItem(): T { - // Skip 2 item if Mentions feature is enabled, else 1 - skipItems(if (FeatureFlags.Mentions.defaultValue(aBuildMeta())) 2 else 1) - return awaitItem() - } - private fun TestScope.createMessagesPresenter( coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(), matrixRoom: MatrixRoom = FakeMatrixRoom( @@ -993,83 +998,34 @@ class MessagesPresenterTest { navigator: FakeMessagesNavigator = FakeMessagesNavigator(), clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(), analyticsService: FakeAnalyticsService = FakeAnalyticsService(), - permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(), endPollAction: EndPollAction = FakeEndPollAction(), permalinkParser: PermalinkParser = FakePermalinkParser(), + messageComposerPresenter: Presenter = Presenter { + aMessageComposerState( + // Use TextEditorState.Markdown, so that we can request focus manually. + textEditorState = TextEditorState.Markdown(MarkdownTextEditorState(initialText = "", initialFocus = false)) + ) + }, + actionListEventSink: (ActionListEvents) -> Unit = {}, ): MessagesPresenter { - val mediaSender = MediaSender(FakeMediaPreProcessor(), matrixRoom) - val permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter) - val sessionPreferencesStore = InMemorySessionPreferencesStore() - val mentionSpanProvider = MentionSpanProvider(FakePermalinkParser()) - val messageComposerPresenter = MessageComposerPresenter( - appCoroutineScope = this, - room = matrixRoom, - mediaPickerProvider = FakePickerProvider(), - featureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.NotificationSettings.key to true)), - sessionPreferencesStore = InMemorySessionPreferencesStore(), - localMediaFactory = FakeLocalMediaFactory(mockMediaUrl), - mediaSender = mediaSender, - snackbarDispatcher = SnackbarDispatcher(), - analyticsService = analyticsService, - messageComposerContext = DefaultMessageComposerContext(), - richTextEditorStateFactory = TestRichTextEditorStateFactory(), - roomAliasSuggestionsDataSource = FakeRoomAliasSuggestionsDataSource(), - permissionsPresenterFactory = permissionsPresenterFactory, - permalinkParser = FakePermalinkParser(), - permalinkBuilder = FakePermalinkBuilder(), - timelineController = TimelineController(matrixRoom), - draftService = FakeComposerDraftService(), - mentionSpanProvider = mentionSpanProvider, - pillificationHelper = FakeTextPillificationHelper(), - roomMemberProfilesCache = RoomMemberProfilesCache(), - suggestionsProcessor = SuggestionsProcessor(), - ).apply { - showTextFormatting = true - isTesting = true - } - val voiceMessageComposerPresenter = VoiceMessageComposerPresenter( - this, - FakeVoiceRecorder(), - analyticsService, - mediaSender, - player = VoiceMessageComposerPlayer(FakeMediaPlayer(), this), - messageComposerContext = FakeMessageComposerContext(), - permissionsPresenterFactory, - ) - val timelinePresenter = TimelinePresenter( - timelineItemsFactoryCreator = aTimelineItemsFactoryCreator(), - room = matrixRoom, - dispatchers = coroutineDispatchers, - appScope = this, - navigator = navigator, - redactedVoiceMessageManager = FakeRedactedVoiceMessageManager(), - endPollAction = endPollAction, - sendPollResponseAction = FakeSendPollResponseAction(), - sessionPreferencesStore = sessionPreferencesStore, - timelineItemIndexer = TimelineItemIndexer(), - timelineController = TimelineController(matrixRoom), - resolveVerifiedUserSendFailurePresenter = { aResolveVerifiedUserSendFailureState() }, - typingNotificationPresenter = { aTypingNotificationState() }, - ) val timelinePresenterFactory = object : TimelinePresenter.Factory { override fun create(navigator: MessagesNavigator): TimelinePresenter { - return timelinePresenter + return createTimelinePresenter( + endPollAction = endPollAction, + ) } } val featureFlagService = FakeFeatureFlagService() - val readReceiptBottomSheetPresenter = ReadReceiptBottomSheetPresenter() - val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider()) - val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom) return MessagesPresenter( room = matrixRoom, composerPresenter = messageComposerPresenter, - voiceMessageComposerPresenter = voiceMessageComposerPresenter, + voiceMessageComposerPresenter = { aVoiceMessageComposerState() }, timelinePresenterFactory = timelinePresenterFactory, timelineProtectionPresenter = { aTimelineProtectionState() }, - actionListPresenterFactory = FakeActionListPresenter.Factory, - customReactionPresenter = customReactionPresenter, - reactionSummaryPresenter = reactionSummaryPresenter, - readReceiptBottomSheetPresenter = readReceiptBottomSheetPresenter, + actionListPresenterFactory = FakeActionListPresenter.Factory(actionListEventSink), + customReactionPresenter = { aCustomReactionState() }, + reactionSummaryPresenter = { aReactionSummaryState() }, + readReceiptBottomSheetPresenter = { aReadReceiptBottomSheetState() }, pinnedMessagesBannerPresenter = { aLoadedPinnedMessagesBannerState() }, networkMonitor = FakeNetworkMonitor(), snackbarDispatcher = SnackbarDispatcher(), diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/FakeActionListPresenter.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/FakeActionListPresenter.kt index 468fd0620b..14f62a1daf 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/FakeActionListPresenter.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/FakeActionListPresenter.kt @@ -10,15 +10,15 @@ package io.element.android.features.messages.impl.actionlist import androidx.compose.runtime.Composable import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor -class FakeActionListPresenter : ActionListPresenter { - object Factory : ActionListPresenter.Factory { +class FakeActionListPresenter(private val eventSink: (ActionListEvents) -> Unit = {}) : ActionListPresenter { + class Factory(private val eventSink: (ActionListEvents) -> Unit = {}) : ActionListPresenter.Factory { override fun create(postProcessor: TimelineItemActionPostProcessor): ActionListPresenter { - return FakeActionListPresenter() + return FakeActionListPresenter(eventSink) } } @Composable override fun present(): ActionListState { - return anActionListState() + return anActionListState(eventSink = eventSink) } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt index b8715568d1..1036788cbb 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt @@ -312,7 +312,7 @@ class PinnedMessagesListPresenterTest { timelineProvider = timelineProvider, timelineProtectionPresenter = { aTimelineProtectionState() }, snackbarDispatcher = SnackbarDispatcher(), - actionListPresenterFactory = FakeActionListPresenter.Factory, + actionListPresenterFactory = FakeActionListPresenter.Factory(), analyticsService = analyticsService, appCoroutineScope = this, ) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt index a008feac24..74df8ee46c 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt @@ -656,34 +656,34 @@ import kotlin.time.Duration.Companion.seconds private suspend fun ReceiveTurbine.awaitFirstItem(): T { return awaitItem() } +} - private fun TestScope.createTimelinePresenter( - timeline: Timeline = FakeTimeline(), - room: FakeMatrixRoom = FakeMatrixRoom( - liveTimeline = timeline, - canUserSendMessageResult = { _, _ -> Result.success(true) } - ), - redactedVoiceMessageManager: RedactedVoiceMessageManager = FakeRedactedVoiceMessageManager(), - messagesNavigator: FakeMessagesNavigator = FakeMessagesNavigator(), - endPollAction: EndPollAction = FakeEndPollAction(), - sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(), - sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(), - timelineItemIndexer: TimelineItemIndexer = TimelineItemIndexer(), - ): TimelinePresenter { - return TimelinePresenter( - timelineItemsFactoryCreator = aTimelineItemsFactoryCreator(), - room = room, - dispatchers = testCoroutineDispatchers(), - appScope = this, - navigator = messagesNavigator, - redactedVoiceMessageManager = redactedVoiceMessageManager, - endPollAction = endPollAction, - sendPollResponseAction = sendPollResponseAction, - sessionPreferencesStore = sessionPreferencesStore, - timelineItemIndexer = timelineItemIndexer, - timelineController = TimelineController(room), - resolveVerifiedUserSendFailurePresenter = { aResolveVerifiedUserSendFailureState() }, - typingNotificationPresenter = { aTypingNotificationState() }, - ) - } +internal fun TestScope.createTimelinePresenter( + timeline: Timeline = FakeTimeline(), + room: FakeMatrixRoom = FakeMatrixRoom( + liveTimeline = timeline, + canUserSendMessageResult = { _, _ -> Result.success(true) } + ), + redactedVoiceMessageManager: RedactedVoiceMessageManager = FakeRedactedVoiceMessageManager(), + messagesNavigator: FakeMessagesNavigator = FakeMessagesNavigator(), + endPollAction: EndPollAction = FakeEndPollAction(), + sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(), + sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(), + timelineItemIndexer: TimelineItemIndexer = TimelineItemIndexer(), +): TimelinePresenter { + return TimelinePresenter( + timelineItemsFactoryCreator = aTimelineItemsFactoryCreator(), + room = room, + dispatchers = testCoroutineDispatchers(), + appScope = this, + navigator = messagesNavigator, + redactedVoiceMessageManager = redactedVoiceMessageManager, + endPollAction = endPollAction, + sendPollResponseAction = sendPollResponseAction, + sessionPreferencesStore = sessionPreferencesStore, + timelineItemIndexer = timelineItemIndexer, + timelineController = TimelineController(room), + resolveVerifiedUserSendFailurePresenter = { aResolveVerifiedUserSendFailureState() }, + typingNotificationPresenter = { aTypingNotificationState() }, + ) }