Kick/ban room members (#2501)

* Add a temporary membershipChangePublisher on the timeline.

* Kick/Ban users from a room.

* Unit tests.

* Disable autocorrection on the members list search field.
This commit is contained in:
Doug
2024-02-27 13:02:35 +00:00
committed by GitHub
parent 1afde10ce2
commit fdbaef5aee
30 changed files with 561 additions and 43 deletions
@@ -0,0 +1,115 @@
//
// 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 RoomMembersListManageMemberSheet: View {
let member: RoomMemberDetails
let context: RoomMembersListScreenViewModel.Context
@State private var isPresentingRemoveConfirmation = false
var body: some View {
Form {
AvatarHeaderView(member: member,
avatarSize: .user(on: .memberDetails),
imageProvider: context.imageProvider) {
EmptyView()
}
Section {
ListRow(label: .default(title: L10n.screenRoomMemberListManageMemberUserInfo,
icon: \.info),
kind: .button {
context.send(viewAction: .showMemberDetails(member))
})
if !member.isBanned {
ListRow(label: .default(title: L10n.screenRoomMemberListManageMemberRemove,
icon: \.block,
role: .destructive),
kind: .button {
isPresentingRemoveConfirmation = true
})
} else {
// Theoretically we shouldn't reach this branch but just in case we do.
ListRow(label: .default(title: L10n.screenRoomMemberListManageMemberUnbanAction,
icon: \.block,
role: .destructive),
kind: .button {
context.send(viewAction: .unbanMember(member))
})
}
}
}
.compoundList()
.scrollBounceBehavior(.basedOnSize)
.presentationDragIndicator(.visible)
.presentationDetents([.large, .fraction(0.5)]) // TODO: Use the ideal height somehow?
.confirmationDialog(L10n.screenRoomMemberListManageMemberRemoveConfirmationTitle,
isPresented: $isPresentingRemoveConfirmation,
titleVisibility: .visible) {
if context.viewState.canKickUsers {
Button(L10n.screenRoomMemberListManageMemberRemoveConfirmationKick) {
context.send(viewAction: .kickMember(member))
}
}
if context.viewState.canBanUsers {
Button(L10n.screenRoomMemberListManageMemberRemoveConfirmationBan, role: .destructive) {
context.send(viewAction: .banMember(member))
}
}
}
}
}
struct RoomMembersListManageMemberSheet_Previews: PreviewProvider, TestablePreview {
static let viewModel = RoomMembersListScreenViewModel.mock
static var previews: some View {
RoomMembersListManageMemberSheet(member: .init(withProxy: RoomMemberProxyMock.mockDan),
context: viewModel.context)
.previewDisplayName("Joined")
RoomMembersListManageMemberSheet(member: .init(withProxy: RoomMemberProxyMock.mockBanned[3]),
context: viewModel.context)
.previewDisplayName("Banned")
}
}
struct RoomMembersListManageMemberSheetLive_Previews: PreviewProvider {
static let viewModel = RoomMembersListScreenViewModel.mock
static var previews: some View {
Color.clear
.sheet(isPresented: .constant(true)) {
RoomMembersListManageMemberSheet(member: .init(withProxy: RoomMemberProxyMock.mockDan),
context: viewModel.context)
}
.previewDisplayName("Sheet")
}
}
private extension RoomMembersListScreenViewModel {
static var mock: RoomMembersListScreenViewModel {
RoomMembersListScreenViewModel(initialMode: .members,
roomProxy: RoomProxyMock(with: .init()),
mediaProvider: MockMediaProvider(),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
appSettings: ServiceLocator.shared.settings)
}
}
@@ -43,14 +43,14 @@ struct RoomMembersListScreen: View {
}
.searchable(text: $context.searchQuery, placement: .navigationBarDrawer(displayMode: .always))
.compoundSearchField()
.autocorrectionDisabled()
.background(Color.compound.bgCanvasDefault.ignoresSafeArea())
.navigationTitle(L10n.commonPeople)
.alert(item: $context.alertInfo)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
inviteButton
}
.sheet(item: $context.memberToManage) {
RoomMembersListManageMemberSheet(member: $0, context: context)
}
.alert(item: $context.alertInfo)
.toolbar { toolbar }
.track(screen: .RoomMembers)
}
@@ -91,13 +91,13 @@ struct RoomMembersListScreen: View {
}
}
@ViewBuilder
private var inviteButton: some View {
if context.viewState.canInviteUsers {
Button {
context.send(viewAction: .invite)
} label: {
Text(L10n.actionInvite)
@ToolbarContentBuilder
private var toolbar: some ToolbarContent {
ToolbarItem(placement: .confirmationAction) {
if context.viewState.canInviteUsers {
Button(L10n.actionInvite) {
context.send(viewAction: .invite)
}
}
}
}
@@ -162,6 +162,7 @@ struct RoomMembersListScreen_Previews: PreviewProvider, TestablePreview {
return RoomMembersListScreenViewModel(initialMode: initialMode,
roomProxy: RoomProxyMock(with: .init(name: "Some room", members: members)),
mediaProvider: MockMediaProvider(),
userIndicatorController: ServiceLocator.shared.userIndicatorController)
userIndicatorController: ServiceLocator.shared.userIndicatorController,
appSettings: ServiceLocator.shared.settings)
}
}
@@ -22,7 +22,7 @@ struct RoomMembersListScreenMemberCell: View {
var body: some View {
Button {
context.send(viewAction: .selectMember(id: member.id))
context.send(viewAction: .selectMember(member))
} label: {
HStack(spacing: 8) {
LoadableAvatarImage(url: avatarURL,
@@ -109,7 +109,8 @@ struct RoomMembersListMemberCell_Previews: PreviewProvider, TestablePreview {
static let viewModel = RoomMembersListScreenViewModel(roomProxy: RoomProxyMock(with: .init(name: "Some room",
members: members)),
mediaProvider: MockMediaProvider(),
userIndicatorController: ServiceLocator.shared.userIndicatorController)
userIndicatorController: ServiceLocator.shared.userIndicatorController,
appSettings: ServiceLocator.shared.settings)
static var previews: some View {
VStack(spacing: 12) {
Section("Invited/Joined") {