mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-10-31 16:24:11 +00:00
refactor: manage member sheet in timeline
better implementation updated tests Update ElementX/Sources/Screens/Timeline/TimelineViewModel.swift Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> fix
This commit is contained in:
@@ -80,31 +80,15 @@ struct RoomMembersListScreenViewStateBindings {
|
||||
var searchQuery = ""
|
||||
/// The current mode the screen is in.
|
||||
var mode: RoomMembersListScreenMode = .members
|
||||
/// A selected member to kick, ban, promote etc.
|
||||
var memberToManage: RoomMembersListScreenManagementDetails?
|
||||
/// A sheet model for the selected member to kick, ban, promote etc.
|
||||
var manageMemeberViewModel: ManageRoomMemberSheetViewModel?
|
||||
|
||||
/// Information describing the currently displayed alert.
|
||||
var alertInfo: AlertInfo<RoomMembersListScreenAlertType>?
|
||||
}
|
||||
|
||||
/// Information about managing a particular room member.
|
||||
struct RoomMembersListScreenManagementDetails: Identifiable {
|
||||
var id: String { member.id }
|
||||
|
||||
/// The member that is being managed.
|
||||
let member: RoomMemberDetails
|
||||
|
||||
/// A management action that can be performed on the member.
|
||||
enum Action { case kick, ban }
|
||||
/// The management actions available for `member`.
|
||||
let actions: [Action]
|
||||
}
|
||||
|
||||
enum RoomMembersListScreenViewAction {
|
||||
case selectMember(RoomMemberDetails)
|
||||
case showMemberDetails(RoomMemberDetails)
|
||||
case kickMember(RoomMemberDetails)
|
||||
case banMember(RoomMemberDetails)
|
||||
case unbanMember(RoomMemberDetails)
|
||||
case invite
|
||||
}
|
||||
|
||||
+30
-68
@@ -15,8 +15,10 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
|
||||
private let roomProxy: JoinedRoomProxyProtocol
|
||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||
private let analytics: AnalyticsService
|
||||
private let mediaProvider: MediaProviderProtocol
|
||||
|
||||
private var members: [RoomMemberProxyProtocol] = []
|
||||
private var currentUserProxy: RoomMemberProxyProtocol?
|
||||
|
||||
private var actionsSubject: PassthroughSubject<RoomMembersListScreenViewModelAction, Never> = .init()
|
||||
|
||||
@@ -34,6 +36,7 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
|
||||
self.roomProxy = roomProxy
|
||||
self.userIndicatorController = userIndicatorController
|
||||
self.analytics = analytics
|
||||
self.mediaProvider = mediaProvider
|
||||
|
||||
super.init(initialViewState: .init(joinedMembersCount: roomProxy.infoPublisher.value.joinedMembersCount,
|
||||
bindings: .init(mode: initialMode)),
|
||||
@@ -48,34 +51,6 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
|
||||
switch viewAction {
|
||||
case .selectMember(let member):
|
||||
selectMember(member)
|
||||
case .showMemberDetails(let member):
|
||||
showMemberDetails(member)
|
||||
case .kickMember(let member):
|
||||
var reason: String?
|
||||
let binding: Binding<String> = .init(get: { reason ?? "" },
|
||||
set: { reason = $0.isBlank ? nil : $0 })
|
||||
state.bindings.alertInfo = .init(id: .kickConfirmation,
|
||||
title: L10n.screenRoomMemberListKickMemberConfirmationTitle,
|
||||
message: L10n.screenRoomMemberListKickMemberConfirmationDescription,
|
||||
primaryButton: .init(title: L10n.actionCancel, role: .cancel) { },
|
||||
secondaryButton: .init(title: L10n.screenRoomMemberListKickMemberConfirmationAction) { [weak self] in Task { await self?.kickMember(member, reason: reason) } },
|
||||
textFields: [.init(placeholder: L10n.commonReason,
|
||||
text: binding,
|
||||
autoCapitalization: .sentences,
|
||||
autoCorrectionDisabled: false)])
|
||||
case .banMember(let member):
|
||||
var reason: String?
|
||||
let binding: Binding<String> = .init(get: { reason ?? "" },
|
||||
set: { reason = $0.isBlank ? nil : $0 })
|
||||
state.bindings.alertInfo = .init(id: .banConfirmation,
|
||||
title: L10n.screenRoomMemberListBanMemberConfirmationTitle,
|
||||
message: L10n.screenRoomMemberListBanMemberConfirmationDescription,
|
||||
primaryButton: .init(title: L10n.actionCancel, role: .cancel) { },
|
||||
secondaryButton: .init(title: L10n.screenRoomMemberListBanMemberConfirmationAction) { [weak self] in Task { await self?.banMember(member, reason: reason) } },
|
||||
textFields: [.init(placeholder: L10n.commonReason,
|
||||
text: binding,
|
||||
autoCapitalization: .sentences,
|
||||
autoCorrectionDisabled: false)])
|
||||
case .unbanMember(let member):
|
||||
Task { await unbanMember(member) }
|
||||
case .invite:
|
||||
@@ -118,6 +93,7 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
|
||||
let members = members.sorted()
|
||||
let roomMembersDetails = await buildMembersDetails(members: members)
|
||||
self.members = members
|
||||
self.currentUserProxy = members.first { $0.userID == roomProxy.ownUserID }
|
||||
|
||||
self.state = .init(joinedMembersCount: roomProxy.infoPublisher.value.joinedMembersCount,
|
||||
joinedMembers: roomMembersDetails.joinedMembers,
|
||||
@@ -168,6 +144,12 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
|
||||
}
|
||||
|
||||
private func selectMember(_ member: RoomMemberDetails) {
|
||||
guard let currentUserProxy,
|
||||
currentUserProxy.powerLevel > member.powerLevel else {
|
||||
showMemberDetails(member)
|
||||
return
|
||||
}
|
||||
|
||||
if member.isBanned { // No need to check canBan here, banned users are only shown when it is true.
|
||||
state.bindings.alertInfo = AlertInfo(id: .unbanConfirmation(member),
|
||||
title: L10n.screenRoomMemberListManageMemberUnbanTitle,
|
||||
@@ -179,16 +161,26 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
|
||||
return
|
||||
}
|
||||
|
||||
var actions = [RoomMembersListScreenManagementDetails.Action]()
|
||||
if state.canKickUsers, member.role != .administrator {
|
||||
actions.append(.kick)
|
||||
}
|
||||
if state.canBanUsers, member.role != .administrator {
|
||||
actions.append(.ban)
|
||||
}
|
||||
|
||||
if !actions.isEmpty {
|
||||
state.bindings.memberToManage = .init(member: member, actions: actions)
|
||||
if state.canKickUsers || state.canBanUsers {
|
||||
let manageMemeberViewModel = ManageRoomMemberSheetViewModel(member: member,
|
||||
canKick: state.canKickUsers,
|
||||
canBan: state.canBanUsers,
|
||||
roomProxy: roomProxy,
|
||||
userIndicatorController: userIndicatorController,
|
||||
analyticsService: analytics,
|
||||
mediaProvider: mediaProvider)
|
||||
manageMemeberViewModel.actions.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .dismiss(let shouldShowDetails):
|
||||
state.bindings.manageMemeberViewModel = nil
|
||||
if shouldShowDetails {
|
||||
showMemberDetails(member)
|
||||
}
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
state.bindings.manageMemeberViewModel = manageMemeberViewModel
|
||||
} else {
|
||||
showMemberDetails(member)
|
||||
}
|
||||
@@ -200,46 +192,16 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
|
||||
return
|
||||
}
|
||||
actionsSubject.send(.selectMember(member))
|
||||
state.bindings.memberToManage = nil
|
||||
}
|
||||
|
||||
// MARK: - Member Management
|
||||
|
||||
private func kickMember(_ member: RoomMemberDetails, reason: String?) async {
|
||||
let indicatorTitle = L10n.screenRoomMemberListRemovingUser(member.name ?? member.id)
|
||||
showManageMemberIndicator(title: indicatorTitle)
|
||||
|
||||
switch await roomProxy.kickUser(member.id, reason: reason) {
|
||||
case .success:
|
||||
state.bindings.memberToManage = nil
|
||||
hideManageMemberIndicator(title: indicatorTitle)
|
||||
analytics.trackRoomModeration(action: .KickMember, role: nil)
|
||||
case .failure:
|
||||
showManageMemberFailure(title: indicatorTitle)
|
||||
}
|
||||
}
|
||||
|
||||
private func banMember(_ member: RoomMemberDetails, reason: String?) async {
|
||||
let indicatorTitle = L10n.screenRoomMemberListBanningUser(member.name ?? member.id)
|
||||
showManageMemberIndicator(title: indicatorTitle)
|
||||
|
||||
switch await roomProxy.banUser(member.id, reason: reason) {
|
||||
case .success:
|
||||
state.bindings.memberToManage = nil
|
||||
hideManageMemberIndicator(title: indicatorTitle)
|
||||
analytics.trackRoomModeration(action: .BanMember, role: nil)
|
||||
case .failure:
|
||||
showManageMemberFailure(title: indicatorTitle)
|
||||
}
|
||||
}
|
||||
|
||||
private func unbanMember(_ member: RoomMemberDetails) async {
|
||||
let indicatorTitle = L10n.screenRoomMemberListUnbanningUser(member.name ?? member.id)
|
||||
showManageMemberIndicator(title: indicatorTitle)
|
||||
|
||||
switch await roomProxy.unbanUser(member.id) {
|
||||
case .success:
|
||||
state.bindings.memberToManage = nil
|
||||
hideManageMemberIndicator(title: indicatorTitle)
|
||||
analytics.trackRoomModeration(action: .UnbanMember, role: nil)
|
||||
case .failure:
|
||||
|
||||
-96
@@ -1,96 +0,0 @@
|
||||
//
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Compound
|
||||
import SwiftUI
|
||||
|
||||
struct RoomMembersListManageMemberSheet: View {
|
||||
let member: RoomMemberDetails
|
||||
let actions: [RoomMembersListScreenManagementDetails.Action]
|
||||
|
||||
@ObservedObject var context: RoomMembersListScreenViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
AvatarHeaderView(member: member,
|
||||
avatarSize: .user(on: .memberDetails),
|
||||
mediaProvider: context.mediaProvider) {
|
||||
EmptyView()
|
||||
}
|
||||
|
||||
Section {
|
||||
ListRow(label: .default(title: L10n.screenRoomMemberListManageMemberUserInfo,
|
||||
icon: \.userProfileSolid),
|
||||
kind: .button {
|
||||
context.send(viewAction: .showMemberDetails(member))
|
||||
})
|
||||
|
||||
if actions.contains(.kick) {
|
||||
ListRow(label: .default(title: L10n.screenRoomMemberListManageMemberRemove,
|
||||
icon: \.close),
|
||||
kind: .button {
|
||||
context.send(viewAction: .kickMember(member))
|
||||
})
|
||||
}
|
||||
|
||||
if actions.contains(.ban) {
|
||||
ListRow(label: .default(title: L10n.screenRoomMemberListManageMemberBan,
|
||||
icon: \.block,
|
||||
role: .destructive),
|
||||
kind: .button {
|
||||
context.send(viewAction: .banMember(member))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
.compoundList()
|
||||
.scrollBounceBehavior(.basedOnSize)
|
||||
.presentationDragIndicator(.visible)
|
||||
.presentationDetents([.large, .fraction(0.54)]) // Maybe find a way to use the ideal height somehow?
|
||||
}
|
||||
}
|
||||
|
||||
struct RoomMembersListManageMemberSheet_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = RoomMembersListScreenViewModel.mock
|
||||
|
||||
static var previews: some View {
|
||||
RoomMembersListManageMemberSheet(member: .init(withProxy: RoomMemberProxyMock.mockDan),
|
||||
actions: [.kick, .ban],
|
||||
context: viewModel.context)
|
||||
.previewDisplayName("Joined")
|
||||
|
||||
RoomMembersListManageMemberSheet(member: .init(withProxy: RoomMemberProxyMock.mockBanned[3]),
|
||||
actions: [],
|
||||
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),
|
||||
actions: [.kick, .ban],
|
||||
context: viewModel.context)
|
||||
}
|
||||
.previewDisplayName("Sheet")
|
||||
}
|
||||
}
|
||||
|
||||
private extension RoomMembersListScreenViewModel {
|
||||
static var mock: RoomMembersListScreenViewModel {
|
||||
RoomMembersListScreenViewModel(initialMode: .members,
|
||||
clientProxy: ClientProxyMock(.init()),
|
||||
roomProxy: JoinedRoomProxyMock(.init(members: .allMembersAsAdmin)),
|
||||
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
analytics: ServiceLocator.shared.analytics)
|
||||
}
|
||||
}
|
||||
@@ -47,8 +47,8 @@ struct RoomMembersListScreen: View {
|
||||
.autocorrectionDisabled()
|
||||
.background(Color.compound.bgCanvasDefault.ignoresSafeArea())
|
||||
.navigationTitle(L10n.commonPeople)
|
||||
.sheet(item: $context.memberToManage) {
|
||||
RoomMembersListManageMemberSheet(member: $0.member, actions: $0.actions, context: context)
|
||||
.sheet(item: $context.manageMemeberViewModel) {
|
||||
ManageRoomMemberSheetView(context: $0.context)
|
||||
}
|
||||
.alert(item: $context.alertInfo)
|
||||
.toolbar { toolbar }
|
||||
|
||||
Reference in New Issue
Block a user