diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContentStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContentStateProvider.kt new file mode 100644 index 0000000000..7f066628df --- /dev/null +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContentStateProvider.kt @@ -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 { + override val values: Sequence + get() = sequenceOf( + aRoomsContentState(), + aSkeletonContentState(), + anEmptyContentState(), + aMigrationContentState(), + ) +} + +internal fun aRoomsContentState( + invitesState: InvitesState = InvitesState.NoInvites, + securityBannerState: SecurityBannerState = SecurityBannerState.None, + summaries: ImmutableList = 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) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt index 4bd26a62cf..5197361f2d 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt @@ -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.RoomListDataSource 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.RoomListSearchState import io.element.android.libraries.architecture.AsyncData @@ -89,7 +89,7 @@ class RoomListPresenter @Inject constructor( private val indicatorService: IndicatorService, private val filtersPresenter: Presenter, private val searchPresenter: Presenter, - private val migrationScreenPresenter: MigrationScreenPresenter, + private val migrationScreenPresenter: Presenter, private val sessionPreferencesStore: SessionPreferencesStore, private val analyticsService: AnalyticsService, ) : Presenter { @@ -196,23 +196,23 @@ class RoomListPresenter @Inject constructor( } val loadingState by roomListDataSource.loadingState.collectAsState() val showMigration = migrationScreenPresenter.present().isMigrating - val showSkeleton by remember { - derivedStateOf { - loadingState == RoomList.LoadingState.NotLoaded || roomSummaries is AsyncData.Loading - } - } val showEmpty by remember { derivedStateOf { (loadingState as? RoomList.LoadingState.Loaded)?.numberOfRooms == 0 } } + val showSkeleton by remember { + derivedStateOf { + loadingState == RoomList.LoadingState.NotLoaded || roomSummaries is AsyncData.Loading + } + } return when { showMigration -> RoomListContentState.Migration - showSkeleton -> RoomListContentState.Skeleton(count = 16) showEmpty -> { val invitesState = inviteStateDataSource.inviteState() RoomListContentState.Empty(invitesState) } + showSkeleton -> RoomListContentState.Skeleton(count = 16) else -> { val invitesState = inviteStateDataSource.inviteState() val securityBannerState by securityBannerState(securityBannerDismissed) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt index 58b36636c8..73a1460ad9 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt @@ -67,6 +67,7 @@ enum class SecurityBannerState { RecoveryKeyConfirmation, } +@Immutable sealed interface RoomListContentState { data object Migration : RoomListContentState data class Skeleton(val count: Int) : RoomListContentState diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt index a47baf4bbc..3d577d9ea9 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt @@ -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.search.RoomListSearchState 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.AvatarSize 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 kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toPersistentList open class RoomListStateProvider : PreviewParameterProvider { override val values: Sequence @@ -43,15 +41,15 @@ open class RoomListStateProvider : PreviewParameterProvider { aRoomListState(), aRoomListState(snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete)), aRoomListState(hasNetworkConnection = false), - aRoomListState(invitesState = InvitesState.SeenInvites), - aRoomListState(invitesState = InvitesState.NewInvites), + aRoomListState(contentState = aRoomsContentState(invitesState = InvitesState.SeenInvites)), + aRoomListState(contentState = aRoomsContentState(invitesState = InvitesState.NewInvites)), aRoomListState(contextMenu = aContextMenuShown(roomName = "A nice room name")), aRoomListState(contextMenu = aContextMenuShown(isFavorite = true)), - aRoomListState(securityBannerState = SecurityBannerState.SessionVerification), - aRoomListState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation), - aRoomListState(roomList = AsyncData.Success(persistentListOf())), - //aRoomListState(roomList = AsyncData.Loading(prevData = RoomListRoomSummaryFactory.createFakeList())), - aRoomListState(matrixUser = null, displayMigrationStatus = true), + aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SessionVerification)), + aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation)), + aRoomListState(contentState = anEmptyContentState()), + aRoomListState(contentState = aSkeletonContentState()), + aRoomListState(matrixUser = null, contentState = aMigrationContentState()), aRoomListState(searchState = aRoomListSearchState(isSearchActive = true, query = "Test")), aRoomListState(filtersState = aRoomListFiltersState(isFeatureEnabled = true)), ) @@ -60,16 +58,13 @@ open class RoomListStateProvider : PreviewParameterProvider { internal fun aRoomListState( matrixUser: MatrixUser? = MatrixUser(userId = UserId("@id:domain"), displayName = "User#1"), showAvatarIndicator: Boolean = false, - roomList: AsyncData> = AsyncData.Success(aRoomListRoomSummaryList()), hasNetworkConnection: Boolean = true, snackbarMessage: SnackbarMessage? = null, - securityBannerState: SecurityBannerState = SecurityBannerState.None, - invitesState: InvitesState = InvitesState.NoInvites, contextMenu: RoomListState.ContextMenu = RoomListState.ContextMenu.Hidden, leaveRoomState: LeaveRoomState = aLeaveRoomState(), searchState: RoomListSearchState = aRoomListSearchState(), filtersState: RoomListFiltersState = aRoomListFiltersState(isFeatureEnabled = false), - displayMigrationStatus: Boolean = false, + contentState: RoomListContentState = aRoomsContentState(), eventSink: (RoomListEvents) -> Unit = {} ) = RoomListState( matrixUser = matrixUser, @@ -80,11 +75,7 @@ internal fun aRoomListState( leaveRoomState = leaveRoomState, filtersState = filtersState, searchState = searchState, - contentState = RoomListContentState.Rooms( - invitesState = invitesState, - securityBannerState = securityBannerState, - summaries = roomList.dataOrNull().orEmpty().toPersistentList(), - ), + contentState = contentState, eventSink = eventSink, ) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListContentView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListContentView.kt index 612264bb0c..1e3d66750f 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListContentView.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListContentView.kt @@ -17,7 +17,6 @@ package io.element.android.features.roomlist.impl.components import androidx.annotation.StringRes -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box 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.res.stringResource 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.dp 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.R 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.SecurityBannerState 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.RoomListFiltersEmptyStateResources 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.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.HorizontalDivider import io.element.android.libraries.designsystem.theme.components.IconSource import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.ImmutableList @Composable fun RoomListContentView( @@ -214,7 +219,6 @@ private fun RoomsViewList( modifier = modifier.nestedScroll(nestedScrollConnection), // FAB height is 56dp, bottom padding is 16dp, we add 8dp as extra margin -> 56+16+8 = 80 contentPadding = PaddingValues(bottom = 80.dp) - ) { when (state.securityBannerState) { SecurityBannerState.SessionVerification -> { @@ -261,7 +265,7 @@ private fun RoomsViewList( @Composable private fun EmptyViewForFilterStates( - selectedFilters: List, + selectedFilters: ImmutableList, modifier: Modifier = Modifier, ) { val emptyStateResources = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters) ?: return @@ -301,3 +305,18 @@ private fun EmptyScaffold( 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 = { }) +} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSource.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSource.kt index 5d92a55ba7..2f078d7a69 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSource.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSource.kt @@ -29,12 +29,9 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactory.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactory.kt index aea57d32ab..07cdabea8e 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactory.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactory.kt @@ -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.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.roomlist.RoomSummary -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList import javax.inject.Inject class RoomListRoomSummaryFactory @Inject constructor( @@ -52,7 +50,6 @@ class RoomListRoomSummaryFactory @Inject constructor( isFavorite = false, ) } - } fun create(roomSummary: RoomSummary.Filled): RoomListRoomSummary { diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/di/RoomListModule.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/di/RoomListModule.kt index b66401695e..7176ddef1e 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/di/RoomListModule.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/di/RoomListModule.kt @@ -21,6 +21,8 @@ import dagger.Binds import dagger.Module 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.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.RoomListSearchState import io.element.android.libraries.architecture.Presenter @@ -34,4 +36,7 @@ interface RoomListModule { @Binds fun bindFiltersPresenter(presenter: RoomListFiltersPresenter): Presenter + + @Binds + fun bindMigrationScreenPresenter(presenter: MigrationScreenPresenter): Presenter } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersEmptyStateResources.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersEmptyStateResources.kt index f0f29b0db6..53763abd0d 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersEmptyStateResources.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersEmptyStateResources.kt @@ -28,7 +28,6 @@ data class RoomListFiltersEmptyStateResources( @StringRes val title: Int, @StringRes val subtitle: Int, ) { - companion object { /** * Create a [RoomListFiltersEmptyStateResources] from a list of selected filters. diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersPresenter.kt index 058bfbbd8c..27edc91627 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersPresenter.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersPresenter.kt @@ -34,14 +34,11 @@ class RoomListFiltersPresenter @Inject constructor( private val featureFlagService: FeatureFlagService, private val filterSelectionStrategy: FilterSelectionStrategy, ) : Presenter { - @Composable override fun present(): RoomListFiltersState { - val isFeatureEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.RoomListFilters).collectAsState(false) val filters by filterSelectionStrategy.filterSelectionStates.collectAsState() - fun handleEvents(event: RoomListFiltersEvents) { when (event) { RoomListFiltersEvents.ClearSelectedFilters -> { @@ -75,7 +72,6 @@ class RoomListFiltersPresenter @Inject constructor( roomListService.allRooms.updateFilter(allRoomsFilter) } - return RoomListFiltersState( filterSelectionStates = filters.toPersistentList(), isFeatureEnabled = isFeatureEnabled, diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersState.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersState.kt index 7fce841aaf..14850ef82e 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersState.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersState.kt @@ -18,6 +18,7 @@ package io.element.android.features.roomlist.impl.filters import io.element.android.features.roomlist.impl.filters.selection.FilterSelectionState import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toPersistentList data class RoomListFiltersState( val filterSelectionStates: ImmutableList, @@ -26,7 +27,10 @@ data class RoomListFiltersState( ) { val hasAnyFilterSelected = filterSelectionStates.any { it.isSelected } - fun selectedFilters(): List { - return filterSelectionStates.filter { it.isSelected }.map { it.filter } + fun selectedFilters(): ImmutableList { + return filterSelectionStates + .filter { it.isSelected } + .map { it.filter } + .toPersistentList() } } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersView.kt index 57f4fa0a9c..fcdc260c6e 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersView.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersView.kt @@ -53,7 +53,6 @@ fun RoomListFiltersView( state: RoomListFiltersState, modifier: Modifier = Modifier ) { - fun onClearFiltersClicked() { state.eventSink(RoomListFiltersEvents.ClearSelectedFilters) } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/selection/DefaultFilterSelectionStrategy.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/selection/DefaultFilterSelectionStrategy.kt index 4c2b4013b6..d56c84a572 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/selection/DefaultFilterSelectionStrategy.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/selection/DefaultFilterSelectionStrategy.kt @@ -24,7 +24,6 @@ import javax.inject.Inject @ContributesBinding(SessionScope::class) class DefaultFilterSelectionStrategy @Inject constructor() : FilterSelectionStrategy { - private val selectedFilters = LinkedHashSet() override val filterSelectionStates = MutableStateFlow(buildFilters()) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/selection/FilterSelectionStrategy.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/selection/FilterSelectionStrategy.kt index aa2f42384b..415c67fc7b 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/selection/FilterSelectionStrategy.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/selection/FilterSelectionStrategy.kt @@ -20,7 +20,6 @@ import io.element.android.features.roomlist.impl.filters.RoomListFilter import kotlinx.coroutines.flow.StateFlow interface FilterSelectionStrategy { - val filterSelectionStates: StateFlow> fun select(filter: RoomListFilter) diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt index 4e83e681fd..aa89c4fac2 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt @@ -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.filters.RoomListFiltersState 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.MigrationScreenPresenter +import io.element.android.features.roomlist.impl.migration.MigrationScreenState 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.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.RecoveryState 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.timeline.ReceiptType 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.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_NAME 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.test.FakeAnalyticsService 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.consumeItemsUntilPredicate import io.element.android.tests.testutils.testCoroutineDispatchers @@ -127,7 +126,6 @@ class RoomListPresenterTests { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(1) val initialState = awaitItem() assertThat(initialState.showAvatarIndicator).isTrue() sessionVerificationService.givenCanVerifySession(false) @@ -169,11 +167,9 @@ class RoomListPresenterTests { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = consumeItemsUntilPredicate(timeout = 3.seconds) { state -> state.roomList.dataOrNull()?.size == 16 }.last() - // Room list is loaded with 16 placeholders - val initialItems = initialState.roomList.dataOrNull().orEmpty() - assertThat(initialItems.size).isEqualTo(16) - assertThat(initialItems.all { it.isPlaceholder }).isTrue() + val initialState = consumeItemsUntilPredicate(timeout = 3.seconds) { state -> state.contentState is RoomListContentState.Skeleton }.last() + assertThat(initialState.contentState).isInstanceOf(RoomListContentState.Skeleton::class.java) + roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) roomListService.postAllRooms( listOf( aRoomSummaryFilled( @@ -182,10 +178,10 @@ class RoomListPresenterTests { ) ) ) - val withRoomState = consumeItemsUntilPredicate { state -> state.roomList.dataOrNull()?.size == 1 }.last() - val withRoomStateItems = withRoomState.roomList.dataOrNull().orEmpty() - assertThat(withRoomStateItems.size).isEqualTo(1) - assertThat(withRoomStateItems.first()).isEqualTo( + val withRoomsState = + consumeItemsUntilPredicate { state -> state.contentState is RoomListContentState.Rooms && state.contentAsRooms().summaries.isNotEmpty() }.last() + assertThat(withRoomsState.contentAsRooms().summaries).hasSize(1) + assertThat(withRoomsState.contentAsRooms().summaries.first()).isEqualTo( createRoomListRoomSummary( numberOfUnreadMentions = 1, numberOfUnreadMessages = 2, @@ -241,23 +237,28 @@ class RoomListPresenterTests { @Test fun `present - handle RecoveryKeyConfirmation last session`() = runTest { val scope = CoroutineScope(context = coroutineContext + SupervisorJob()) + val roomListService = FakeRoomListService().apply { + postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) + } val presenter = createRoomListPresenter( coroutineScope = scope, client = FakeMatrixClient( encryptionService = FakeEncryptionService().apply { emitIsLastDevice(true) - } + }, + roomListService = roomListService ), ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(1) - val eventSink = awaitItem().eventSink + val eventSink = consumeItemsUntilPredicate { + it.contentState is RoomListContentState.Rooms + }.last().eventSink // 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) - assertThat(awaitItem().securityBannerState).isEqualTo(SecurityBannerState.None) + assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None) scope.cancel() } } @@ -265,16 +266,22 @@ class RoomListPresenterTests { @Test fun `present - handle DismissRequestVerificationPrompt`() = runTest { val scope = CoroutineScope(context = coroutineContext + SupervisorJob()) + val roomListService = FakeRoomListService().apply { + postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) + } val presenter = createRoomListPresenter( + client = FakeMatrixClient(roomListService = roomListService), coroutineScope = scope, ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val eventSink = awaitItem().eventSink - assertThat(awaitItem().securityBannerState).isEqualTo(SecurityBannerState.SessionVerification) + val eventSink = consumeItemsUntilPredicate { + it.contentState is RoomListContentState.Rooms + }.last().eventSink + assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.SessionVerification) eventSink(RoomListEvents.DismissRequestVerificationPrompt) - assertThat(awaitItem().securityBannerState).isEqualTo(SecurityBannerState.None) + assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None) scope.cancel() } } @@ -282,7 +289,11 @@ class RoomListPresenterTests { @Test fun `present - handle DismissRecoveryKeyPrompt`() = runTest { val encryptionService = FakeEncryptionService() + val roomListService = FakeRoomListService().apply { + postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) + } val matrixClient = FakeMatrixClient( + roomListService = roomListService, encryptionService = encryptionService, sessionVerificationService = FakeSessionVerificationService().apply { givenCanVerifySession(false) @@ -297,15 +308,16 @@ class RoomListPresenterTests { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(1) - val initialState = awaitItem() - assertThat(initialState.securityBannerState).isEqualTo(SecurityBannerState.None) + val initialState = consumeItemsUntilPredicate { + it.contentState is RoomListContentState.Rooms + }.last() + assertThat(initialState.contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None) encryptionService.emitRecoveryState(RecoveryState.INCOMPLETE) val nextState = awaitItem() - assertThat(nextState.securityBannerState).isEqualTo(SecurityBannerState.RecoveryKeyConfirmation) + assertThat(nextState.contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.RecoveryKeyConfirmation) nextState.eventSink(RoomListEvents.DismissRecoveryKeyPrompt) val finalState = awaitItem() - assertThat(finalState.securityBannerState).isEqualTo(SecurityBannerState.None) + assertThat(finalState.contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None) scope.cancel() } } @@ -314,22 +326,30 @@ class RoomListPresenterTests { fun `present - sets invite state`() = runTest { val inviteStateFlow = MutableStateFlow(InvitesState.NoInvites) val inviteStateDataSource = FakeInviteDataSource(inviteStateFlow) + val roomListService = FakeRoomListService() 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) { presenter.present() }.test { - skipItems(1) - assertThat(awaitItem().invitesState).isEqualTo(InvitesState.NoInvites) + consumeItemsUntilPredicate { + it.contentState is RoomListContentState.Rooms + } + assertThat(awaitItem().contentAsRooms().invitesState).isEqualTo(InvitesState.NoInvites) inviteStateFlow.value = InvitesState.SeenInvites - assertThat(awaitItem().invitesState).isEqualTo(InvitesState.SeenInvites) + assertThat(awaitItem().contentAsRooms().invitesState).isEqualTo(InvitesState.SeenInvites) inviteStateFlow.value = InvitesState.NewInvites - assertThat(awaitItem().invitesState).isEqualTo(InvitesState.NewInvites) + assertThat(awaitItem().contentAsRooms().invitesState).isEqualTo(InvitesState.NewInvites) inviteStateFlow.value = InvitesState.NoInvites - assertThat(awaitItem().invitesState).isEqualTo(InvitesState.NoInvites) + assertThat(awaitItem().contentAsRooms().invitesState).isEqualTo(InvitesState.NoInvites) scope.cancel() } } @@ -477,6 +497,7 @@ class RoomListPresenterTests { val userDefinedMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY val notificationSettingsService = FakeNotificationSettingsService() val roomListService = FakeRoomListService() + roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) roomListService.postAllRooms(listOf(aRoomSummaryFilled(notificationMode = userDefinedMode))) val matrixClient = FakeMatrixClient( roomListService = roomListService, @@ -488,12 +509,13 @@ class RoomListPresenterTests { presenter.present() }.test { notificationSettingsService.setRoomNotificationMode(A_ROOM_ID, userDefinedMode) - 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() - 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) cancelAndIgnoreRemainingEvents() scope.cancel() @@ -526,30 +548,46 @@ class RoomListPresenterTests { } } - fun `present - change in migration presenter state modifies isMigrating`() = runTest { - val client = FakeMatrixClient(sessionId = A_SESSION_ID) - val migrationStore = InMemoryMigrationScreenStore() - val migrationScreenPresenter = MigrationScreenPresenter(client, migrationStore) + @Test + fun `present - change in migration presenter state modifies contentState`() = runTest { + val migrationScreenPresenter = MutablePresenter(MigrationScreenState(true)) val scope = CoroutineScope(coroutineContext + SupervisorJob()) val presenter = createRoomListPresenter( - client = client, coroutineScope = scope, migrationScreenPresenter = migrationScreenPresenter, ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { + skipItems(1) val initialState = awaitItem() // The migration screen is shown if the migration screen has not been shown before - assertThat(initialState.displayMigrationStatus).isTrue() - skipItems(2) - + assertThat(initialState.contentState).isInstanceOf(RoomListContentState.Migration::class.java) // 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) - migrationStore.setMigrationScreenShown(A_SESSION_ID) - + migrationScreenPresenter.updateState(MigrationScreenState(false)) // 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() } } @@ -609,10 +647,7 @@ class RoomListPresenterTests { sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(), featureFlagService: FeatureFlagService = FakeFeatureFlagService(), coroutineScope: CoroutineScope, - migrationScreenPresenter: MigrationScreenPresenter = MigrationScreenPresenter( - matrixClient = client, - migrationScreenStore = InMemoryMigrationScreenStore(), - ), + migrationScreenPresenter: Presenter = Presenter { MigrationScreenState(false) }, analyticsService: AnalyticsService = FakeAnalyticsService(), filtersPresenter: Presenter = Presenter { aRoomListFiltersState() }, searchPresenter: Presenter = Presenter { aRoomListSearchState() }, diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt new file mode 100644 index 0000000000..27c4f33f1b --- /dev/null +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt @@ -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 diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt index 347ff65687..f545b27860 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt @@ -26,7 +26,6 @@ import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTouchInput import androidx.test.ext.junit.runners.AndroidJUnit4 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.ui.strings.CommonStrings 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.ensureCalledOnce import io.element.android.tests.testutils.ensureCalledOnceWithParam -import kotlinx.collections.immutable.persistentListOf import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule @@ -50,7 +48,7 @@ class RoomListViewTest { val eventsRecorder = EventsRecorder() rule.setRoomListView( state = aRoomListState( - securityBannerState = SecurityBannerState.SessionVerification, + contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SessionVerification), eventSink = eventsRecorder, ) ) @@ -65,7 +63,7 @@ class RoomListViewTest { ensureCalledOnce { callback -> rule.setRoomListView( state = aRoomListState( - securityBannerState = SecurityBannerState.SessionVerification, + contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SessionVerification), eventSink = eventsRecorder, ), onVerifyClicked = callback, @@ -79,7 +77,7 @@ class RoomListViewTest { val eventsRecorder = EventsRecorder() rule.setRoomListView( state = aRoomListState( - securityBannerState = SecurityBannerState.RecoveryKeyConfirmation, + contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation), eventSink = eventsRecorder, ) ) @@ -94,7 +92,7 @@ class RoomListViewTest { ensureCalledOnce { callback -> rule.setRoomListView( state = aRoomListState( - securityBannerState = SecurityBannerState.RecoveryKeyConfirmation, + contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation), eventSink = eventsRecorder, ), onConfirmRecoveryKeyClicked = callback, @@ -110,7 +108,7 @@ class RoomListViewTest { rule.setRoomListView( state = aRoomListState( eventSink = eventsRecorder, - roomList = AsyncData.Success(persistentListOf()), + contentState = anEmptyContentState(), ), onCreateRoomClicked = callback, ) @@ -124,7 +122,7 @@ class RoomListViewTest { val state = aRoomListState( eventSink = eventsRecorder, ) - val room0 = state.roomList.dataOrNull()!!.first() + val room0 = state.contentAsRooms().summaries.first() ensureCalledOnceWithParam(room0.roomId) { callback -> rule.setRoomListView( state = state, @@ -140,7 +138,7 @@ class RoomListViewTest { val state = aRoomListState( eventSink = eventsRecorder, ) - val room0 = state.roomList.dataOrNull()!!.first() + val room0 = state.contentAsRooms().summaries.first() rule.setRoomListView( state = state, ) @@ -170,7 +168,7 @@ class RoomListViewTest { fun `clicking on invites invokes the expected callback`() { val eventsRecorder = EventsRecorder() val state = aRoomListState( - invitesState = InvitesState.NewInvites, + contentState = aRoomsContentState(invitesState = InvitesState.NewInvites), eventSink = eventsRecorder, ) ensureCalledOnce { callback -> diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt index 0b4c47800c..e651e8f968 100644 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt @@ -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.RoomListRoomSummaryFactory 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.SharedPrefsMigrationScreenStore import io.element.android.features.roomlist.impl.search.RoomListSearchDataSource @@ -127,6 +128,7 @@ class RoomListScreen( filtersPresenter = RoomListFiltersPresenter( roomListService = matrixClient.roomListService, featureFlagService = featureFlagService, + filterSelectionStrategy = DefaultFilterSelectionStrategy(), ), analyticsService = NoopAnalyticsService(), ) diff --git a/tests/testutils/build.gradle.kts b/tests/testutils/build.gradle.kts index 4b586043dc..8e985bf2ca 100644 --- a/tests/testutils/build.gradle.kts +++ b/tests/testutils/build.gradle.kts @@ -30,6 +30,7 @@ dependencies { implementation(libs.test.junit) implementation(libs.test.truth) implementation(libs.coroutines.test) + implementation(projects.libraries.architecture) implementation(projects.libraries.core) implementation(projects.libraries.uiStrings) implementation(libs.test.turbine) diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/MutablePresenter.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/MutablePresenter.kt new file mode 100644 index 0000000000..02870f88c5 --- /dev/null +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/MutablePresenter.kt @@ -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(initialState: State) : Presenter { + private val stateFlow = MutableStateFlow(initialState) + + fun updateState(state: State) { + stateFlow.value = state + } + + @Composable + override fun present(): State { + return stateFlow.collectAsState().value + } +}