mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +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:
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// Copyright 2025 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.
|
||||
//
|
||||
|
||||
enum ManageRoomMemberSheetViewModelAction: Equatable {
|
||||
case dismiss(shouldShowDetails: Bool)
|
||||
}
|
||||
|
||||
struct ManageRoomMemberSheetViewState: BindableState {
|
||||
let member: RoomMemberDetails
|
||||
let canKick: Bool
|
||||
let canBan: Bool
|
||||
|
||||
var bindings = ManageRoomMemberSheetViewStateBindings()
|
||||
}
|
||||
|
||||
struct ManageRoomMemberSheetViewStateBindings {
|
||||
var alertInfo: AlertInfo<ManageRoomMemberSheetViewAlertType>?
|
||||
}
|
||||
|
||||
enum ManageRoomMemberSheetViewAlertType {
|
||||
case kick
|
||||
case ban
|
||||
}
|
||||
|
||||
enum ManageRoomMemberSheetViewAction {
|
||||
case kick
|
||||
case ban
|
||||
case displayDetails
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
//
|
||||
// Copyright 2025 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 Combine
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
typealias ManageRoomMemberSheetViewModelType = StateStoreViewModel<ManageRoomMemberSheetViewState, ManageRoomMemberSheetViewAction>
|
||||
|
||||
class ManageRoomMemberSheetViewModel: ManageRoomMemberSheetViewModelType, ManageRoomMemberSheetViewModelProtocol {
|
||||
private let roomProxy: JoinedRoomProxyProtocol
|
||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||
private let analyticsService: AnalyticsService
|
||||
|
||||
private var actionsSubject: PassthroughSubject<ManageRoomMemberSheetViewModelAction, Never> = .init()
|
||||
|
||||
var actions: AnyPublisher<ManageRoomMemberSheetViewModelAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(member: RoomMemberDetails,
|
||||
canKick: Bool,
|
||||
canBan: Bool,
|
||||
roomProxy: JoinedRoomProxyProtocol,
|
||||
userIndicatorController: UserIndicatorControllerProtocol,
|
||||
analyticsService: AnalyticsService,
|
||||
mediaProvider: MediaProviderProtocol) {
|
||||
self.userIndicatorController = userIndicatorController
|
||||
self.roomProxy = roomProxy
|
||||
self.analyticsService = analyticsService
|
||||
super.init(initialViewState: .init(member: member, canKick: canKick, canBan: canBan), mediaProvider: mediaProvider)
|
||||
}
|
||||
|
||||
override func process(viewAction: ManageRoomMemberSheetViewAction) {
|
||||
switch viewAction {
|
||||
case .kick:
|
||||
displayAlert(.kick)
|
||||
case .ban:
|
||||
displayAlert(.ban)
|
||||
case .displayDetails:
|
||||
actionsSubject.send(.dismiss(shouldShowDetails: true))
|
||||
}
|
||||
}
|
||||
|
||||
private func displayAlert(_ alertType: ManageRoomMemberSheetViewAlertType) {
|
||||
let member = state.member
|
||||
var reason: String?
|
||||
let binding: Binding<String> = .init(get: { reason ?? "" },
|
||||
set: { reason = $0.isBlank ? nil : $0 })
|
||||
switch alertType {
|
||||
case .kick:
|
||||
state.bindings.alertInfo = .init(id: alertType,
|
||||
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) } },
|
||||
textFields: [.init(placeholder: L10n.commonReason,
|
||||
text: binding,
|
||||
autoCapitalization: .sentences,
|
||||
autoCorrectionDisabled: false)])
|
||||
case .ban:
|
||||
state.bindings.alertInfo = .init(id: alertType,
|
||||
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) } },
|
||||
textFields: [.init(placeholder: L10n.commonReason,
|
||||
text: binding,
|
||||
autoCapitalization: .sentences,
|
||||
autoCorrectionDisabled: false)])
|
||||
}
|
||||
}
|
||||
|
||||
private func kickMember(_ member: RoomMemberDetails, reason: String?) async {
|
||||
let indicatorTitle = L10n.screenBottomSheetManageRoomMemberRemovingUser(member.name ?? member.id)
|
||||
showManageMemberIndicator(title: indicatorTitle)
|
||||
|
||||
switch await roomProxy.kickUser(member.id, reason: reason) {
|
||||
case .success:
|
||||
hideManageMemberIndicator(title: indicatorTitle)
|
||||
analyticsService.trackRoomModeration(action: .KickMember, role: nil)
|
||||
actionsSubject.send(.dismiss(shouldShowDetails: false))
|
||||
case .failure:
|
||||
showManageMemberFailure(title: indicatorTitle)
|
||||
}
|
||||
}
|
||||
|
||||
private func banMember(_ member: RoomMemberDetails, reason: String?) async {
|
||||
let indicatorTitle = L10n.screenBottomSheetManageRoomMemberBanningUser(member.name ?? member.id)
|
||||
showManageMemberIndicator(title: indicatorTitle)
|
||||
|
||||
switch await roomProxy.banUser(member.id, reason: reason) {
|
||||
case .success:
|
||||
hideManageMemberIndicator(title: indicatorTitle)
|
||||
analyticsService.trackRoomModeration(action: .BanMember, 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),
|
||||
title: title,
|
||||
persistent: true))
|
||||
}
|
||||
|
||||
private func hideManageMemberIndicator(title: String) {
|
||||
userIndicatorController.retractIndicatorWithId(title)
|
||||
}
|
||||
|
||||
private func showManageMemberFailure(title: String) {
|
||||
userIndicatorController.retractIndicatorWithId(title)
|
||||
userIndicatorController.submitIndicator(UserIndicator(title: L10n.commonFailed, iconName: "xmark"))
|
||||
}
|
||||
}
|
||||
|
||||
extension ManageRoomMemberSheetViewModel: Identifiable {
|
||||
var id: String { state.member.id }
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// Copyright 2025 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 Combine
|
||||
|
||||
@MainActor
|
||||
protocol ManageRoomMemberSheetViewModelProtocol {
|
||||
var actions: AnyPublisher<ManageRoomMemberSheetViewModelAction, Never> { get }
|
||||
var context: ManageRoomMemberSheetViewModelType.Context { get }
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
//
|
||||
// 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 ManageRoomMemberSheetView: View {
|
||||
@ObservedObject var context: ManageRoomMemberSheetViewModelType.Context
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
AvatarHeaderView(member: context.viewState.member,
|
||||
avatarSize: .user(on: .memberDetails),
|
||||
mediaProvider: context.mediaProvider) {
|
||||
EmptyView()
|
||||
}
|
||||
|
||||
Section {
|
||||
ListRow(label: .default(title: L10n.screenBottomSheetManageRoomMemberMemberUserInfo,
|
||||
icon: \.userProfileSolid),
|
||||
kind: .navigationLink {
|
||||
context.send(viewAction: .displayDetails)
|
||||
})
|
||||
}
|
||||
|
||||
Section {
|
||||
if context.viewState.canKick {
|
||||
ListRow(label: .default(title: L10n.screenBottomSheetManageRoomMemberRemove,
|
||||
icon: \.close,
|
||||
role: .destructive),
|
||||
kind: .button {
|
||||
context.send(viewAction: .kick)
|
||||
})
|
||||
}
|
||||
|
||||
if context.viewState.canBan {
|
||||
ListRow(label: .default(title: L10n.screenBottomSheetManageRoomMemberBan,
|
||||
icon: \.block,
|
||||
role: .destructive),
|
||||
kind: .button {
|
||||
context.send(viewAction: .ban)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
.compoundList()
|
||||
.scrollBounceBehavior(.basedOnSize)
|
||||
.presentationDragIndicator(.visible)
|
||||
.presentationDetents([.large, .fraction(0.67)]) // Maybe find a way to use the ideal height somehow?
|
||||
.alert(item: $context.alertInfo)
|
||||
}
|
||||
}
|
||||
|
||||
struct ManageRoomMemberSheetView_Previews: PreviewProvider, TestablePreview {
|
||||
static let allActionsViewModel = ManageRoomMemberSheetViewModel.mock()
|
||||
|
||||
static let kickOnlyViewModel = ManageRoomMemberSheetViewModel.mock(canBan: false)
|
||||
|
||||
static let banOnlyViewModel = ManageRoomMemberSheetViewModel.mock(canKick: false)
|
||||
|
||||
static var previews: some View {
|
||||
ManageRoomMemberSheetView(context: allActionsViewModel.context)
|
||||
.previewDisplayName("All Actions")
|
||||
ManageRoomMemberSheetView(context: kickOnlyViewModel.context)
|
||||
.previewDisplayName("Kick Only")
|
||||
ManageRoomMemberSheetView(context: banOnlyViewModel.context)
|
||||
.previewDisplayName("Ban 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,
|
||||
roomProxy: JoinedRoomProxyMock(.init()),
|
||||
userIndicatorController: UserIndicatorControllerMock(),
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
mediaProvider: MediaProviderMock(configuration: .init()))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user