RoomList: refactor and fix tests

This commit is contained in:
ganfra 2024-03-12 15:38:33 +01:00
parent a2c4d7debd
commit bf68261ed9
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.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<RoomListFiltersState>,
private val searchPresenter: Presenter<RoomListSearchState>,
private val migrationScreenPresenter: MigrationScreenPresenter,
private val migrationScreenPresenter: Presenter<MigrationScreenState>,
private val sessionPreferencesStore: SessionPreferencesStore,
private val analyticsService: AnalyticsService,
) : Presenter<RoomListState> {
@ -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)

View file

@ -67,6 +67,7 @@ enum class SecurityBannerState {
RecoveryKeyConfirmation,
}
@Immutable
sealed interface RoomListContentState {
data object Migration : 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.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<RoomListState> {
override val values: Sequence<RoomListState>
@ -43,15 +41,15 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
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<RoomListState> {
internal fun aRoomListState(
matrixUser: MatrixUser? = MatrixUser(userId = UserId("@id:domain"), displayName = "User#1"),
showAvatarIndicator: Boolean = false,
roomList: AsyncData<ImmutableList<RoomListRoomSummary>> = 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,
)

View file

@ -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<RoomListFilter>,
selectedFilters: ImmutableList<RoomListFilter>,
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 = { })
}

View file

@ -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

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.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 {

View file

@ -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<RoomListFiltersState>
@Binds
fun bindMigrationScreenPresenter(presenter: MigrationScreenPresenter): Presenter<MigrationScreenState>
}

View file

@ -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.

View file

@ -34,14 +34,11 @@ class RoomListFiltersPresenter @Inject constructor(
private val featureFlagService: FeatureFlagService,
private val filterSelectionStrategy: FilterSelectionStrategy,
) : Presenter<RoomListFiltersState> {
@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,

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 kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList
data class RoomListFiltersState(
val filterSelectionStates: ImmutableList<FilterSelectionState>,
@ -26,7 +27,10 @@ data class RoomListFiltersState(
) {
val hasAnyFilterSelected = filterSelectionStates.any { it.isSelected }
fun selectedFilters(): List<RoomListFilter> {
return filterSelectionStates.filter { it.isSelected }.map { it.filter }
fun selectedFilters(): ImmutableList<RoomListFilter> {
return filterSelectionStates
.filter { it.isSelected }
.map { it.filter }
.toPersistentList()
}
}

View file

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

View file

@ -24,7 +24,6 @@ import javax.inject.Inject
@ContributesBinding(SessionScope::class)
class DefaultFilterSelectionStrategy @Inject constructor() : FilterSelectionStrategy {
private val selectedFilters = LinkedHashSet<RoomListFilter>()
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
interface FilterSelectionStrategy {
val filterSelectionStates: StateFlow<Set<FilterSelectionState>>
fun select(filter: RoomListFilter)