RoomList: refactor and fix tests

This commit is contained in:
ganfra 2024-03-12 15:38:33 +01:00
parent 6ac66d08fd
commit 3cb189f475
20 changed files with 252 additions and 107 deletions

View file

@ -0,0 +1,49 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.roomlist.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
import kotlinx.collections.immutable.ImmutableList
open class RoomListContentStateProvider : PreviewParameterProvider<RoomListContentState> {
override val values: Sequence<RoomListContentState>
get() = sequenceOf(
aRoomsContentState(),
aSkeletonContentState(),
anEmptyContentState(),
aMigrationContentState(),
)
}
internal fun aRoomsContentState(
invitesState: InvitesState = InvitesState.NoInvites,
securityBannerState: SecurityBannerState = SecurityBannerState.None,
summaries: ImmutableList<RoomListRoomSummary> = aRoomListRoomSummaryList(),
) = RoomListContentState.Rooms(
invitesState = invitesState,
securityBannerState = securityBannerState,
summaries = summaries,
)
internal fun aMigrationContentState() = RoomListContentState.Migration
internal fun aSkeletonContentState() = RoomListContentState.Skeleton(16)
internal fun anEmptyContentState(
invitesState: InvitesState = InvitesState.NoInvites,
) = RoomListContentState.Empty(invitesState)

View file

