diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt index 07b4be123c..dbb994e23e 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt @@ -9,6 +9,7 @@ package io.element.android.features.home.impl import io.element.android.features.home.impl.roomlist.RoomListState +import io.element.android.features.home.impl.spacefilters.SpaceFiltersState import io.element.android.features.home.impl.spaces.HomeSpacesState import io.element.android.features.logout.api.direct.DirectLogoutState import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage @@ -31,6 +32,7 @@ data class HomeState( val directLogoutState: DirectLogoutState, val eventSink: (HomeEvent) -> Unit, ) { + val isBackHandlerEnabled = currentHomeNavigationBarItem != HomeNavigationBarItem.Chats || roomListState.spaceFiltersState is SpaceFiltersState.Selected val displayActions = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats val displayRoomListFilters = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats && roomListState.displayFilters val showNavigationBar = homeSpacesState.canCreateSpaces || homeSpacesState.spaceRooms.isNotEmpty() diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt index 7d210c3447..eee6f49db3 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt @@ -50,6 +50,9 @@ import io.element.android.features.home.impl.roomlist.RoomListDeclineInviteMenu import io.element.android.features.home.impl.roomlist.RoomListEvent import io.element.android.features.home.impl.roomlist.RoomListState import io.element.android.features.home.impl.search.RoomListSearchView +import io.element.android.features.home.impl.spacefilters.SpaceFiltersEvent +import io.element.android.features.home.impl.spacefilters.SpaceFiltersState +import io.element.android.features.home.impl.spacefilters.SpaceFiltersView import io.element.android.features.home.impl.spaces.HomeSpacesView import io.element.android.libraries.androidutils.throttler.FirstThrottler import io.element.android.libraries.designsystem.preview.ElementPreview @@ -153,10 +156,15 @@ private fun HomeScaffold( val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage) val roomListState: RoomListState = state.roomListState - BackHandler( - enabled = state.currentHomeNavigationBarItem != HomeNavigationBarItem.Chats, - ) { - state.eventSink(HomeEvent.SelectHomeNavigationBarItem(HomeNavigationBarItem.Chats)) + BackHandler(enabled = state.isBackHandlerEnabled) { + if (state.currentHomeNavigationBarItem != HomeNavigationBarItem.Chats) { + state.eventSink(HomeEvent.SelectHomeNavigationBarItem(HomeNavigationBarItem.Chats)) + } else { + val spaceFiltersState = state.roomListState.spaceFiltersState + if (spaceFiltersState is SpaceFiltersState.Selected) { + spaceFiltersState.eventSink(SpaceFiltersEvent.Selected.ClearSelection) + } + } } val hazeState = rememberHazeState() @@ -168,7 +176,6 @@ private fun HomeScaffold( topBar = { HomeTopBar( selectedNavigationItem = state.currentHomeNavigationBarItem, - title = stringResource(state.currentHomeNavigationBarItem.labelRes), currentUserAndNeighbors = state.currentUserAndNeighbors, showAvatarIndicator = state.showAvatarIndicator, areSearchResultsDisplayed = roomListState.searchState.isSearchActive, @@ -182,6 +189,7 @@ private fun HomeScaffold( scrollBehavior = scrollBehavior, displayFilters = state.displayRoomListFilters, filtersState = roomListState.filtersState, + spaceFiltersState = roomListState.spaceFiltersState, canCreateSpaces = state.homeSpacesState.canCreateSpaces, canReportBug = state.canReportBug, modifier = Modifier.hazeEffect( @@ -227,6 +235,7 @@ private fun HomeScaffold( RoomListContentView( contentState = roomListState.contentState, filtersState = roomListState.filtersState, + spaceFiltersState = roomListState.spaceFiltersState, lazyListState = roomsLazyListState, hideInvitesAvatars = roomListState.hideInvitesAvatars, eventSink = roomListState.eventSink, @@ -256,6 +265,7 @@ private fun HomeScaffold( .consumeWindowInsets(padding) .hazeSource(state = hazeState) ) + SpaceFiltersView(roomListState.spaceFiltersState) } HomeNavigationBarItem.Spaces -> { HomeSpacesView( diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt index c92a5b9fb8..f8786c544c 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/HomeTopBar.kt @@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.pager.VerticalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.rememberTopAppBarState @@ -44,6 +45,10 @@ import io.element.android.features.home.impl.R import io.element.android.features.home.impl.filters.RoomListFiltersState import io.element.android.features.home.impl.filters.RoomListFiltersView import io.element.android.features.home.impl.filters.aRoomListFiltersState +import io.element.android.features.home.impl.spacefilters.SpaceFiltersEvent +import io.element.android.features.home.impl.spacefilters.SpaceFiltersState +import io.element.android.features.home.impl.spacefilters.aSelectedSpaceFiltersState +import io.element.android.features.home.impl.spacefilters.anUnselectedSpaceFiltersState import io.element.android.libraries.designsystem.atomic.atoms.RedIndicatorAtom import io.element.android.libraries.designsystem.components.TopAppBarScrollBehaviorLayout import io.element.android.libraries.designsystem.components.avatar.Avatar @@ -75,7 +80,6 @@ import kotlinx.collections.immutable.toImmutableList @Composable fun HomeTopBar( selectedNavigationItem: HomeNavigationBarItem, - title: String, currentUserAndNeighbors: ImmutableList, showAvatarIndicator: Boolean, areSearchResultsDisplayed: Boolean, @@ -89,6 +93,7 @@ fun HomeTopBar( canReportBug: Boolean, displayFilters: Boolean, filtersState: RoomListFiltersState, + spaceFiltersState: SpaceFiltersState, modifier: Modifier = Modifier, ) { Column(modifier) { @@ -103,12 +108,21 @@ fun HomeTopBar( scrolledContainerColor = Color.Transparent, ), title = { + val displayTitle = when (selectedNavigationItem) { + HomeNavigationBarItem.Chats -> { + when (spaceFiltersState) { + is SpaceFiltersState.Selected -> spaceFiltersState.selectedFilter.spaceRoom.displayName + else -> stringResource(selectedNavigationItem.labelRes) + } + } + HomeNavigationBarItem.Spaces -> stringResource(selectedNavigationItem.labelRes) + } Text( modifier = Modifier.semantics { heading() }, style = ElementTheme.typography.aliasScreenTitle, - text = title, + text = displayTitle, ) }, navigationIcon = { @@ -124,7 +138,8 @@ fun HomeTopBar( HomeNavigationBarItem.Chats -> RoomListMenuItems( onToggleSearch = onToggleSearch, onMenuActionClick = onMenuActionClick, - canReportBug = canReportBug + canReportBug = canReportBug, + spaceFiltersState = spaceFiltersState, ) HomeNavigationBarItem.Spaces -> SpacesMenuItems( canCreateSpaces = canCreateSpaces, @@ -154,6 +169,7 @@ private fun RoomListMenuItems( onToggleSearch: () -> Unit, onMenuActionClick: (RoomListMenuAction) -> Unit, canReportBug: Boolean, + spaceFiltersState: SpaceFiltersState, ) { IconButton( onClick = onToggleSearch, @@ -163,6 +179,7 @@ private fun RoomListMenuItems( contentDescription = stringResource(CommonStrings.action_search), ) } + SpaceFilterButton(spaceFiltersState = spaceFiltersState) if (RoomListConfig.HAS_DROP_DOWN_MENU) { var showMenu by remember { mutableStateOf(false) } IconButton( @@ -228,6 +245,38 @@ private fun SpacesMenuItems( } } +@Composable +private fun SpaceFilterButton( + spaceFiltersState: SpaceFiltersState, +) { + if (spaceFiltersState == SpaceFiltersState.Disabled) return + + fun onClick() { + when (spaceFiltersState) { + is SpaceFiltersState.Unselected -> spaceFiltersState.eventSink(SpaceFiltersEvent.Unselected.ShowFilters) + is SpaceFiltersState.Selected -> spaceFiltersState.eventSink(SpaceFiltersEvent.Selected.ClearSelection) + else -> Unit + } + } + val isSelected = spaceFiltersState is SpaceFiltersState.Selected + IconButton( + onClick = ::onClick, + colors = if (isSelected) { + IconButtonDefaults.iconButtonColors( + containerColor = ElementTheme.colors.bgAccentRest, + contentColor = ElementTheme.colors.iconOnSolidPrimary, + ) + } else { + IconButtonDefaults.iconButtonColors() + }, + ) { + Icon( + imageVector = CompoundIcons.Filter(), + contentDescription = stringResource(R.string.screen_roomlist_your_spaces), + ) + } +} + @Composable private fun NavigationIcon( currentUserAndNeighbors: ImmutableList, @@ -309,7 +358,6 @@ private fun AccountIcon( internal fun HomeTopBarPreview() = ElementPreview { HomeTopBar( selectedNavigationItem = HomeNavigationBarItem.Chats, - title = stringResource(R.string.screen_roomlist_main_space_title), currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")), showAvatarIndicator = false, areSearchResultsDisplayed = false, @@ -322,6 +370,30 @@ internal fun HomeTopBarPreview() = ElementPreview { canReportBug = true, displayFilters = true, filtersState = aRoomListFiltersState(), + spaceFiltersState = anUnselectedSpaceFiltersState(), + onMenuActionClick = {}, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@PreviewsDayNight +@Composable +internal fun HomeTopBarSpaceFiltersSelectedPreview() = ElementPreview { + HomeTopBar( + selectedNavigationItem = HomeNavigationBarItem.Chats, + currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")), + showAvatarIndicator = false, + areSearchResultsDisplayed = false, + scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()), + onOpenSettings = {}, + onAccountSwitch = {}, + onToggleSearch = {}, + onCreateSpace = {}, + canCreateSpaces = true, + canReportBug = true, + displayFilters = true, + filtersState = aRoomListFiltersState(), + spaceFiltersState = aSelectedSpaceFiltersState(), onMenuActionClick = {}, ) } @@ -332,7 +404,6 @@ internal fun HomeTopBarPreview() = ElementPreview { internal fun HomeTopBarSpacesPreview() = ElementPreview { HomeTopBar( selectedNavigationItem = HomeNavigationBarItem.Spaces, - title = stringResource(R.string.screen_home_tab_spaces), currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")), showAvatarIndicator = false, areSearchResultsDisplayed = false, @@ -345,6 +416,7 @@ internal fun HomeTopBarSpacesPreview() = ElementPreview { canReportBug = true, displayFilters = false, filtersState = aRoomListFiltersState(), + spaceFiltersState = anUnselectedSpaceFiltersState(), onMenuActionClick = {}, ) } @@ -355,7 +427,6 @@ internal fun HomeTopBarSpacesPreview() = ElementPreview { internal fun HomeTopBarWithIndicatorPreview() = ElementPreview { HomeTopBar( selectedNavigationItem = HomeNavigationBarItem.Chats, - title = stringResource(R.string.screen_roomlist_main_space_title), currentUserAndNeighbors = persistentListOf(MatrixUser(UserId("@id:domain"), "Alice")), showAvatarIndicator = true, areSearchResultsDisplayed = false, @@ -368,6 +439,7 @@ internal fun HomeTopBarWithIndicatorPreview() = ElementPreview { canReportBug = true, displayFilters = true, filtersState = aRoomListFiltersState(), + spaceFiltersState = anUnselectedSpaceFiltersState(), onMenuActionClick = {}, ) } @@ -378,7 +450,6 @@ internal fun HomeTopBarWithIndicatorPreview() = ElementPreview { internal fun HomeTopBarMultiAccountPreview() = ElementPreview { HomeTopBar( selectedNavigationItem = HomeNavigationBarItem.Chats, - title = stringResource(R.string.screen_roomlist_main_space_title), currentUserAndNeighbors = aMatrixUserList().take(3).toImmutableList(), showAvatarIndicator = false, areSearchResultsDisplayed = false, @@ -391,6 +462,7 @@ internal fun HomeTopBarMultiAccountPreview() = ElementPreview { canReportBug = true, displayFilters = true, filtersState = aRoomListFiltersState(), + spaceFiltersState = anUnselectedSpaceFiltersState(), onMenuActionClick = {}, ) } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt index f3628fce9d..a03399baf7 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt @@ -45,6 +45,8 @@ import io.element.android.features.home.impl.roomlist.RoomListContentState import io.element.android.features.home.impl.roomlist.RoomListContentStateProvider import io.element.android.features.home.impl.roomlist.RoomListEvent import io.element.android.features.home.impl.roomlist.SecurityBannerState +import io.element.android.features.home.impl.spacefilters.SpaceFiltersState +import io.element.android.features.home.impl.spacefilters.anUnselectedSpaceFiltersState 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 @@ -59,6 +61,7 @@ import kotlinx.collections.immutable.ImmutableList fun RoomListContentView( contentState: RoomListContentState, filtersState: RoomListFiltersState, + spaceFiltersState: SpaceFiltersState, lazyListState: LazyListState, hideInvitesAvatars: Boolean, eventSink: (RoomListEvent) -> Unit, @@ -93,6 +96,7 @@ fun RoomListContentView( state = contentState, hideInvitesAvatars = hideInvitesAvatars, filtersState = filtersState, + spaceFiltersState = spaceFiltersState, eventSink = eventSink, onSetUpRecoveryClick = onSetUpRecoveryClick, onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick, @@ -172,6 +176,7 @@ private fun RoomsView( state: RoomListContentState.Rooms, hideInvitesAvatars: Boolean, filtersState: RoomListFiltersState, + spaceFiltersState: SpaceFiltersState, eventSink: (RoomListEvent) -> Unit, onSetUpRecoveryClick: () -> Unit, onConfirmRecoveryKeyClick: () -> Unit, @@ -180,9 +185,12 @@ private fun RoomsView( lazyListState: LazyListState, modifier: Modifier = Modifier, ) { - if (state.summaries.isEmpty() && filtersState.hasAnyFilterSelected) { + val isSpaceFilterSelected = spaceFiltersState is SpaceFiltersState.Selected + val hasAnyFilterSelected = filtersState.hasAnyFilterSelected || isSpaceFilterSelected + if (state.summaries.isEmpty() && hasAnyFilterSelected) { EmptyViewForFilterStates( selectedFilters = filtersState.selectedFilters(), + isSpaceFilterSelected = isSpaceFilterSelected, modifier = modifier.fillMaxSize() ) } else { @@ -278,9 +286,10 @@ private fun RoomsViewList( @Composable private fun EmptyViewForFilterStates( selectedFilters: ImmutableList, + isSpaceFilterSelected: Boolean, modifier: Modifier = Modifier, ) { - val emptyStateResources = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters) ?: return + val emptyStateResources = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters, isSpaceFilterSelected) ?: return EmptyScaffold( title = emptyStateResources.title, subtitle = emptyStateResources.subtitle, @@ -331,6 +340,7 @@ internal fun RoomListContentViewPreview(@PreviewParameter(RoomListContentStatePr ) } ), + spaceFiltersState = anUnselectedSpaceFiltersState(), hideInvitesAvatars = false, eventSink = {}, onSetUpRecoveryClick = {}, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/di/RoomListModule.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/di/RoomListModule.kt index ea80c1b3cb..1eeba4fff7 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/di/RoomListModule.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/di/RoomListModule.kt @@ -17,6 +17,8 @@ import io.element.android.features.home.impl.roomlist.RoomListPresenter import io.element.android.features.home.impl.roomlist.RoomListState import io.element.android.features.home.impl.search.RoomListSearchPresenter import io.element.android.features.home.impl.search.RoomListSearchState +import io.element.android.features.home.impl.spacefilters.SpaceFiltersPresenter +import io.element.android.features.home.impl.spacefilters.SpaceFiltersState import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.di.SessionScope @@ -31,4 +33,7 @@ interface RoomListModule { @Binds fun bindFiltersPresenter(presenter: RoomListFiltersPresenter): Presenter + + @Binds + fun bindSpaceFiltersPresenter(presenter: SpaceFiltersPresenter): Presenter } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFilter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFilter.kt index 1f627eca4e..3e07c565db 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFilter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFilter.kt @@ -9,6 +9,7 @@ package io.element.android.features.home.impl.filters import io.element.android.features.home.impl.R +import io.element.android.libraries.matrix.api.roomlist.RoomListFilter as MatrixRoomListFilter /** * Enum class representing the different filters that can be applied to the room list. @@ -30,3 +31,13 @@ enum class RoomListFilter(val stringResource: Int) { Invites -> setOf(Rooms, People, Unread, Favourites) } } + +fun RoomListFilter.into(): MatrixRoomListFilter { + return when (this) { + RoomListFilter.Rooms -> MatrixRoomListFilter.Category.Group + RoomListFilter.People -> MatrixRoomListFilter.Category.People + RoomListFilter.Unread -> MatrixRoomListFilter.Unread + RoomListFilter.Favourites -> MatrixRoomListFilter.Favorite + RoomListFilter.Invites -> MatrixRoomListFilter.Invite + } +} diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEmptyStateResources.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEmptyStateResources.kt index 7381ac308e..084c3c9c0c 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEmptyStateResources.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEmptyStateResources.kt @@ -24,8 +24,12 @@ data class RoomListFiltersEmptyStateResources( /** * Create a [RoomListFiltersEmptyStateResources] from a list of selected filters. */ - fun fromSelectedFilters(selectedFilters: List): RoomListFiltersEmptyStateResources? { + fun fromSelectedFilters(selectedFilters: List, isSpaceFilterSelected: Boolean): RoomListFiltersEmptyStateResources? { return when { + isSpaceFilterSelected -> RoomListFiltersEmptyStateResources( + title = R.string.screen_roomlist_filter_mixed_empty_state_title, + subtitle = R.string.screen_roomlist_filter_mixed_empty_state_subtitle + ) selectedFilters.isEmpty() -> null selectedFilters.size == 1 -> { when (selectedFilters.first()) { diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenter.kt index 3c808045fd..e73660219c 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenter.kt @@ -9,24 +9,17 @@ package io.element.android.features.home.impl.filters import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.produceState import dev.zacsweers.metro.Inject -import io.element.android.features.home.impl.datasource.RoomListDataSource import io.element.android.features.home.impl.filters.selection.FilterSelectionStrategy import io.element.android.libraries.architecture.Presenter import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.map -import io.element.android.libraries.matrix.api.roomlist.RoomListFilter as MatrixRoomListFilter @Inject class RoomListFiltersPresenter( - private val roomListDataSource: RoomListDataSource, private val filterSelectionStrategy: FilterSelectionStrategy, ) : Presenter { - private val initialFilters = filterSelectionStrategy.filterSelectionStates.value.toImmutableList() - @Composable override fun present(): RoomListFiltersState { fun handleEvent(event: RoomListFiltersEvent) { @@ -40,31 +33,9 @@ class RoomListFiltersPresenter( } } - val filters by produceState(initialValue = initialFilters) { - filterSelectionStrategy.filterSelectionStates - .map { filters -> - value = filters.toImmutableList() - filters.mapNotNull { filterState -> - if (!filterState.isSelected) { - return@mapNotNull null - } - when (filterState.filter) { - RoomListFilter.Rooms -> MatrixRoomListFilter.Category.Group - RoomListFilter.People -> MatrixRoomListFilter.Category.People - RoomListFilter.Unread -> MatrixRoomListFilter.Unread - RoomListFilter.Favourites -> MatrixRoomListFilter.Favorite - RoomListFilter.Invites -> MatrixRoomListFilter.Invite - } - } - } - .collectLatest { filters -> - val result = MatrixRoomListFilter.All(filters) - roomListDataSource.updateFilter(result) - } - } - + val filters by filterSelectionStrategy.filterSelectionStates.collectAsState() return RoomListFiltersState( - filterSelectionStates = filters, + filterSelectionStates = filters.toImmutableList(), eventSink = ::handleEvent, ) } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/DefaultFilterSelectionStrategy.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/DefaultFilterSelectionStrategy.kt index 877e934727..847dbc2c39 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/DefaultFilterSelectionStrategy.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/DefaultFilterSelectionStrategy.kt @@ -16,6 +16,8 @@ import kotlinx.coroutines.flow.MutableStateFlow @ContributesBinding(SessionScope::class) class DefaultFilterSelectionStrategy : FilterSelectionStrategy { private val selectedFilters = LinkedHashSet() + private val availableFilters + get() = RoomListFilter.entries.toSet() override val filterSelectionStates = MutableStateFlow(buildFilters()) @@ -45,7 +47,7 @@ class DefaultFilterSelectionStrategy : FilterSelectionStrategy { isSelected = true ) } - val unselectedFilters = RoomListFilter.entries - selectedFilters - selectedFilters.flatMap { it.incompatibleFilters }.toSet() + val unselectedFilters = availableFilters - selectedFilters - selectedFilters.flatMap { it.incompatibleFilters }.toSet() val unselectedFilterStates = unselectedFilters.map { FilterSelectionState( filter = it, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/FilterSelectionStrategy.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/FilterSelectionStrategy.kt index f0877b5e0d..ebdb58fa2b 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/FilterSelectionStrategy.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/selection/FilterSelectionStrategy.kt @@ -13,7 +13,6 @@ import kotlinx.coroutines.flow.StateFlow interface FilterSelectionStrategy { val filterSelectionStates: StateFlow> - fun select(filter: RoomListFilter) fun deselect(filter: RoomListFilter) fun isSelected(filter: RoomListFilter): Boolean diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt index 830a84ee4d..2010555cd7 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt @@ -28,9 +28,14 @@ import im.vector.app.features.analytics.plan.Interaction import io.element.android.features.announcement.api.Announcement import io.element.android.features.announcement.api.AnnouncementService import io.element.android.features.home.impl.datasource.RoomListDataSource +import io.element.android.features.home.impl.filters.RoomListFilter.Rooms import io.element.android.features.home.impl.filters.RoomListFiltersState +import io.element.android.features.home.impl.filters.into import io.element.android.features.home.impl.search.RoomListSearchEvent import io.element.android.features.home.impl.search.RoomListSearchState +import io.element.android.features.home.impl.spacefilters.SpaceFiltersState +import io.element.android.features.home.impl.spacefilters.into +import io.element.android.features.home.impl.spacefilters.selectedFilter import io.element.android.features.invite.api.SeenInvitesStore import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents.AcceptInvite import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents.DeclineInvite @@ -44,6 +49,7 @@ import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.encryption.RecoveryState import io.element.android.libraries.matrix.api.roomlist.RoomList +import io.element.android.libraries.matrix.api.roomlist.RoomListFilter import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.ui.safety.rememberHideInvitesAvatar import io.element.android.libraries.preferences.api.store.AppPreferencesStore @@ -83,6 +89,7 @@ class RoomListPresenter( private val seenInvitesStore: SeenInvitesStore, private val announcementService: AnnouncementService, private val coldStartWatcher: AnalyticsColdStartWatcher, + private val spaceFiltersPresenter: Presenter, ) : Presenter { private val encryptionService = client.encryptionService @@ -92,6 +99,7 @@ class RoomListPresenter( val leaveRoomState = leaveRoomPresenter.present() val filtersState = filtersPresenter.present() val searchState = searchPresenter.present() + val spaceFiltersState = spaceFiltersPresenter.present() val acceptDeclineInviteState = acceptDeclineInvitePresenter.present() LaunchedEffect(Unit) { @@ -150,6 +158,13 @@ class RoomListPresenter( } } + LaunchedEffect(filtersState.filterSelectionStates, spaceFiltersState.selectedFilter()) { + val selectedFilters = filtersState.selectedFilters().map { filter -> filter.into() } + val selectedSpaceFilter = spaceFiltersState.selectedFilter().into() + val allFilters = RoomListFilter.All(selectedFilters + listOfNotNull(selectedSpaceFilter)) + roomListDataSource.updateFilter(allFilters) + } + val contentState = roomListContentState( securityBannerDismissed, showNewNotificationSoundBanner, @@ -163,6 +178,7 @@ class RoomListPresenter( leaveRoomState = leaveRoomState, filtersState = filtersState, searchState = searchState, + spaceFiltersState = spaceFiltersState, contentState = contentState, acceptDeclineInviteState = acceptDeclineInviteState, hideInvitesAvatars = hideInvitesAvatar, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt index b19344ba1d..e0f4943621 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.Immutable import io.element.android.features.home.impl.filters.RoomListFiltersState import io.element.android.features.home.impl.model.RoomListRoomSummary import io.element.android.features.home.impl.search.RoomListSearchState +import io.element.android.features.home.impl.spacefilters.SpaceFiltersState import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState @@ -26,6 +27,7 @@ data class RoomListState( val leaveRoomState: LeaveRoomState, val filtersState: RoomListFiltersState, val searchState: RoomListSearchState, + val spaceFiltersState: SpaceFiltersState, val contentState: RoomListContentState, val acceptDeclineInviteState: AcceptDeclineInviteState, val hideInvitesAvatars: Boolean, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateProvider.kt index 1b61ba20c7..97600e0247 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateProvider.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateProvider.kt @@ -18,6 +18,8 @@ import io.element.android.features.home.impl.model.aRoomListRoomSummary import io.element.android.features.home.impl.model.anInviteSender import io.element.android.features.home.impl.search.RoomListSearchState import io.element.android.features.home.impl.search.aRoomListSearchState +import io.element.android.features.home.impl.spacefilters.SpaceFiltersState +import io.element.android.features.home.impl.spacefilters.anUnselectedSpaceFiltersState import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInviteState import io.element.android.features.leaveroom.api.LeaveRoomEvent @@ -52,6 +54,7 @@ internal fun aRoomListState( leaveRoomState: LeaveRoomState = aLeaveRoomState(), searchState: RoomListSearchState = aRoomListSearchState(), filtersState: RoomListFiltersState = aRoomListFiltersState(), + spaceFiltersState: SpaceFiltersState = anUnselectedSpaceFiltersState(), contentState: RoomListContentState = aRoomsContentState(), acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(), hideInvitesAvatars: Boolean = false, @@ -63,6 +66,7 @@ internal fun aRoomListState( leaveRoomState = leaveRoomState, filtersState = filtersState, searchState = searchState, + spaceFiltersState = spaceFiltersState, contentState = contentState, acceptDeclineInviteState = acceptDeclineInviteState, hideInvitesAvatars = hideInvitesAvatars, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersEvent.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersEvent.kt new file mode 100644 index 0000000000..b57b274cd5 --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersEvent.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.home.impl.spacefilters + +import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter + +sealed interface SpaceFiltersEvent { + // Only valid in Unselected state + sealed interface Unselected : SpaceFiltersEvent { + data object ShowFilters : Unselected + } + + // Only valid in Selecting state + sealed interface Selecting : SpaceFiltersEvent { + data object Cancel : Selecting + data class SelectFilter(val spaceFilter: SpaceServiceFilter) : Selecting + } + + // Only valid in Selected state + sealed interface Selected : SpaceFiltersEvent { + data object ClearSelection : Selected + } +} diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenter.kt new file mode 100644 index 0000000000..9813732bdb --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenter.kt @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.home.impl.spacefilters + +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import dev.zacsweers.metro.Inject +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.map + +@Inject +class SpaceFiltersPresenter( + private val featureFlagService: FeatureFlagService, + private val matrixClient: MatrixClient, +) : Presenter { + @Composable + override fun present(): SpaceFiltersState { + val isFeatureEnabled by featureFlagService + .isFeatureEnabledFlow(FeatureFlags.RoomListSpaceFilters) + .collectAsState(initial = false) + + val availableFilters by remember { + matrixClient.spaceService.spaceFiltersFlow.map { it.toImmutableList() } + }.collectAsState(initial = persistentListOf()) + + if (!isFeatureEnabled || availableFilters.isEmpty()) { + return SpaceFiltersState.Disabled + } + + var selectionMode by remember { mutableStateOf(SelectionMode.Unselected) } + + fun handleUnselectedEvent(event: SpaceFiltersEvent.Unselected) { + when (event) { + SpaceFiltersEvent.Unselected.ShowFilters -> { + selectionMode = SelectionMode.Selecting + } + } + } + + fun handleSelectingEvent(event: SpaceFiltersEvent.Selecting) { + when (event) { + SpaceFiltersEvent.Selecting.Cancel -> { + selectionMode = SelectionMode.Unselected + } + is SpaceFiltersEvent.Selecting.SelectFilter -> { + selectionMode = SelectionMode.Selected(event.spaceFilter) + } + } + } + + fun handleSelectedEvent(event: SpaceFiltersEvent.Selected) { + when (event) { + SpaceFiltersEvent.Selected.ClearSelection -> { + selectionMode = SelectionMode.Unselected + } + } + } + + return when (val mode = selectionMode) { + SelectionMode.Unselected -> SpaceFiltersState.Unselected( + eventSink = ::handleUnselectedEvent, + ) + SelectionMode.Selecting -> { + val searchQuery = rememberTextFieldState() + SpaceFiltersState.Selecting( + availableFilters = availableFilters, + searchQuery = searchQuery, + eventSink = ::handleSelectingEvent, + ) + } + is SelectionMode.Selected -> { + var selectedFilter by remember { mutableStateOf(mode.filter) } + // Makes sure the selectedFilter stays in sync with the available filters + LaunchedEffect(availableFilters) { + val upToDateFilter = availableFilters + .firstOrNull { it.spaceRoom.roomId == mode.filter.spaceRoom.roomId } + if (upToDateFilter == null) { + selectionMode = SelectionMode.Unselected + } else { + selectedFilter = upToDateFilter + } + } + SpaceFiltersState.Selected( + selectedFilter = selectedFilter, + eventSink = ::handleSelectedEvent, + ) + } + } + } +} + +private sealed interface SelectionMode { + data object Unselected : SelectionMode + data object Selecting : SelectionMode + data class Selected(val filter: SpaceServiceFilter) : SelectionMode +} diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersState.kt new file mode 100644 index 0000000000..347439e853 --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersState.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.home.impl.spacefilters + +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.runtime.Immutable +import io.element.android.libraries.matrix.api.roomlist.RoomListFilter +import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +@Immutable +sealed interface SpaceFiltersState { + data object Disabled : SpaceFiltersState + + data class Unselected( + val eventSink: (SpaceFiltersEvent.Unselected) -> Unit, + ) : SpaceFiltersState + + data class Selecting( + val availableFilters: ImmutableList, + val searchQuery: TextFieldState, + val eventSink: (SpaceFiltersEvent.Selecting) -> Unit, + ) : SpaceFiltersState { + val visibleFilters: ImmutableList + get() { + val query = searchQuery.text.toString() + if (query.isBlank()) return availableFilters + return availableFilters.filter { filter -> + filter.spaceRoom.displayName.contains(query, ignoreCase = true) || + (filter.spaceRoom.canonicalAlias?.value ?: "").contains(query, ignoreCase = true) + }.toImmutableList() + } + } + + data class Selected( + val selectedFilter: SpaceServiceFilter, + val eventSink: (SpaceFiltersEvent.Selected) -> Unit, + ) : SpaceFiltersState +} + +fun SpaceFiltersState.selectedFilter(): SpaceServiceFilter? { + return when (this) { + is SpaceFiltersState.Selected -> this.selectedFilter + else -> null + } +} + +fun SpaceServiceFilter?.into(): RoomListFilter? { + return this?.let { RoomListFilter.Identifiers(descendants) } +} diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersStateProvider.kt new file mode 100644 index 0000000000..264d122836 --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersStateProvider.kt @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.home.impl.spacefilters + +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.matrix.api.core.RoomAlias +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter +import io.element.android.libraries.previewutils.room.aSpaceRoom +import kotlinx.collections.immutable.toImmutableList + +class SpaceFiltersStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aSelectingSpaceFiltersState(), + aSelectingSpaceFiltersState(searchQuery = "Pr") + ) +} + +fun aDisabledSpaceFiltersState() = SpaceFiltersState.Disabled + +fun anUnselectedSpaceFiltersState( + eventSink: (SpaceFiltersEvent.Unselected) -> Unit = {}, +) = SpaceFiltersState.Unselected( + eventSink = eventSink, +) + +fun aSelectingSpaceFiltersState( + availableFilters: List = listOf( + aSpaceServiceFilter( + displayName = "Work", + canonicalAlias = RoomAlias("#work:example.com"), + ), + aSpaceServiceFilter( + displayName = "Personal", + roomId = RoomId("!personal:example.com"), + ), + aSpaceServiceFilter( + displayName = "Projects", + roomId = RoomId("!projects:example.com"), + canonicalAlias = RoomAlias("#projects:example.com"), + level = 1, + ), + aSpaceServiceFilter( + displayName = "Gaming", + roomId = RoomId("!gaming:example.com"), + ), + ), + searchQuery: String = "", + eventSink: (SpaceFiltersEvent.Selecting) -> Unit = {}, +) = SpaceFiltersState.Selecting( + availableFilters = availableFilters.toImmutableList(), + searchQuery = TextFieldState(searchQuery), + eventSink = eventSink, +) + +fun aSelectedSpaceFiltersState( + selectedFilter: SpaceServiceFilter = aSpaceServiceFilter(displayName = "Work"), + eventSink: (SpaceFiltersEvent.Selected) -> Unit = {}, +) = SpaceFiltersState.Selected( + selectedFilter = selectedFilter, + eventSink = eventSink, +) + +fun aSpaceServiceFilter( + displayName: String = "Space", + roomId: RoomId = RoomId("!space:example.com"), + canonicalAlias: RoomAlias? = null, + level: Int = 0, + descendants: List = emptyList(), +) = SpaceServiceFilter( + spaceRoom = aSpaceRoom(displayName = displayName, roomId = roomId, canonicalAlias = canonicalAlias), + level = level, + descendants = descendants, +) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt new file mode 100644 index 0000000000..fb77c74203 --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.home.impl.spacefilters + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SheetValue +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.features.home.impl.R +import io.element.android.libraries.designsystem.components.avatar.Avatar +import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.designsystem.components.avatar.AvatarType +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet +import io.element.android.libraries.designsystem.theme.components.SearchField +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter +import io.element.android.libraries.matrix.ui.model.getAvatarData +import io.element.android.libraries.ui.strings.CommonStrings + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SpaceFiltersView( + state: SpaceFiltersState, + modifier: Modifier = Modifier +) { + val isSelecting by rememberUpdatedState(state is SpaceFiltersState.Selecting) + val sheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = true, + confirmValueChange = { sheetValueTarget -> + // This ensures the hide animation is not cancelled + when (sheetValueTarget) { + SheetValue.Expanded -> isSelecting + else -> true + } + } + ) + LaunchedEffect(isSelecting) { + if (!isSelecting) { + sheetState.hide() + } + } + if (sheetState.isVisible || isSelecting) { + ModalBottomSheet( + modifier = modifier + .systemBarsPadding() + .navigationBarsPadding(), + sheetState = sheetState, + onDismissRequest = { + if (state is SpaceFiltersState.Selecting) { + state.eventSink(SpaceFiltersEvent.Selecting.Cancel) + } + } + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight(0.9f) + ) { + if (state is SpaceFiltersState.Selecting) { + SpaceFiltersBottomSheetContent( + filters = state.visibleFilters, + searchQuery = state.searchQuery, + onFilterSelected = { filter -> + state.eventSink(SpaceFiltersEvent.Selecting.SelectFilter(filter)) + } + ) + } + } + } + } +} + +@Composable +private fun SpaceFiltersBottomSheetContent( + filters: List, + searchQuery: TextFieldState, + onFilterSelected: (SpaceServiceFilter) -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier.padding(vertical = 16.dp) + ) { + Text( + text = stringResource(R.string.screen_roomlist_your_spaces), + style = ElementTheme.typography.fontHeadingSmMedium, + modifier = Modifier.padding(horizontal = 16.dp), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Spacer(modifier = Modifier.height(12.dp)) + SearchField( + state = searchQuery, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + placeholder = stringResource(CommonStrings.action_search), + ) + Spacer(modifier = Modifier.height(16.dp)) + LazyColumn { + items(filters) { filter -> + SpaceFilterItem( + filter = filter, + onClick = { onFilterSelected(filter) } + ) + } + } + } +} + +@Composable +private fun SpaceFilterItem( + filter: SpaceServiceFilter, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + val spaceRoom = filter.spaceRoom + val supportingText = spaceRoom.canonicalAlias?.value + + Row( + modifier = modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + // Level-based indentation + Spacer(modifier = Modifier.width((16 * filter.level).dp)) + Avatar( + avatarData = spaceRoom.getAvatarData(AvatarSize.RoomSelectRoomListItem), + avatarType = AvatarType.Space(), + ) + Spacer(modifier = Modifier.width(16.dp)) + Column { + Text( + text = spaceRoom.displayName, + style = ElementTheme.typography.fontBodyLgMedium, + color = ElementTheme.colors.textPrimary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + if (supportingText != null) { + Text( + text = supportingText, + style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textSecondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + } +} + +@PreviewsDayNight +@Composable +internal fun SpaceFiltersViewPreview(@PreviewParameter(SpaceFiltersStateProvider::class) state: SpaceFiltersState) = ElementPreview { + SpaceFiltersView(state = state) +} diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenter.kt index d9e6aaa4d3..707ac73261 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenter.kt @@ -36,7 +36,7 @@ class HomeSpacesPresenter( val canCreateSpaces by featureFlagsService.isFeatureEnabledFlow(FeatureFlags.CreateSpaces).collectAsState(false) val hideInvitesAvatar by client.rememberHideInvitesAvatar() val spaceRooms by remember { - client.spaceService.spaceRoomsFlow.map { it.toImmutableList() } + client.spaceService.topLevelSpacesFlow.map { it.toImmutableList() } }.collectAsState(persistentListOf()) val seenSpaceInvites by remember { diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEmptyStateResourcesTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEmptyStateResourcesTest.kt index 250f43ee8f..1762d0d6bf 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEmptyStateResourcesTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersEmptyStateResourcesTest.kt @@ -16,14 +16,14 @@ class RoomListFiltersEmptyStateResourcesTest { @Test fun `fromSelectedFilters should return null when selectedFilters is empty`() { val selectedFilters = emptyList() - val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters) + val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters, isSpaceFilterSelected = false) assertThat(result).isNull() } @Test fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has only unread filter`() { val selectedFilters = listOf(RoomListFilter.Unread) - val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters) + val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters, isSpaceFilterSelected = false) assertThat(result).isNotNull() assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_unreads_empty_state_title) assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_subtitle) @@ -32,7 +32,7 @@ class RoomListFiltersEmptyStateResourcesTest { @Test fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has only people filter`() { val selectedFilters = listOf(RoomListFilter.People) - val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters) + val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters, isSpaceFilterSelected = false) assertThat(result).isNotNull() assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_people_empty_state_title) assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_subtitle) @@ -41,7 +41,7 @@ class RoomListFiltersEmptyStateResourcesTest { @Test fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has only rooms filter`() { val selectedFilters = listOf(RoomListFilter.Rooms) - val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters) + val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters, isSpaceFilterSelected = false) assertThat(result).isNotNull() assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_rooms_empty_state_title) assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_subtitle) @@ -50,7 +50,7 @@ class RoomListFiltersEmptyStateResourcesTest { @Test fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has only favourites filter`() { val selectedFilters = listOf(RoomListFilter.Favourites) - val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters) + val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters, isSpaceFilterSelected = false) assertThat(result).isNotNull() assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_favourites_empty_state_title) assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_favourites_empty_state_subtitle) @@ -59,7 +59,7 @@ class RoomListFiltersEmptyStateResourcesTest { @Test fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has only invites filter`() { val selectedFilters = listOf(RoomListFilter.Invites) - val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters) + val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters, isSpaceFilterSelected = false) assertThat(result).isNotNull() assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_invites_empty_state_title) assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_subtitle) @@ -68,7 +68,15 @@ class RoomListFiltersEmptyStateResourcesTest { @Test fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has multiple filters`() { val selectedFilters = listOf(RoomListFilter.Unread, RoomListFilter.People, RoomListFilter.Rooms, RoomListFilter.Favourites) - val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters) + val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters, isSpaceFilterSelected = false) + assertThat(result).isNotNull() + assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_title) + assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_subtitle) + } + + @Test + fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when isSpaceFilterSelected is true`() { + val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(emptyList(), isSpaceFilterSelected = true) assertThat(result).isNotNull() assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_title) assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_subtitle) diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenterTest.kt index c30e279b2e..9fb87c0eec 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenterTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersPresenterTest.kt @@ -9,23 +9,10 @@ package io.element.android.features.home.impl.filters import com.google.common.truth.Truth.assertThat -import io.element.android.features.home.impl.FakeDateTimeObserver -import io.element.android.features.home.impl.datasource.RoomListDataSource -import io.element.android.features.home.impl.datasource.aRoomListRoomSummaryFactory import io.element.android.features.home.impl.filters.selection.DefaultFilterSelectionStrategy import io.element.android.features.home.impl.filters.selection.FilterSelectionState -import io.element.android.libraries.dateformatter.api.DateFormatter -import io.element.android.libraries.dateformatter.test.FakeDateFormatter -import io.element.android.libraries.eventformatter.api.RoomLatestEventFormatter -import io.element.android.libraries.eventformatter.test.FakeRoomLatestEventFormatter -import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService -import io.element.android.libraries.matrix.api.roomlist.RoomListService -import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService -import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService -import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.awaitLastSequentialItem import io.element.android.tests.testutils.test -import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceUntilIdle @@ -54,8 +41,7 @@ class RoomListFiltersPresenterTest { @Test @OptIn(ExperimentalCoroutinesApi::class) fun `present - toggle rooms filter`() = runTest { - val roomListService = FakeRoomListService() - val presenter = createRoomListFiltersPresenter(roomListService) + val presenter = createRoomListFiltersPresenter() presenter.test { awaitItem().eventSink.invoke(RoomListFiltersEvent.ToggleFilter(RoomListFilter.Rooms)) awaitLastSequentialItem().let { state -> @@ -89,8 +75,7 @@ class RoomListFiltersPresenterTest { @Test @OptIn(ExperimentalCoroutinesApi::class) fun `present - clear filters event`() = runTest { - val roomListService = FakeRoomListService() - val presenter = createRoomListFiltersPresenter(roomListService) + val presenter = createRoomListFiltersPresenter() presenter.test { awaitItem().eventSink.invoke(RoomListFiltersEvent.ToggleFilter(RoomListFilter.Rooms)) awaitLastSequentialItem().let { state -> @@ -110,25 +95,8 @@ private fun filterSelectionState(filter: RoomListFilter, selected: Boolean) = Fi isSelected = selected, ) -private fun TestScope.createRoomListFiltersPresenter( - roomListService: RoomListService = FakeRoomListService(), - notificationSettingsService: NotificationSettingsService = FakeNotificationSettingsService(), - dateFormatter: DateFormatter = FakeDateFormatter(), - roomLatestEventFormatter: RoomLatestEventFormatter = FakeRoomLatestEventFormatter(), -): RoomListFiltersPresenter { +private fun TestScope.createRoomListFiltersPresenter(): RoomListFiltersPresenter { return RoomListFiltersPresenter( - roomListDataSource = RoomListDataSource( - roomListService = roomListService, - roomListRoomSummaryFactory = aRoomListRoomSummaryFactory( - dateFormatter = dateFormatter, - roomLatestEventFormatter = roomLatestEventFormatter, - ), - coroutineDispatchers = testCoroutineDispatchers(), - notificationSettingsService = notificationSettingsService, - sessionCoroutineScope = backgroundScope, - dateTimeObserver = FakeDateTimeObserver(), - analyticsService = FakeAnalyticsService(), - ), filterSelectionStrategy = DefaultFilterSelectionStrategy(), ) } diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt index 9ee95cc811..99a5550a08 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt @@ -21,6 +21,8 @@ import io.element.android.features.home.impl.model.createRoomListRoomSummary import io.element.android.features.home.impl.search.RoomListSearchEvent import io.element.android.features.home.impl.search.RoomListSearchState import io.element.android.features.home.impl.search.aRoomListSearchState +import io.element.android.features.home.impl.spacefilters.SpaceFiltersState +import io.element.android.features.home.impl.spacefilters.aDisabledSpaceFiltersState import io.element.android.features.invite.api.SeenInvitesStore import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState @@ -660,6 +662,7 @@ class RoomListPresenterTest { analyticsService: AnalyticsService = FakeAnalyticsService(), filtersPresenter: Presenter = Presenter { aRoomListFiltersState() }, searchPresenter: Presenter = Presenter { aRoomListSearchState() }, + spaceFiltersPresenter: Presenter = Presenter { aDisabledSpaceFiltersState() }, acceptDeclineInvitePresenter: Presenter = Presenter { anAcceptDeclineInviteState() }, notificationCleaner: NotificationCleaner = FakeNotificationCleaner(), appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(), @@ -683,6 +686,7 @@ class RoomListPresenterTest { searchPresenter = searchPresenter, sessionPreferencesStore = sessionPreferencesStore, filtersPresenter = filtersPresenter, + spaceFiltersPresenter = spaceFiltersPresenter, analyticsService = analyticsService, acceptDeclineInvitePresenter = acceptDeclineInvitePresenter, fullScreenIntentPermissionsPresenter = { aFullScreenIntentPermissionsState() }, diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenterTest.kt new file mode 100644 index 0000000000..278a268864 --- /dev/null +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersPresenterTest.kt @@ -0,0 +1,313 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.home.impl.spacefilters + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.spaces.FakeSpaceService +import io.element.android.tests.testutils.awaitLastSequentialItem +import io.element.android.tests.testutils.test +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class SpaceFiltersPresenterTest { + @Test + fun `present - when feature flag is disabled returns Disabled state`() = runTest { + val presenter = createSpaceFiltersPresenter( + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to false) + ) + ) + presenter.test { + val state = awaitItem() + assertThat(state).isEqualTo(SpaceFiltersState.Disabled) + } + } + + @Test + fun `present - when available filters is empty returns Disabled state`() = runTest { + val presenter = createSpaceFiltersPresenter( + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true) + ) + ) + presenter.test { + val state = awaitLastSequentialItem() + assertThat(state).isEqualTo(SpaceFiltersState.Disabled) + } + } + + @Test + fun `present - when feature flag is enabled and filters exist returns Unselected state`() = runTest { + val spaceFilter = aSpaceServiceFilter(displayName = "Test Space") + val spaceService = FakeSpaceService() + val matrixClient = FakeMatrixClient(spaceService = spaceService) + + val presenter = createSpaceFiltersPresenter( + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true) + ), + matrixClient = matrixClient, + ) + presenter.test { + // Emit filters + spaceService.emitSpaceFilters(listOf(spaceFilter)) + + val state = awaitLastSequentialItem() + assertThat(state).isInstanceOf(SpaceFiltersState.Unselected::class.java) + } + } + + @Test + fun `present - ShowFilters event transitions from Unselected to Selecting`() = runTest { + val spaceFilter = aSpaceServiceFilter(displayName = "Test Space") + val spaceService = FakeSpaceService() + val matrixClient = FakeMatrixClient(spaceService = spaceService) + + val presenter = createSpaceFiltersPresenter( + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true) + ), + matrixClient = matrixClient, + ) + presenter.test { + // Emit filters first + spaceService.emitSpaceFilters(listOf(spaceFilter)) + + val unselectedState = awaitLastSequentialItem() as SpaceFiltersState.Unselected + unselectedState.eventSink(SpaceFiltersEvent.Unselected.ShowFilters) + + val selectingState = awaitLastSequentialItem() + assertThat(selectingState).isInstanceOf(SpaceFiltersState.Selecting::class.java) + } + } + + @Test + fun `present - Cancel event in Selecting state transitions back to Unselected`() = runTest { + val spaceFilter = aSpaceServiceFilter(displayName = "Test Space") + val spaceService = FakeSpaceService() + val matrixClient = FakeMatrixClient(spaceService = spaceService) + + val presenter = createSpaceFiltersPresenter( + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true) + ), + matrixClient = matrixClient, + ) + presenter.test { + // Emit filters first + spaceService.emitSpaceFilters(listOf(spaceFilter)) + + // Start in Unselected + val unselectedState = awaitLastSequentialItem() as SpaceFiltersState.Unselected + unselectedState.eventSink(SpaceFiltersEvent.Unselected.ShowFilters) + + // Now in Selecting + val selectingState = awaitLastSequentialItem() as SpaceFiltersState.Selecting + selectingState.eventSink(SpaceFiltersEvent.Selecting.Cancel) + + // Back to Unselected + val finalState = awaitLastSequentialItem() + assertThat(finalState).isInstanceOf(SpaceFiltersState.Unselected::class.java) + } + } + + @Test + fun `present - SelectFilter event in Selecting state transitions to Selected`() = runTest { + val spaceFilter = aSpaceServiceFilter(displayName = "Test Space") + val spaceService = FakeSpaceService() + val matrixClient = FakeMatrixClient(spaceService = spaceService) + + val presenter = createSpaceFiltersPresenter( + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true) + ), + matrixClient = matrixClient, + ) + presenter.test { + // Emit filters first + spaceService.emitSpaceFilters(listOf(spaceFilter)) + + // Start in Unselected + val unselectedState = awaitLastSequentialItem() as SpaceFiltersState.Unselected + unselectedState.eventSink(SpaceFiltersEvent.Unselected.ShowFilters) + + // Now in Selecting + val selectingState = awaitLastSequentialItem() as SpaceFiltersState.Selecting + selectingState.eventSink(SpaceFiltersEvent.Selecting.SelectFilter(spaceFilter)) + + // Now in Selected + val selectedState = awaitLastSequentialItem() as SpaceFiltersState.Selected + assertThat(selectedState.selectedFilter).isEqualTo(spaceFilter) + } + } + + @Test + fun `present - ClearSelection event in Selected state transitions back to Unselected`() = runTest { + val spaceFilter = aSpaceServiceFilter(displayName = "Test Space") + val spaceService = FakeSpaceService() + val matrixClient = FakeMatrixClient(spaceService = spaceService) + + val presenter = createSpaceFiltersPresenter( + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true) + ), + matrixClient = matrixClient, + ) + presenter.test { + // Emit filters first + spaceService.emitSpaceFilters(listOf(spaceFilter)) + + // Start in Unselected + val unselectedState = awaitLastSequentialItem() as SpaceFiltersState.Unselected + unselectedState.eventSink(SpaceFiltersEvent.Unselected.ShowFilters) + + // Now in Selecting + val selectingState = awaitLastSequentialItem() as SpaceFiltersState.Selecting + selectingState.eventSink(SpaceFiltersEvent.Selecting.SelectFilter(spaceFilter)) + + // Now in Selected + val selectedState = awaitLastSequentialItem() as SpaceFiltersState.Selected + selectedState.eventSink(SpaceFiltersEvent.Selected.ClearSelection) + + // Back to Unselected + val finalState = awaitLastSequentialItem() + assertThat(finalState).isInstanceOf(SpaceFiltersState.Unselected::class.java) + } + } + + @Test + fun `present - available filters are passed from SpaceService`() = runTest { + val spaceFilter1 = aSpaceServiceFilter(displayName = "Work", roomId = RoomId("!work:example.com")) + val spaceFilter2 = aSpaceServiceFilter(displayName = "Personal", roomId = RoomId("!personal:example.com")) + val spaceFilters = listOf(spaceFilter1, spaceFilter2) + + val spaceService = FakeSpaceService() + val matrixClient = FakeMatrixClient(spaceService = spaceService) + + val presenter = createSpaceFiltersPresenter( + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true) + ), + matrixClient = matrixClient, + ) + presenter.test { + // Emit space filters + spaceService.emitSpaceFilters(spaceFilters) + + // Start in Unselected + val unselectedState = awaitLastSequentialItem() as SpaceFiltersState.Unselected + unselectedState.eventSink(SpaceFiltersEvent.Unselected.ShowFilters) + + // Now in Selecting with available filters + val selectingState = awaitLastSequentialItem() as SpaceFiltersState.Selecting + assertThat(selectingState.availableFilters).containsExactly(spaceFilter1, spaceFilter2).inOrder() + } + } + + @Test + fun `present - selected filter is cleared when space is removed from available filters`() = runTest { + val spaceFilter = aSpaceServiceFilter(displayName = "Work", roomId = RoomId("!work:example.com")) + val otherSpaceFilter = aSpaceServiceFilter(displayName = "Personal", roomId = RoomId("!personal:example.com")) + + val spaceService = FakeSpaceService() + val matrixClient = FakeMatrixClient(spaceService = spaceService) + + val presenter = createSpaceFiltersPresenter( + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true) + ), + matrixClient = matrixClient, + ) + presenter.test { + // Emit filters first + spaceService.emitSpaceFilters(listOf(spaceFilter, otherSpaceFilter)) + + // Go to Selecting + val unselectedState = awaitLastSequentialItem() as SpaceFiltersState.Unselected + unselectedState.eventSink(SpaceFiltersEvent.Unselected.ShowFilters) + + // Select the filter + val selectingState = awaitLastSequentialItem() as SpaceFiltersState.Selecting + selectingState.eventSink(SpaceFiltersEvent.Selecting.SelectFilter(spaceFilter)) + + // Verify in Selected state + val selectedState = awaitLastSequentialItem() as SpaceFiltersState.Selected + assertThat(selectedState.selectedFilter).isEqualTo(spaceFilter) + + // Remove the selected space from available filters (but keep other spaces) + spaceService.emitSpaceFilters(listOf(otherSpaceFilter)) + + // Should auto-transition to Unselected + val finalState = awaitLastSequentialItem() + assertThat(finalState).isInstanceOf(SpaceFiltersState.Unselected::class.java) + } + } + + @Test + fun `present - selected filter stays in sync when available filters update`() = runTest { + val originalFilter = aSpaceServiceFilter( + displayName = "Work", + roomId = RoomId("!work:example.com"), + descendants = listOf(RoomId("!room1:example.com")) + ) + val updatedFilter = aSpaceServiceFilter( + displayName = "Work", + roomId = RoomId("!work:example.com"), + descendants = listOf(RoomId("!room1:example.com"), RoomId("!room2:example.com")) + ) + + val spaceService = FakeSpaceService() + val matrixClient = FakeMatrixClient(spaceService = spaceService) + + val presenter = createSpaceFiltersPresenter( + featureFlagService = FakeFeatureFlagService( + initialState = mapOf(FeatureFlags.RoomListSpaceFilters.key to true) + ), + matrixClient = matrixClient, + ) + presenter.test { + // Emit initial space filters + spaceService.emitSpaceFilters(listOf(originalFilter)) + + // Start in Unselected + val unselectedState = awaitLastSequentialItem() as SpaceFiltersState.Unselected + unselectedState.eventSink(SpaceFiltersEvent.Unselected.ShowFilters) + + // Now in Selecting + val selectingState = awaitLastSequentialItem() as SpaceFiltersState.Selecting + selectingState.eventSink(SpaceFiltersEvent.Selecting.SelectFilter(originalFilter)) + + // Now in Selected + val selectedState = awaitLastSequentialItem() as SpaceFiltersState.Selected + assertThat(selectedState.selectedFilter.descendants).hasSize(1) + + // Emit updated space filters + spaceService.emitSpaceFilters(listOf(updatedFilter)) + + // Selected filter should be updated + val updatedSelectedState = awaitLastSequentialItem() as SpaceFiltersState.Selected + assertThat(updatedSelectedState.selectedFilter.descendants).hasSize(2) + } + } + + private fun createSpaceFiltersPresenter( + featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(), + matrixClient: FakeMatrixClient = FakeMatrixClient(), + ): SpaceFiltersPresenter { + return SpaceFiltersPresenter( + featureFlagService = featureFlagService, + matrixClient = matrixClient, + ) + } +} diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersViewTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersViewTest.kt new file mode 100644 index 0000000000..5c1325b107 --- /dev/null +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersViewTest.kt @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.home.impl.spacefilters + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.libraries.matrix.test.A_ROOM_ALIAS +import io.element.android.tests.testutils.EventsRecorder +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class SpaceFiltersViewTest { + @get:Rule + val rule = createAndroidComposeRule() + + @Test + fun `clicking on a filter with alias shows display name and alias`() { + val filter = aSpaceServiceFilter( + displayName = "Test Space", + canonicalAlias = A_ROOM_ALIAS, + ) + val eventsRecorder = EventsRecorder() + rule.setSpaceFiltersView( + state = aSelectingSpaceFiltersState( + availableFilters = listOf(filter), + eventSink = eventsRecorder, + ) + ) + + // Both display name and alias should be visible + rule.onNodeWithText(filter.spaceRoom.displayName).assertExists() + rule.onNodeWithText(A_ROOM_ALIAS.value).assertExists() + + rule.onNodeWithText(filter.spaceRoom.displayName).performClick() + + eventsRecorder.assertSingle(SpaceFiltersEvent.Selecting.SelectFilter(filter)) + } + + @Test + fun `multiple filters are displayed and clickable`() { + val filter1 = aSpaceServiceFilter(displayName = "Space One") + val filter2 = aSpaceServiceFilter(displayName = "Space Two") + val eventsRecorder = EventsRecorder() + rule.setSpaceFiltersView( + state = aSelectingSpaceFiltersState( + availableFilters = listOf(filter1, filter2), + eventSink = eventsRecorder, + ) + ) + + // Both filters should be visible + rule.onNodeWithText(filter1.spaceRoom.displayName).assertExists() + rule.onNodeWithText(filter2.spaceRoom.displayName).assertExists() + + // Click on second filter + rule.onNodeWithText(filter2.spaceRoom.displayName).performClick() + + eventsRecorder.assertSingle(SpaceFiltersEvent.Selecting.SelectFilter(filter2)) + } +} + +private fun AndroidComposeTestRule.setSpaceFiltersView( + state: SpaceFiltersState, +) { + setContent { + SpaceFiltersView(state = state) + } +} diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt index 4422330924..46f39b2e99 100644 --- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt +++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt @@ -84,6 +84,13 @@ enum class FeatureFlags( defaultValue = { false }, isFinished = false, ), + RoomListSpaceFilters( + key = "feature.roomListSpaceFilters", + title = "Room list space filters", + description = "Allow filtering the room list by space.", + defaultValue = { false }, + isFinished = false, + ), PrintLogsToLogcat( key = "feature.print_logs_to_logcat", title = "Print logs to logcat", diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListFilter.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListFilter.kt index 3c6e35d339..11eed2128b 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListFilter.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListFilter.kt @@ -8,6 +8,8 @@ package io.element.android.libraries.matrix.api.roomlist +import io.element.android.libraries.matrix.api.core.RoomId + sealed interface RoomListFilter { companion object { /** @@ -41,6 +43,10 @@ sealed interface RoomListFilter { val filters: List ) : RoomListFilter + data class Identifiers( + val values: List, + ) : RoomListFilter + /** * A filter that matches rooms that are unread. */ diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceService.kt index 1122415d58..299209e188 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceService.kt @@ -12,9 +12,8 @@ import io.element.android.libraries.matrix.api.core.RoomId import kotlinx.coroutines.flow.SharedFlow interface SpaceService { - val spaceRoomsFlow: SharedFlow> - suspend fun joinedSpaces(): Result> - + val topLevelSpacesFlow: SharedFlow> + val spaceFiltersFlow: SharedFlow> suspend fun joinedParents(spaceId: RoomId): Result> suspend fun getSpaceRoom(spaceId: RoomId): SpaceRoom? diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceServiceFilter.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceServiceFilter.kt new file mode 100644 index 0000000000..e599353876 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceServiceFilter.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.api.spaces + +import io.element.android.libraries.matrix.api.core.RoomId + +/** + * Represents a space filter for filtering rooms by space membership. + * + * @property spaceRoom The space room associated with this filter. + * @property level The nesting level of the space (0 = top level, 1 = first level child, etc.). + * @property descendants The list of room IDs that are descendants of this space. + */ +data class SpaceServiceFilter( + val spaceRoom: SpaceRoom, + val level: Int, + val descendants: List, +) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFilterMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFilterMapper.kt index fdee790b19..648376a9cc 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFilterMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFilterMapper.kt @@ -14,6 +14,7 @@ import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.Any import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.Category import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.DeduplicateVersions import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.Favourite +import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.Identifiers import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.Invite import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.NonLeft import org.matrix.rustcomponents.sdk.RoomListEntriesDynamicFilterKind.NonSpace @@ -60,6 +61,7 @@ internal object RoomListFilterMapper { return when (filter) { is RoomListFilter.All -> All(filters = filter.filters.map { mapFilter(it) }) is RoomListFilter.Any -> Any(filters = filter.filters.map { mapFilter(it) }) + is RoomListFilter.Identifiers -> Identifiers(identifiers = filter.values.map { it.value }) RoomListFilter.None -> None RoomListFilter.Category.Group -> Category(RoomListFilterCategory.GROUP) RoomListFilter.Category.People -> Category(RoomListFilterCategory.PEOPLE) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceService.kt index 0bf677e09d..0f2c92dae2 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceService.kt @@ -16,6 +16,7 @@ import io.element.android.libraries.matrix.api.spaces.LeaveSpaceHandle import io.element.android.libraries.matrix.api.spaces.SpaceRoom import io.element.android.libraries.matrix.api.spaces.SpaceRoomList import io.element.android.libraries.matrix.api.spaces.SpaceService +import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter import io.element.android.libraries.matrix.impl.util.cancelAndDestroy import io.element.android.services.analytics.api.AnalyticsService import kotlinx.coroutines.CoroutineDispatcher @@ -31,9 +32,11 @@ import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.withContext +import org.matrix.rustcomponents.sdk.SpaceFilterUpdate import org.matrix.rustcomponents.sdk.SpaceListUpdate import org.matrix.rustcomponents.sdk.SpaceServiceInterface import org.matrix.rustcomponents.sdk.SpaceServiceJoinedSpacesListener +import org.matrix.rustcomponents.sdk.SpaceServiceSpaceFiltersListener import timber.log.Timber import org.matrix.rustcomponents.sdk.SpaceService as ClientSpaceService @@ -45,20 +48,20 @@ class RustSpaceService( private val analyticsService: AnalyticsService, ) : SpaceService { private val spaceRoomMapper = SpaceRoomMapper() - override val spaceRoomsFlow = MutableSharedFlow>(replay = 1, extraBufferCapacity = 1) + private val spaceFilterMapper = SpaceServiceFilterMapper(spaceRoomMapper) + + override val topLevelSpacesFlow = MutableSharedFlow>(replay = 1, extraBufferCapacity = 1) private val spaceListUpdateProcessor = SpaceListUpdateProcessor( - spaceRoomsFlow = spaceRoomsFlow, + spaceRoomsFlow = topLevelSpacesFlow, mapper = spaceRoomMapper, analyticsService = analyticsService, ) - override suspend fun joinedSpaces(): Result> = withContext(sessionDispatcher) { - runCatchingExceptions { - innerSpaceService - .topLevelJoinedSpaces() - .map(spaceRoomMapper::map) - } - } + override val spaceFiltersFlow = MutableSharedFlow>(replay = 1, extraBufferCapacity = 1) + private val spaceFilterUpdateProcessor = SpaceServiceFilterUpdateProcessor( + spaceFiltersFlow = spaceFiltersFlow, + mapper = spaceFilterMapper, + ) override suspend fun joinedParents(spaceId: RoomId): Result> = withContext(sessionDispatcher) { runCatchingExceptions { @@ -123,6 +126,13 @@ class RustSpaceService( spaceListUpdateProcessor.postUpdates(updates) } .launchIn(sessionCoroutineScope) + + innerSpaceService + .spaceFilterListUpdate() + .onEach { updates -> + spaceFilterUpdateProcessor.postUpdates(updates) + } + .launchIn(sessionCoroutineScope) } } @@ -142,3 +152,20 @@ internal fun SpaceServiceInterface.spaceListUpdate(): Flow }.catch { Timber.d(it, "spaceDiffFlow() failed") }.buffer(Channel.UNLIMITED) + +internal fun SpaceServiceInterface.spaceFilterListUpdate(): Flow> = + callbackFlow { + val listener = object : SpaceServiceSpaceFiltersListener { + override fun onUpdate(filterUpdates: List) { + trySendBlocking(filterUpdates) + } + } + Timber.d("Open spaceFilterDiffFlow for SpaceServiceInterface ${this@spaceFilterListUpdate}") + val taskHandle = subscribeToSpaceFilters(listener) + awaitClose { + Timber.d("Close spaceFilterDiffFlow for SpaceServiceInterface ${this@spaceFilterListUpdate}") + taskHandle.cancelAndDestroy() + } + }.catch { + Timber.d(it, "spaceFilterListUpdate() failed") + }.buffer(Channel.UNLIMITED) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceServiceFilterMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceServiceFilterMapper.kt new file mode 100644 index 0000000000..50c06ac002 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceServiceFilterMapper.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.spaces + +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter +import org.matrix.rustcomponents.sdk.SpaceFilter as RustSpaceFilter + +class SpaceServiceFilterMapper( + private val spaceRoomMapper: SpaceRoomMapper, +) { + fun map(spaceFilter: RustSpaceFilter): SpaceServiceFilter { + return SpaceServiceFilter( + spaceRoom = spaceRoomMapper.map(spaceFilter.spaceRoom), + level = spaceFilter.level.toInt(), + descendants = spaceFilter.descendants.map { RoomId(it) }, + ) + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceServiceFilterUpdateProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceServiceFilterUpdateProcessor.kt new file mode 100644 index 0000000000..6d037b725c --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceServiceFilterUpdateProcessor.kt @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.spaces + +import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import org.matrix.rustcomponents.sdk.SpaceFilterUpdate +import timber.log.Timber + +internal class SpaceServiceFilterUpdateProcessor( + private val spaceFiltersFlow: MutableSharedFlow>, + private val mapper: SpaceServiceFilterMapper, +) { + private val mutex = Mutex() + + suspend fun postUpdates(updates: List) { + Timber.v("Update space filters from postUpdates (with ${updates.size} items) on ${Thread.currentThread()}") + updateSpaceFilters { + updates.forEach { update -> applyUpdate(update) } + } + } + + private suspend fun updateSpaceFilters(block: MutableList.() -> Unit) = + mutex.withLock { + val spaceFilters = if (spaceFiltersFlow.replayCache.isNotEmpty()) { + spaceFiltersFlow.first().toMutableList() + } else { + mutableListOf() + } + block(spaceFilters) + spaceFiltersFlow.emit(spaceFilters) + } + + private fun MutableList.applyUpdate(update: SpaceFilterUpdate) { + when (update) { + is SpaceFilterUpdate.Append -> { + val newFilters = update.values.map(mapper::map) + addAll(newFilters) + } + SpaceFilterUpdate.Clear -> clear() + is SpaceFilterUpdate.Insert -> { + val newFilter = mapper.map(update.value) + add(update.index.toInt(), newFilter) + } + SpaceFilterUpdate.PopBack -> { + removeAt(lastIndex) + } + SpaceFilterUpdate.PopFront -> { + removeAt(0) + } + is SpaceFilterUpdate.PushBack -> { + val newFilter = mapper.map(update.value) + add(newFilter) + } + is SpaceFilterUpdate.PushFront -> { + val newFilter = mapper.map(update.value) + add(0, newFilter) + } + is SpaceFilterUpdate.Remove -> { + removeAt(update.index.toInt()) + } + is SpaceFilterUpdate.Reset -> { + clear() + val newFilters = update.values.map(mapper::map) + addAll(newFilters) + } + is SpaceFilterUpdate.Set -> { + val newFilter = mapper.map(update.value) + this[update.index.toInt()] = newFilter + } + is SpaceFilterUpdate.Truncate -> { + subList(update.length.toInt(), size).clear() + } + } + } +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/spaces/FakeSpaceService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/spaces/FakeSpaceService.kt index b7a40fdeef..f6deef5c9a 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/spaces/FakeSpaceService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/spaces/FakeSpaceService.kt @@ -13,6 +13,7 @@ import io.element.android.libraries.matrix.api.spaces.LeaveSpaceHandle import io.element.android.libraries.matrix.api.spaces.SpaceRoom import io.element.android.libraries.matrix.api.spaces.SpaceRoomList import io.element.android.libraries.matrix.api.spaces.SpaceService +import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.simulateLongTask import kotlinx.coroutines.flow.MutableSharedFlow @@ -20,7 +21,6 @@ import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow class FakeSpaceService( - private val joinedSpacesResult: () -> Result> = { lambdaError() }, private val spaceRoomListResult: (RoomId) -> SpaceRoomList = { lambdaError() }, private val leaveSpaceHandleResult: (RoomId) -> LeaveSpaceHandle = { lambdaError() }, private val removeChildFromSpaceResult: (RoomId, RoomId) -> Result = { _, _ -> lambdaError() }, @@ -29,16 +29,20 @@ class FakeSpaceService( private val editableSpacesResult: () -> Result> = { lambdaError() }, private val addChildToSpaceResult: (RoomId, RoomId) -> Result = { _, _ -> lambdaError() }, ) : SpaceService { - private val _spaceRoomsFlow = MutableSharedFlow>() - override val spaceRoomsFlow: SharedFlow> - get() = _spaceRoomsFlow.asSharedFlow() + private val _topLevelSpacesFlow = MutableSharedFlow>() + override val topLevelSpacesFlow: SharedFlow> + get() = _topLevelSpacesFlow.asSharedFlow() - suspend fun emitSpaceRoomList(value: List) { - _spaceRoomsFlow.emit(value) + suspend fun emitTopLevelSpaces(value: List) { + _topLevelSpacesFlow.emit(value) } - override suspend fun joinedSpaces(): Result> = simulateLongTask { - return joinedSpacesResult() + private val _spaceFiltersFlow = MutableSharedFlow>() + override val spaceFiltersFlow: SharedFlow> + get() = _spaceFiltersFlow.asSharedFlow() + + suspend fun emitSpaceFilters(value: List) { + _spaceFiltersFlow.emit(value) } override suspend fun joinedParents(spaceId: RoomId): Result> { diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt index 245f841d1a..261ec3834d 100644 --- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt @@ -89,6 +89,7 @@ class KonsistPreviewTest { "GradientFloatingActionButtonCircleShapePreview", "HeaderFooterPageScrollablePreview", "HomeTopBarMultiAccountPreview", + "HomeTopBarSpaceFiltersSelectedPreview", "HomeTopBarSpacesPreview", "HomeTopBarWithIndicatorPreview", "IconsOtherPreview", diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarMultiAccount_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarMultiAccount_Day_0_en.png index ea9abe74c0..1d20b765e7 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarMultiAccount_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarMultiAccount_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6992a2fbaebed88e22c58e393d086bbcba50afd822ac869b9e27dc68ed3a493e -size 22007 +oid sha256:d03c47b707264a6aad1ff7f75419eee07a379c51cbcb07ac504d48983b362225 +size 22182 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarMultiAccount_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarMultiAccount_Night_0_en.png index e3f6f0ea4e..e88eb16bfa 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarMultiAccount_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarMultiAccount_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:67a87f2231268e26e06e62aada18c4c62e8ee53e1eb1ddc538f8c7e98881acbc -size 20256 +oid sha256:af5561d67d9d28418c1f9d106527c674ffbd63aacc793e371942e9216d2aadc8 +size 20425 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarSpaceFiltersSelected_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarSpaceFiltersSelected_Day_0_en.png new file mode 100644 index 0000000000..d2f36e66d7 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarSpaceFiltersSelected_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:601cff975cb714b8fafdbee6741f6f642ea744880b31bf060540f821fc5f911e +size 23195 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarSpaceFiltersSelected_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarSpaceFiltersSelected_Night_0_en.png new file mode 100644 index 0000000000..719a95f1a3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarSpaceFiltersSelected_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8722652998d147e103dfb84934787b406c93508aa6a13815b82f36a424e02863 +size 21316 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarWithIndicator_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarWithIndicator_Day_0_en.png index 93b29cd67f..dee99e6989 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarWithIndicator_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarWithIndicator_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0fdbe7e7ab22f44cc60a04582d5a5159055d4af3af0d3d2a7cd012f48abd9592 -size 22443 +oid sha256:76867d38b4b78d36837bbafc93c442b714fc1f0941a1cbe0cf46f7befb4f5347 +size 22611 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarWithIndicator_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarWithIndicator_Night_0_en.png index a796c3a10e..db984f351e 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarWithIndicator_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBarWithIndicator_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96942af60064898a853268f8aa8103f04a836332fef09fbfa83ad01cccae54d3 -size 20651 +oid sha256:1fb40adfd3139dd952b9707825e4938d630dba7f00d3192b3aef300de90ca0ba +size 20823 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBar_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBar_Day_0_en.png index 1df7a1d9ec..4bb3b53cea 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBar_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBar_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e38f16c942bcfb31103b5e80d168a5425f181632ea4440a054aa4d0985bd335a -size 22106 +oid sha256:a03be8035fc5e5a4a653744504389d9bd2c9f35e1ab9a4070d73dbb08f934e4d +size 22277 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBar_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBar_Night_0_en.png index ef2d6e3500..947371e698 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBar_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.components_HomeTopBar_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:677c21f745e4b9c7984f6c8fe3f84a27fabe6aeaf5f8cac82bb5ac692bd6a797 -size 20308 +oid sha256:67f04b8baa89527233d9cfbe0faf6e57b2ad384d35935cf866e46cf3d05f0fc7 +size 20474 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Day_0_en.png new file mode 100644 index 0000000000..d189f2ea85 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5ed03e5c6103dd4d96a0c4a8fda97808801f853cc3e05595e339a67e0b228027 +size 30472 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Day_1_en.png new file mode 100644 index 0000000000..df44e09e95 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dfeca128a35edebe19f59749bedcd7335422377fd2627f95f81d7e3e28dd61e5 +size 18173 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Night_0_en.png new file mode 100644 index 0000000000..8601473ecb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:543f80c91d04f44c4c62a06c9e44b334826d5bd92f2b17a4eeeb7e15dfbed0c1 +size 29510 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Night_1_en.png new file mode 100644 index 0000000000..7ac4ff5a03 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.spacefilters_SpaceFiltersView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8735caab11d89a689591c1d1ad5c1483d0c1fc01ce7b5933c7546ce2c2641d4a +size 17162 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeViewA11y_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeViewA11y_en.png index 98ce0d3bc1..4226e61fae 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeViewA11y_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeViewA11y_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cc3fcb72de86766e3b97ffe0802551d7597b688f2a52ef73938d91c9cfdf0633 -size 141868 +oid sha256:1bf2830f59241a4f4fe7b6d5a53576d5b5738f036044dc925794c4a035d1284b +size 143545 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_0_en.png index edc3a6f1f1..e2bbfae52f 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:02f12178991ed984da4b2d0a9883250750b6e37e7c9ce8494ca12bb987f29ad4 -size 65455 +oid sha256:fb6a0347fa3b5639fa30cf4e353c85b5cab45bb07375bf622d46c896a6ff1c36 +size 65612 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_10_en.png index eb4002392c..4cdc19760e 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4eb7b8997069e2a4b37cd27c33e2862cba1871e1b58e85feb0b1bc4da6a3fce8 -size 33631 +oid sha256:f3ad2e84a242788e35b221311348d3cf7053ae14501b668ba91015b4a51b81fd +size 33797 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_11_en.png index 886d567126..4c09e8a3f4 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ff84f90db0deea1d8edc52a2067855cf277cc747f5c357dc0758ce72bbc6d0c8 -size 28014 +oid sha256:daeec92b6da82df24261e400005662a2cf367328837256873615bf5357d36ddc +size 28212 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_13_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_13_en.png index 583ae6e976..f7de233e1a 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_13_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_13_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e8cd2089f07a93e3dc62e41d80a3253d800b4463947276ccd461428280aff6b8 -size 84644 +oid sha256:46ea20dea8e41276db4062ee86348833192398a826836a2bfbb7c065e8f91a46 +size 84802 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_14_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_14_en.png index faee0bc22d..76f4fa0d78 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_14_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_14_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:71f336f93dd3fe736187a38aba6c1f14420bb0da9d574603f6df2042bce8f8d1 -size 83116 +oid sha256:8bae76f03ed015e34032a531b21d238f0553a09335bc7ea545c9690631965754 +size 83259 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_15_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_15_en.png index 7d6ed57883..5901b196ac 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_15_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_15_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d7840847edf48b171393418d6fc26d846d772fa8be919f55dfee086d85cab814 -size 51404 +oid sha256:90ff370cbe3d2e7ea25471dc29e2593100dcfbef4f25dec51f03003871e55e4e +size 51563 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_1_en.png index edc3a6f1f1..e2bbfae52f 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:02f12178991ed984da4b2d0a9883250750b6e37e7c9ce8494ca12bb987f29ad4 -size 65455 +oid sha256:fb6a0347fa3b5639fa30cf4e353c85b5cab45bb07375bf622d46c896a6ff1c36 +size 65612 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_2_en.png index edc3a6f1f1..e2bbfae52f 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:02f12178991ed984da4b2d0a9883250750b6e37e7c9ce8494ca12bb987f29ad4 -size 65455 +oid sha256:fb6a0347fa3b5639fa30cf4e353c85b5cab45bb07375bf622d46c896a6ff1c36 +size 65612 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_3_en.png index b14e265de2..0c6414f84f 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:93b7d7b0d7df69921015ece40ae3ab859c81cd3e78828143f660a481a72b73e2 -size 62305 +oid sha256:d5674b09940d6dfd1361a37289bcdeeb2a1ac7f04d9cff287552540a9ddae95c +size 62470 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_5_en.png index edc3a6f1f1..e2bbfae52f 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:02f12178991ed984da4b2d0a9883250750b6e37e7c9ce8494ca12bb987f29ad4 -size 65455 +oid sha256:fb6a0347fa3b5639fa30cf4e353c85b5cab45bb07375bf622d46c896a6ff1c36 +size 65612 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_6_en.png index e1b311a970..dd3aa8cd85 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6bb7a5a23947cd3a311c8d9526508c121beb4e0ee37f337bf8361845be10d172 -size 54415 +oid sha256:429218b356c23d6cb934ebbcb144e838e75332b6f67f61e96c3a88893d33d5c1 +size 54556 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_7_en.png index 1e79e25769..3717d09c9d 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:11e9f28e6131eae66e268b72daab6d404d1f1f8b0eba78eb11d4376284eb11e3 -size 54220 +oid sha256:94aa6527f1fbebb1a3c5ca4a7f09ddbd7b9d8d6f7c0ee1dcbb34a83180391f6b +size 54352 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_8_en.png index eb5ead6254..530a3806ce 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:50e8fedd31aaa5dbdb63e088709a5e237f8c5400ca7209390d837a07d9b86973 -size 52404 +oid sha256:80c54edb1449c682fd21efb6593d22e0a5b936a7ba4ff879cf38c19deaf9acc4 +size 52538 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_9_en.png index 0f387ca179..d0b7b52ab7 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9fb19cc1e169e81f301a12ff3e30b8118682248ec3c13ea29b54f87398a54c3e -size 82977 +oid sha256:b434402aac34cb7888714911af5f2fb710d41e932fad31660b2dac5bbda6a748 +size 83136 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_0_en.png index e011cf5f07..3d7fe2d829 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:62f84bd6941a00ee6b006318b8ac9e659694c2749a3bfa1d8ca72ca2ce413c90 -size 62152 +oid sha256:38f16bdafa24411b2b4e33980871972073500c2e86bb74e7277d8b9d8155413a +size 62300 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_10_en.png index 1052c94a6e..39b3383938 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ab4a977d119ae80ddb92198b9c26b4b42d842eba0fd0543896f55914018d34f2 -size 30548 +oid sha256:f411dee9755509ebf93d9452bcbd3979555775245798d12f819b515701d09556 +size 30696 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_11_en.png index 40455a3752..65f49e179a 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:66824bcff1cf8fdd0fd88c3e0272bd11eaf6fdd16d24120c9346c99378226eee -size 24633 +oid sha256:fbc5042340e85c931531525fe05ef820e6d92a10f53d2c94b6878afb1ab776e0 +size 24796 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_13_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_13_en.png index 69ec4c780c..ac5dac47cf 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_13_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_13_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7d1745b2665a9329808f69e2750b50d59eac2d92216a25bae03a52907b6c7681 -size 80341 +oid sha256:fe2614c4255ff8ea6a4e25219f396b3a69da0283373cd8c06b8e3f9396e1c1b8 +size 80491 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_14_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_14_en.png index ffba98b54e..6a33b13a2e 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_14_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_14_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9017e66bb482de6a517065efe2e9d66ce4eb561fb848ca29d3245a2abc02d4c0 -size 79209 +oid sha256:7c74fa9af01f3d7e3762416d6c40b3abff7159b70a8328db42be68c622cb93ce +size 79359 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_15_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_15_en.png index c58e0fe485..432628718d 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_15_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_15_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6fd8d9d809b65df1690bf132351eec86736224d50fdd1bc9f9d2e731a4669ad6 -size 47687 +oid sha256:af4734d49824617816471fd49a64b49c568b77780b1c84fc0ab9c6248ae57c92 +size 47843 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_1_en.png index e011cf5f07..3d7fe2d829 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:62f84bd6941a00ee6b006318b8ac9e659694c2749a3bfa1d8ca72ca2ce413c90 -size 62152 +oid sha256:38f16bdafa24411b2b4e33980871972073500c2e86bb74e7277d8b9d8155413a +size 62300 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_2_en.png index e011cf5f07..3d7fe2d829 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:62f84bd6941a00ee6b006318b8ac9e659694c2749a3bfa1d8ca72ca2ce413c90 -size 62152 +oid sha256:38f16bdafa24411b2b4e33980871972073500c2e86bb74e7277d8b9d8155413a +size 62300 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_3_en.png index 044c2a9fbc..a538bf6454 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a8db3006e7ea4826022d47a4ae37dab2a7ddd064d4cc2c48b44deca0f158541c -size 59301 +oid sha256:850ea9b0435ce5f46f6fb4c717a8746e864ec1a1a94a7be327b33b8b3156cecc +size 59458 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_5_en.png index e011cf5f07..3d7fe2d829 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:62f84bd6941a00ee6b006318b8ac9e659694c2749a3bfa1d8ca72ca2ce413c90 -size 62152 +oid sha256:38f16bdafa24411b2b4e33980871972073500c2e86bb74e7277d8b9d8155413a +size 62300 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_6_en.png index 442eba42bc..0b1af1d838 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:957b8712456f13d83857616e2ffab05c5d60f50e6b39037bb47af0d272c53b02 -size 51863 +oid sha256:307f3be8a656c4f66f30fc6aa7ae9b97871cb62084b52e66f4cc81186b521838 +size 52001 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_7_en.png index 34ae9c102a..8a98b7f0c1 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:beac305c3cc0661633873276a99798b1fa693342bd4ab9424bc44759a1f745ec -size 51695 +oid sha256:bf8dca9a57cc3b867a010568612c8f9f25983e27543e4980bd31183e6b6c0539 +size 51839 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_8_en.png index 06d338c4c6..0d41fac91e 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a83be8296a2abd4a2bd28f08c8687cac1dddaaedce4ccf784538892da7b069c -size 49831 +oid sha256:11ce6dfc9128bf150eedd9113272ff4a9b2eb9afd3262cab4b7d4c9ec0c8279b +size 49980 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_9_en.png index a3e0ad8645..dae4931e9e 100644 --- a/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.home.impl_HomeView_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6c7790afd67222ce61ea0b72f1af3c1877b7e88580a644524c33f18d6fe76137 -size 79096 +oid sha256:84f78878963f2ead0ca82985bb5686f56cdf33f5ee45470b066d9e3f4c13db03 +size 79239