From 5dcc9fba29791311e0aec3902cf2c9640487efd8 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 7 Feb 2024 21:27:23 +0100 Subject: [PATCH] RoomListFilters : first iteration on the design --- .../roomlist/impl/RoomListPresenter.kt | 4 + .../features/roomlist/impl/RoomListState.kt | 2 + .../roomlist/impl/RoomListStateProvider.kt | 2 + .../features/roomlist/impl/RoomListView.kt | 24 +-- .../roomlist/impl/filters/RoomListFilter.kt | 40 +++++ .../impl/filters/RoomListFiltersEvents.kt | 22 +++ .../impl/filters/RoomListFiltersPresenter.kt | 69 +++++++++ .../impl/filters/RoomListFiltersState.kt | 27 ++++ .../filters/RoomListFiltersStateProvider.kt | 42 +++++ .../impl/filters/RoomListFiltersView.kt | 144 ++++++++++++++++++ 10 files changed, 366 insertions(+), 10 deletions(-) create mode 100644 features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFilter.kt create mode 100644 features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersEvents.kt create mode 100644 features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersPresenter.kt create mode 100644 features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersState.kt create mode 100644 features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersStateProvider.kt create mode 100644 features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersView.kt diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt index 421cdb0c62..9835c56199 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt @@ -33,6 +33,7 @@ import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.roomlist.impl.datasource.InviteStateDataSource import io.element.android.features.roomlist.impl.datasource.RoomListDataSource +import io.element.android.features.roomlist.impl.filters.RoomListFiltersPresenter import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher @@ -63,6 +64,7 @@ class RoomListPresenter @Inject constructor( private val encryptionService: EncryptionService, private val featureFlagService: FeatureFlagService, private val indicatorService: IndicatorService, + private val filtersPresenter: RoomListFiltersPresenter, ) : Presenter { @Composable override fun present(): RoomListState { @@ -76,6 +78,7 @@ class RoomListPresenter @Inject constructor( val filteredRoomList by roomListDataSource.filteredRooms.collectAsState() val filter by roomListDataSource.filter.collectAsState() val networkConnectionStatus by networkMonitor.connectivity.collectAsState() + val filtersState = filtersPresenter.present() LaunchedEffect(Unit) { roomListDataSource.launchIn(this) @@ -148,6 +151,7 @@ class RoomListPresenter @Inject constructor( displaySearchResults = displaySearchResults, contextMenu = contextMenu, leaveRoomState = leaveRoomState, + filtersState = filtersState, eventSink = ::handleEvents ) } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt index b1180aecb3..8926e792b8 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt @@ -18,6 +18,7 @@ package io.element.android.features.roomlist.impl import androidx.compose.runtime.Immutable import io.element.android.features.leaveroom.api.LeaveRoomState +import io.element.android.features.roomlist.impl.filters.RoomListFiltersState import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage @@ -40,6 +41,7 @@ data class RoomListState( val displaySearchResults: Boolean, val contextMenu: ContextMenu, val leaveRoomState: LeaveRoomState, + val filtersState: RoomListFiltersState, val eventSink: (RoomListEvents) -> Unit, ) { sealed interface ContextMenu { diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt index 55cbcf925b..589d4dcaff 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt @@ -19,6 +19,7 @@ package io.element.android.features.roomlist.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.leaveroom.api.aLeaveRoomState import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory +import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.features.roomlist.impl.model.aRoomListRoomSummary import io.element.android.libraries.architecture.AsyncData @@ -70,6 +71,7 @@ internal fun aRoomListState() = RoomListState( displaySearchResults = false, contextMenu = RoomListState.ContextMenu.Hidden, leaveRoomState = aLeaveRoomState(), + filtersState = aRoomListFiltersState(), eventSink = {} ) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt index ace8b9ce60..09f2aa75b8 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt @@ -54,6 +54,7 @@ import io.element.android.features.roomlist.impl.components.RequestVerificationH import io.element.android.features.roomlist.impl.components.RoomListMenuAction import io.element.android.features.roomlist.impl.components.RoomListTopBar import io.element.android.features.roomlist.impl.components.RoomSummaryRow +import io.element.android.features.roomlist.impl.filters.RoomListFiltersView import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.features.roomlist.impl.search.RoomListSearchResultView import io.element.android.libraries.architecture.AsyncData @@ -209,16 +210,19 @@ private fun RoomListContent( Scaffold( modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { - RoomListTopBar( - matrixUser = state.matrixUser, - showAvatarIndicator = state.showAvatarIndicator, - areSearchResultsDisplayed = state.displaySearchResults, - onFilterChanged = { state.eventSink(RoomListEvents.UpdateFilter(it)) }, - onToggleSearch = { state.eventSink(RoomListEvents.ToggleSearchResults) }, - onMenuActionClicked = onMenuActionClicked, - onOpenSettings = onOpenSettings, - scrollBehavior = scrollBehavior, - ) + Column { + RoomListTopBar( + matrixUser = state.matrixUser, + showAvatarIndicator = state.showAvatarIndicator, + areSearchResultsDisplayed = state.displaySearchResults, + onFilterChanged = { state.eventSink(RoomListEvents.UpdateFilter(it)) }, + onToggleSearch = { state.eventSink(RoomListEvents.ToggleSearchResults) }, + onMenuActionClicked = onMenuActionClicked, + onOpenSettings = onOpenSettings, + scrollBehavior = scrollBehavior, + ) + RoomListFiltersView(state = state.filtersState) + } }, content = { padding -> if (state.roomList is AsyncData.Success && state.roomList.data.isEmpty()) { diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFilter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFilter.kt new file mode 100644 index 0000000000..35e118af41 --- /dev/null +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFilter.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomlist.impl.filters + +import io.element.android.features.roomlist.impl.R + +/** + * Enum class representing the different filters that can be applied to the room list. + * Order is important. + */ +enum class RoomListFilter(val stringResource: Int){ + Rooms(R.string.screen_roomlist_filter_rooms), + People(R.string.screen_roomlist_filter_people), + Unread(R.string.screen_roomlist_filter_unreads), + Favourites(R.string.screen_roomlist_filter_favourites), + LowPriority(R.string.screen_roomlist_filter_low_priority); + + val oppositeFilter: RoomListFilter? + get() = when (this) { + Rooms -> People + People -> Rooms + Unread -> null + Favourites -> LowPriority + LowPriority -> Favourites + } +} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersEvents.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersEvents.kt new file mode 100644 index 0000000000..d243ea7ca0 --- /dev/null +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersEvents.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomlist.impl.filters + +sealed interface RoomListFiltersEvents { + data object ClearSelectedFilters : RoomListFiltersEvents + data class ToggleFilter(val filter: RoomListFilter) : RoomListFiltersEvents +} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersPresenter.kt new file mode 100644 index 0000000000..278af4f7da --- /dev/null +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersPresenter.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomlist.impl.filters + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import io.element.android.libraries.architecture.Presenter +import kotlinx.collections.immutable.toPersistentList +import javax.inject.Inject + +class RoomListFiltersPresenter @Inject constructor() : Presenter { + + @Composable + override fun present(): RoomListFiltersState { + var unselectedFilters: Set by rememberSaveable { + mutableStateOf(RoomListFilter.entries.toSet()) + } + var selectedFilters: Set by rememberSaveable { + mutableStateOf(emptySet()) + } + + fun updateFilters(newSelectedFilters: Set) { + selectedFilters = newSelectedFilters + unselectedFilters = RoomListFilter.entries.toSet() - + selectedFilters - + selectedFilters.mapNotNull { it.oppositeFilter }.toSet() + } + + fun handleEvents(event: RoomListFiltersEvents) { + when (event) { + is RoomListFiltersEvents.ToggleFilter -> { + val newSelectedFilters = if (selectedFilters.contains(event.filter)) { + selectedFilters - event.filter + } else { + selectedFilters + event.filter + } + updateFilters(newSelectedFilters) + } + RoomListFiltersEvents.ClearSelectedFilters -> { + updateFilters(newSelectedFilters = emptySet()) + } + } + } + + return RoomListFiltersState( + unselectedFilters = unselectedFilters.toPersistentList(), + selectedFilters = selectedFilters.toPersistentList(), + eventSink = ::handleEvents + ) + } +} + diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersState.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersState.kt new file mode 100644 index 0000000000..8f7a6907be --- /dev/null +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersState.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomlist.impl.filters + +import kotlinx.collections.immutable.ImmutableList + +data class RoomListFiltersState( + val unselectedFilters: ImmutableList, + val selectedFilters: ImmutableList, + val eventSink: (RoomListFiltersEvents) -> Unit, +) { + val showClearFilterButton = selectedFilters.isNotEmpty() +} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersStateProvider.kt new file mode 100644 index 0000000000..6467e11562 --- /dev/null +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersStateProvider.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomlist.impl.filters + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList + +class RoomListFiltersStateProvider : PreviewParameterProvider { + + override val values: Sequence + get() = sequenceOf( + aRoomListFiltersState(), + aRoomListFiltersState( + selectedFilters = persistentListOf(RoomListFilter.Rooms, RoomListFilter.Favourites), + unselectedFilters = persistentListOf(RoomListFilter.Unread), + ) + ) +} + +fun aRoomListFiltersState( + unselectedFilters: ImmutableList = RoomListFilter.entries.toImmutableList(), + selectedFilters: ImmutableList = persistentListOf(), +) = RoomListFiltersState( + unselectedFilters = unselectedFilters, + selectedFilters = selectedFilters, +) {} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersView.kt new file mode 100644 index 0000000000..99bad60255 --- /dev/null +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersView.kt @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomlist.impl.filters + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.IconButton +import io.element.android.libraries.designsystem.theme.components.Text + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun RoomListFiltersView( + state: RoomListFiltersState, + modifier: Modifier = Modifier +) { + + fun onClearFiltersClicked() { + state.eventSink(RoomListFiltersEvents.ClearSelectedFilters) + } + + fun onFilterClicked(filter: RoomListFilter) { + state.eventSink(RoomListFiltersEvents.ToggleFilter(filter)) + } + + val horizontalPadding = if(state.showClearFilterButton) 4.dp else 16.dp + + Row(modifier.padding(horizontal = horizontalPadding)) { + AnimatedVisibility(visible = state.showClearFilterButton) { + RoomListClearFiltersButton(onClick = ::onClearFiltersClicked) + } + LazyRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + items(state.selectedFilters) { filter -> + RoomListFilterView( + roomListFilter = filter, + selected = true, + onClick = ::onFilterClicked, + modifier = Modifier.animateItemPlacement(), + ) + } + items(state.unselectedFilters) { filter -> + RoomListFilterView( + roomListFilter = filter, + selected = false, + onClick = ::onFilterClicked, + modifier = Modifier.animateItemPlacement(), + ) + } + } + } +} + +@Composable +private fun RoomListClearFiltersButton( + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + IconButton( + modifier = modifier, + onClick = onClick, + ) { + Box( + modifier = Modifier + .clip(CircleShape) + .background(ElementTheme.colors.bgActionPrimaryRest) + ) { + Icon( + modifier = Modifier.align(Alignment.Center), + imageVector = CompoundIcons.Close, + tint = ElementTheme.colors.iconOnSolidPrimary, + contentDescription = null, + ) + } + } +} + +@Composable +private fun RoomListFilterView( + roomListFilter: RoomListFilter, + selected: Boolean, + onClick: (RoomListFilter) -> Unit, + modifier: Modifier = Modifier +) { + FilterChip( + selected = selected, + onClick = { onClick(roomListFilter) }, + modifier = modifier, + shape = CircleShape, + colors = FilterChipDefaults.filterChipColors( + containerColor = ElementTheme.colors.bgCanvasDefault, + selectedContainerColor = ElementTheme.colors.bgActionPrimaryRest, + labelColor = ElementTheme.colors.textPrimary, + selectedLabelColor = ElementTheme.colors.textOnSolidPrimary, + ), + label = { + Text(text = stringResource(id = roomListFilter.stringResource)) + } + ) +} + +@PreviewsDayNight +@Composable +internal fun RoomListFiltersViewPreview(@PreviewParameter(RoomListFiltersStateProvider::class) state: RoomListFiltersState) = ElementPreview { + RoomListFiltersView( + state = state, + ) +}