Remove dependencies to other presenters in MessagesPresenter.

This commit is contained in:
Benoit Marty 2024-10-07 11:38:12 +02:00 committed by Benoit Marty
parent 62cbff0ffe
commit 6d87c8ad19
7 changed files with 238 additions and 257 deletions

View file

@ -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.TimelineItemAction
import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor 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.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.messagecomposer.MessageComposerState
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState 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.TimelineController
import io.element.android.features.messages.impl.timeline.TimelineEvents 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.TimelinePresenter
import io.element.android.features.messages.impl.timeline.TimelineState 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.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.model.TimelineItem 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.TimelineItemPollContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent 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.model.event.TimelineItemTextBasedContent
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState 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.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
@ -88,14 +87,14 @@ import timber.log.Timber
class MessagesPresenter @AssistedInject constructor( class MessagesPresenter @AssistedInject constructor(
@Assisted private val navigator: MessagesNavigator, @Assisted private val navigator: MessagesNavigator,
private val room: MatrixRoom, private val room: MatrixRoom,
private val composerPresenter: MessageComposerPresenter, private val composerPresenter: Presenter<MessageComposerState>,
private val voiceMessageComposerPresenter: VoiceMessageComposerPresenter, private val voiceMessageComposerPresenter: Presenter<VoiceMessageComposerState>,
timelinePresenterFactory: TimelinePresenter.Factory, timelinePresenterFactory: TimelinePresenter.Factory,
private val timelineProtectionPresenter: Presenter<TimelineProtectionState>, private val timelineProtectionPresenter: Presenter<TimelineProtectionState>,
private val actionListPresenterFactory: ActionListPresenter.Factory, private val actionListPresenterFactory: ActionListPresenter.Factory,
private val customReactionPresenter: CustomReactionPresenter, private val customReactionPresenter: Presenter<CustomReactionState>,
private val reactionSummaryPresenter: ReactionSummaryPresenter, private val reactionSummaryPresenter: Presenter<ReactionSummaryState>,
private val readReceiptBottomSheetPresenter: ReadReceiptBottomSheetPresenter, private val readReceiptBottomSheetPresenter: Presenter<ReadReceiptBottomSheetState>,
private val pinnedMessagesBannerPresenter: Presenter<PinnedMessagesBannerState>, private val pinnedMessagesBannerPresenter: Presenter<PinnedMessagesBannerState>,
private val networkMonitor: NetworkMonitor, private val networkMonitor: NetworkMonitor,
private val snackbarDispatcher: SnackbarDispatcher, private val snackbarDispatcher: SnackbarDispatcher,

View file

@ -12,12 +12,22 @@ import dagger.Binds
import dagger.Module import dagger.Module
import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailurePresenter 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.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.PinnedMessagesBannerPresenter
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState 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.TimelineProtectionPresenter
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState 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.TypingNotificationPresenter
import io.element.android.features.messages.impl.typing.TypingNotificationState 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.architecture.Presenter
import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.RoomScope
@ -35,4 +45,19 @@ interface MessagesModule {
@Binds @Binds
fun bindTimelineProtectionPresenter(presenter: TimelineProtectionPresenter): Presenter<TimelineProtectionState> fun bindTimelineProtectionPresenter(presenter: TimelineProtectionPresenter): Presenter<TimelineProtectionState>
@Binds
fun bindMessageComposerPresenter(presenter: MessageComposerPresenter): Presenter<MessageComposerState>
@Binds
fun bindVoiceMessageComposerPresenter(presenter: VoiceMessageComposerPresenter): Presenter<VoiceMessageComposerState>
@Binds
fun bindCustomReactionPresenter(presenter: CustomReactionPresenter): Presenter<CustomReactionState>
@Binds
fun bindReactionSummaryPresenter(presenter: ReactionSummaryPresenter): Presenter<ReactionSummaryState>
@Binds
fun bindReadReceiptBottomSheetPresenter(presenter: ReadReceiptBottomSheetPresenter): Presenter<ReadReceiptBottomSheetState>
} }

View file

@ -33,6 +33,7 @@ fun aMessageComposerState(
canCreatePoll: Boolean = true, canCreatePoll: Boolean = true,
attachmentsState: AttachmentsState = AttachmentsState.None, attachmentsState: AttachmentsState = AttachmentsState.None,
suggestions: ImmutableList<ResolvedSuggestion> = persistentListOf(), suggestions: ImmutableList<ResolvedSuggestion> = persistentListOf(),
eventSink: (MessageComposerEvents) -> Unit = {},
) = MessageComposerState( ) = MessageComposerState(
textEditorState = textEditorState, textEditorState = textEditorState,
isFullScreen = isFullScreen, isFullScreen = isFullScreen,
@ -44,5 +45,5 @@ fun aMessageComposerState(
attachmentsState = attachmentsState, attachmentsState = attachmentsState,
suggestions = suggestions, suggestions = suggestions,
resolveMentionDisplay = { _, _ -> TextDisplay.Plain }, resolveMentionDisplay = { _, _ -> TextDisplay.Plain },
eventSink = {}, eventSink = eventSink,
) )

View file

@ -7,33 +7,23 @@
package io.element.android.features.messages.impl package io.element.android.features.messages.impl
import android.net.Uri
import app.cash.molecule.RecompositionMode import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
import app.cash.turbine.ReceiveTurbine
import app.cash.turbine.test 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.PinUnpinAction 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.ActionListState
import io.element.android.features.messages.impl.actionlist.FakeActionListPresenter 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.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.aMessageEvent
import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactoryCreator import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
import io.element.android.features.messages.impl.messagecomposer.DefaultMessageComposerContext import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
import io.element.android.features.messages.impl.messagecomposer.FakeRoomAliasSuggestionsDataSource import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState
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.pinned.banner.aLoadedPinnedMessagesBannerState 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.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.TimelinePresenter
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter import io.element.android.features.messages.impl.timeline.createTimelinePresenter
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.model.event.TimelineItemFileContent 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.TimelineItemImageContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent 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.aTimelineItemPollContent
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent 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.timeline.protection.aTimelineProtectionState
import io.element.android.features.messages.impl.typing.aTypingNotificationState import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessageComposerState
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.test.timeline.FakeHtmlConverterProvider import io.element.android.features.messages.test.timeline.FakeHtmlConverterProvider
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
import io.element.android.features.poll.api.actions.EndPollAction 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.FakeEndPollAction
import io.element.android.features.poll.test.actions.FakeSendPollResponseAction
import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper
import io.element.android.libraries.architecture.AsyncData 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.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.mimetype.MimeTypes 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.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize 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.SnackbarDispatcher
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService 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.EventId
import io.element.android.libraries.matrix.api.core.TransactionId 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.MessageEventType
import io.element.android.libraries.matrix.api.room.RoomMembershipState 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_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_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_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_SESSION_ID_2
import io.element.android.libraries.matrix.test.A_THROWABLE 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.A_UNIQUE_ID
import io.element.android.libraries.matrix.test.core.aBuildMeta 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.permalink.FakePermalinkParser
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom 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.aRoomInfo
import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.matrix.test.room.aRoomMember
import io.element.android.libraries.matrix.test.timeline.FakeTimeline 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.matrix.ui.messages.reply.InReplyToDetails
import io.element.android.libraries.mediapickers.test.FakePickerProvider import io.element.android.libraries.textcomposer.model.MarkdownTextEditorState
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.MessageComposerMode 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.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.consumeItemsUntilPredicate import io.element.android.tests.testutils.consumeItemsUntilPredicate
import io.element.android.tests.testutils.consumeItemsUntilTimeout 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.lambdaRecorder
import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import io.mockk.mockk
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -123,8 +97,6 @@ class MessagesPresenterTest {
@get:Rule @get:Rule
val warmUpRule = WarmUpRule() val warmUpRule = WarmUpRule()
private val mockMediaUrl: Uri = mockk("localMediaUri")
@Test @Test
fun `present - initial state`() = runTest { fun `present - initial state`() = runTest {
val presenter = createMessagesPresenter() val presenter = createMessagesPresenter()
@ -212,15 +184,13 @@ class MessagesPresenterTest {
}.test { }.test {
skipItems(1) skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", A_UNIQUE_ID)) initialState.eventSink(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)
assert(toggleReactionSuccess) assert(toggleReactionSuccess)
.isCalledOnce() .isCalledOnce()
.with(value("👍"), value(A_UNIQUE_ID)) .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) assert(toggleReactionFailure)
.isCalledOnce() .isCalledOnce()
.with(value("👍"), value(A_UNIQUE_ID)) .with(value("👍"), value(A_UNIQUE_ID))
@ -248,15 +218,16 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", A_UNIQUE_ID)) initialState.eventSink(MessagesEvents.ToggleReaction("👍", A_UNIQUE_ID))
initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", A_UNIQUE_ID)) initialState.eventSink(MessagesEvents.ToggleReaction("👍", A_UNIQUE_ID))
assert(toggleReactionSuccess) assert(toggleReactionSuccess)
.isCalledExactly(2) .isCalledExactly(2)
.withSequence( .withSequence(
listOf(value("👍"), value(A_UNIQUE_ID)), listOf(value("👍"), value(A_UNIQUE_ID)),
listOf(value("👍"), value(A_UNIQUE_ID)), listOf(value("👍"), value(A_UNIQUE_ID)),
) )
skipItems(1)
} }
} }
@ -267,9 +238,8 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1)
val initialState = awaitItem() 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(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
assertThat(navigator.onForwardEventClickedCount).isEqualTo(1) assertThat(navigator.onForwardEventClickedCount).isEqualTo(1)
} }
@ -283,9 +253,9 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Copy, event)) initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Copy, event))
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) skipItems(2)
assertThat(clipboardHelper.clipboardContents).isEqualTo((event.content as TimelineItemTextContent).body) assertThat(clipboardHelper.clipboardContents).isEqualTo((event.content as TimelineItemTextContent).body)
} }
} }
@ -310,24 +280,33 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.CopyLink, event)) initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.CopyLink, event))
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) skipItems(2)
assertThat(clipboardHelper.clipboardContents).isEqualTo("a link") assertThat(clipboardHelper.clipboardContents).isEqualTo("a link")
} }
} }
@Test @Test
fun `present - handle action reply`() = runTest { fun `present - handle action reply`() = runTest {
val presenter = createMessagesPresenter() val composerRecorder = EventsRecorder<MessageComposerEvents>()
val presenter = createMessagesPresenter(
messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) },
)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, aMessageEvent())) initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, aMessageEvent()))
val finalState = awaitItem() awaitItem()
assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java) composerRecorder.assertSingle(
assertThat(finalState.actionListState.target).isEqualTo(ActionListState.Target.None) MessageComposerEvents.SetMode(
composerMode = MessageComposerMode.Reply(
replyToDetails = InReplyToDetails.Loading(AN_EVENT_ID),
hideImage = false,
)
)
)
} }
} }
@ -337,21 +316,22 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, aMessageEvent(eventId = null))) initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, aMessageEvent(eventId = null)))
assertThat(initialState.actionListState.target).isEqualTo(ActionListState.Target.None) skipItems(1)
// Otherwise we would have some extra items here
ensureAllEventsConsumed()
} }
} }
@Test @Test
fun `present - handle action reply to an image media message`() = runTest { fun `present - handle action reply to an image media message`() = runTest {
val presenter = createMessagesPresenter() val composerRecorder = EventsRecorder<MessageComposerEvents>()
val presenter = createMessagesPresenter(
messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) },
)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitItem()
val mediaMessage = aMessageEvent( val mediaMessage = aMessageEvent(
content = TimelineItemImageContent( content = TimelineItemImageContent(
body = "image.jpg", body = "image.jpg",
@ -368,22 +348,29 @@ class MessagesPresenterTest {
formattedFileSize = "4MB" formattedFileSize = "4MB"
) )
) )
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, mediaMessage)) initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, mediaMessage))
val finalState = awaitItem() awaitItem()
assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java) composerRecorder.assertSingle(
val replyMode = finalState.composerState.mode as MessageComposerMode.Reply MessageComposerEvents.SetMode(
assertThat(replyMode.replyToDetails).isInstanceOf(InReplyToDetails.Loading::class.java) composerMode = MessageComposerMode.Reply(
assertThat(finalState.actionListState.target).isEqualTo(ActionListState.Target.None) replyToDetails = InReplyToDetails.Loading(AN_EVENT_ID),
hideImage = false,
)
)
)
} }
} }
@Test @Test
fun `present - handle action reply to a video media message`() = runTest { fun `present - handle action reply to a video media message`() = runTest {
val presenter = createMessagesPresenter() val composerRecorder = EventsRecorder<MessageComposerEvents>()
val presenter = createMessagesPresenter(
messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) },
)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitItem()
val mediaMessage = aMessageEvent( val mediaMessage = aMessageEvent(
content = TimelineItemVideoContent( content = TimelineItemVideoContent(
body = "video.mp4", body = "video.mp4",
@ -401,22 +388,29 @@ class MessagesPresenterTest {
formattedFileSize = "50MB" formattedFileSize = "50MB"
) )
) )
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, mediaMessage)) initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, mediaMessage))
val finalState = awaitItem() awaitItem()
assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java) composerRecorder.assertSingle(
val replyMode = finalState.composerState.mode as MessageComposerMode.Reply MessageComposerEvents.SetMode(
assertThat(replyMode.replyToDetails).isInstanceOf(InReplyToDetails.Loading::class.java) composerMode = MessageComposerMode.Reply(
assertThat(finalState.actionListState.target).isEqualTo(ActionListState.Target.None) replyToDetails = InReplyToDetails.Loading(AN_EVENT_ID),
hideImage = false,
)
)
)
} }
} }
@Test @Test
fun `present - handle action reply to a file media message`() = runTest { fun `present - handle action reply to a file media message`() = runTest {
val presenter = createMessagesPresenter() val composerRecorder = EventsRecorder<MessageComposerEvents>()
val presenter = createMessagesPresenter(
messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) },
)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitItem()
val mediaMessage = aMessageEvent( val mediaMessage = aMessageEvent(
content = TimelineItemFileContent( content = TimelineItemFileContent(
body = "file.pdf", body = "file.pdf",
@ -427,26 +421,40 @@ class MessagesPresenterTest {
fileExtension = "pdf", fileExtension = "pdf",
) )
) )
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, mediaMessage)) initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, mediaMessage))
val finalState = awaitItem() awaitItem()
assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java) composerRecorder.assertSingle(
val replyMode = finalState.composerState.mode as MessageComposerMode.Reply MessageComposerEvents.SetMode(
assertThat(replyMode.replyToDetails).isInstanceOf(InReplyToDetails.Loading::class.java) composerMode = MessageComposerMode.Reply(
assertThat(finalState.actionListState.target).isEqualTo(ActionListState.Target.None) replyToDetails = InReplyToDetails.Loading(AN_EVENT_ID),
hideImage = false,
)
)
)
} }
} }
@Test @Test
fun `present - handle action edit`() = runTest { fun `present - handle action edit`() = runTest {
val presenter = createMessagesPresenter() val composerRecorder = EventsRecorder<MessageComposerEvents>()
val presenter = createMessagesPresenter(
messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) },
)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Edit, aMessageEvent())) initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Edit, aMessageEvent()))
val finalState = awaitItem() awaitItem()
assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Edit::class.java) composerRecorder.assertSingle(
assertThat(finalState.actionListState.target).isEqualTo(ActionListState.Target.None) 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) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Edit, aMessageEvent(content = aTimelineItemPollContent()))) initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Edit, aMessageEvent(content = aTimelineItemPollContent())))
awaitItem()
assertThat(navigator.onEditPollClickedCount).isEqualTo(1) assertThat(navigator.onEditPollClickedCount).isEqualTo(1)
} }
} }
@ -470,9 +479,9 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitItem()
endPollAction.verifyExecutionCount(0) endPollAction.verifyExecutionCount(0)
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.EndPoll, aMessageEvent(content = aTimelineItemPollContent()))) initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.EndPoll, aMessageEvent(content = aTimelineItemPollContent())))
delay(1) delay(1)
endPollAction.verifyExecutionCount(1) endPollAction.verifyExecutionCount(1)
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
@ -496,16 +505,17 @@ class MessagesPresenterTest {
val redactEventLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String? -> Result.success(Unit) } val redactEventLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String? -> Result.success(Unit) }
liveTimeline.redactEventLambda = redactEventLambda liveTimeline.redactEventLambda = redactEventLambda
val presenter = createMessagesPresenter(
val presenter = createMessagesPresenter(matrixRoom = matrixRoom, coroutineDispatchers = coroutineDispatchers) matrixRoom = matrixRoom,
coroutineDispatchers = coroutineDispatchers,
)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
val messageEvent = aMessageEvent() val messageEvent = aMessageEvent()
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Redact, messageEvent)) initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Redact, messageEvent))
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) awaitItem()
assert(redactEventLambda) assert(redactEventLambda)
.isCalledOnce() .isCalledOnce()
.with(value(messageEvent.eventId), value(messageEvent.transactionId), value(null)) .with(value(messageEvent.eventId), value(messageEvent.transactionId), value(null))
@ -519,9 +529,8 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1)
val initialState = awaitItem() 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(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
assertThat(navigator.onReportContentClickedCount).isEqualTo(1) assertThat(navigator.onReportContentClickedCount).isEqualTo(1)
} }
@ -533,9 +542,8 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.Dismiss) initialState.eventSink(MessagesEvents.Dismiss)
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
} }
} }
@ -547,9 +555,8 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1)
val initialState = awaitItem() 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(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
assertThat(navigator.onShowEventDebugInfoClickedCount).isEqualTo(1) assertThat(navigator.onShowEventDebugInfoClickedCount).isEqualTo(1)
} }
@ -572,21 +579,18 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
// Initially the composer doesn't have focus, so we don't show the alert // Initially the composer doesn't have focus, so we don't show the alert
assertThat(initialState.showReinvitePrompt).isFalse() assertThat(initialState.showReinvitePrompt).isFalse()
// When the input field is focused we show the alert // When the input field is focused we show the alert
initialState.composerState.textEditorState.requestFocus() (initialState.composerState.textEditorState as TextEditorState.Markdown).state.hasFocus = true
val focusedState = consumeItemsUntilPredicate(timeout = 250.milliseconds) { state -> skipItems(1)
state.showReinvitePrompt val focusedState = awaitItem()
}.last()
assertThat(focusedState.showReinvitePrompt).isTrue() assertThat(focusedState.showReinvitePrompt).isTrue()
// If it's dismissed then we stop showing the alert // If it's dismissed then we stop showing the alert
initialState.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Cancel)) initialState.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Cancel))
val dismissedState = consumeItemsUntilPredicate(timeout = 250.milliseconds) { state -> skipItems(1)
!state.showReinvitePrompt val dismissedState = awaitItem()
}.last()
assertThat(dismissedState.showReinvitePrompt).isFalse() assertThat(dismissedState.showReinvitePrompt).isFalse()
} }
} }
@ -608,9 +612,9 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitItem()
assertThat(initialState.showReinvitePrompt).isFalse() assertThat(initialState.showReinvitePrompt).isFalse()
initialState.composerState.textEditorState.requestFocus() (initialState.composerState.textEditorState as TextEditorState.Markdown).state.hasFocus = true
val focusedState = awaitItem() val focusedState = awaitItem()
assertThat(focusedState.showReinvitePrompt).isFalse() assertThat(focusedState.showReinvitePrompt).isFalse()
} }
@ -633,9 +637,9 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitItem()
assertThat(initialState.showReinvitePrompt).isFalse() assertThat(initialState.showReinvitePrompt).isFalse()
initialState.composerState.textEditorState.requestFocus() (initialState.composerState.textEditorState as TextEditorState.Markdown).state.hasFocus = true
val focusedState = awaitItem() val focusedState = awaitItem()
assertThat(focusedState.showReinvitePrompt).isFalse() assertThat(focusedState.showReinvitePrompt).isFalse()
} }
@ -799,7 +803,8 @@ class MessagesPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val state = awaitFirstItem() skipItems(1)
val state = awaitItem()
assertThat(state.userEventPermissions.canSendMessage).isTrue() assertThat(state.userEventPermissions.canSendMessage).isTrue()
} }
} }
@ -826,9 +831,7 @@ class MessagesPresenterTest {
}.test { }.test {
// Default value // Default value
assertThat(awaitItem().userEventPermissions.canSendMessage).isTrue() assertThat(awaitItem().userEventPermissions.canSendMessage).isTrue()
skipItems(1)
assertThat(awaitItem().userEventPermissions.canSendMessage).isFalse() assertThat(awaitItem().userEventPermissions.canSendMessage).isFalse()
cancelAndIgnoreRemainingEvents()
} }
} }
@ -876,21 +879,27 @@ class MessagesPresenterTest {
@Test @Test
fun `present - handle action reply to a poll`() = runTest { fun `present - handle action reply to a poll`() = runTest {
val presenter = createMessagesPresenter() val composerRecorder = EventsRecorder<MessageComposerEvents>()
val presenter = createMessagesPresenter(
messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder) },
)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitItem()
val poll = aMessageEvent( val poll = aMessageEvent(
content = aTimelineItemPollContent() content = aTimelineItemPollContent()
) )
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, poll)) initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, poll))
val finalState = awaitItem() val finalState = awaitItem()
assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java) composerRecorder.assertSingle(
val replyMode = finalState.composerState.mode as MessageComposerMode.Reply MessageComposerEvents.SetMode(
composerMode = MessageComposerMode.Reply(
assertThat(replyMode.replyToDetails).isInstanceOf(InReplyToDetails.Loading::class.java) replyToDetails = InReplyToDetails.Loading(AN_EVENT_ID),
assertThat(finalState.actionListState.target).isEqualTo(ActionListState.Target.None) hideImage = false,
)
)
)
} }
} }
@ -916,15 +925,16 @@ class MessagesPresenterTest {
val messageEvent = aMessageEvent( val messageEvent = aMessageEvent(
content = aTimelineItemTextContent() content = aTimelineItemTextContent()
) )
val initialState = awaitFirstItem() val initialState = awaitItem()
timeline.pinEventLambda = successPinEventLambda 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)) assert(successPinEventLambda).isCalledOnce().with(value(messageEvent.eventId))
timeline.pinEventLambda = failurePinEventLambda 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)) assert(failurePinEventLambda).isCalledOnce().with(value(messageEvent.eventId))
skipItems(1)
assertThat(awaitItem().snackbarMessage).isNotNull() assertThat(awaitItem().snackbarMessage).isNotNull()
assertThat(analyticsService.capturedEvents).containsExactly( assertThat(analyticsService.capturedEvents).containsExactly(
PinUnpinAction(kind = PinUnpinAction.Kind.Pin, from = PinUnpinAction.From.Timeline), PinUnpinAction(kind = PinUnpinAction.Kind.Pin, from = PinUnpinAction.From.Timeline),
@ -955,15 +965,16 @@ class MessagesPresenterTest {
val messageEvent = aMessageEvent( val messageEvent = aMessageEvent(
content = aTimelineItemTextContent() content = aTimelineItemTextContent()
) )
val initialState = awaitFirstItem() val initialState = awaitItem()
timeline.unpinEventLambda = successUnpinEventLambda 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)) assert(successUnpinEventLambda).isCalledOnce().with(value(messageEvent.eventId))
timeline.unpinEventLambda = failureUnpinEventLambda 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)) assert(failureUnpinEventLambda).isCalledOnce().with(value(messageEvent.eventId))
skipItems(1)
assertThat(awaitItem().snackbarMessage).isNotNull() assertThat(awaitItem().snackbarMessage).isNotNull()
assertThat(analyticsService.capturedEvents).containsExactly( assertThat(analyticsService.capturedEvents).containsExactly(
PinUnpinAction(kind = PinUnpinAction.Kind.Unpin, from = PinUnpinAction.From.Timeline), PinUnpinAction(kind = PinUnpinAction.Kind.Unpin, from = PinUnpinAction.From.Timeline),
@ -972,12 +983,6 @@ class MessagesPresenterTest {
} }
} }
private suspend fun <T> ReceiveTurbine<T>.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( private fun TestScope.createMessagesPresenter(
coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(), coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
matrixRoom: MatrixRoom = FakeMatrixRoom( matrixRoom: MatrixRoom = FakeMatrixRoom(
@ -993,83 +998,34 @@ class MessagesPresenterTest {
navigator: FakeMessagesNavigator = FakeMessagesNavigator(), navigator: FakeMessagesNavigator = FakeMessagesNavigator(),
clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(), clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(),
analyticsService: FakeAnalyticsService = FakeAnalyticsService(), analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(),
endPollAction: EndPollAction = FakeEndPollAction(), endPollAction: EndPollAction = FakeEndPollAction(),
permalinkParser: PermalinkParser = FakePermalinkParser(), permalinkParser: PermalinkParser = FakePermalinkParser(),
messageComposerPresenter: Presenter<MessageComposerState> = Presenter {
aMessageComposerState(
// Use TextEditorState.Markdown, so that we can request focus manually.
textEditorState = TextEditorState.Markdown(MarkdownTextEditorState(initialText = "", initialFocus = false))
)
},
actionListEventSink: (ActionListEvents) -> Unit = {},
): MessagesPresenter { ): 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 { val timelinePresenterFactory = object : TimelinePresenter.Factory {
override fun create(navigator: MessagesNavigator): TimelinePresenter { override fun create(navigator: MessagesNavigator): TimelinePresenter {
return timelinePresenter return createTimelinePresenter(
endPollAction = endPollAction,
)
} }
} }
val featureFlagService = FakeFeatureFlagService() val featureFlagService = FakeFeatureFlagService()
val readReceiptBottomSheetPresenter = ReadReceiptBottomSheetPresenter()
val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider())
val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom)
return MessagesPresenter( return MessagesPresenter(
room = matrixRoom, room = matrixRoom,
composerPresenter = messageComposerPresenter, composerPresenter = messageComposerPresenter,
voiceMessageComposerPresenter = voiceMessageComposerPresenter, voiceMessageComposerPresenter = { aVoiceMessageComposerState() },
timelinePresenterFactory = timelinePresenterFactory, timelinePresenterFactory = timelinePresenterFactory,
timelineProtectionPresenter = { aTimelineProtectionState() }, timelineProtectionPresenter = { aTimelineProtectionState() },
actionListPresenterFactory = FakeActionListPresenter.Factory, actionListPresenterFactory = FakeActionListPresenter.Factory(actionListEventSink),
customReactionPresenter = customReactionPresenter, customReactionPresenter = { aCustomReactionState() },
reactionSummaryPresenter = reactionSummaryPresenter, reactionSummaryPresenter = { aReactionSummaryState() },
readReceiptBottomSheetPresenter = readReceiptBottomSheetPresenter, readReceiptBottomSheetPresenter = { aReadReceiptBottomSheetState() },
pinnedMessagesBannerPresenter = { aLoadedPinnedMessagesBannerState() }, pinnedMessagesBannerPresenter = { aLoadedPinnedMessagesBannerState() },
networkMonitor = FakeNetworkMonitor(), networkMonitor = FakeNetworkMonitor(),
snackbarDispatcher = SnackbarDispatcher(), snackbarDispatcher = SnackbarDispatcher(),

View file

@ -10,15 +10,15 @@ package io.element.android.features.messages.impl.actionlist
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor
class FakeActionListPresenter : ActionListPresenter { class FakeActionListPresenter(private val eventSink: (ActionListEvents) -> Unit = {}) : ActionListPresenter {
object Factory : ActionListPresenter.Factory { class Factory(private val eventSink: (ActionListEvents) -> Unit = {}) : ActionListPresenter.Factory {
override fun create(postProcessor: TimelineItemActionPostProcessor): ActionListPresenter { override fun create(postProcessor: TimelineItemActionPostProcessor): ActionListPresenter {
return FakeActionListPresenter() return FakeActionListPresenter(eventSink)
} }
} }
@Composable @Composable
override fun present(): ActionListState { override fun present(): ActionListState {
return anActionListState() return anActionListState(eventSink = eventSink)
} }
} }

View file

@ -312,7 +312,7 @@ class PinnedMessagesListPresenterTest {
timelineProvider = timelineProvider, timelineProvider = timelineProvider,
timelineProtectionPresenter = { aTimelineProtectionState() }, timelineProtectionPresenter = { aTimelineProtectionState() },
snackbarDispatcher = SnackbarDispatcher(), snackbarDispatcher = SnackbarDispatcher(),
actionListPresenterFactory = FakeActionListPresenter.Factory, actionListPresenterFactory = FakeActionListPresenter.Factory(),
analyticsService = analyticsService, analyticsService = analyticsService,
appCoroutineScope = this, appCoroutineScope = this,
) )

View file

@ -656,34 +656,34 @@ import kotlin.time.Duration.Companion.seconds
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T { private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
return awaitItem() return awaitItem()
} }
}
private fun TestScope.createTimelinePresenter(
timeline: Timeline = FakeTimeline(), internal fun TestScope.createTimelinePresenter(
room: FakeMatrixRoom = FakeMatrixRoom( timeline: Timeline = FakeTimeline(),
liveTimeline = timeline, room: FakeMatrixRoom = FakeMatrixRoom(
canUserSendMessageResult = { _, _ -> Result.success(true) } liveTimeline = timeline,
), canUserSendMessageResult = { _, _ -> Result.success(true) }
redactedVoiceMessageManager: RedactedVoiceMessageManager = FakeRedactedVoiceMessageManager(), ),
messagesNavigator: FakeMessagesNavigator = FakeMessagesNavigator(), redactedVoiceMessageManager: RedactedVoiceMessageManager = FakeRedactedVoiceMessageManager(),
endPollAction: EndPollAction = FakeEndPollAction(), messagesNavigator: FakeMessagesNavigator = FakeMessagesNavigator(),
sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(), endPollAction: EndPollAction = FakeEndPollAction(),
sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(), sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(),
timelineItemIndexer: TimelineItemIndexer = TimelineItemIndexer(), sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(),
): TimelinePresenter { timelineItemIndexer: TimelineItemIndexer = TimelineItemIndexer(),
return TimelinePresenter( ): TimelinePresenter {
timelineItemsFactoryCreator = aTimelineItemsFactoryCreator(), return TimelinePresenter(
room = room, timelineItemsFactoryCreator = aTimelineItemsFactoryCreator(),
dispatchers = testCoroutineDispatchers(), room = room,
appScope = this, dispatchers = testCoroutineDispatchers(),
navigator = messagesNavigator, appScope = this,
redactedVoiceMessageManager = redactedVoiceMessageManager, navigator = messagesNavigator,
endPollAction = endPollAction, redactedVoiceMessageManager = redactedVoiceMessageManager,
sendPollResponseAction = sendPollResponseAction, endPollAction = endPollAction,
sessionPreferencesStore = sessionPreferencesStore, sendPollResponseAction = sendPollResponseAction,
timelineItemIndexer = timelineItemIndexer, sessionPreferencesStore = sessionPreferencesStore,
timelineController = TimelineController(room), timelineItemIndexer = timelineItemIndexer,
resolveVerifiedUserSendFailurePresenter = { aResolveVerifiedUserSendFailureState() }, timelineController = TimelineController(room),
typingNotificationPresenter = { aTypingNotificationState() }, resolveVerifiedUserSendFailurePresenter = { aResolveVerifiedUserSendFailureState() },
) typingNotificationPresenter = { aTypingNotificationState() },
} )
} }