Fix various bugs in the moderation feature. (#2608)

* Fix search field string.

* Show your own user as an Admin when changing roles.

* Also show invited users when changing roles.

* Don't allow admin's to kick/ban other admins or themselves.

* Fix a bug when left members were counted as admins/moderators.

* Show when a member is pending.

* Add sections to the change role screen.
This commit is contained in:
Doug
2024-03-27 10:33:40 +00:00
committed by GitHub
parent bb725db6bb
commit e424a02a2b
51 changed files with 413 additions and 148 deletions
@@ -24,8 +24,13 @@ enum RoomChangeRolesScreenViewModelAction {
struct RoomChangeRolesScreenViewState: BindableState {
/// The screen's current mode (which role we are promoting/demoting users to/from.
let mode: RoomMemberDetails.Role
/// All of the room's members.
var members: [RoomMemberDetails]
/// All of the room's members who are currently admins.
var administrators: [RoomMemberDetails]
/// All of the room's members who are currently moderators.
var moderators: [RoomMemberDetails]
/// All of the room's members who are currently neither an admin or moderator.
var users: [RoomMemberDetails]
var bindings: RoomChangeRolesScreenViewStateBindings
/// The members selected for promotion to the current role.
@@ -48,19 +53,24 @@ struct RoomChangeRolesScreenViewState: BindableState {
}
}
/// The visible members in the screen (after searching).
var visibleMembers: [RoomMemberDetails] {
guard !bindings.searchQuery.isEmpty else { return members }
return members.filter { member in
member.name?.localizedStandardContains(bindings.searchQuery) == true
|| member.id.localizedStandardContains(bindings.searchQuery)
}
/// The visible admins in the screen (after searching).
var visibleAdministrators: [RoomMemberDetails] {
administrators.filter { $0.matches(searchQuery: bindings.searchQuery) }
}
/// The visible mods in the screen (after searching).
var visibleModerators: [RoomMemberDetails] {
moderators.filter { $0.matches(searchQuery: bindings.searchQuery) }
}
/// The visible regular users in the screen (after searching).
var visibleUsers: [RoomMemberDetails] {
users.filter { $0.matches(searchQuery: bindings.searchQuery) }
}
/// All of the members who will gain/keep this screen's role after saving any changes.
var membersWithRole: [RoomMemberDetails] {
members.filter(isMemberSelected)
administrators.filter(isMemberSelected) + moderators.filter(isMemberSelected) + users.filter(isMemberSelected)
}
/// Whether or not any changes have been made to the members.
@@ -40,7 +40,9 @@ class RoomChangeRolesScreenViewModel: RoomChangeRolesScreenViewModelType, RoomCh
self.analytics = analytics
super.init(initialViewState: RoomChangeRolesScreenViewState(mode: mode,
members: [],
administrators: [],
moderators: [],
users: [],
bindings: .init()))
roomProxy.membersPublisher
@@ -82,10 +84,27 @@ class RoomChangeRolesScreenViewModel: RoomChangeRolesScreenViewModelType, RoomCh
// MARK: - Private
private func updateMembers(_ members: [RoomMemberProxyProtocol]) {
state.members = members.sorted().compactMap { member in
guard member.membership == .join, member.userID != roomProxy.ownUserID else { return nil }
return RoomMemberDetails(withProxy: member)
var administrators = [RoomMemberDetails]()
var moderators = [RoomMemberDetails]()
var users = [RoomMemberDetails]()
for member in members.sorted() {
guard member.isActive else { continue }
let memberDetails = RoomMemberDetails(withProxy: member)
switch member.role {
case .administrator:
administrators.append(memberDetails)
case .moderator:
moderators.append(memberDetails)
case .user:
users.append(memberDetails)
}
}
state.administrators = administrators
state.moderators = moderators
state.users = users
}
private func toggleMember(_ member: RoomMemberDetails) {
@@ -56,34 +56,20 @@ struct RoomChangeRolesScreen: View {
}
}
membersSection
RoomChangeRolesScreenSection(members: context.viewState.administrators,
title: L10n.screenRoomChangeRoleSectionAdministrators,
context: context)
RoomChangeRolesScreenSection(members: context.viewState.moderators,
title: L10n.screenRoomChangeRoleSectionModerators,
context: context)
RoomChangeRolesScreenSection(members: context.viewState.users,
title: L10n.screenRoomChangeRoleSectionUsers,
context: context)
}
}
}
@ViewBuilder
private var membersSection: some View {
if !context.viewState.visibleMembers.isEmpty {
Section {
ForEach(context.viewState.visibleMembers, id: \.id) { member in
RoomChangeRolesScreenRow(member: member,
imageProvider: context.imageProvider,
isSelected: context.viewState.isMemberSelected(member)) {
context.send(viewAction: .toggleMember(member))
}
.disabled(member.role == .administrator)
}
} header: {
Text(L10n.screenRoomMemberListRoomMembersHeaderTitle)
.compoundListSectionHeader()
}
} else {
Section.empty
}
}
@ScaledMetric private var cellWidth: CGFloat = 72
private var membersWithRoleSection: some View {
ScrollView(.horizontal, showsIndicators: false) {
ScrollViewReader { scrollView in
@@ -28,10 +28,20 @@ struct RoomChangeRolesScreenRow: View {
let action: () -> Void
var body: some View {
ListRow(label: .avatar(title: member.name ?? member.id,
ListRow(label: .avatar(title: memberName,
description: member.name == nil ? nil : member.id,
icon: avatar),
kind: isEnabled ? .multiSelection(isSelected: isSelected, action: action) : .label)
kind: .multiSelection(isSelected: isSelected, action: action))
}
var memberName: String {
let name = member.name ?? member.id
return if member.isInvited {
L10n.screenRoomChangeRoleInvitedMemberName(name)
} else {
name
}
}
var avatar: LoadableAvatarImage {
@@ -58,6 +68,11 @@ struct RoomChangeRolesScreenRow_Previews: PreviewProvider, TestablePreview {
isSelected: false,
action: action)
RoomChangeRolesScreenRow(member: .init(withProxy: RoomMemberProxyMock.mockInvited),
imageProvider: MockMediaProvider(),
isSelected: false,
action: action)
RoomChangeRolesScreenRow(member: .init(withProxy: RoomMemberProxyMock.mockCharlie),
imageProvider: MockMediaProvider(),
isSelected: true,
@@ -0,0 +1,43 @@
//
// Copyright 2024 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.
//
import Compound
import SwiftUI
struct RoomChangeRolesScreenSection: View {
let members: [RoomMemberDetails]
let title: String
@ObservedObject var context: RoomChangeRolesScreenViewModel.Context
var body: some View {
if !members.isEmpty {
Section {
ForEach(members, id: \.id) { member in
RoomChangeRolesScreenRow(member: member,
imageProvider: context.imageProvider,
isSelected: context.viewState.isMemberSelected(member)) {
context.send(viewAction: .toggleMember(member))
}
.disabled(member.role == .administrator)
}
} header: {
Text(title)
.compoundListSectionHeader()
}
}
}
}