@ -40,7 +40,7 @@ import io.element.android.features.preferences.api.store.SessionPreferencesStore
import io.element.android.features.roomlist.impl.datasource.InviteStateDataSource import io.element.android.features.roomlist.impl.datasource.InviteStateDataSource
import io.element.android.features.roomlist.impl.datasource.RoomListDataSource import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
import io.element.android.features.roomlist.impl.migration.MigrationScreenPresenter import io.element.android.features.roomlist.impl.migration.MigrationScreenState
import io.element.android.features.roomlist.impl.search.RoomListSearchEvents import io.element.android.features.roomlist.impl.search.RoomListSearchEvents
import io.element.android.features.roomlist.impl.search.RoomListSearchState import io.element.android.features.roomlist.impl.search.RoomListSearchState
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
@ -89,7 +89,7 @@ class RoomListPresenter @Inject constructor(
private val indicatorService: IndicatorService, private val indicatorService: IndicatorService,
private val filtersPresenter: Presenter<RoomListFiltersState>, private val filtersPresenter: Presenter<RoomListFiltersState>,
private val searchPresenter: Presenter<RoomListSearchState>, private val searchPresenter: Presenter<RoomListSearchState>,
private val migrationScreenPresenter: MigrationScreenPresenter, private val migrationScreenPresenter: Presenter<MigrationScreenState>,
private val sessionPreferencesStore: SessionPreferencesStore, private val sessionPreferencesStore: SessionPreferencesStore,
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
) : Presenter<RoomListState> { ) : Presenter<RoomListState> {
@ -196,23 +196,23 @@ class RoomListPresenter @Inject constructor(
} }
val loadingState by roomListDataSource.loadingState.collectAsState() val loadingState by roomListDataSource.loadingState.collectAsState()
val showMigration = migrationScreenPresenter.present().isMigrating val showMigration = migrationScreenPresenter.present().isMigrating
val showSkeleton by remember {
derivedStateOf {
loadingState == RoomList.LoadingState.NotLoaded || roomSummaries is AsyncData.Loading
}
}
val showEmpty by remember { val showEmpty by remember {
derivedStateOf { derivedStateOf {
(loadingState as? RoomList.LoadingState.Loaded)?.numberOfRooms == 0 (loadingState as? RoomList.LoadingState.Loaded)?.numberOfRooms == 0
} }
} }
val showSkeleton by remember {
derivedStateOf {
loadingState == RoomList.LoadingState.NotLoaded || roomSummaries is AsyncData.Loading
}
}
return when { return when {
showMigration -> RoomListContentState.Migration showMigration -> RoomListContentState.Migration
showSkeleton -> RoomListContentState.Skeleton(count = 16)
showEmpty -> { showEmpty -> {
val invitesState = inviteStateDataSource.inviteState() val invitesState = inviteStateDataSource.inviteState()
RoomListContentState.Empty(invitesState) RoomListContentState.Empty(invitesState)
} }
showSkeleton -> RoomListContentState.Skeleton(count = 16)
else -> { else -> {
val invitesState = inviteStateDataSource.inviteState() val invitesState = inviteStateDataSource.inviteState()
val securityBannerState by securityBannerState(securityBannerDismissed) val securityBannerState by securityBannerState(securityBannerDismissed)

View file

@ -67,6 +67,7 @@ enum class SecurityBannerState {
RecoveryKeyConfirmation, RecoveryKeyConfirmation,
} }
@Immutable
sealed interface RoomListContentState { sealed interface RoomListContentState {
data object Migration : RoomListContentState data object Migration : RoomListContentState
data class Skeleton(val count: Int) : RoomListContentState data class Skeleton(val count: Int) : RoomListContentState

View file

@ -25,7 +25,6 @@ import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
import io.element.android.features.roomlist.impl.model.aRoomListRoomSummary import io.element.android.features.roomlist.impl.model.aRoomListRoomSummary
import io.element.android.features.roomlist.impl.search.RoomListSearchState import io.element.android.features.roomlist.impl.search.RoomListSearchState
import io.element.android.features.roomlist.impl.search.aRoomListSearchState import io.element.android.features.roomlist.impl.search.aRoomListSearchState
import io.element.android.libraries.architecture.AsyncData
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.SnackbarMessage import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
@ -35,7 +34,6 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
open class RoomListStateProvider : PreviewParameterProvider<RoomListState> { open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
override val values: Sequence<RoomListState> override val values: Sequence<RoomListState>
@ -43,15 +41,15 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
aRoomListState(), aRoomListState(),
aRoomListState(snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete)), aRoomListState(snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete)),
aRoomListState(hasNetworkConnection = false), aRoomListState(hasNetworkConnection = false),
aRoomListState(invitesState = InvitesState.SeenInvites), aRoomListState(contentState = aRoomsContentState(invitesState = InvitesState.SeenInvites)),
aRoomListState(invitesState = InvitesState.NewInvites), aRoomListState(contentState = aRoomsContentState(invitesState = InvitesState.NewInvites)),
aRoomListState(contextMenu = aContextMenuShown(roomName = "A nice room name")), aRoomListState(contextMenu = aContextMenuShown(roomName = "A nice room name")),
aRoomListState(contextMenu = aContextMenuShown(isFavorite = true)), aRoomListState(contextMenu = aContextMenuShown(isFavorite = true)),
aRoomListState(securityBannerState = SecurityBannerState.SessionVerification), aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SessionVerification)),
aRoomListState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation), aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation)),
aRoomListState(roomList = AsyncData.Success(persistentListOf())), aRoomListState(contentState = anEmptyContentState()),
//aRoomListState(roomList = AsyncData.Loading(prevData = RoomListRoomSummaryFactory.createFakeList())), aRoomListState(contentState = aSkeletonContentState()),
aRoomListState(matrixUser = null, displayMigrationStatus = true), aRoomListState(matrixUser = null, contentState = aMigrationContentState()),
aRoomListState(searchState = aRoomListSearchState(isSearchActive = true, query = "Test")), aRoomListState(searchState = aRoomListSearchState(isSearchActive = true, query = "Test")),
aRoomListState(filtersState = aRoomListFiltersState(isFeatureEnabled = true)), aRoomListState(filtersState = aRoomListFiltersState(isFeatureEnabled = true)),
) )
@ -60,16 +58,13 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
internal fun aRoomListState( internal fun aRoomListState(
matrixUser: MatrixUser? = MatrixUser(userId = UserId("@id:domain"), displayName = "User#1"), matrixUser: MatrixUser? = MatrixUser(userId = UserId("@id:domain"), displayName = "User#1"),
showAvatarIndicator: Boolean = false, showAvatarIndicator: Boolean = false,
roomList: AsyncData<ImmutableList<RoomListRoomSummary>> = AsyncData.Success(aRoomListRoomSummaryList()),
hasNetworkConnection: Boolean = true, hasNetworkConnection: Boolean = true,
snackbarMessage: SnackbarMessage? = null, snackbarMessage: SnackbarMessage? = null,
securityBannerState: SecurityBannerState = SecurityBannerState.None,
invitesState: InvitesState = InvitesState.NoInvites,
contextMenu: RoomListState.ContextMenu = RoomListState.ContextMenu.Hidden, contextMenu: RoomListState.ContextMenu = RoomListState.ContextMenu.Hidden,
leaveRoomState: LeaveRoomState = aLeaveRoomState(), leaveRoomState: LeaveRoomState = aLeaveRoomState(),
searchState: RoomListSearchState = aRoomListSearchState(), searchState: RoomListSearchState = aRoomListSearchState(),
filtersState: RoomListFiltersState = aRoomListFiltersState(isFeatureEnabled = false), filtersState: RoomListFiltersState = aRoomListFiltersState(isFeatureEnabled = false),
displayMigrationStatus: Boolean = false, contentState: RoomListContentState = aRoomsContentState(),
eventSink: (RoomListEvents) -> Unit = {} eventSink: (RoomListEvents) -> Unit = {}
) = RoomListState( ) = RoomListState(
matrixUser = matrixUser, matrixUser = matrixUser,
@ -80,11 +75,7 @@ internal fun aRoomListState(
leaveRoomState = leaveRoomState, leaveRoomState = leaveRoomState,
filtersState = filtersState, filtersState = filtersState,
searchState = searchState, searchState = searchState,
contentState = RoomListContentState.Rooms( contentState = contentState,
invitesState = invitesState,
securityBannerState = securityBannerState,
summaries = roomList.dataOrNull().orEmpty().toPersistentList(),
),
eventSink = eventSink, eventSink = eventSink,
) )

View file

@ -17,7 +17,6 @@
package io.element.android.features.roomlist.impl.components package io.element.android.features.roomlist.impl.components
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -40,6 +39,7 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.Velocity import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
@ -48,19 +48,24 @@ import io.element.android.features.roomlist.impl.InvitesEntryPointView
import io.element.android.features.roomlist.impl.InvitesState import io.element.android.features.roomlist.impl.InvitesState
import io.element.android.features.roomlist.impl.R import io.element.android.features.roomlist.impl.R
import io.element.android.features.roomlist.impl.RoomListContentState import io.element.android.features.roomlist.impl.RoomListContentState
import io.element.android.features.roomlist.impl.RoomListContentStateProvider
import io.element.android.features.roomlist.impl.RoomListEvents import io.element.android.features.roomlist.impl.RoomListEvents
import io.element.android.features.roomlist.impl.SecurityBannerState import io.element.android.features.roomlist.impl.SecurityBannerState
import io.element.android.features.roomlist.impl.contentType import io.element.android.features.roomlist.impl.contentType
import io.element.android.features.roomlist.impl.filters.RoomListFilter import io.element.android.features.roomlist.impl.filters.RoomListFilter
import io.element.android.features.roomlist.impl.filters.RoomListFiltersEmptyStateResources import io.element.android.features.roomlist.impl.filters.RoomListFiltersEmptyStateResources
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState
import io.element.android.features.roomlist.impl.migration.MigrationScreenView import io.element.android.features.roomlist.impl.migration.MigrationScreenView
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
import io.element.android.libraries.designsystem.theme.components.IconSource import io.element.android.libraries.designsystem.theme.components.IconSource
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList
@Composable @Composable
fun RoomListContentView( fun RoomListContentView(
@ -214,7 +219,6 @@ private fun RoomsViewList(
modifier = modifier.nestedScroll(nestedScrollConnection), modifier = modifier.nestedScroll(nestedScrollConnection),
// FAB height is 56dp, bottom padding is 16dp, we add 8dp as extra margin -> 56+16+8 = 80 // FAB height is 56dp, bottom padding is 16dp, we add 8dp as extra margin -> 56+16+8 = 80
contentPadding = PaddingValues(bottom = 80.dp) contentPadding = PaddingValues(bottom = 80.dp)
) { ) {
when (state.securityBannerState) { when (state.securityBannerState) {
SecurityBannerState.SessionVerification -> { SecurityBannerState.SessionVerification -> {
@ -261,7 +265,7 @@ private fun RoomsViewList(
@Composable @Composable
private fun EmptyViewForFilterStates( private fun EmptyViewForFilterStates(
selectedFilters: List<RoomListFilter>, selectedFilters: ImmutableList<RoomListFilter>,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val emptyStateResources = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters) ?: return val emptyStateResources = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters) ?: return
@ -301,3 +305,18 @@ private fun EmptyScaffold(
action?.invoke(this) action?.invoke(this)
} }
} }
@PreviewsDayNight
@Composable
internal fun RoomListContentViewPreview(@PreviewParameter(RoomListContentStateProvider::class) state: RoomListContentState) = ElementPreview {
RoomListContentView(
contentState = state,
filtersState = aRoomListFiltersState(),
eventSink = {},
onVerifyClicked = { },
onConfirmRecoveryKeyClicked = { },
onRoomClicked = {},
onRoomLongClicked = {},
onCreateRoomClicked = { },
onInvitesClicked = { })
}

View file

@ -29,12 +29,9 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext

View file

@ -24,8 +24,6 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import javax.inject.Inject import javax.inject.Inject
class RoomListRoomSummaryFactory @Inject constructor( class RoomListRoomSummaryFactory @Inject constructor(
@ -52,7 +50,6 @@ class RoomListRoomSummaryFactory @Inject constructor(
isFavorite = false, isFavorite = false,
) )
} }
} }
fun create(roomSummary: RoomSummary.Filled): RoomListRoomSummary { fun create(roomSummary: RoomSummary.Filled): RoomListRoomSummary {

View file

@ -21,6 +21,8 @@ import dagger.Binds
import dagger.Module import dagger.Module
import io.element.android.features.roomlist.impl.filters.RoomListFiltersPresenter import io.element.android.features.roomlist.impl.filters.RoomListFiltersPresenter
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
import io.element.android.features.roomlist.impl.migration.MigrationScreenPresenter
import io.element.android.features.roomlist.impl.migration.MigrationScreenState
import io.element.android.features.roomlist.impl.search.RoomListSearchPresenter import io.element.android.features.roomlist.impl.search.RoomListSearchPresenter
import io.element.android.features.roomlist.impl.search.RoomListSearchState import io.element.android.features.roomlist.impl.search.RoomListSearchState
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
@ -34,4 +36,7 @@ interface RoomListModule {
@Binds @Binds
fun bindFiltersPresenter(presenter: RoomListFiltersPresenter): Presenter<RoomListFiltersState> fun bindFiltersPresenter(presenter: RoomListFiltersPresenter): Presenter<RoomListFiltersState>
@Binds
fun bindMigrationScreenPresenter(presenter: MigrationScreenPresenter): Presenter<MigrationScreenState>
} }

View file

@ -28,7 +28,6 @@ data class RoomListFiltersEmptyStateResources(
@StringRes val title: Int, @StringRes val title: Int,
@StringRes val subtitle: Int, @StringRes val subtitle: Int,
) { ) {
companion object { companion object {
/** /**
* Create a [RoomListFiltersEmptyStateResources] from a list of selected filters. * Create a [RoomListFiltersEmptyStateResources] from a list of selected filters.

View file

@ -34,14 +34,11 @@ class RoomListFiltersPresenter @Inject constructor(
private val featureFlagService: FeatureFlagService, private val featureFlagService: FeatureFlagService,
private val filterSelectionStrategy: FilterSelectionStrategy, private val filterSelectionStrategy: FilterSelectionStrategy,
) : Presenter<RoomListFiltersState> { ) : Presenter<RoomListFiltersState> {
@Composable @Composable
override fun present(): RoomListFiltersState { override fun present(): RoomListFiltersState {
val isFeatureEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.RoomListFilters).collectAsState(false) val isFeatureEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.RoomListFilters).collectAsState(false)
val filters by filterSelectionStrategy.filterSelectionStates.collectAsState() val filters by filterSelectionStrategy.filterSelectionStates.collectAsState()
fun handleEvents(event: RoomListFiltersEvents) { fun handleEvents(event: RoomListFiltersEvents) {
when (event) { when (event) {
RoomListFiltersEvents.ClearSelectedFilters -> { RoomListFiltersEvents.ClearSelectedFilters -> {
@ -75,7 +72,6 @@ class RoomListFiltersPresenter @Inject constructor(
roomListService.allRooms.updateFilter(allRoomsFilter) roomListService.allRooms.updateFilter(allRoomsFilter)
} }
return RoomListFiltersState( return RoomListFiltersState(
filterSelectionStates = filters.toPersistentList(), filterSelectionStates = filters.toPersistentList(),
isFeatureEnabled = isFeatureEnabled, isFeatureEnabled = isFeatureEnabled,

View file

@ -18,6 +18,7 @@ package io.element.android.features.roomlist.impl.filters
import io.element.android.features.roomlist.impl.filters.selection.FilterSelectionState import io.element.android.features.roomlist.impl.filters.selection.FilterSelectionState
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList
data class RoomListFiltersState( data class RoomListFiltersState(
val filterSelectionStates: ImmutableList<FilterSelectionState>, val filterSelectionStates: ImmutableList<FilterSelectionState>,
@ -26,7 +27,10 @@ data class RoomListFiltersState(
) { ) {
val hasAnyFilterSelected = filterSelectionStates.any { it.isSelected } val hasAnyFilterSelected = filterSelectionStates.any { it.isSelected }
fun selectedFilters(): List<RoomListFilter> { fun selectedFilters(): ImmutableList<RoomListFilter> {
return filterSelectionStates.filter { it.isSelected }.map { it.filter } return filterSelectionStates
.filter { it.isSelected }
.map { it.filter }
.toPersistentList()
} }
} }

View file

@ -53,7 +53,6 @@ fun RoomListFiltersView(
state: RoomListFiltersState, state: RoomListFiltersState,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
fun onClearFiltersClicked() { fun onClearFiltersClicked() {
state.eventSink(RoomListFiltersEvents.ClearSelectedFilters) state.eventSink(RoomListFiltersEvents.ClearSelectedFilters)
} }

View file

@ -24,7 +24,6 @@ import javax.inject.Inject
@ContributesBinding(SessionScope::class) @ContributesBinding(SessionScope::class)
class DefaultFilterSelectionStrategy @Inject constructor() : FilterSelectionStrategy { class DefaultFilterSelectionStrategy @Inject constructor() : FilterSelectionStrategy {
private val selectedFilters = LinkedHashSet<RoomListFilter>() private val selectedFilters = LinkedHashSet<RoomListFilter>()
override val filterSelectionStates = MutableStateFlow(buildFilters()) override val filterSelectionStates = MutableStateFlow(buildFilters())

View file

@ -20,7 +20,6 @@ import io.element.android.features.roomlist.impl.filters.RoomListFilter
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
interface FilterSelectionStrategy { interface FilterSelectionStrategy {
val filterSelectionStates: StateFlow<Set<FilterSelectionState>> val filterSelectionStates: StateFlow<Set<FilterSelectionState>>
fun select(filter: RoomListFilter) fun select(filter: RoomListFilter)

View file

@ -33,8 +33,7 @@ import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState
import io.element.android.features.roomlist.impl.migration.InMemoryMigrationScreenStore import io.element.android.features.roomlist.impl.migration.MigrationScreenState
import io.element.android.features.roomlist.impl.migration.MigrationScreenPresenter
import io.element.android.features.roomlist.impl.model.createRoomListRoomSummary import io.element.android.features.roomlist.impl.model.createRoomListRoomSummary
import io.element.android.features.roomlist.impl.search.RoomListSearchEvents import io.element.android.features.roomlist.impl.search.RoomListSearchEvents
import io.element.android.features.roomlist.impl.search.RoomListSearchState import io.element.android.features.roomlist.impl.search.RoomListSearchState
@ -54,13 +53,12 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.encryption.BackupState import io.element.android.libraries.matrix.api.encryption.BackupState
import io.element.android.libraries.matrix.api.encryption.RecoveryState import io.element.android.libraries.matrix.api.encryption.RecoveryState
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.roomlist.RoomList
import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.timeline.ReceiptType
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_EXCEPTION import io.element.android.libraries.matrix.test.AN_EXCEPTION
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_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_NAME import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
@ -75,6 +73,7 @@ import io.element.android.libraries.matrix.test.verification.FakeSessionVerifica
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
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.EventsRecorder
import io.element.android.tests.testutils.MutablePresenter
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.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
@ -127,7 +126,6 @@ class RoomListPresenterTests {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.showAvatarIndicator).isTrue() assertThat(initialState.showAvatarIndicator).isTrue()
sessionVerificationService.givenCanVerifySession(false) sessionVerificationService.givenCanVerifySession(false)
@ -169,11 +167,9 @@ class RoomListPresenterTests {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = consumeItemsUntilPredicate(timeout = 3.seconds) { state -> state.roomList.dataOrNull()?.size == 16 }.last() val initialState = consumeItemsUntilPredicate(timeout = 3.seconds) { state -> state.contentState is RoomListContentState.Skeleton }.last()
// Room list is loaded with 16 placeholders assertThat(initialState.contentState).isInstanceOf(RoomListContentState.Skeleton::class.java)
val initialItems = initialState.roomList.dataOrNull().orEmpty() roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
assertThat(initialItems.size).isEqualTo(16)
assertThat(initialItems.all { it.isPlaceholder }).isTrue()
roomListService.postAllRooms( roomListService.postAllRooms(
listOf( listOf(
aRoomSummaryFilled( aRoomSummaryFilled(
@ -182,10 +178,10 @@ class RoomListPresenterTests {
) )
) )
) )
val withRoomState = consumeItemsUntilPredicate { state -> state.roomList.dataOrNull()?.size == 1 }.last() val withRoomsState =
val withRoomStateItems = withRoomState.roomList.dataOrNull().orEmpty() consumeItemsUntilPredicate { state -> state.contentState is RoomListContentState.Rooms && state.contentAsRooms().summaries.isNotEmpty() }.last()
assertThat(withRoomStateItems.size).isEqualTo(1) assertThat(withRoomsState.contentAsRooms().summaries).hasSize(1)
assertThat(withRoomStateItems.first()).isEqualTo( assertThat(withRoomsState.contentAsRooms().summaries.first()).isEqualTo(
createRoomListRoomSummary( createRoomListRoomSummary(
numberOfUnreadMentions = 1, numberOfUnreadMentions = 1,
numberOfUnreadMessages = 2, numberOfUnreadMessages = 2,
@ -241,23 +237,28 @@ class RoomListPresenterTests {
@Test @Test
fun `present - handle RecoveryKeyConfirmation last session`() = runTest { fun `present - handle RecoveryKeyConfirmation last session`() = runTest {
val scope = CoroutineScope(context = coroutineContext + SupervisorJob()) val scope = CoroutineScope(context = coroutineContext + SupervisorJob())
val roomListService = FakeRoomListService().apply {
postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
}
val presenter = createRoomListPresenter( val presenter = createRoomListPresenter(
coroutineScope = scope, coroutineScope = scope,
client = FakeMatrixClient( client = FakeMatrixClient(
encryptionService = FakeEncryptionService().apply { encryptionService = FakeEncryptionService().apply {
emitIsLastDevice(true) emitIsLastDevice(true)
} },
roomListService = roomListService
), ),
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) val eventSink = consumeItemsUntilPredicate {
val eventSink = awaitItem().eventSink it.contentState is RoomListContentState.Rooms
}.last().eventSink
// For the last session, the state is not SessionVerification, but RecoveryKeyConfirmation // For the last session, the state is not SessionVerification, but RecoveryKeyConfirmation
assertThat(awaitItem().securityBannerState).isEqualTo(SecurityBannerState.RecoveryKeyConfirmation) assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.RecoveryKeyConfirmation)
eventSink(RoomListEvents.DismissRequestVerificationPrompt) eventSink(RoomListEvents.DismissRequestVerificationPrompt)
assertThat(awaitItem().securityBannerState).isEqualTo(SecurityBannerState.None) assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None)
scope.cancel() scope.cancel()
} }
} }
@ -265,16 +266,22 @@ class RoomListPresenterTests {
@Test @Test
fun `present - handle DismissRequestVerificationPrompt`() = runTest { fun `present - handle DismissRequestVerificationPrompt`() = runTest {
val scope = CoroutineScope(context = coroutineContext + SupervisorJob()) val scope = CoroutineScope(context = coroutineContext + SupervisorJob())
val roomListService = FakeRoomListService().apply {
postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
}
val presenter = createRoomListPresenter( val presenter = createRoomListPresenter(
client = FakeMatrixClient(roomListService = roomListService),
coroutineScope = scope, coroutineScope = scope,
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val eventSink = awaitItem().eventSink val eventSink = consumeItemsUntilPredicate {
assertThat(awaitItem().securityBannerState).isEqualTo(SecurityBannerState.SessionVerification) it.contentState is RoomListContentState.Rooms
}.last().eventSink
assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.SessionVerification)
eventSink(RoomListEvents.DismissRequestVerificationPrompt) eventSink(RoomListEvents.DismissRequestVerificationPrompt)
assertThat(awaitItem().securityBannerState).isEqualTo(SecurityBannerState.None) assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None)
scope.cancel() scope.cancel()
} }
} }
@ -282,7 +289,11 @@ class RoomListPresenterTests {
@Test @Test
fun `present - handle DismissRecoveryKeyPrompt`() = runTest { fun `present - handle DismissRecoveryKeyPrompt`() = runTest {
val encryptionService = FakeEncryptionService() val encryptionService = FakeEncryptionService()
val roomListService = FakeRoomListService().apply {
postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
}
val matrixClient = FakeMatrixClient( val matrixClient = FakeMatrixClient(
roomListService = roomListService,
encryptionService = encryptionService, encryptionService = encryptionService,
sessionVerificationService = FakeSessionVerificationService().apply { sessionVerificationService = FakeSessionVerificationService().apply {
givenCanVerifySession(false) givenCanVerifySession(false)
@ -297,15 +308,16 @@ class RoomListPresenterTests {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) val initialState = consumeItemsUntilPredicate {
val initialState = awaitItem() it.contentState is RoomListContentState.Rooms
assertThat(initialState.securityBannerState).isEqualTo(SecurityBannerState.None) }.last()
assertThat(initialState.contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None)
encryptionService.emitRecoveryState(RecoveryState.INCOMPLETE) encryptionService.emitRecoveryState(RecoveryState.INCOMPLETE)
val nextState = awaitItem() val nextState = awaitItem()
assertThat(nextState.securityBannerState).isEqualTo(SecurityBannerState.RecoveryKeyConfirmation) assertThat(nextState.contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.RecoveryKeyConfirmation)
nextState.eventSink(RoomListEvents.DismissRecoveryKeyPrompt) nextState.eventSink(RoomListEvents.DismissRecoveryKeyPrompt)
val finalState = awaitItem() val finalState = awaitItem()
assertThat(finalState.securityBannerState).isEqualTo(SecurityBannerState.None) assertThat(finalState.contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None)
scope.cancel() scope.cancel()
} }
} }
@ -314,22 +326,30 @@ class RoomListPresenterTests {
fun `present - sets invite state`() = runTest { fun `present - sets invite state`() = runTest {
val inviteStateFlow = MutableStateFlow(InvitesState.NoInvites) val inviteStateFlow = MutableStateFlow(InvitesState.NoInvites)
val inviteStateDataSource = FakeInviteDataSource(inviteStateFlow) val inviteStateDataSource = FakeInviteDataSource(inviteStateFlow)
val roomListService = FakeRoomListService()
val scope = CoroutineScope(coroutineContext + SupervisorJob()) val scope = CoroutineScope(coroutineContext + SupervisorJob())
val presenter = createRoomListPresenter(inviteStateDataSource = inviteStateDataSource, coroutineScope = scope) val presenter = createRoomListPresenter(
inviteStateDataSource = inviteStateDataSource,
coroutineScope = scope,
client = FakeMatrixClient(roomListService = roomListService),
)
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1) consumeItemsUntilPredicate {
assertThat(awaitItem().invitesState).isEqualTo(InvitesState.NoInvites) it.contentState is RoomListContentState.Rooms
}
assertThat(awaitItem().contentAsRooms().invitesState).isEqualTo(InvitesState.NoInvites)
inviteStateFlow.value = InvitesState.SeenInvites inviteStateFlow.value = InvitesState.SeenInvites
assertThat(awaitItem().invitesState).isEqualTo(InvitesState.SeenInvites) assertThat(awaitItem().contentAsRooms().invitesState).isEqualTo(InvitesState.SeenInvites)
inviteStateFlow.value = InvitesState.NewInvites inviteStateFlow.value = InvitesState.NewInvites
assertThat(awaitItem().invitesState).isEqualTo(InvitesState.NewInvites) assertThat(awaitItem().contentAsRooms().invitesState).isEqualTo(InvitesState.NewInvites)
inviteStateFlow.value = InvitesState.NoInvites inviteStateFlow.value = InvitesState.NoInvites
assertThat(awaitItem().invitesState).isEqualTo(InvitesState.NoInvites) assertThat(awaitItem().contentAsRooms().invitesState).isEqualTo(InvitesState.NoInvites)
scope.cancel() scope.cancel()
} }
} }
@ -477,6 +497,7 @@ class RoomListPresenterTests {
val userDefinedMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY val userDefinedMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY
val notificationSettingsService = FakeNotificationSettingsService() val notificationSettingsService = FakeNotificationSettingsService()
val roomListService = FakeRoomListService() val roomListService = FakeRoomListService()
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
roomListService.postAllRooms(listOf(aRoomSummaryFilled(notificationMode = userDefinedMode))) roomListService.postAllRooms(listOf(aRoomSummaryFilled(notificationMode = userDefinedMode)))
val matrixClient = FakeMatrixClient( val matrixClient = FakeMatrixClient(
roomListService = roomListService, roomListService = roomListService,
@ -488,12 +509,13 @@ class RoomListPresenterTests {
presenter.present() presenter.present()
}.test { }.test {
notificationSettingsService.setRoomNotificationMode(A_ROOM_ID, userDefinedMode) notificationSettingsService.setRoomNotificationMode(A_ROOM_ID, userDefinedMode)
val updatedState = consumeItemsUntilPredicate { state -> val updatedState = consumeItemsUntilPredicate { state ->
state.roomList.dataOrNull().orEmpty().any { it.id == A_ROOM_ID.value && it.userDefinedNotificationMode == userDefinedMode } (state.contentState as? RoomListContentState.Rooms)?.summaries.orEmpty().any { summary ->
summary.id == A_ROOM_ID.value && summary.userDefinedNotificationMode == userDefinedMode
}
}.last() }.last()
val room = updatedState.roomList.dataOrNull()?.find { it.id == A_ROOM_ID.value } val room = updatedState.contentAsRooms().summaries.find { it.id == A_ROOM_ID.value }
assertThat(room?.userDefinedNotificationMode).isEqualTo(userDefinedMode) assertThat(room?.userDefinedNotificationMode).isEqualTo(userDefinedMode)
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
scope.cancel() scope.cancel()
@ -526,30 +548,46 @@ class RoomListPresenterTests {
} }
} }
fun `present - change in migration presenter state modifies isMigrating`() = runTest { @Test
val client = FakeMatrixClient(sessionId = A_SESSION_ID) fun `present - change in migration presenter state modifies contentState`() = runTest {
val migrationStore = InMemoryMigrationScreenStore() val migrationScreenPresenter = MutablePresenter(MigrationScreenState(true))
val migrationScreenPresenter = MigrationScreenPresenter(client, migrationStore)
val scope = CoroutineScope(coroutineContext + SupervisorJob()) val scope = CoroutineScope(coroutineContext + SupervisorJob())
val presenter = createRoomListPresenter( val presenter = createRoomListPresenter(
client = client,
coroutineScope = scope, coroutineScope = scope,
migrationScreenPresenter = migrationScreenPresenter, migrationScreenPresenter = migrationScreenPresenter,
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
// The migration screen is shown if the migration screen has not been shown before // The migration screen is shown if the migration screen has not been shown before
assertThat(initialState.displayMigrationStatus).isTrue() assertThat(initialState.contentState).isInstanceOf(RoomListContentState.Migration::class.java)
skipItems(2)
// Set migration as done and set the room list service as running to trigger a refresh of the presenter value // Set migration as done and set the room list service as running to trigger a refresh of the presenter value
(client.roomListService as FakeRoomListService).postState(RoomListService.State.Running) migrationScreenPresenter.updateState(MigrationScreenState(false))
migrationStore.setMigrationScreenShown(A_SESSION_ID)
// The migration screen is not shown anymore // The migration screen is not shown anymore
assertThat(awaitItem().displayMigrationStatus).isFalse() assertThat(awaitItem().contentState).isInstanceOf(RoomListContentState.Skeleton::class.java)
scope.cancel()
}
}
@Test
fun `present - when room service returns no room, then contentState is Empty `() = runTest {
val scope = CoroutineScope(coroutineContext + SupervisorJob())
val roomListService = FakeRoomListService()
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(0))
val matrixClient = FakeMatrixClient(
roomListService = roomListService,
)
val presenter = createRoomListPresenter(
client = matrixClient,
coroutineScope = scope,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
assertThat(awaitItem().contentState).isInstanceOf(RoomListContentState.Empty::class.java)
scope.cancel() scope.cancel()
} }
} }
@ -609,10 +647,7 @@ class RoomListPresenterTests {
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(), sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(),
featureFlagService: FeatureFlagService = FakeFeatureFlagService(), featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
coroutineScope: CoroutineScope, coroutineScope: CoroutineScope,
migrationScreenPresenter: MigrationScreenPresenter = MigrationScreenPresenter( migrationScreenPresenter: Presenter<MigrationScreenState> = Presenter { MigrationScreenState(false) },
matrixClient = client,
migrationScreenStore = InMemoryMigrationScreenStore(),
),
analyticsService: AnalyticsService = FakeAnalyticsService(), analyticsService: AnalyticsService = FakeAnalyticsService(),
filtersPresenter: Presenter<RoomListFiltersState> = Presenter { aRoomListFiltersState() }, filtersPresenter: Presenter<RoomListFiltersState> = Presenter { aRoomListFiltersState() },
searchPresenter: Presenter<RoomListSearchState> = Presenter { aRoomListSearchState() }, searchPresenter: Presenter<RoomListSearchState> = Presenter { aRoomListSearchState() },

View file

@ -0,0 +1,19 @@
/*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.roomlist.impl
internal fun RoomListState.contentAsRooms() = contentState as RoomListContentState.Rooms

View file

@ -26,7 +26,6 @@ import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.test.performTouchInput
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.roomlist.impl.components.RoomListMenuAction import io.element.android.features.roomlist.impl.components.RoomListMenuAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalled
@ -35,7 +34,6 @@ import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.ensureCalledOnceWithParam import io.element.android.tests.testutils.ensureCalledOnceWithParam
import kotlinx.collections.immutable.persistentListOf
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule import org.junit.rules.TestRule
@ -50,7 +48,7 @@ class RoomListViewTest {
val eventsRecorder = EventsRecorder<RoomListEvents>() val eventsRecorder = EventsRecorder<RoomListEvents>()
rule.setRoomListView( rule.setRoomListView(
state = aRoomListState( state = aRoomListState(
securityBannerState = SecurityBannerState.SessionVerification, contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SessionVerification),
eventSink = eventsRecorder, eventSink = eventsRecorder,
) )
) )
@ -65,7 +63,7 @@ class RoomListViewTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setRoomListView( rule.setRoomListView(
state = aRoomListState( state = aRoomListState(
securityBannerState = SecurityBannerState.SessionVerification, contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SessionVerification),
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
onVerifyClicked = callback, onVerifyClicked = callback,
@ -79,7 +77,7 @@ class RoomListViewTest {
val eventsRecorder = EventsRecorder<RoomListEvents>() val eventsRecorder = EventsRecorder<RoomListEvents>()
rule.setRoomListView( rule.setRoomListView(
state = aRoomListState( state = aRoomListState(
securityBannerState = SecurityBannerState.RecoveryKeyConfirmation, contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation),
eventSink = eventsRecorder, eventSink = eventsRecorder,
) )
) )
@ -94,7 +92,7 @@ class RoomListViewTest {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setRoomListView( rule.setRoomListView(
state = aRoomListState( state = aRoomListState(
securityBannerState = SecurityBannerState.RecoveryKeyConfirmation, contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation),
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
onConfirmRecoveryKeyClicked = callback, onConfirmRecoveryKeyClicked = callback,
@ -110,7 +108,7 @@ class RoomListViewTest {
rule.setRoomListView( rule.setRoomListView(
state = aRoomListState( state = aRoomListState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
roomList = AsyncData.Success(persistentListOf()), contentState = anEmptyContentState(),
), ),
onCreateRoomClicked = callback, onCreateRoomClicked = callback,
) )
@ -124,7 +122,7 @@ class RoomListViewTest {
val state = aRoomListState( val state = aRoomListState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
) )
val room0 = state.roomList.dataOrNull()!!.first() val room0 = state.contentAsRooms().summaries.first()
ensureCalledOnceWithParam(room0.roomId) { callback -> ensureCalledOnceWithParam(room0.roomId) { callback ->
rule.setRoomListView( rule.setRoomListView(
state = state, state = state,
@ -140,7 +138,7 @@ class RoomListViewTest {
val state = aRoomListState( val state = aRoomListState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
) )
val room0 = state.roomList.dataOrNull()!!.first() val room0 = state.contentAsRooms().summaries.first()
rule.setRoomListView( rule.setRoomListView(
state = state, state = state,
) )
@ -170,7 +168,7 @@ class RoomListViewTest {
fun `clicking on invites invokes the expected callback`() { fun `clicking on invites invokes the expected callback`() {
val eventsRecorder = EventsRecorder<RoomListEvents>() val eventsRecorder = EventsRecorder<RoomListEvents>()
val state = aRoomListState( val state = aRoomListState(
invitesState = InvitesState.NewInvites, contentState = aRoomsContentState(invitesState = InvitesState.NewInvites),
eventSink = eventsRecorder, eventSink = eventsRecorder,
) )
ensureCalledOnce { callback -> ensureCalledOnce { callback ->

View file

@ -29,6 +29,7 @@ import io.element.android.features.roomlist.impl.datasource.DefaultInviteStateDa
import io.element.android.features.roomlist.impl.datasource.RoomListDataSource import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory
import io.element.android.features.roomlist.impl.filters.RoomListFiltersPresenter import io.element.android.features.roomlist.impl.filters.RoomListFiltersPresenter
import io.element.android.features.roomlist.impl.filters.selection.DefaultFilterSelectionStrategy
import io.element.android.features.roomlist.impl.migration.MigrationScreenPresenter import io.element.android.features.roomlist.impl.migration.MigrationScreenPresenter
import io.element.android.features.roomlist.impl.migration.SharedPrefsMigrationScreenStore import io.element.android.features.roomlist.impl.migration.SharedPrefsMigrationScreenStore
import io.element.android.features.roomlist.impl.search.RoomListSearchDataSource import io.element.android.features.roomlist.impl.search.RoomListSearchDataSource
@ -127,6 +128,7 @@ class RoomListScreen(
filtersPresenter = RoomListFiltersPresenter( filtersPresenter = RoomListFiltersPresenter(
roomListService = matrixClient.roomListService, roomListService = matrixClient.roomListService,
featureFlagService = featureFlagService, featureFlagService = featureFlagService,
filterSelectionStrategy = DefaultFilterSelectionStrategy(),
), ),
analyticsService = NoopAnalyticsService(), analyticsService = NoopAnalyticsService(),
) )

View file

@ -30,6 +30,7 @@ dependencies {
implementation(libs.test.junit) implementation(libs.test.junit)
implementation(libs.test.truth) implementation(libs.test.truth)
implementation(libs.coroutines.test) implementation(libs.coroutines.test)
implementation(projects.libraries.architecture)
implementation(projects.libraries.core) implementation(projects.libraries.core)
implementation(projects.libraries.uiStrings) implementation(projects.libraries.uiStrings)
implementation(libs.test.turbine) implementation(libs.test.turbine)

View file

@ -0,0 +1,35 @@
/*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.tests.testutils
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import io.element.android.libraries.architecture.Presenter
import kotlinx.coroutines.flow.MutableStateFlow
class MutablePresenter<State>(initialState: State) : Presenter<State> {
private val stateFlow = MutableStateFlow(initialState)
fun updateState(state: State) {
stateFlow.value = state
}
@Composable
override fun present(): State {
return stateFlow.collectAsState().value
}
}