Always open manage member sheet (#4110)

* always open manage member sheet

* Update ElementX/Sources/Screens/ManageRoomMemberSheet/ManageRoomMemberSheetModels.swift

Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com>

* Update ElementX/Sources/Screens/ManageRoomMemberSheet/ManageRoomMemberSheetModels.swift

Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com>

* pr suggestions and handled the buttons actions correctly

* Update ElementX/Sources/Screens/ManageRoomMemberSheet/View/ManageRoomMemberSheetView.swift

Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com>

* fixed some compilation errors

* added some documentation

* using a struct instead of a protocol

* using arguments instead of a struct

---------

Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com>
This commit is contained in:
Mauro
2025-05-19 16:17:57 +02:00
committed by GitHub
parent 4f4b4b236d
commit 9a8819ac26
25 changed files with 334 additions and 170 deletions
@@ -10,9 +10,35 @@ enum ManageRoomMemberSheetViewModelAction: Equatable {
}
struct ManageRoomMemberSheetViewState: BindableState {
let member: RoomMemberDetails
let canKick: Bool
let canBan: Bool
let memberDetails: ManageRoomMemberDetails
let permissions: ManageRoomMemberPermissions
var isBanUnbanDisabled: Bool {
// This is a best effort check, if we haven't fetched the member yet we assume we can peform the action
guard case let .memberDetails(member) = memberDetails else {
return false
}
return permissions.ownPowerLevel <= member.powerLevel
}
var isKickDisabled: Bool {
// This is a best effort check, if we haven't fetched the member yet we assume we can peform the action
guard case let .memberDetails(member) = memberDetails else {
return false
}
return !member.isActive || permissions.ownPowerLevel <= member.powerLevel
}
var isMemberBanned: Bool {
// This is a best effort check, if we haven't fetched the member yet we assume the member is not banned
guard case let .memberDetails(member) = memberDetails else {
return false
}
return member.isBanned
}
var bindings = ManageRoomMemberSheetViewStateBindings()
}
@@ -24,10 +50,41 @@ struct ManageRoomMemberSheetViewStateBindings {
enum ManageRoomMemberSheetViewAlertType {
case kick
case ban
case unban
}
enum ManageRoomMemberSheetViewAction {
case kick
case ban
case unban
case displayDetails
}
enum ManageRoomMemberDetails {
case memberDetails(roomMember: RoomMemberDetails)
case loadingMemberDetails(sender: TimelineItemSender)
var id: String {
switch self {
case let .memberDetails(roomMember):
roomMember.id
case let .loadingMemberDetails(sender):
sender.id
}
}
var name: String? {
switch self {
case let .memberDetails(roomMember):
roomMember.name
case let .loadingMemberDetails(sender):
sender.displayName
}
}
}
struct ManageRoomMemberPermissions {
let canKick: Bool
let canBan: Bool
let ownPowerLevel: Int
}
@@ -22,9 +22,8 @@ class ManageRoomMemberSheetViewModel: ManageRoomMemberSheetViewModelType, Manage
actionsSubject.eraseToAnyPublisher()
}
init(member: RoomMemberDetails,
canKick: Bool,
canBan: Bool,
init(memberDetails: ManageRoomMemberDetails,
permissions: ManageRoomMemberPermissions,
roomProxy: JoinedRoomProxyProtocol,
userIndicatorController: UserIndicatorControllerProtocol,
analyticsService: AnalyticsService,
@@ -32,7 +31,7 @@ class ManageRoomMemberSheetViewModel: ManageRoomMemberSheetViewModelType, Manage
self.userIndicatorController = userIndicatorController
self.roomProxy = roomProxy
self.analyticsService = analyticsService
super.init(initialViewState: .init(member: member, canKick: canKick, canBan: canBan), mediaProvider: mediaProvider)
super.init(initialViewState: .init(memberDetails: memberDetails, permissions: permissions), mediaProvider: mediaProvider)
}
override func process(viewAction: ManageRoomMemberSheetViewAction) {
@@ -43,11 +42,15 @@ class ManageRoomMemberSheetViewModel: ManageRoomMemberSheetViewModelType, Manage
displayAlert(.ban)
case .displayDetails:
actionsSubject.send(.dismiss(shouldShowDetails: true))
case .unban:
displayAlert(.unban)
}
}
private func displayAlert(_ alertType: ManageRoomMemberSheetViewAlertType) {
let member = state.member
let memberID = state.memberDetails.id
let memberName = state.memberDetails.name
var reason: String?
let binding: Binding<String> = .init(get: { reason ?? "" },
set: { reason = $0.isBlank ? nil : $0 })
@@ -57,7 +60,7 @@ class ManageRoomMemberSheetViewModel: ManageRoomMemberSheetViewModelType, Manage
title: L10n.screenBottomSheetManageRoomMemberKickMemberConfirmationTitle,
message: L10n.screenBottomSheetManageRoomMemberKickMemberConfirmationDescription,
primaryButton: .init(title: L10n.actionCancel, role: .cancel) { },
secondaryButton: .init(title: L10n.screenBottomSheetManageRoomMemberKickMemberConfirmationAction) { [weak self] in Task { await self?.kickMember(member, reason: reason) } },
secondaryButton: .init(title: L10n.screenBottomSheetManageRoomMemberKickMemberConfirmationAction) { [weak self] in Task { await self?.kickMember(id: memberID, name: memberName, reason: reason) } },
textFields: [.init(placeholder: L10n.commonReason,
text: binding,
autoCapitalization: .sentences,
@@ -67,19 +70,25 @@ class ManageRoomMemberSheetViewModel: ManageRoomMemberSheetViewModelType, Manage
title: L10n.screenBottomSheetManageRoomMemberBanMemberConfirmationTitle,
message: L10n.screenBottomSheetManageRoomMemberBanMemberConfirmationDescription,
primaryButton: .init(title: L10n.actionCancel, role: .cancel) { },
secondaryButton: .init(title: L10n.screenBottomSheetManageRoomMemberBanMemberConfirmationAction) { [weak self] in Task { await self?.banMember(member, reason: reason) } },
secondaryButton: .init(title: L10n.screenBottomSheetManageRoomMemberBanMemberConfirmationAction) { [weak self] in Task { await self?.banMember(id: memberID, name: memberName, reason: reason) } },
textFields: [.init(placeholder: L10n.commonReason,
text: binding,
autoCapitalization: .sentences,
autoCorrectionDisabled: false)])
case .unban:
state.bindings.alertInfo = .init(id: alertType,
title: L10n.screenBottomSheetManageRoomMemberUnbanMemberConfirmationTitle,
message: L10n.screenBottomSheetManageRoomMemberUnbanMemberConfirmationDescription,
primaryButton: .init(title: L10n.actionCancel, role: .cancel) { },
secondaryButton: .init(title: L10n.screenBottomSheetManageRoomMemberUnbanMemberConfirmationAction) { [weak self] in Task { await self?.unbanMember(id: memberID, name: memberName) } })
}
}
private func kickMember(_ member: RoomMemberDetails, reason: String?) async {
let indicatorTitle = L10n.screenBottomSheetManageRoomMemberRemovingUser(member.name ?? member.id)
private func kickMember(id: String, name: String?, reason: String?) async {
let indicatorTitle = L10n.screenBottomSheetManageRoomMemberRemovingUser(name ?? id)
showManageMemberIndicator(title: indicatorTitle)
switch await roomProxy.kickUser(member.id, reason: reason) {
switch await roomProxy.kickUser(id, reason: reason) {
case .success:
hideManageMemberIndicator(title: indicatorTitle)
analyticsService.trackRoomModeration(action: .KickMember, role: nil)
@@ -89,11 +98,11 @@ class ManageRoomMemberSheetViewModel: ManageRoomMemberSheetViewModelType, Manage
}
}
private func banMember(_ member: RoomMemberDetails, reason: String?) async {
let indicatorTitle = L10n.screenBottomSheetManageRoomMemberBanningUser(member.name ?? member.id)
private func banMember(id: String, name: String?, reason: String?) async {
let indicatorTitle = L10n.screenBottomSheetManageRoomMemberBanningUser(name ?? id)
showManageMemberIndicator(title: indicatorTitle)
switch await roomProxy.banUser(member.id, reason: reason) {
switch await roomProxy.banUser(id, reason: reason) {
case .success:
hideManageMemberIndicator(title: indicatorTitle)
analyticsService.trackRoomModeration(action: .BanMember, role: nil)
@@ -103,6 +112,20 @@ class ManageRoomMemberSheetViewModel: ManageRoomMemberSheetViewModelType, Manage
}
}
private func unbanMember(id: String, name: String?) async {
let indicatorTitle = L10n.screenBottomSheetManageRoomMemberUnbanningUser(name ?? id)
showManageMemberIndicator(title: indicatorTitle)
switch await roomProxy.unbanUser(id) {
case .success:
hideManageMemberIndicator(title: indicatorTitle)
analyticsService.trackRoomModeration(action: .UnbanMember, role: nil)
actionsSubject.send(.dismiss(shouldShowDetails: false))
case .failure:
showManageMemberFailure(title: indicatorTitle)
}
}
private func showManageMemberIndicator(title: String) {
userIndicatorController.submitIndicator(UserIndicator(id: title,
type: .toast(progress: .indeterminate),
@@ -121,5 +144,7 @@ class ManageRoomMemberSheetViewModel: ManageRoomMemberSheetViewModelType, Manage
}
extension ManageRoomMemberSheetViewModel: Identifiable {
var id: String { state.member.id }
var id: String {
state.memberDetails.id
}
}
@@ -13,10 +13,19 @@ struct ManageRoomMemberSheetView: View {
var body: some View {
Form {
AvatarHeaderView(member: context.viewState.member,
avatarSize: .user(on: .memberDetails),
mediaProvider: context.mediaProvider) {
EmptyView()
switch context.viewState.memberDetails {
case .memberDetails(let member):
AvatarHeaderView(member: member,
avatarSize: .user(on: .memberDetails),
mediaProvider: context.mediaProvider) {
EmptyView()
}
case .loadingMemberDetails(let sender):
AvatarHeaderView(sender: sender,
avatarSize: .user(on: .memberDetails),
mediaProvider: context.mediaProvider) {
EmptyView()
}
}
Section {
@@ -28,22 +37,28 @@ struct ManageRoomMemberSheetView: View {
}
Section {
if context.viewState.canKick {
if context.viewState.permissions.canKick {
ListRow(label: .default(title: L10n.screenBottomSheetManageRoomMemberRemove,
icon: \.close,
role: .destructive),
kind: .button {
context.send(viewAction: .kick)
})
.disabled(context.viewState.isKickDisabled)
}
if context.viewState.canBan {
ListRow(label: .default(title: L10n.screenBottomSheetManageRoomMemberBan,
icon: \.block,
if context.viewState.permissions.canBan {
let title = context.viewState.isMemberBanned ? L10n.screenBottomSheetManageRoomMemberUnban : L10n.screenBottomSheetManageRoomMemberBan
let icon: KeyPath<CompoundIcons, Image> = context.viewState.isMemberBanned ? \.restart : \.block
let action: ManageRoomMemberSheetViewAction = context.viewState.isMemberBanned ? .unban : .ban
ListRow(label: .default(title: title,
icon: icon,
role: .destructive),
kind: .button {
context.send(viewAction: .ban)
context.send(viewAction: action)
})
.disabled(context.viewState.isBanUnbanDisabled)
}
}
}
@@ -58,27 +73,42 @@ struct ManageRoomMemberSheetView: View {
struct ManageRoomMemberSheetView_Previews: PreviewProvider, TestablePreview {
static let allActionsViewModel = ManageRoomMemberSheetViewModel.mock()
static let allActionsDisabledViewModel = ManageRoomMemberSheetViewModel.mock(powerLevel: 0)
static let kickOnlyViewModel = ManageRoomMemberSheetViewModel.mock(canBan: false)
static let banOnlyViewModel = ManageRoomMemberSheetViewModel.mock(canKick: false)
static let unbanOnlyViewModel = ManageRoomMemberSheetViewModel.mock(canKick: false, memberIsBanned: true)
static var previews: some View {
ManageRoomMemberSheetView(context: allActionsViewModel.context)
.previewDisplayName("All Actions")
ManageRoomMemberSheetView(context: allActionsDisabledViewModel.context)
.previewDisplayName("All Actions Disabled")
ManageRoomMemberSheetView(context: kickOnlyViewModel.context)
.previewDisplayName("Kick Only")
ManageRoomMemberSheetView(context: banOnlyViewModel.context)
.previewDisplayName("Ban Only")
ManageRoomMemberSheetView(context: unbanOnlyViewModel.context)
.previewDisplayName("Unban Only")
}
}
private extension ManageRoomMemberSheetViewModel {
static func mock(canKick: Bool = true,
canBan: Bool = true) -> ManageRoomMemberSheetViewModel {
let member = RoomMemberDetails(withProxy: RoomMemberProxyMock.mockDan)
return ManageRoomMemberSheetViewModel(member: member,
canKick: canKick,
canBan: canBan,
canBan: Bool = true,
memberIsBanned: Bool = false,
powerLevel: Int = 100) -> ManageRoomMemberSheetViewModel {
let member = if memberIsBanned {
RoomMemberDetails(withProxy: RoomMemberProxyMock.mockBanned[0])
} else {
RoomMemberDetails(withProxy: RoomMemberProxyMock.mockDan)
}
return ManageRoomMemberSheetViewModel(memberDetails: .memberDetails(roomMember: member),
permissions: .init(canKick: canKick,
canBan: canBan,
ownPowerLevel: powerLevel),
roomProxy: JoinedRoomProxyMock(.init()),
userIndicatorController: UserIndicatorControllerMock(),
analyticsService: ServiceLocator.shared.analytics,