From e0e21d4282b2b82df107047e82c0f813cec40e96 Mon Sep 17 00:00:00 2001 From: Nikita Fedrunov <66663241+fedrunov@users.noreply.github.com> Date: Thu, 11 Aug 2022 16:26:24 +0200 Subject: [PATCH] filtered room list at home (#6724) --- changelog.d/6505.wip | 1 + .../src/main/res/values/styles_tablayout.xml | 18 +++ .../home/room/list/home/HomeRoomListAction.kt | 2 + .../room/list/home/HomeRoomListFragment.kt | 24 ++-- .../room/list/home/HomeRoomListViewModel.kt | 117 +++++++++++++++--- .../home/room/list/home/HomeRoomSection.kt | 6 +- .../filter/HomeFilteredRoomsController.kt | 70 +++++++++++ .../room/list/home/filter/HomeRoomFilter.kt | 27 ++++ .../list/home/filter/RoomFilterHeaderItem.kt | 65 ++++++++++ .../main/res/layout/item_home_filter_tabs.xml | 27 ++++ vector/src/main/res/values/strings.xml | 5 + 11 files changed, 336 insertions(+), 26 deletions(-) create mode 100644 changelog.d/6505.wip create mode 100644 library/ui-styles/src/main/res/values/styles_tablayout.xml create mode 100644 vector/src/main/java/im/vector/app/features/home/room/list/home/filter/HomeFilteredRoomsController.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/list/home/filter/HomeRoomFilter.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/list/home/filter/RoomFilterHeaderItem.kt create mode 100644 vector/src/main/res/layout/item_home_filter_tabs.xml diff --git a/changelog.d/6505.wip b/changelog.d/6505.wip new file mode 100644 index 0000000000..1109c5fff1 --- /dev/null +++ b/changelog.d/6505.wip @@ -0,0 +1 @@ +added filter tabs for new App layout's Home screen diff --git a/library/ui-styles/src/main/res/values/styles_tablayout.xml b/library/ui-styles/src/main/res/values/styles_tablayout.xml new file mode 100644 index 0000000000..ab26972995 --- /dev/null +++ b/library/ui-styles/src/main/res/values/styles_tablayout.xml @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListAction.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListAction.kt index 04c8524b50..6d17792969 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListAction.kt @@ -17,6 +17,7 @@ package im.vector.app.features.home.room.list.home import im.vector.app.core.platform.VectorViewModelAction +import im.vector.app.features.home.room.list.home.filter.HomeRoomFilter import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState @@ -25,4 +26,5 @@ sealed class HomeRoomListAction : VectorViewModelAction { data class ChangeRoomNotificationState(val roomId: String, val notificationState: RoomNotificationState) : HomeRoomListAction() data class ToggleTag(val roomId: String, val tag: String) : HomeRoomListAction() data class LeaveRoom(val roomId: String) : HomeRoomListAction() + data class ChangeRoomFilter(val filter: HomeRoomFilter) : HomeRoomListAction() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListFragment.kt index 43a6f25841..c98d22a34f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListFragment.kt @@ -37,14 +37,14 @@ import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.UserPreferencesProvider import im.vector.app.databinding.FragmentRoomListBinding import im.vector.app.features.analytics.plan.ViewRoom -import im.vector.app.features.home.RoomListDisplayMode import im.vector.app.features.home.room.list.RoomListAnimator import im.vector.app.features.home.room.list.RoomListListener import im.vector.app.features.home.room.list.RoomSummaryItemFactory -import im.vector.app.features.home.room.list.RoomSummaryPagedController import im.vector.app.features.home.room.list.actions.RoomListQuickActionsBottomSheet import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedAction import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel +import im.vector.app.features.home.room.list.home.filter.HomeFilteredRoomsController +import im.vector.app.features.home.room.list.home.filter.HomeRoomFilter import im.vector.app.features.home.room.list.home.recent.RecentRoomCarouselController import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -207,28 +207,36 @@ class HomeRoomListFragment @Inject constructor( .show() } - private fun getAdapterForData(data: HomeRoomSection): EpoxyControllerAdapter { - return when (data) { + private fun getAdapterForData(section: HomeRoomSection): EpoxyControllerAdapter { + return when (section) { is HomeRoomSection.RoomSummaryData -> { - RoomSummaryPagedController( + HomeFilteredRoomsController( roomSummaryItemFactory, - RoomListDisplayMode.ROOMS + showFilters = section.showFilters, ).also { controller -> controller.listener = this - data.list.observe(viewLifecycleOwner) { list -> + controller.onFilterChanged = ::onRoomFilterChanged + section.filtersData.onEach { + controller.submitFiltersData(it) + }.launchIn(lifecycleScope) + section.list.observe(viewLifecycleOwner) { list -> controller.submitList(list) } }.adapter } is HomeRoomSection.RecentRoomsData -> recentRoomCarouselController.also { controller -> controller.listener = this - data.list.observe(viewLifecycleOwner) { list -> + section.list.observe(viewLifecycleOwner) { list -> controller.submitList(list) } }.adapter } } + private fun onRoomFilterChanged(filter: HomeRoomFilter) { + roomListViewModel.handle(HomeRoomListAction.ChangeRoomFilter(filter)) + } + private fun handleSelectRoom(event: HomeRoomListViewEvents.SelectRoom, isInviteAlreadyAccepted: Boolean) { navigator.openRoom( context = requireActivity(), diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewModel.kt index 479e22497f..1fed9eba86 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewModel.kt @@ -27,33 +27,38 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.StateView import im.vector.app.core.platform.VectorViewModel -import im.vector.app.features.settings.VectorPreferences +import im.vector.app.features.home.room.list.home.filter.HomeRoomFilter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.query.QueryStringValue -import org.matrix.android.sdk.api.query.SpaceFilter +import org.matrix.android.sdk.api.query.RoomCategoryFilter +import org.matrix.android.sdk.api.query.RoomTagQueryFilter import org.matrix.android.sdk.api.query.toActiveSpaceOrNoFilter -import org.matrix.android.sdk.api.query.toActiveSpaceOrOrphanRooms import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.getRoom +import org.matrix.android.sdk.api.session.room.RoomSortOrder import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams +import org.matrix.android.sdk.api.session.room.UpdatableLivePageResult import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.tag.RoomTag import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.api.session.room.state.isPublic +import org.matrix.android.sdk.flow.flow class HomeRoomListViewModel @AssistedInject constructor( @Assisted initialState: HomeRoomListViewState, private val session: Session, private val spaceStateHandler: SpaceStateHandler, - private val vectorPreferences: VectorPreferences, ) : VectorViewModel(initialState) { @AssistedFactory @@ -73,6 +78,8 @@ class HomeRoomListViewModel @AssistedInject constructor( private val _sections = MutableSharedFlow>(replay = 1) val sections = _sections.asSharedFlow() + private var filteredPagedRoomSummariesLive: UpdatableLivePageResult? = null + init { configureSections() } @@ -81,7 +88,7 @@ class HomeRoomListViewModel @AssistedInject constructor( val newSections = mutableSetOf() newSections.add(getRecentRoomsSection()) - newSections.add(getAllRoomsSection()) + newSections.add(getFilteredRoomsSection()) viewModelScope.launch { _sections.emit(newSections) @@ -104,15 +111,21 @@ class HomeRoomListViewModel @AssistedInject constructor( ) } - private fun getAllRoomsSection(): HomeRoomSection.RoomSummaryData { + private fun getFilteredRoomsSection(): HomeRoomSection.RoomSummaryData { val builder = RoomSummaryQueryParams.Builder().also { it.memberships = listOf(Membership.JOIN) } - val filteredPagedRoomSummariesLive = session.roomService().getFilteredPagedRoomSummariesLive( - builder.build(), - pagedListConfig - ) + val params = getFilteredQueryParams(HomeRoomFilter.ALL, builder.build()) + val sortOrder = RoomSortOrder.ACTIVITY // #6506 + + val liveResults = session.roomService().getFilteredPagedRoomSummariesLive( + params, + pagedListConfig, + sortOrder + ).also { + this.filteredPagedRoomSummariesLive = it + } spaceStateHandler.getSelectedSpaceFlow() .distinctUntilChanged() @@ -121,20 +134,83 @@ class HomeRoomListViewModel @AssistedInject constructor( } .onEach { selectedSpaceOption -> val selectedSpace = selectedSpaceOption.orNull() - filteredPagedRoomSummariesLive.queryParams = filteredPagedRoomSummariesLive.queryParams.copy( - spaceFilter = getSpaceFilter(selectedSpaceId = selectedSpace?.roomId) + liveResults.queryParams = liveResults.queryParams.copy( + spaceFilter = selectedSpace?.roomId.toActiveSpaceOrNoFilter() ) }.launchIn(viewModelScope) return HomeRoomSection.RoomSummaryData( - list = filteredPagedRoomSummariesLive.livePagedList + list = liveResults.livePagedList, + showFilters = true, // #6506 + filtersData = getFiltersDataFlow() ) } - private fun getSpaceFilter(selectedSpaceId: String?): SpaceFilter { - return when { - vectorPreferences.prefSpacesShowAllRoomInHome() -> selectedSpaceId.toActiveSpaceOrNoFilter() - else -> selectedSpaceId.toActiveSpaceOrOrphanRooms() + private fun getFiltersDataFlow(): SharedFlow> { + val flow = MutableSharedFlow>(replay = 1) + + val favouritesFlow = session.flow() + .liveRoomSummaries( + RoomSummaryQueryParams.Builder().also { builder -> + builder.roomTagQueryFilter = RoomTagQueryFilter(true, null, null) + }.build() + ) + .map { it.isNotEmpty() } + .distinctUntilChanged() + + val dmsFLow = session.flow() + .liveRoomSummaries( + RoomSummaryQueryParams.Builder().also { builder -> + builder.memberships = listOf(Membership.JOIN) + builder.roomCategoryFilter = RoomCategoryFilter.ONLY_DM + }.build() + ) + .map { it.isNotEmpty() } + .distinctUntilChanged() + + favouritesFlow.combine(dmsFLow) { hasFavourite, hasDm -> + hasFavourite to hasDm + }.onEach { (hasFavourite, hasDm) -> + val filtersData = mutableListOf( + HomeRoomFilter.ALL, + HomeRoomFilter.UNREADS + ) + if (hasFavourite) { + filtersData.add( + HomeRoomFilter.FAVOURITES + ) + } + if (hasDm) { + filtersData.add( + HomeRoomFilter.PEOPlE + ) + } + + flow.emit(filtersData) + }.launchIn(viewModelScope) + + return flow + } + + private fun getFilteredQueryParams(filter: HomeRoomFilter, currentParams: RoomSummaryQueryParams): RoomSummaryQueryParams { + return when (filter) { + HomeRoomFilter.ALL -> currentParams.copy( + roomCategoryFilter = null, + roomTagQueryFilter = null + ) + HomeRoomFilter.UNREADS -> currentParams.copy( + roomCategoryFilter = RoomCategoryFilter.ONLY_WITH_NOTIFICATIONS, + roomTagQueryFilter = RoomTagQueryFilter(null, false, null) + ) + HomeRoomFilter.FAVOURITES -> + currentParams.copy( + roomCategoryFilter = null, + roomTagQueryFilter = RoomTagQueryFilter(true, null, null) + ) + HomeRoomFilter.PEOPlE -> currentParams.copy( + roomCategoryFilter = RoomCategoryFilter.ONLY_DM, + roomTagQueryFilter = null + ) } } @@ -144,6 +220,13 @@ class HomeRoomListViewModel @AssistedInject constructor( is HomeRoomListAction.LeaveRoom -> handleLeaveRoom(action) is HomeRoomListAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action) is HomeRoomListAction.ToggleTag -> handleToggleTag(action) + is HomeRoomListAction.ChangeRoomFilter -> handleChangeRoomFilter(action) + } + } + + private fun handleChangeRoomFilter(action: HomeRoomListAction.ChangeRoomFilter) { + filteredPagedRoomSummariesLive?.let { liveResults -> + liveResults.queryParams = getFilteredQueryParams(action.filter, liveResults.queryParams) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomSection.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomSection.kt index 14c76b08bf..f51b479d37 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomSection.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomSection.kt @@ -18,11 +18,15 @@ package im.vector.app.features.home.room.list.home import androidx.lifecycle.LiveData import androidx.paging.PagedList +import im.vector.app.features.home.room.list.home.filter.HomeRoomFilter +import kotlinx.coroutines.flow.SharedFlow import org.matrix.android.sdk.api.session.room.model.RoomSummary sealed class HomeRoomSection { data class RoomSummaryData( - val list: LiveData> + val list: LiveData>, + val showFilters: Boolean, + val filtersData: SharedFlow> ) : HomeRoomSection() data class RecentRoomsData( diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/filter/HomeFilteredRoomsController.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/filter/HomeFilteredRoomsController.kt new file mode 100644 index 0000000000..7c1f154d52 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/filter/HomeFilteredRoomsController.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2022 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 im.vector.app.features.home.room.list.home.filter + +import com.airbnb.epoxy.EpoxyModel +import com.airbnb.epoxy.paging.PagedListEpoxyController +import im.vector.app.core.utils.createUIHandler +import im.vector.app.features.home.RoomListDisplayMode +import im.vector.app.features.home.room.list.RoomListListener +import im.vector.app.features.home.room.list.RoomSummaryItemFactory +import im.vector.app.features.home.room.list.RoomSummaryItemPlaceHolder_ +import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState +import org.matrix.android.sdk.api.session.room.model.RoomSummary + +class HomeFilteredRoomsController( + private val roomSummaryItemFactory: RoomSummaryItemFactory, + private val showFilters: Boolean, +) : PagedListEpoxyController( + // Important it must match the PageList builder notify Looper + modelBuildingHandler = createUIHandler() +) { + + private var roomChangeMembershipStates: Map? = null + set(value) { + field = value + // ideally we could search for visible models and update only those + requestForcedModelBuild() + } + + var listener: RoomListListener? = null + var onFilterChanged: ((HomeRoomFilter) -> Unit)? = null + + private var filtersData: List? = null + + override fun addModels(models: List>) { + val host = this + if (showFilters) { + roomFilterHeaderItem { + id("filter_header") + filtersData(host.filtersData) + onFilterChangedListener(host.onFilterChanged) + } + } + super.addModels(models) + } + + fun submitFiltersData(data: List) { + this.filtersData = data + requestForcedModelBuild() + } + + override fun buildItemModel(currentPosition: Int, item: RoomSummary?): EpoxyModel<*> { + item ?: return RoomSummaryItemPlaceHolder_().apply { id(currentPosition) } + return roomSummaryItemFactory.create(item, roomChangeMembershipStates.orEmpty(), emptySet(), RoomListDisplayMode.ROOMS, listener) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/filter/HomeRoomFilter.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/filter/HomeRoomFilter.kt new file mode 100644 index 0000000000..ce33440238 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/filter/HomeRoomFilter.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2022 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 im.vector.app.features.home.room.list.home.filter + +import androidx.annotation.StringRes +import im.vector.app.R + +enum class HomeRoomFilter(@StringRes val titleRes: Int) { + ALL(R.string.room_list_filter_all), + UNREADS(R.string.room_list_filter_unreads), + FAVOURITES(R.string.room_list_filter_favourites), + PEOPlE(R.string.room_list_filter_people), +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/filter/RoomFilterHeaderItem.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/filter/RoomFilterHeaderItem.kt new file mode 100644 index 0000000000..bbe503806b --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/filter/RoomFilterHeaderItem.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2022 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 im.vector.app.features.home.room.list.home.filter + +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.google.android.material.tabs.TabLayout +import im.vector.app.R +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel + +@EpoxyModelClass +abstract class RoomFilterHeaderItem : VectorEpoxyModel(R.layout.item_home_filter_tabs) { + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + var onFilterChangedListener: ((HomeRoomFilter) -> Unit)? = null + + @EpoxyAttribute + var filtersData: List? = null + + override fun bind(holder: Holder) { + super.bind(holder) + with(holder.tabLayout) { + removeAllTabs() + + filtersData?.forEach { filter -> + addTab(newTab().setText(filter.titleRes).setTag(filter)) + } + + addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { + override fun onTabSelected(tab: TabLayout.Tab?) { + (tab?.tag as? HomeRoomFilter)?.let { filter -> + onFilterChangedListener?.invoke(filter) + } + } + + override fun onTabUnselected(tab: TabLayout.Tab?) = Unit + override fun onTabReselected(tab: TabLayout.Tab?) = Unit + }) + } + } + + override fun unbind(holder: Holder) { + holder.tabLayout.clearOnTabSelectedListeners() + super.unbind(holder) + } + + class Holder : VectorEpoxyHolder() { + val tabLayout by bind(R.id.home_filter_tabs_tabs) + } +} diff --git a/vector/src/main/res/layout/item_home_filter_tabs.xml b/vector/src/main/res/layout/item_home_filter_tabs.xml new file mode 100644 index 0000000000..7ea78a8bf1 --- /dev/null +++ b/vector/src/main/res/layout/item_home_filter_tabs.xml @@ -0,0 +1,27 @@ + + + + + + + + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 7c01eed715..f204276a7f 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1610,6 +1610,11 @@ Your rooms will be displayed here. Tap the + at the bottom right to find existing ones or start some of your own. + All + Unreads + Favourites + People + Reactions Add Reaction View Reactions