Merge pull request #2196 from vector-im/ons/filter_room_members

Add SearchView to filter room members
This commit is contained in:
Benoit Marty 2020-10-06 10:08:56 +02:00 committed by GitHub
commit 8d05ef703a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 202 additions and 48 deletions

View File

@ -11,6 +11,7 @@ Improvements 🙌:
- Small optimisation of scrolling experience in timeline (#2114)
- Allow user to reset cross signing if he has no way to recover (#2052)
- Create home shortcut for any room (#1525)
- Filter room member (and banned users) by name (#2184)
Bugfix 🐛:
- Improve support for image/audio/video/file selection with intent changes (#1376)

View File

@ -25,7 +25,7 @@ class RoomListNameFilter @Inject constructor() : Predicate<RoomSummary> {
var filter: String = ""
override fun test(roomSummary: RoomSummary): Boolean {
if (filter.isBlank()) {
if (filter.isEmpty()) {
// No filter
return true
}

View File

@ -22,4 +22,5 @@ import im.vector.app.core.platform.VectorViewModelAction
sealed class RoomBannedListMemberAction : VectorViewModelAction {
data class QueryInfo(val roomMemberSummary: RoomMemberSummary) : RoomBannedListMemberAction()
data class UnBanUser(val roomMemberSummary: RoomMemberSummary) : RoomBannedListMemberAction()
data class Filter(val filter: String) : RoomBannedListMemberAction()
}

View File

@ -35,6 +35,7 @@ import org.matrix.android.sdk.internal.util.awaitCallback
import org.matrix.android.sdk.rx.rx
import org.matrix.android.sdk.rx.unwrap
import im.vector.app.R
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
@ -90,6 +91,15 @@ class RoomBannedListMemberViewModel @AssistedInject constructor(@Assisted initia
when (action) {
is RoomBannedListMemberAction.QueryInfo -> onQueryBanInfo(action.roomMemberSummary)
is RoomBannedListMemberAction.UnBanUser -> unBanUser(action.roomMemberSummary)
is RoomBannedListMemberAction.Filter -> handleFilter(action)
}.exhaustive
}
private fun handleFilter(action: RoomBannedListMemberAction.Filter) {
setState {
copy(
filter = action.filter
)
}
}

View File

@ -17,8 +17,6 @@
package im.vector.app.features.roomprofile.banned
import com.airbnb.epoxy.TypedEpoxyController
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.util.toMatrixItem
import im.vector.app.R
import im.vector.app.core.epoxy.dividerItem
import im.vector.app.core.epoxy.profiles.buildProfileSection
@ -28,11 +26,15 @@ import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.list.genericFooterItem
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.roomprofile.members.RoomMemberSummaryFilter
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
class RoomBannedMemberListController @Inject constructor(
private val avatarRenderer: AvatarRenderer,
private val stringProvider: StringProvider,
private val roomMemberSummaryFilter: RoomMemberSummaryFilter,
colorProvider: ColorProvider
) : TypedEpoxyController<RoomBannedMemberListViewState>() {
@ -63,34 +65,37 @@ class RoomBannedMemberListController @Inject constructor(
} else {
buildProfileSection(quantityString)
bannedList.join(
each = { _, roomMember ->
val actionInProgress = data.onGoingModerationAction.contains(roomMember.userId)
profileMatrixItemWithProgress {
id(roomMember.userId)
matrixItem(roomMember.toMatrixItem())
avatarRenderer(avatarRenderer)
apply {
if (actionInProgress) {
inProgress(true)
editable(false)
} else {
inProgress(false)
editable(true)
clickListener { _ ->
callback?.onUnbanClicked(roomMember)
roomMemberSummaryFilter.filter = data.filter
bannedList
.filter { roomMemberSummaryFilter.test(it) }
.join(
each = { _, roomMember ->
val actionInProgress = data.onGoingModerationAction.contains(roomMember.userId)
profileMatrixItemWithProgress {
id(roomMember.userId)
matrixItem(roomMember.toMatrixItem())
avatarRenderer(avatarRenderer)
apply {
if (actionInProgress) {
inProgress(true)
editable(false)
} else {
inProgress(false)
editable(true)
clickListener { _ ->
callback?.onUnbanClicked(roomMember)
}
}
}
}
},
between = { _, roomMemberBefore ->
dividerItem {
id("divider_${roomMemberBefore.userId}")
color(dividerColor)
}
}
}
},
between = { _, roomMemberBefore ->
dividerItem {
id("divider_${roomMemberBefore.userId}")
color(dividerColor)
}
}
)
)
}
}
}

View File

@ -19,6 +19,8 @@ package im.vector.app.features.roomprofile.banned
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.SearchView
import androidx.core.view.isVisible
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
@ -53,6 +55,7 @@ class RoomBannedMemberListFragment @Inject constructor(
super.onViewCreated(view, savedInstanceState)
roomMemberListController.callback = this
setupToolbar(roomSettingsToolbar)
setupSearchView()
recyclerView.configureWith(roomMemberListController, hasFixedSize = true)
viewModel.observeViewEvents {
@ -84,6 +87,21 @@ class RoomBannedMemberListFragment @Inject constructor(
super.onDestroyView()
}
private fun setupSearchView() {
searchViewAppBarLayout.isVisible = true
searchView.queryHint = getString(R.string.search_banned_user_hint)
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
return true
}
override fun onQueryTextChange(newText: String): Boolean {
viewModel.handle(RoomBannedListMemberAction.Filter(newText))
return true
}
})
}
override fun invalidate() = withState(viewModel) { viewState ->
roomMemberListController.setData(viewState)
renderRoomSummary(viewState)

View File

@ -27,6 +27,7 @@ data class RoomBannedMemberListViewState(
val roomId: String,
val roomSummary: Async<RoomSummary> = Uninitialized,
val bannedMemberSummaries: Async<List<RoomMemberSummary>> = Uninitialized,
val filter: String = "",
val onGoingModerationAction: List<String> = emptyList(),
val canUserBan: Boolean = false
) : MvRxState {

View File

@ -20,4 +20,5 @@ import im.vector.app.core.platform.VectorViewModelAction
sealed class RoomMemberListAction : VectorViewModelAction {
data class RevokeThreePidInvite(val stateKey: String) : RoomMemberListAction()
data class FilterMemberList(val searchTerm: String) : RoomMemberListAction()
}

View File

@ -17,12 +17,6 @@
package im.vector.app.features.roomprofile.members
import com.airbnb.epoxy.TypedEpoxyController
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomThirdPartyInviteContent
import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem
import im.vector.app.R
import im.vector.app.core.epoxy.dividerItem
import im.vector.app.core.epoxy.profiles.buildProfileSection
@ -31,17 +25,24 @@ import im.vector.app.core.extensions.join
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.home.AvatarRenderer
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomThirdPartyInviteContent
import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
class RoomMemberListController @Inject constructor(
private val avatarRenderer: AvatarRenderer,
private val stringProvider: StringProvider,
private val roomMemberSummaryFilter: RoomMemberSummaryFilter,
colorProvider: ColorProvider
) : TypedEpoxyController<RoomMemberListViewState>() {
interface Callback {
fun onRoomMemberClicked(roomMember: RoomMemberSummary)
fun onThreePidInvites(event: Event)
fun onThreePidInviteClicked(event: Event)
}
private val dividerColor = colorProvider.getColorFromAttribute(R.attr.vctr_list_divider_color)
@ -53,17 +54,29 @@ class RoomMemberListController @Inject constructor(
}
override fun buildModels(data: RoomMemberListViewState?) {
val roomMembersByPowerLevel = data?.roomMemberSummaries?.invoke() ?: return
val threePidInvites = data.threePidInvites().orEmpty()
data ?: return
roomMemberSummaryFilter.filter = data.filter
val roomMembersByPowerLevel = data.roomMemberSummaries.invoke() ?: return
val threePidInvites = data.threePidInvites()
?.filter { event ->
event.content.toModel<RoomThirdPartyInviteContent>()
?.takeIf {
data.filter.isEmpty() || it.displayName.contains(data.filter, ignoreCase = true)
} != null
}
.orEmpty()
var threePidInvitesDone = threePidInvites.isEmpty()
for ((powerLevelCategory, roomMemberList) in roomMembersByPowerLevel) {
if (roomMemberList.isEmpty()) {
val filteredRoomMemberList = roomMemberList.filter { roomMemberSummaryFilter.test(it) }
if (filteredRoomMemberList.isEmpty()) {
continue
}
if (powerLevelCategory == RoomMemberListCategories.USER && !threePidInvitesDone) {
// If there is not regular invite, display threepid invite before the regular user
// If there is no regular invite, display threepid invite before the regular user
buildProfileSection(
stringProvider.getString(RoomMemberListCategories.INVITE.titleRes)
)
@ -75,7 +88,7 @@ class RoomMemberListController @Inject constructor(
buildProfileSection(
stringProvider.getString(powerLevelCategory.titleRes)
)
roomMemberList.join(
filteredRoomMemberList.join(
each = { _, roomMember ->
profileMatrixItem {
id(roomMember.userId)
@ -94,12 +107,13 @@ class RoomMemberListController @Inject constructor(
}
}
)
if (powerLevelCategory == RoomMemberListCategories.INVITE) {
if (powerLevelCategory == RoomMemberListCategories.INVITE && !threePidInvitesDone) {
// Display the threepid invite after the regular invite
dividerItem {
id("divider_threepidinvites")
color(dividerColor)
}
buildThreePidInvites(data)
threePidInvitesDone = true
}
@ -128,7 +142,7 @@ class RoomMemberListController @Inject constructor(
avatarRenderer(avatarRenderer)
editable(data.actionsPermissions.canRevokeThreePidInvite)
clickListener { _ ->
callback?.onThreePidInvites(event)
callback?.onThreePidInviteClicked(event)
}
}
}

View File

@ -21,6 +21,8 @@ import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.SearchView
import androidx.core.view.isVisible
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
@ -72,12 +74,28 @@ class RoomMemberListFragment @Inject constructor(
super.onViewCreated(view, savedInstanceState)
roomMemberListController.callback = this
setupToolbar(roomSettingsToolbar)
setupSearchView()
recyclerView.configureWith(roomMemberListController, hasFixedSize = true)
viewModel.selectSubscribe(this, RoomMemberListViewState::actionsPermissions) {
invalidateOptionsMenu()
}
}
private fun setupSearchView() {
searchViewAppBarLayout.isVisible = true
searchView.queryHint = getString(R.string.search_members_hint)
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
return true
}
override fun onQueryTextChange(newText: String): Boolean {
viewModel.handle(RoomMemberListAction.FilterMemberList(newText))
return true
}
})
}
override fun onDestroyView() {
recyclerView.cleanup()
super.onDestroyView()
@ -92,7 +110,7 @@ class RoomMemberListFragment @Inject constructor(
navigator.openRoomMemberProfile(roomMember.userId, roomId = roomProfileArgs.roomId, context = requireActivity())
}
override fun onThreePidInvites(event: Event) {
override fun onThreePidInviteClicked(event: Event) {
// Display a dialog to revoke invite if power level is high enough
val content = event.content.toModel<RoomThirdPartyInviteContent>() ?: return
val stateKey = event.stateKey ?: return

View File

@ -188,6 +188,7 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState
override fun handle(action: RoomMemberListAction) {
when (action) {
is RoomMemberListAction.RevokeThreePidInvite -> handleRevokeThreePidInvite(action)
is RoomMemberListAction.FilterMemberList -> handleFilterMemberList(action)
}.exhaustive
}
@ -201,4 +202,12 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState
)
}
}
private fun handleFilterMemberList(action: RoomMemberListAction.FilterMemberList) {
setState {
copy(
filter = action.searchTerm
)
}
}
}

View File

@ -31,6 +31,7 @@ data class RoomMemberListViewState(
val roomId: String,
val roomSummary: Async<RoomSummary> = Uninitialized,
val roomMemberSummaries: Async<RoomMemberSummaries> = Uninitialized,
val filter: String = "",
val threePidInvites: Async<List<Event>> = Uninitialized,
val trustLevelMap: Async<Map<String, RoomEncryptionTrustLevel?>> = Uninitialized,
val actionsPermissions: ActionPermissions = ActionPermissions()

View File

@ -0,0 +1,37 @@
/*
* Copyright (c) 2020 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.roomprofile.members
import io.reactivex.functions.Predicate
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import javax.inject.Inject
class RoomMemberSummaryFilter @Inject constructor() : Predicate<RoomMemberSummary> {
var filter: String = ""
override fun test(roomMemberSummary: RoomMemberSummary): Boolean {
if (filter.isEmpty()) {
// No filter
return true
}
return roomMemberSummary.displayName?.contains(filter, ignoreCase = true).orFalse()
// We should maybe exclude the domain from the userId
|| roomMemberSummary.userId.contains(filter, ignoreCase = true)
}
}

View File

@ -53,16 +53,53 @@
</androidx.appcompat.widget.Toolbar>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="0dp"
android:layout_height="0dp"
android:overScrollMode="always"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/roomSettingsToolbar"
tools:listitem="@layout/item_profile_action" />
app:layout_constraintTop_toBottomOf="@+id/roomSettingsToolbar">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:overScrollMode="always"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:listitem="@layout/item_profile_action" />
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/searchViewAppBarLayout"
style="@style/VectorAppBarLayoutStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="4dp"
android:visibility="gone"
tools:visibility="visible">
<!-- Use an extra container for the margin to be taken into account by the layout_scrollFlags -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_scrollFlags="scroll|enterAlways|snap">
<androidx.appcompat.widget.SearchView
android:id="@+id/searchView"
style="@style/VectorSearchView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:background="@null"
android:minHeight="0dp"
tools:queryHint="@string/search_hint" />
</FrameLayout>
</com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<include layout="@layout/merge_overlay_waiting_view" />

View File

@ -623,6 +623,7 @@
<!-- Search -->
<string name="search_hint">Search</string>
<string name="search_members_hint">Filter room members</string>
<string name="search_banned_user_hint">Filter banned users</string>
<string name="search_no_results">No results</string>
<string name="tab_title_search_rooms">ROOMS</string>
<string name="tab_title_search_messages">MESSAGES</string>