Pinned messages list : fix and add tests

This commit is contained in:
ganfra
2024-09-03 18:21:42 +02:00
parent 80f750ae34
commit 2c41d4583a
12 changed files with 293 additions and 33 deletions

View File

@@ -23,9 +23,11 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import com.squareup.anvil.annotations.ContributesBinding
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.element.android.features.messages.api.pinned.IsPinnedMessagesFeatureEnabled
import io.element.android.features.messages.impl.UserEventPermissions
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor
@@ -40,8 +42,7 @@ import io.element.android.features.messages.impl.timeline.model.event.canBeCopie
import io.element.android.features.messages.impl.timeline.model.event.canBeForwarded
import io.element.android.features.messages.impl.timeline.model.event.canReact
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
@@ -52,16 +53,24 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
class ActionListPresenter @AssistedInject constructor(
interface ActionListPresenter : Presenter<ActionListState> {
interface Factory {
fun create(postProcessor: TimelineItemActionPostProcessor): ActionListPresenter
}
}
class DefaultActionListPresenter @AssistedInject constructor(
@Assisted
private val postProcessor: TimelineItemActionPostProcessor,
private val appPreferencesStore: AppPreferencesStore,
private val featureFlagsService: FeatureFlagService,
private val isPinnedMessagesFeatureEnabled: IsPinnedMessagesFeatureEnabled,
private val room: MatrixRoom,
) : Presenter<ActionListState> {
) : ActionListPresenter {
@AssistedFactory
interface Factory {
fun create(postProcessor: TimelineItemActionPostProcessor): ActionListPresenter
@ContributesBinding(RoomScope::class)
interface Factory: ActionListPresenter.Factory {
override fun create(postProcessor: TimelineItemActionPostProcessor): DefaultActionListPresenter
}
@Composable
@@ -73,7 +82,7 @@ class ActionListPresenter @AssistedInject constructor(
}
val isDeveloperModeEnabled by appPreferencesStore.isDeveloperModeEnabledFlow().collectAsState(initial = false)
val isPinnedEventsEnabled by featureFlagsService.isFeatureEnabledFlow(FeatureFlags.PinnedEvents).collectAsState(initial = false)
val isPinnedEventsEnabled = isPinnedMessagesFeatureEnabled()
val pinnedEventIds by remember {
room.roomInfoFlow.map { it.pinnedEventIds }
}.collectAsState(initial = persistentListOf())

View File

@@ -16,7 +16,6 @@
package io.element.android.features.messages.impl.pinned.list
import io.element.android.features.messages.impl.MessagesEvents
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
import io.element.android.features.messages.impl.timeline.model.TimelineItem

View File

@@ -83,7 +83,6 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
TimelineRoomInfo(
isDm = room.isDm,
name = room.displayName,
isMainTimeline = false,
// We don't need to compute those values
userHasPermissionToSendMessage = false,
userHasPermissionToSendReaction = false,

View File

@@ -222,7 +222,6 @@ class TimelinePresenter @AssistedInject constructor(
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
userHasPermissionToSendReaction = userHasPermissionToSendReaction,
isCallOngoing = roomInfo?.hasRoomCall.orFalse(),
isMainTimeline = true
)
}
}

View File

@@ -75,5 +75,4 @@ data class TimelineRoomInfo(
val userHasPermissionToSendMessage: Boolean,
val userHasPermissionToSendReaction: Boolean,
val isCallOngoing: Boolean,
val isMainTimeline: Boolean,
)

View File

@@ -242,12 +242,10 @@ internal fun aTimelineRoomInfo(
name: String = "Room name",
isDm: Boolean = false,
userHasPermissionToSendMessage: Boolean = true,
isMainTimeline: Boolean = true,
) = TimelineRoomInfo(
isDm = isDm,
name = name,
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
userHasPermissionToSendReaction = true,
isCallOngoing = false,
isMainTimeline = isMainTimeline,
)

View File

@@ -24,7 +24,10 @@ import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
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.anActionListState
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.draft.FakeComposerDraftService
import io.element.android.features.messages.impl.fixtures.aMessageEvent
import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactory
@@ -60,6 +63,7 @@ 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
@@ -1052,11 +1056,6 @@ class MessagesPresenterTest {
}
}
val featureFlagService = FakeFeatureFlagService()
val actionListPresenter = ActionListPresenter(
appPreferencesStore = appPreferencesStore,
featureFlagsService = featureFlagService,
room = matrixRoom,
)
val typingNotificationPresenter = TypingNotificationPresenter(
room = matrixRoom,
sessionPreferencesStore = sessionPreferencesStore,
@@ -1072,7 +1071,7 @@ class MessagesPresenterTest {
voiceMessageComposerPresenter = voiceMessageComposerPresenter,
timelinePresenterFactory = timelinePresenterFactory,
typingNotificationPresenter = typingNotificationPresenter,
actionListPresenter = actionListPresenter,
actionListPresenterFactory = FakeActionListPresenter.Factory,
customReactionPresenter = customReactionPresenter,
reactionSummaryPresenter = reactionSummaryPresenter,
readReceiptBottomSheetPresenter = readReceiptBottomSheetPresenter,

View File

@@ -22,6 +22,7 @@ import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.messages.impl.aUserEventPermissions
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.fixtures.aMessageEvent
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent
@@ -974,14 +975,10 @@ private fun createActionListPresenter(
room: MatrixRoom = FakeMatrixRoom(),
): ActionListPresenter {
val preferencesStore = InMemoryAppPreferencesStore(isDeveloperModeEnabled = isDeveloperModeEnabled)
val featureFlagsService = FakeFeatureFlagService(
initialState = mapOf(
FeatureFlags.PinnedEvents.key to isPinFeatureEnabled,
)
)
return ActionListPresenter(
return DefaultActionListPresenter(
postProcessor = TimelineItemActionPostProcessor.Default,
appPreferencesStore = preferencesStore,
featureFlagsService = featureFlagsService,
isPinnedMessagesFeatureEnabled = {isPinFeatureEnabled},
room = room
)
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl.actionlist
import androidx.compose.runtime.Composable
import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor
class FakeActionListPresenter : ActionListPresenter {
object Factory : ActionListPresenter.Factory {
override fun create(postProcessor: TimelineItemActionPostProcessor): ActionListPresenter {
return FakeActionListPresenter()
}
}
@Composable
override fun present(): ActionListState {
return anActionListState()
}
}

View File

@@ -17,9 +17,12 @@
package io.element.android.features.messages.impl.pinned.banner
import com.google.common.truth.Truth.assertThat
import io.element.android.features.messages.impl.pinned.PinnedEventsTimelineProvider
import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
import io.element.android.libraries.eventformatter.test.FakePinnedMessagesBannerFormatter
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.test.AN_EVENT_ID
@@ -31,8 +34,12 @@ import io.element.android.libraries.matrix.test.timeline.aMessageContent
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
import io.element.android.tests.testutils.test
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -65,7 +72,7 @@ class PinnedMessagesBannerPresenterTest {
}
val presenter = createPinnedMessagesBannerPresenter(room = room)
presenter.test {
skipItems(1)
skipItems(2)
val loadingState = awaitItem()
assertThat(loadingState).isEqualTo(PinnedMessagesBannerState.Loading(1))
assertThat(loadingState.pinnedMessagesCount()).isEqualTo(1)
@@ -96,7 +103,7 @@ class PinnedMessagesBannerPresenterTest {
}
val presenter = createPinnedMessagesBannerPresenter(room = room)
presenter.test {
skipItems(2)
skipItems(3)
val loadedState = awaitItem() as PinnedMessagesBannerState.Loaded
assertThat(loadedState.currentPinnedMessageIndex).isEqualTo(0)
assertThat(loadedState.loadedPinnedMessagesCount).isEqualTo(1)
@@ -135,7 +142,7 @@ class PinnedMessagesBannerPresenterTest {
}
val presenter = createPinnedMessagesBannerPresenter(room = room)
presenter.test {
skipItems(2)
skipItems(3)
awaitItem().also { loadedState ->
loadedState as PinnedMessagesBannerState.Loaded
assertThat(loadedState.currentPinnedMessageIndex).isEqualTo(1)
@@ -170,7 +177,7 @@ class PinnedMessagesBannerPresenterTest {
}
val presenter = createPinnedMessagesBannerPresenter(room = room)
presenter.test {
skipItems(1)
skipItems(2)
awaitItem().also { loadingState ->
assertThat(loadingState).isEqualTo(PinnedMessagesBannerState.Loading(1))
assertThat(loadingState.pinnedMessagesCount()).isEqualTo(1)
@@ -193,11 +200,19 @@ class PinnedMessagesBannerPresenterTest {
networkMonitor: NetworkMonitor = FakeNetworkMonitor(),
isFeatureEnabled: Boolean = true,
): PinnedMessagesBannerPresenter {
val timelineProvider = PinnedEventsTimelineProvider(
room = room,
networkMonitor = networkMonitor,
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.PinnedEvents.key to isFeatureEnabled)
)
)
timelineProvider.launchIn(backgroundScope)
return PinnedMessagesBannerPresenter(
room = room,
itemFactory = itemFactory,
isFeatureEnabled = { isFeatureEnabled },
networkMonitor = networkMonitor,
pinnedEventsTimelineProvider = timelineProvider,
)
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl.pinned.list
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
class FakePinnedMessagesListNavigator: PinnedMessagesListNavigator {
var onViewInTimelineClickLambda: ((EventId) -> Unit)? = null
override fun onViewInTimelineClick(eventId: EventId) {
onViewInTimelineClickLambda?.invoke(eventId)
}
var onShowEventDebugInfoClickLambda: ((EventId?, TimelineItemDebugInfo) -> Unit)? = null
override fun onShowEventDebugInfoClick(eventId: EventId?, debugInfo: TimelineItemDebugInfo) {
onShowEventDebugInfoClickLambda?.invoke(eventId, debugInfo)
}
var onForwardEventClickLambda: ((EventId) -> Unit)? = null
override fun onForwardEventClick(eventId: EventId) {
onForwardEventClickLambda?.invoke(eventId)
}
}

View File

@@ -0,0 +1,174 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl.pinned.list
import com.google.common.truth.Truth.assertThat
import io.element.android.features.messages.impl.actionlist.FakeActionListPresenter
import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactory
import io.element.android.features.messages.impl.pinned.PinnedEventsTimelineProvider
import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
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.room.MatrixRoom
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.test.AN_EVENT_ID
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.timeline.FakeTimeline
import io.element.android.libraries.matrix.test.timeline.aMessageContent
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
import io.element.android.tests.testutils.test
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
class PinnedMessagesListPresenterTest {
@Test
fun `present - initial state feature disabled`() = runTest {
val room = FakeMatrixRoom(
canRedactOwnResult = { Result.success(true) },
canRedactOtherResult = { Result.success(true) },
canUserPinUnpinResult = { Result.success(true) },
)
val presenter = createPinnedMessagesListPresenter(room = room, isFeatureEnabled = false)
presenter.test {
val initialState = awaitItem()
assertThat(initialState).isEqualTo(PinnedMessagesListState.Loading)
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - initial state feature enabled`() = runTest {
val room = FakeMatrixRoom(
canRedactOwnResult = { Result.success(true) },
canRedactOtherResult = { Result.success(true) },
canUserPinUnpinResult = { Result.success(true) },
).apply {
givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID)))
}
val presenter = createPinnedMessagesListPresenter(room = room, isFeatureEnabled = true)
presenter.test {
val initialState = awaitItem()
assertThat(initialState).isEqualTo(PinnedMessagesListState.Loading)
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - timeline failure state`() = runTest {
val room = FakeMatrixRoom(
pinnedEventsTimelineResult = { Result.failure(RuntimeException()) },
canRedactOwnResult = { Result.success(true) },
canRedactOtherResult = { Result.success(true) },
canUserPinUnpinResult = { Result.success(true) },
).apply {
givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID)))
}
val presenter = createPinnedMessagesListPresenter(room = room, isFeatureEnabled = true)
presenter.test {
skipItems(3)
val failureState = awaitItem()
assertThat(failureState).isEqualTo(PinnedMessagesListState.Failed)
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - empty state`() = runTest {
val room = FakeMatrixRoom(
pinnedEventsTimelineResult = { Result.success(FakeTimeline()) },
canRedactOwnResult = { Result.success(true) },
canRedactOtherResult = { Result.success(true) },
canUserPinUnpinResult = { Result.success(true) },
).apply {
givenRoomInfo(aRoomInfo(pinnedEventIds = listOf()))
}
val presenter = createPinnedMessagesListPresenter(room = room, isFeatureEnabled = true)
presenter.test {
skipItems(3)
val emptyState = awaitItem()
assertThat(emptyState).isEqualTo(PinnedMessagesListState.Empty)
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - filled state`() = runTest {
val messageContent = aMessageContent("A message")
val pinnedEventsTimeline = FakeTimeline(
timelineItems = flowOf(
listOf(
MatrixTimelineItem.Event(
uniqueId = "FAKE_UNIQUE_ID",
event = anEventTimelineItem(
eventId = AN_EVENT_ID,
content = messageContent,
),
)
)
)
)
val room = FakeMatrixRoom(
pinnedEventsTimelineResult = { Result.success(pinnedEventsTimeline) },
canRedactOwnResult = { Result.success(true) },
canRedactOtherResult = { Result.success(true) },
canUserPinUnpinResult = { Result.success(true) },
).apply {
givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID)))
}
val presenter = createPinnedMessagesListPresenter(room = room, isFeatureEnabled = true)
presenter.test {
skipItems(3)
val filledState = awaitItem() as PinnedMessagesListState.Filled
assertThat(filledState.timelineItems).hasSize(1)
assertThat(filledState.loadedPinnedMessagesCount).isEqualTo(1)
assertThat(filledState.userEventPermissions.canRedactOwn).isTrue()
assertThat(filledState.userEventPermissions.canRedactOther).isTrue()
assertThat(filledState.userEventPermissions.canPinUnpin).isTrue()
cancelAndIgnoreRemainingEvents()
}
}
private fun TestScope.createPinnedMessagesListPresenter(
navigator: PinnedMessagesListNavigator = FakePinnedMessagesListNavigator(),
room: MatrixRoom = FakeMatrixRoom(),
networkMonitor: NetworkMonitor = FakeNetworkMonitor(),
isFeatureEnabled: Boolean = true,
): PinnedMessagesListPresenter {
val timelineProvider = PinnedEventsTimelineProvider(
room = room,
networkMonitor = networkMonitor,
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.PinnedEvents.key to isFeatureEnabled)
)
)
timelineProvider.launchIn(backgroundScope)
return PinnedMessagesListPresenter(
navigator = navigator,
room = room,
timelineItemsFactory = aTimelineItemsFactory(),
timelineProvider = timelineProvider,
snackbarDispatcher = SnackbarDispatcher(),
actionListPresenterFactory = FakeActionListPresenter.Factory,
)
}
}