diff --git a/CHANGES.md b/CHANGES.md index 4a158086a2..9103acaa60 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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) diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListNameFilter.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListNameFilter.kt index 4c4a9755d2..6e787b8b95 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListNameFilter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListNameFilter.kt @@ -25,7 +25,7 @@ class RoomListNameFilter @Inject constructor() : Predicate { var filter: String = "" override fun test(roomSummary: RoomSummary): Boolean { - if (filter.isBlank()) { + if (filter.isEmpty()) { // No filter return true } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedListMemberAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedListMemberAction.kt index 9af554e883..67cacf4541 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedListMemberAction.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedListMemberAction.kt @@ -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() } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedListMemberViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedListMemberViewModel.kt index b65b3de807..d1acd80c25 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedListMemberViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedListMemberViewModel.kt @@ -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 + ) } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListController.kt index a65a30441d..2a0c787a7a 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListController.kt @@ -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() { @@ -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) - } - } - ) + ) } } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListFragment.kt index 480deea6af..84eccccfae 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListFragment.kt @@ -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) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewState.kt index ee8e534b1f..6d8bb2f42a 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewState.kt @@ -27,6 +27,7 @@ data class RoomBannedMemberListViewState( val roomId: String, val roomSummary: Async = Uninitialized, val bannedMemberSummaries: Async> = Uninitialized, + val filter: String = "", val onGoingModerationAction: List = emptyList(), val canUserBan: Boolean = false ) : MvRxState { diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListAction.kt index c61dcb98f5..342a2e8585 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListAction.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListAction.kt @@ -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() } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt index 59b1bca26f..71ac7fcec4 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt @@ -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() { 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() + ?.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) } } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt index 46d0f35b95..67577e866e 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt @@ -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() ?: return val stateKey = event.stateKey ?: return diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt index fedf1729ad..9e402c675b 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt @@ -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 + ) + } + } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewState.kt index 4d21aa103f..787ae920e1 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewState.kt @@ -31,6 +31,7 @@ data class RoomMemberListViewState( val roomId: String, val roomSummary: Async = Uninitialized, val roomMemberSummaries: Async = Uninitialized, + val filter: String = "", val threePidInvites: Async> = Uninitialized, val trustLevelMap: Async> = Uninitialized, val actionsPermissions: ActionPermissions = ActionPermissions() diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberSummaryFilter.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberSummaryFilter.kt new file mode 100644 index 0000000000..e2cc3f7b99 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberSummaryFilter.kt @@ -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 { + 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) + } +} diff --git a/vector/src/main/res/layout/fragment_room_setting_generic.xml b/vector/src/main/res/layout/fragment_room_setting_generic.xml index aa86ee342b..07744436ea 100644 --- a/vector/src/main/res/layout/fragment_room_setting_generic.xml +++ b/vector/src/main/res/layout/fragment_room_setting_generic.xml @@ -53,16 +53,53 @@ - + app:layout_constraintTop_toBottomOf="@+id/roomSettingsToolbar"> + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index c025054f98..d48162de29 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -623,6 +623,7 @@ Search Filter room members + Filter banned users No results ROOMS MESSAGES