filtered room list at home (#6724)

This commit is contained in:
Nikita Fedrunov 2022-08-11 16:26:24 +02:00 committed by GitHub
parent fa8f72c909
commit e0e21d4282
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 336 additions and 26 deletions

1
changelog.d/6505.wip Normal file
View File

@ -0,0 +1 @@
added filter tabs for new App layout's Home screen

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Widget.Vector.TabLayout" parent="Widget.MaterialComponents.TabLayout">
<item name="materialThemeOverlay">@style/ThemeOverlay.Vector.HomeFilterTabLayout</item>
<item name="tabTextAppearance">@style/TextAppearance.Vector.FilterTabTextAppearance</item>
</style>
<style name="TextAppearance.Vector.FilterTabTextAppearance" parent="TextAppearance.Vector.Subtitle">
<item name="textAllCaps">false</item>
</style>
<style name="ThemeOverlay.Vector.HomeFilterTabLayout" parent="Theme.Vector.Launcher">
<item name="colorSurface">?vctr_toolbar_background</item>
<item name="colorOnSurface">?vctr_content_secondary</item>
</style>
</resources>

View File

@ -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()
}

View File

@ -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(),

View File

@ -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<HomeRoomListViewState, HomeRoomListAction, HomeRoomListViewEvents>(initialState) {
@AssistedFactory
@ -73,6 +78,8 @@ class HomeRoomListViewModel @AssistedInject constructor(
private val _sections = MutableSharedFlow<Set<HomeRoomSection>>(replay = 1)
val sections = _sections.asSharedFlow()
private var filteredPagedRoomSummariesLive: UpdatableLivePageResult? = null
init {
configureSections()
}
@ -81,7 +88,7 @@ class HomeRoomListViewModel @AssistedInject constructor(
val newSections = mutableSetOf<HomeRoomSection>()
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<List<HomeRoomFilter>> {
val flow = MutableSharedFlow<List<HomeRoomFilter>>(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)
}
}

View File

@ -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<PagedList<RoomSummary>>
val list: LiveData<PagedList<RoomSummary>>,
val showFilters: Boolean,
val filtersData: SharedFlow<List<HomeRoomFilter>>
) : HomeRoomSection()
data class RecentRoomsData(

View File

@ -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<RoomSummary>(
// Important it must match the PageList builder notify Looper
modelBuildingHandler = createUIHandler()
) {
private var roomChangeMembershipStates: Map<String, ChangeMembershipState>? = 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<HomeRoomFilter>? = null
override fun addModels(models: List<EpoxyModel<*>>) {
val host = this
if (showFilters) {
roomFilterHeaderItem {
id("filter_header")
filtersData(host.filtersData)
onFilterChangedListener(host.onFilterChanged)
}
}
super.addModels(models)
}
fun submitFiltersData(data: List<HomeRoomFilter>) {
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)
}
}

View File

@ -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),
}

View File

@ -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<RoomFilterHeaderItem.Holder>(R.layout.item_home_filter_tabs) {
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
var onFilterChangedListener: ((HomeRoomFilter) -> Unit)? = null
@EpoxyAttribute
var filtersData: List<HomeRoomFilter>? = 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<TabLayout>(R.id.home_filter_tabs_tabs)
}
}

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:viewBindingIgnore="true">
<com.google.android.material.tabs.TabLayout
android:id="@+id/home_filter_tabs_tabs"
style="@style/Widget.Vector.TabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?vctr_content_quaternary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1610,6 +1610,11 @@
<!-- Note to translators: for RTL languages, the + will be at the bottom left. Please translate "bottom left" instead of "bottom right". Thanks! -->
<string name="room_list_rooms_empty_body">Your rooms will be displayed here. Tap the + at the bottom right to find existing ones or start some of your own.</string>
<string name="room_list_filter_all">All</string>
<string name="room_list_filter_unreads">Unreads</string>
<string name="room_list_filter_favourites">Favourites</string>
<string name="room_list_filter_people">People</string>
<string name="title_activity_emoji_reaction_picker">Reactions</string>
<string name="message_add_reaction">Add Reaction</string>
<string name="message_view_reaction">View Reactions</string>