Merge pull request #2196 from vector-im/ons/filter_room_members
Add SearchView to filter room members
This commit is contained in:
commit
8d05ef703a
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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" />
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue