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
@@ -425,7 +425,7 @@
"screen_bottom_sheet_manage_room_member_unban_member_confirmation_action" = "Unban"; "screen_bottom_sheet_manage_room_member_unban_member_confirmation_action" = "Unban";
"screen_bottom_sheet_manage_room_member_unban_member_confirmation_description" = "They would be able to join the room again if invited"; "screen_bottom_sheet_manage_room_member_unban_member_confirmation_description" = "They would be able to join the room again if invited";
"screen_bottom_sheet_manage_room_member_unban_member_confirmation_title" = "Are you sure you want to unban this member?"; "screen_bottom_sheet_manage_room_member_unban_member_confirmation_title" = "Are you sure you want to unban this member?";
"screen_bottom_sheet_manage_room_member_unbanning_user" = "Unbanning $1%@"; "screen_bottom_sheet_manage_room_member_unbanning_user" = "Unbanning %1$@";
"screen_create_room_room_access_section_anyone_option_description" = "Anyone can join this room"; "screen_create_room_room_access_section_anyone_option_description" = "Anyone can join this room";
"screen_create_room_room_access_section_anyone_option_title" = "Anyone"; "screen_create_room_room_access_section_anyone_option_title" = "Anyone";
"screen_create_room_room_access_section_header" = "Room Access"; "screen_create_room_room_access_section_header" = "Room Access";
@@ -618,6 +618,8 @@
"screen_change_server_error_invalid_well_known" = "Server isn't available due to an issue in the .well-known file:\n%1$@"; "screen_change_server_error_invalid_well_known" = "Server isn't available due to an issue in the .well-known file:\n%1$@";
"screen_change_server_error_no_sliding_sync_message" = "The selected account provider does not support sliding sync. An upgrade to the server is needed to use %1$@."; "screen_change_server_error_no_sliding_sync_message" = "The selected account provider does not support sliding sync. An upgrade to the server is needed to use %1$@.";
"screen_change_server_error_unauthorized_homeserver" = "%1$@ is not allowed to connect to %2$@."; "screen_change_server_error_unauthorized_homeserver" = "%1$@ is not allowed to connect to %2$@.";
"screen_change_server_error_unauthorized_homeserver_content" = "This app has been configured to allow: %1$@.";
"screen_change_server_error_unauthorized_homeserver_title" = "Account provider %1$@ not allowed.";
"screen_change_server_form_header" = "Homeserver URL"; "screen_change_server_form_header" = "Homeserver URL";
"screen_change_server_form_notice" = "Enter a domain address."; "screen_change_server_form_notice" = "Enter a domain address.";
"screen_change_server_subtitle" = "What is the address of your server?"; "screen_change_server_subtitle" = "What is the address of your server?";
+9 -1
View File
@@ -1162,7 +1162,7 @@ internal enum L10n {
internal static var screenBottomSheetManageRoomMemberUnbanMemberConfirmationDescription: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_unban_member_confirmation_description") } internal static var screenBottomSheetManageRoomMemberUnbanMemberConfirmationDescription: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_unban_member_confirmation_description") }
/// Are you sure you want to unban this member? /// Are you sure you want to unban this member?
internal static var screenBottomSheetManageRoomMemberUnbanMemberConfirmationTitle: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_unban_member_confirmation_title") } internal static var screenBottomSheetManageRoomMemberUnbanMemberConfirmationTitle: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_unban_member_confirmation_title") }
/// Unbanning $1%@ /// Unbanning %1$@
internal static func screenBottomSheetManageRoomMemberUnbanningUser(_ p1: Any) -> String { internal static func screenBottomSheetManageRoomMemberUnbanningUser(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_unbanning_user", String(describing: p1)) return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_unbanning_user", String(describing: p1))
} }
@@ -1219,6 +1219,14 @@ internal enum L10n {
internal static func screenChangeServerErrorUnauthorizedHomeserver(_ p1: Any, _ p2: Any) -> String { internal static func screenChangeServerErrorUnauthorizedHomeserver(_ p1: Any, _ p2: Any) -> String {
return L10n.tr("Localizable", "screen_change_server_error_unauthorized_homeserver", String(describing: p1), String(describing: p2)) return L10n.tr("Localizable", "screen_change_server_error_unauthorized_homeserver", String(describing: p1), String(describing: p2))
} }
/// This app has been configured to allow: %1$@.
internal static func screenChangeServerErrorUnauthorizedHomeserverContent(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_change_server_error_unauthorized_homeserver_content", String(describing: p1))
}
/// Account provider %1$@ not allowed.
internal static func screenChangeServerErrorUnauthorizedHomeserverTitle(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_change_server_error_unauthorized_homeserver_title", String(describing: p1))
}
/// Homeserver URL /// Homeserver URL
internal static var screenChangeServerFormHeader: String { return L10n.tr("Localizable", "screen_change_server_form_header") } internal static var screenChangeServerFormHeader: String { return L10n.tr("Localizable", "screen_change_server_form_header") }
/// Enter a domain address. /// Enter a domain address.
@@ -110,6 +110,23 @@ struct AvatarHeaderView<Footer: View>: View {
badges = isVerified ? [.verified] : [] badges = isVerified ? [.verified] : []
} }
/// Initialises the view by using the sender,
/// only to be used when a room member has not been loaded yet.
init(sender: TimelineItemSender,
avatarSize: Avatars.Size,
mediaProvider: MediaProviderProtocol? = nil,
onAvatarTap: ((URL) -> Void)? = nil,
@ViewBuilder footer: @escaping () -> Footer) {
let profile = UserProfileProxy(sender: sender)
self.init(user: profile,
isVerified: false,
avatarSize: avatarSize,
mediaProvider: mediaProvider,
onAvatarTap: onAvatarTap,
footer: footer)
}
private var badgesStack: some View { private var badgesStack: some View {
HStack(spacing: 8) { HStack(spacing: 8) {
ForEach(badges, id: \.self) { badge in ForEach(badges, id: \.self) { badge in
@@ -10,9 +10,35 @@ enum ManageRoomMemberSheetViewModelAction: Equatable {
} }
struct ManageRoomMemberSheetViewState: BindableState { struct ManageRoomMemberSheetViewState: BindableState {
let member: RoomMemberDetails let memberDetails: ManageRoomMemberDetails
let canKick: Bool let permissions: ManageRoomMemberPermissions
let canBan: Bool
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() var bindings = ManageRoomMemberSheetViewStateBindings()
} }
@@ -24,10 +50,41 @@ struct ManageRoomMemberSheetViewStateBindings {
enum ManageRoomMemberSheetViewAlertType { enum ManageRoomMemberSheetViewAlertType {
case kick case kick
case ban case ban
case unban
} }
enum ManageRoomMemberSheetViewAction { enum ManageRoomMemberSheetViewAction {
case kick case kick
case ban case ban
case unban
case displayDetails 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() actionsSubject.eraseToAnyPublisher()
} }
init(member: RoomMemberDetails, init(memberDetails: ManageRoomMemberDetails,
canKick: Bool, permissions: ManageRoomMemberPermissions,
canBan: Bool,
roomProxy: JoinedRoomProxyProtocol, roomProxy: JoinedRoomProxyProtocol,
userIndicatorController: UserIndicatorControllerProtocol, userIndicatorController: UserIndicatorControllerProtocol,
analyticsService: AnalyticsService, analyticsService: AnalyticsService,
@@ -32,7 +31,7 @@ class ManageRoomMemberSheetViewModel: ManageRoomMemberSheetViewModelType, Manage
self.userIndicatorController = userIndicatorController self.userIndicatorController = userIndicatorController
self.roomProxy = roomProxy self.roomProxy = roomProxy
self.analyticsService = analyticsService 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) { override func process(viewAction: ManageRoomMemberSheetViewAction) {
@@ -43,11 +42,15 @@ class ManageRoomMemberSheetViewModel: ManageRoomMemberSheetViewModelType, Manage
displayAlert(.ban) displayAlert(.ban)
case .displayDetails: case .displayDetails:
actionsSubject.send(.dismiss(shouldShowDetails: true)) actionsSubject.send(.dismiss(shouldShowDetails: true))
case .unban:
displayAlert(.unban)
} }
} }
private func displayAlert(_ alertType: ManageRoomMemberSheetViewAlertType) { private func displayAlert(_ alertType: ManageRoomMemberSheetViewAlertType) {
let member = state.member let memberID = state.memberDetails.id
let memberName = state.memberDetails.name
var reason: String? var reason: String?
let binding: Binding<String> = .init(get: { reason ?? "" }, let binding: Binding<String> = .init(get: { reason ?? "" },
set: { reason = $0.isBlank ? nil : $0 }) set: { reason = $0.isBlank ? nil : $0 })
@@ -57,7 +60,7 @@ class ManageRoomMemberSheetViewModel: ManageRoomMemberSheetViewModelType, Manage
title: L10n.screenBottomSheetManageRoomMemberKickMemberConfirmationTitle, title: L10n.screenBottomSheetManageRoomMemberKickMemberConfirmationTitle,
message: L10n.screenBottomSheetManageRoomMemberKickMemberConfirmationDescription, message: L10n.screenBottomSheetManageRoomMemberKickMemberConfirmationDescription,
primaryButton: .init(title: L10n.actionCancel, role: .cancel) { }, 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, textFields: [.init(placeholder: L10n.commonReason,
text: binding, text: binding,
autoCapitalization: .sentences, autoCapitalization: .sentences,
@@ -67,19 +70,25 @@ class ManageRoomMemberSheetViewModel: ManageRoomMemberSheetViewModelType, Manage
title: L10n.screenBottomSheetManageRoomMemberBanMemberConfirmationTitle, title: L10n.screenBottomSheetManageRoomMemberBanMemberConfirmationTitle,
message: L10n.screenBottomSheetManageRoomMemberBanMemberConfirmationDescription, message: L10n.screenBottomSheetManageRoomMemberBanMemberConfirmationDescription,
primaryButton: .init(title: L10n.actionCancel, role: .cancel) { }, 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, textFields: [.init(placeholder: L10n.commonReason,
text: binding, text: binding,
autoCapitalization: .sentences, autoCapitalization: .sentences,
autoCorrectionDisabled: false)]) 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 { private func kickMember(id: String, name: String?, reason: String?) async {
let indicatorTitle = L10n.screenBottomSheetManageRoomMemberRemovingUser(member.name ?? member.id) let indicatorTitle = L10n.screenBottomSheetManageRoomMemberRemovingUser(name ?? id)
showManageMemberIndicator(title: indicatorTitle) showManageMemberIndicator(title: indicatorTitle)
switch await roomProxy.kickUser(member.id, reason: reason) { switch await roomProxy.kickUser(id, reason: reason) {
case .success: case .success:
hideManageMemberIndicator(title: indicatorTitle) hideManageMemberIndicator(title: indicatorTitle)
analyticsService.trackRoomModeration(action: .KickMember, role: nil) analyticsService.trackRoomModeration(action: .KickMember, role: nil)
@@ -89,11 +98,11 @@ class ManageRoomMemberSheetViewModel: ManageRoomMemberSheetViewModelType, Manage
} }
} }
private func banMember(_ member: RoomMemberDetails, reason: String?) async { private func banMember(id: String, name: String?, reason: String?) async {
let indicatorTitle = L10n.screenBottomSheetManageRoomMemberBanningUser(member.name ?? member.id) let indicatorTitle = L10n.screenBottomSheetManageRoomMemberBanningUser(name ?? id)
showManageMemberIndicator(title: indicatorTitle) showManageMemberIndicator(title: indicatorTitle)
switch await roomProxy.banUser(member.id, reason: reason) { switch await roomProxy.banUser(id, reason: reason) {
case .success: case .success:
hideManageMemberIndicator(title: indicatorTitle) hideManageMemberIndicator(title: indicatorTitle)
analyticsService.trackRoomModeration(action: .BanMember, role: nil) 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) { private func showManageMemberIndicator(title: String) {
userIndicatorController.submitIndicator(UserIndicator(id: title, userIndicatorController.submitIndicator(UserIndicator(id: title,
type: .toast(progress: .indeterminate), type: .toast(progress: .indeterminate),
@@ -121,5 +144,7 @@ class ManageRoomMemberSheetViewModel: ManageRoomMemberSheetViewModelType, Manage
} }
extension ManageRoomMemberSheetViewModel: Identifiable { 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 { var body: some View {
Form { Form {
AvatarHeaderView(member: context.viewState.member, switch context.viewState.memberDetails {
avatarSize: .user(on: .memberDetails), case .memberDetails(let member):
mediaProvider: context.mediaProvider) { AvatarHeaderView(member: member,
EmptyView() avatarSize: .user(on: .memberDetails),
mediaProvider: context.mediaProvider) {
EmptyView()
}
case .loadingMemberDetails(let sender):
AvatarHeaderView(sender: sender,
avatarSize: .user(on: .memberDetails),
mediaProvider: context.mediaProvider) {
EmptyView()
}
} }
Section { Section {
@@ -28,22 +37,28 @@ struct ManageRoomMemberSheetView: View {
} }
Section { Section {
if context.viewState.canKick { if context.viewState.permissions.canKick {
ListRow(label: .default(title: L10n.screenBottomSheetManageRoomMemberRemove, ListRow(label: .default(title: L10n.screenBottomSheetManageRoomMemberRemove,
icon: \.close, icon: \.close,
role: .destructive), role: .destructive),
kind: .button { kind: .button {
context.send(viewAction: .kick) context.send(viewAction: .kick)
}) })
.disabled(context.viewState.isKickDisabled)
} }
if context.viewState.canBan { if context.viewState.permissions.canBan {
ListRow(label: .default(title: L10n.screenBottomSheetManageRoomMemberBan, let title = context.viewState.isMemberBanned ? L10n.screenBottomSheetManageRoomMemberUnban : L10n.screenBottomSheetManageRoomMemberBan
icon: \.block, 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), role: .destructive),
kind: .button { 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 { struct ManageRoomMemberSheetView_Previews: PreviewProvider, TestablePreview {
static let allActionsViewModel = ManageRoomMemberSheetViewModel.mock() static let allActionsViewModel = ManageRoomMemberSheetViewModel.mock()
static let allActionsDisabledViewModel = ManageRoomMemberSheetViewModel.mock(powerLevel: 0)
static let kickOnlyViewModel = ManageRoomMemberSheetViewModel.mock(canBan: false) static let kickOnlyViewModel = ManageRoomMemberSheetViewModel.mock(canBan: false)
static let banOnlyViewModel = ManageRoomMemberSheetViewModel.mock(canKick: false) static let banOnlyViewModel = ManageRoomMemberSheetViewModel.mock(canKick: false)
static let unbanOnlyViewModel = ManageRoomMemberSheetViewModel.mock(canKick: false, memberIsBanned: true)
static var previews: some View { static var previews: some View {
ManageRoomMemberSheetView(context: allActionsViewModel.context) ManageRoomMemberSheetView(context: allActionsViewModel.context)
.previewDisplayName("All Actions") .previewDisplayName("All Actions")
ManageRoomMemberSheetView(context: allActionsDisabledViewModel.context)
.previewDisplayName("All Actions Disabled")
ManageRoomMemberSheetView(context: kickOnlyViewModel.context) ManageRoomMemberSheetView(context: kickOnlyViewModel.context)
.previewDisplayName("Kick Only") .previewDisplayName("Kick Only")
ManageRoomMemberSheetView(context: banOnlyViewModel.context) ManageRoomMemberSheetView(context: banOnlyViewModel.context)
.previewDisplayName("Ban Only") .previewDisplayName("Ban Only")
ManageRoomMemberSheetView(context: unbanOnlyViewModel.context)
.previewDisplayName("Unban Only")
} }
} }
private extension ManageRoomMemberSheetViewModel { private extension ManageRoomMemberSheetViewModel {
static func mock(canKick: Bool = true, static func mock(canKick: Bool = true,
canBan: Bool = true) -> ManageRoomMemberSheetViewModel { canBan: Bool = true,
let member = RoomMemberDetails(withProxy: RoomMemberProxyMock.mockDan) memberIsBanned: Bool = false,
return ManageRoomMemberSheetViewModel(member: member, powerLevel: Int = 100) -> ManageRoomMemberSheetViewModel {
canKick: canKick, let member = if memberIsBanned {
canBan: canBan, 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()), roomProxy: JoinedRoomProxyMock(.init()),
userIndicatorController: UserIndicatorControllerMock(), userIndicatorController: UserIndicatorControllerMock(),
analyticsService: ServiceLocator.shared.analytics, analyticsService: ServiceLocator.shared.analytics,
@@ -89,7 +89,6 @@ struct RoomMembersListScreenViewStateBindings {
enum RoomMembersListScreenViewAction { enum RoomMembersListScreenViewAction {
case selectMember(RoomMemberDetails) case selectMember(RoomMemberDetails)
case unbanMember(RoomMemberDetails)
case invite case invite
} }
@@ -51,8 +51,6 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
switch viewAction { switch viewAction {
case .selectMember(let member): case .selectMember(let member):
selectMember(member) selectMember(member)
case .unbanMember(let member):
Task { await unbanMember(member) }
case .invite: case .invite:
actionsSubject.send(.invite) actionsSubject.send(.invite)
} }
@@ -144,46 +142,31 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
} }
private func selectMember(_ member: RoomMemberDetails) { private func selectMember(_ member: RoomMemberDetails) {
guard let currentUserProxy, guard currentUserProxy?.userID != member.id else {
currentUserProxy.powerLevel > member.powerLevel else {
showMemberDetails(member) showMemberDetails(member)
return 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,
message: L10n.screenRoomMemberListManageMemberUnbanMessage,
primaryButton: .init(title: L10n.screenRoomMemberListManageMemberUnbanAction) { [weak self] in
self?.context.send(viewAction: .unbanMember(member))
},
secondaryButton: .init(title: L10n.actionCancel, role: .cancel) { })
return
}
if state.canKickUsers || state.canBanUsers { let manageMemeberViewModel = ManageRoomMemberSheetViewModel(memberDetails: .memberDetails(roomMember: member),
let manageMemeberViewModel = ManageRoomMemberSheetViewModel(member: member, permissions: .init(canKick: state.canKickUsers,
canKick: state.canKickUsers, canBan: state.canBanUsers,
canBan: state.canBanUsers, ownPowerLevel: currentUserProxy?.powerLevel ?? 0),
roomProxy: roomProxy, roomProxy: roomProxy,
userIndicatorController: userIndicatorController, userIndicatorController: userIndicatorController,
analyticsService: analytics, analyticsService: analytics,
mediaProvider: mediaProvider) mediaProvider: mediaProvider)
manageMemeberViewModel.actions.sink { [weak self] action in manageMemeberViewModel.actions.sink { [weak self] action in
guard let self else { return } guard let self else { return }
switch action { switch action {
case .dismiss(let shouldShowDetails): case .dismiss(let shouldShowDetails):
state.bindings.manageMemeberViewModel = nil state.bindings.manageMemeberViewModel = nil
if shouldShowDetails { if shouldShowDetails {
showMemberDetails(member) showMemberDetails(member)
}
} }
} }
.store(in: &cancellables)
state.bindings.manageMemeberViewModel = manageMemeberViewModel
} else {
showMemberDetails(member)
} }
.store(in: &cancellables)
state.bindings.manageMemeberViewModel = manageMemeberViewModel
} }
private func showMemberDetails(_ member: RoomMemberDetails) { private func showMemberDetails(_ member: RoomMemberDetails) {
@@ -55,7 +55,7 @@ enum TimelineViewAction {
case displayTimelineItemMenu(itemID: TimelineItemIdentifier) case displayTimelineItemMenu(itemID: TimelineItemIdentifier)
case handleTimelineItemMenuAction(itemID: TimelineItemIdentifier, action: TimelineItemMenuAction) case handleTimelineItemMenuAction(itemID: TimelineItemIdentifier, action: TimelineItemMenuAction)
case tappedOnSenderDetails(userID: String) case tappedOnSenderDetails(sender: TimelineItemSender)
case displayReactionSummary(itemID: TimelineItemIdentifier, key: String) case displayReactionSummary(itemID: TimelineItemIdentifier, key: String)
case displayEmojiPicker(itemID: TimelineItemIdentifier) case displayEmojiPicker(itemID: TimelineItemIdentifier)
case displayReadReceipts(itemID: TimelineItemIdentifier) case displayReadReceipts(itemID: TimelineItemIdentifier)
@@ -183,8 +183,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
timelineInteractionHandler.displayTimelineItemActionMenu(for: itemID) timelineInteractionHandler.displayTimelineItemActionMenu(for: itemID)
case .handleTimelineItemMenuAction(let itemID, let action): case .handleTimelineItemMenuAction(let itemID, let action):
timelineInteractionHandler.handleTimelineItemMenuAction(action, itemID: itemID) timelineInteractionHandler.handleTimelineItemMenuAction(action, itemID: itemID)
case .tappedOnSenderDetails(let userID): case .tappedOnSenderDetails(let sender):
handleTappedOnSenderDetails(userID: userID) handleTappedOnSenderDetails(sender: sender)
case .displayEmojiPicker(let itemID): case .displayEmojiPicker(let itemID):
timelineInteractionHandler.displayEmojiPicker(for: itemID) timelineInteractionHandler.displayEmojiPicker(for: itemID)
case .displayReactionSummary(let itemID, let key): case .displayReactionSummary(let itemID, let key):
@@ -271,39 +271,34 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
// MARK: - Private // MARK: - Private
private func handleTappedOnSenderDetails(userID: String) { private func handleTappedOnSenderDetails(sender: TimelineItemSender) {
// We also need to make sure the user is in the joined state, otherwise we just show the details. let memberDetails: ManageRoomMemberDetails = if let memberProxy = roomProxy.membersPublisher.value.first(where: { $0.userID == sender.id }) {
guard let memberProxy = roomProxy.membersPublisher.value.first(where: { $0.userID == userID && $0.membership == .join }), .memberDetails(roomMember: .init(withProxy: memberProxy))
let currentUserProxy, } else {
currentUserProxy.powerLevel > memberProxy.powerLevel else { .loadingMemberDetails(sender: sender)
actionsSubject.send(.displaySenderDetails(userID: userID))
return
} }
if state.canCurrentUserBan || state.canCurrentUserKick { let viewModel = ManageRoomMemberSheetViewModel(memberDetails: memberDetails,
let member = RoomMemberDetails(withProxy: memberProxy) permissions: .init(canKick: state.canCurrentUserKick,
let manageMemeberViewModel = ManageRoomMemberSheetViewModel(member: member, canBan: state.canCurrentUserBan,
canKick: state.canCurrentUserKick, ownPowerLevel: currentUserProxy?.powerLevel ?? 0),
canBan: state.canCurrentUserBan, roomProxy: roomProxy,
roomProxy: roomProxy, userIndicatorController: userIndicatorController,
userIndicatorController: userIndicatorController, analyticsService: analyticsService,
analyticsService: analyticsService, mediaProvider: mediaProvider)
mediaProvider: mediaProvider)
manageMemeberViewModel.actions.sink { [weak self] action in viewModel.actions.sink { [weak self] action in
guard let self else { return } guard let self else { return }
switch action { switch action {
case .dismiss(let shouldShowDetails): case .dismiss(let shouldShowDetails):
state.bindings.manageMemberViewModel = nil state.bindings.manageMemberViewModel = nil
if shouldShowDetails { if shouldShowDetails {
actionsSubject.send(.displaySenderDetails(userID: userID)) actionsSubject.send(.displaySenderDetails(userID: sender.id))
}
} }
} }
.store(in: &cancellables)
state.bindings.manageMemberViewModel = manageMemeberViewModel
} else {
actionsSubject.send(.displaySenderDetails(userID: userID))
} }
.store(in: &cancellables)
state.bindings.manageMemberViewModel = viewModel
} }
private func focusLive() { private func focusLive() {
@@ -95,7 +95,7 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
// sender info are read inside the `TimelineAccessibilityModifier` // sender info are read inside the `TimelineAccessibilityModifier`
.accessibilityHidden(true) .accessibilityHidden(true)
.onTapGesture { .onTapGesture {
context.send(viewAction: .tappedOnSenderDetails(userID: timelineItem.sender.id)) context.send(viewAction: .tappedOnSenderDetails(sender: timelineItem.sender))
} }
.padding(.top, 8) .padding(.top, 8)
} }
@@ -17,7 +17,8 @@ struct RoomMemberDetails: Identifiable, Hashable {
var isInvited: Bool var isInvited: Bool
var isIgnored: Bool var isIgnored: Bool
var isBanned: Bool var isBanned: Bool
var isActive: Bool
enum Role { case administrator, moderator, user } enum Role { case administrator, moderator, user }
let role: Role let role: Role
let powerLevel: Int let powerLevel: Int
@@ -34,7 +35,7 @@ extension RoomMemberDetails {
name = proxy.displayName name = proxy.displayName
avatarURL = proxy.avatarURL avatarURL = proxy.avatarURL
permalink = proxy.permalink permalink = proxy.permalink
isActive = proxy.isActive
isInvited = proxy.membership == .invite isInvited = proxy.membership == .invite
isIgnored = proxy.isIgnored isIgnored = proxy.isIgnored
isBanned = proxy.membership == .ban isBanned = proxy.membership == .ban
@@ -31,7 +31,7 @@ protocol RoomMemberProxyProtocol: AnyObject {
extension RoomMemberProxyProtocol { extension RoomMemberProxyProtocol {
/// The member is active in the room (joined or invited). /// The member is active in the room (joined or invited).
var isActive: Bool { var isActive: Bool {
membership == .join || membership == .invite membership == .join || membership == .invite || membership == .knock
} }
var permalink: URL? { var permalink: URL? {
@@ -25,6 +25,12 @@ struct UserProfileProxy: Equatable, Hashable {
avatarURL = member.isBanned ? nil : member.avatarURL avatarURL = member.isBanned ? nil : member.avatarURL
} }
init(sender: TimelineItemSender) {
userID = sender.id
displayName = sender.displayName
avatarURL = sender.avatarURL
}
init(sdkUserProfile: MatrixRustSDK.UserProfile) { init(sdkUserProfile: MatrixRustSDK.UserProfile) {
userID = sdkUserProfile.userId userID = sdkUserProfile.userId
displayName = sdkUserProfile.displayName displayName = sdkUserProfile.displayName
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9de850c95f6dbdfad502994a33d0523226cebaa99f8e36564c5c30b3eade2237
size 195361
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ba5ada0c14223f75f6f79112ce5b4741c8fc279289c7077e6b00a5d1545f823e
size 197461
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5ef6e95930907faac60daa81166e91104dbe1821d79e847f8553ff7acaf5eda2
size 135322
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c6b3cee316cfe4edf8fdaeac2fe0bf7d520b58f52138c504e3f55ebe3b68267c
size 140517
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:28206d20d834c4c4804045b9d47e4c95aa6ca16c0168f8267d98c814e2795b15
size 103571
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b4a365e20cb2d85311a1db85f809e65ac0a46af5e9ae9eab7dcd6f31022724b4
size 105093
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:31eb3b34cd36134cfa8c787cc974c01d08eaeb53e8c67b4e023475ba9ead5c93
size 54438
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ebd80833a565d8f865096dc772bd58ef81523e9e219ad600f06f3b5251ba4dfc
size 58583
@@ -27,9 +27,8 @@ class ManageRoomMemberSheetViewModelTests: XCTestCase {
return .success(()) return .success(())
} }
viewModel = ManageRoomMemberSheetViewModel(member: RoomMemberDetails(withProxy: RoomMemberProxyMock.mockAlice), viewModel = ManageRoomMemberSheetViewModel(memberDetails: .memberDetails(roomMember: .init(withProxy: RoomMemberProxyMock.mockAlice)),
canKick: true, permissions: .init(canKick: true, canBan: true, ownPowerLevel: RoomMemberProxyMock.mockAdmin.powerLevel),
canBan: true,
roomProxy: roomProxy, roomProxy: roomProxy,
userIndicatorController: UserIndicatorControllerMock(), userIndicatorController: UserIndicatorControllerMock(),
analyticsService: ServiceLocator.shared.analytics, analyticsService: ServiceLocator.shared.analytics,
@@ -59,9 +58,8 @@ class ManageRoomMemberSheetViewModelTests: XCTestCase {
return .success(()) return .success(())
} }
viewModel = ManageRoomMemberSheetViewModel(member: RoomMemberDetails(withProxy: RoomMemberProxyMock.mockAlice), viewModel = ManageRoomMemberSheetViewModel(memberDetails: .memberDetails(roomMember: .init(withProxy: RoomMemberProxyMock.mockAlice)),
canKick: true, permissions: .init(canKick: true, canBan: true, ownPowerLevel: RoomMemberProxyMock.mockAdmin.powerLevel),
canBan: true,
roomProxy: roomProxy, roomProxy: roomProxy,
userIndicatorController: UserIndicatorControllerMock(), userIndicatorController: UserIndicatorControllerMock(),
analyticsService: ServiceLocator.shared.analytics, analyticsService: ServiceLocator.shared.analytics,
@@ -82,9 +80,8 @@ class ManageRoomMemberSheetViewModelTests: XCTestCase {
func testDisplayDetails() async throws { func testDisplayDetails() async throws {
let roomProxy = JoinedRoomProxyMock(.init(members: [RoomMemberProxyMock.mockAdmin, RoomMemberProxyMock.mockAlice])) let roomProxy = JoinedRoomProxyMock(.init(members: [RoomMemberProxyMock.mockAdmin, RoomMemberProxyMock.mockAlice]))
viewModel = ManageRoomMemberSheetViewModel(member: RoomMemberDetails(withProxy: RoomMemberProxyMock.mockAlice), viewModel = ManageRoomMemberSheetViewModel(memberDetails: .memberDetails(roomMember: .init(withProxy: RoomMemberProxyMock.mockAlice)),
canKick: true, permissions: .init(canKick: true, canBan: true, ownPowerLevel: RoomMemberProxyMock.mockAdmin.powerLevel),
canBan: true,
roomProxy: roomProxy, roomProxy: roomProxy,
userIndicatorController: UserIndicatorControllerMock(), userIndicatorController: UserIndicatorControllerMock(),
analyticsService: ServiceLocator.shared.analytics, analyticsService: ServiceLocator.shared.analytics,
@@ -18,6 +18,11 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
viewModel.context viewModel.context
} }
override func tearDown() {
viewModel = nil
roomProxy = nil
}
func testJoinedMembers() async throws { func testJoinedMembers() async throws {
setup(with: [.mockAlice, .mockBob]) setup(with: [.mockAlice, .mockBob])
@@ -130,11 +135,11 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
func testSelectUserAsUser() async throws { func testSelectUserAsUser() async throws {
// Given the room list viewed as a regular user. // Given the room list viewed as a regular user.
setup(with: .allMembers) setup(with: .allMembers)
let deferred = deferFulfillment(context.$viewState) { !$0.visibleInvitedMembers.isEmpty } var deferred = deferFulfillment(context.$viewState) { !$0.visibleInvitedMembers.isEmpty }
try await deferred.fulfill() try await deferred.fulfill()
// When tapping on another user in the list. // When tapping on another user in the list.
let memberDetailsAction = deferFulfillment(viewModel.actions) { $0.isSelectMember } deferred = deferFulfillment(context.$viewState) { $0.bindings.manageMemeberViewModel != nil }
guard let user = viewModel.state.visibleJoinedMembers.first(where: { $0.member.role == .user && $0.member.id != RoomMemberProxyMock.mockMe.userID })?.member else { guard let user = viewModel.state.visibleJoinedMembers.first(where: { $0.member.role == .user && $0.member.id != RoomMemberProxyMock.mockMe.userID })?.member else {
XCTFail("Expected to find a regular user.") XCTFail("Expected to find a regular user.")
return return
@@ -142,8 +147,11 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
context.send(viewAction: .selectMember(user)) context.send(viewAction: .selectMember(user))
// Then the member's details should be shown. // Then the member's details should be shown.
try await memberDetailsAction.fulfill() try await deferred.fulfill()
XCTAssertNil(context.manageMemeberViewModel) XCTAssertNotNil(context.manageMemeberViewModel)
XCTAssertEqual(context.manageMemeberViewModel?.state.memberDetails.id, user.id)
XCTAssertEqual(context.manageMemeberViewModel?.state.permissions.canKick, false)
XCTAssertEqual(context.manageMemeberViewModel?.state.permissions.canBan, false)
} }
func testSelectUserAsAdmin() async throws { func testSelectUserAsAdmin() async throws {
@@ -163,9 +171,12 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
try await deferred.fulfill() try await deferred.fulfill()
// Then member management should be shown for that user. // Then member management should be shown for that user.
XCTAssertEqual(context.manageMemeberViewModel?.state.member, user) XCTAssertEqual(context.manageMemeberViewModel?.state.memberDetails.id, user.id)
XCTAssertEqual(context.manageMemeberViewModel?.state.canKick, true) XCTAssertEqual(context.manageMemeberViewModel?.state.permissions.canKick, true)
XCTAssertEqual(context.manageMemeberViewModel?.state.canBan, true) XCTAssertEqual(context.manageMemeberViewModel?.state.permissions.canBan, true)
XCTAssertEqual(context.manageMemeberViewModel?.state.isKickDisabled, false)
XCTAssertEqual(context.manageMemeberViewModel?.state.isBanUnbanDisabled, false)
XCTAssertEqual(context.manageMemeberViewModel?.state.isMemberBanned, false)
} }
func testSelectModeratorAsAdmin() async throws { func testSelectModeratorAsAdmin() async throws {
@@ -185,19 +196,22 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
try await deferred.fulfill() try await deferred.fulfill()
// Then member management should be shown for the moderator. // Then member management should be shown for the moderator.
XCTAssertEqual(context.manageMemeberViewModel?.state.member, moderator) XCTAssertEqual(context.manageMemeberViewModel?.state.memberDetails.id, moderator.id)
XCTAssertEqual(context.manageMemeberViewModel?.state.canKick, true) XCTAssertEqual(context.manageMemeberViewModel?.state.permissions.canKick, true)
XCTAssertEqual(context.manageMemeberViewModel?.state.canBan, true) XCTAssertEqual(context.manageMemeberViewModel?.state.permissions.canBan, true)
XCTAssertEqual(context.manageMemeberViewModel?.state.isMemberBanned, false)
XCTAssertEqual(context.manageMemeberViewModel?.state.isKickDisabled, false)
XCTAssertEqual(context.manageMemeberViewModel?.state.isBanUnbanDisabled, false)
} }
func testSelectAdminAsAdmin() async throws { func testSelectAdminAsAdmin() async throws {
// Given the room list viewed as an admin. // Given the room list viewed as an admin.
setup(with: .allMembersAsAdmin) setup(with: .allMembersAsAdmin)
let deferred = deferFulfillment(context.$viewState) { !$0.visibleInvitedMembers.isEmpty } var deferred = deferFulfillment(context.$viewState) { !$0.visibleInvitedMembers.isEmpty && $0.canKickUsers && $0.canBanUsers }
try await deferred.fulfill() try await deferred.fulfill()
// When tapping on another administrator in the list. // When tapping on another administrator in the list.
let memberDetailsAction = deferFulfillment(viewModel.actions) { $0.isSelectMember } deferred = deferFulfillment(context.$viewState) { $0.bindings.manageMemeberViewModel != nil }
guard let admin = viewModel.state.visibleJoinedMembers.first(where: { $0.member.role == .administrator && $0.member.id != RoomMemberProxyMock.mockMe.userID })?.member else { guard let admin = viewModel.state.visibleJoinedMembers.first(where: { $0.member.role == .administrator && $0.member.id != RoomMemberProxyMock.mockMe.userID })?.member else {
XCTFail("Expected to find another admin.") XCTFail("Expected to find another admin.")
return return
@@ -205,8 +219,13 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
context.send(viewAction: .selectMember(admin)) context.send(viewAction: .selectMember(admin))
// Then the administrator's details should be shown. // Then the administrator's details should be shown.
try await memberDetailsAction.fulfill() try await deferred.fulfill()
XCTAssertNil(context.manageMemeberViewModel) XCTAssertEqual(context.manageMemeberViewModel?.state.memberDetails.id, admin.id)
XCTAssertEqual(context.manageMemeberViewModel?.state.permissions.canKick, true)
XCTAssertEqual(context.manageMemeberViewModel?.state.permissions.canBan, true)
XCTAssertEqual(context.manageMemeberViewModel?.state.isKickDisabled, true)
XCTAssertEqual(context.manageMemeberViewModel?.state.isBanUnbanDisabled, true)
XCTAssertEqual(context.manageMemeberViewModel?.state.isMemberBanned, false)
} }
func testSelectOwnMemberAsAdmin() async throws { func testSelectOwnMemberAsAdmin() async throws {
@@ -231,12 +250,12 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
func testSelectBannedMember() async throws { func testSelectBannedMember() async throws {
// Given the room list viewed as an admin. // Given the room list viewed as an admin.
setup(with: .allMembersAsAdmin + RoomMemberProxyMock.mockBanned) setup(with: .allMembersAsAdmin + RoomMemberProxyMock.mockBanned)
var deferred = deferFulfillment(context.$viewState) { !$0.visibleInvitedMembers.isEmpty } var deferred = deferFulfillment(context.$viewState) { !$0.visibleInvitedMembers.isEmpty && $0.canKickUsers && $0.canBanUsers }
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertNil(context.alertInfo) XCTAssertNil(context.alertInfo)
// When tapping on a banned member in the list. // When tapping on a banned member in the list.
deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil } deferred = deferFulfillment(context.$viewState) { $0.bindings.manageMemeberViewModel != nil }
guard let bannedMember = viewModel.state.visibleBannedMembers.first?.member else { guard let bannedMember = viewModel.state.visibleBannedMembers.first?.member else {
XCTFail("Expected to find a banned user.") XCTFail("Expected to find a banned user.")
return return
@@ -245,21 +264,12 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
// Then an alert should be shown to unban the user. // Then an alert should be shown to unban the user.
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertNil(context.manageMemeberViewModel) XCTAssertEqual(context.manageMemeberViewModel?.state.memberDetails.id, bannedMember.id)
XCTAssertNotNil(context.alertInfo) XCTAssertEqual(context.manageMemeberViewModel?.state.permissions.canKick, true)
} XCTAssertEqual(context.manageMemeberViewModel?.state.permissions.canBan, true)
XCTAssertEqual(context.manageMemeberViewModel?.state.isKickDisabled, true)
func testUnbanMember() async throws { XCTAssertEqual(context.manageMemeberViewModel?.state.isBanUnbanDisabled, false)
setup(with: .allMembersAsAdmin) XCTAssertEqual(context.manageMemeberViewModel?.state.isMemberBanned, true)
let deferred = deferFulfillment(context.$viewState) { !$0.visibleJoinedMembers.isEmpty }
try await deferred.fulfill()
context.send(viewAction: .unbanMember(viewModel.state.visibleJoinedMembers[0].member))
// Calling the mock won't actually change any view state, so sleep instead.
try await Task.sleep(for: .milliseconds(100))
XCTAssert(roomProxy.unbanUserCalled)
} }
private func setup(with members: [RoomMemberProxyMock]) { private func setup(with members: [RoomMemberProxyMock]) {
+36 -26
View File
@@ -377,12 +377,14 @@ class TimelineViewModelTests: XCTestCase {
value.bindings.manageMemberViewModel != nil value.bindings.manageMemberViewModel != nil
} }
viewModel.context.send(viewAction: .tappedOnSenderDetails(userID: RoomMemberProxyMock.mockAlice.userID)) viewModel.context.send(viewAction: .tappedOnSenderDetails(sender: .init(with: RoomMemberProxyMock.mockAlice)))
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertEqual(viewModel.context.manageMemberViewModel?.state.member.id, RoomMemberProxyMock.mockAlice.userID) XCTAssertEqual(viewModel.context.manageMemberViewModel?.id, RoomMemberProxyMock.mockAlice.userID)
XCTAssertEqual(viewModel.context.manageMemberViewModel?.state.canBan, true) XCTAssertEqual(viewModel.context.manageMemberViewModel?.state.permissions.canBan, true)
XCTAssertEqual(viewModel.context.manageMemberViewModel?.state.canKick, true) XCTAssertEqual(viewModel.context.manageMemberViewModel?.state.permissions.canKick, true)
XCTAssertEqual(viewModel.context.manageMemberViewModel?.state.isKickDisabled, false)
XCTAssertEqual(viewModel.context.manageMemberViewModel?.state.isBanUnbanDisabled, false)
} }
func testShowDetailsForAnAdmin() async throws { func testShowDetailsForAnAdmin() async throws {
@@ -402,25 +404,24 @@ class TimelineViewModelTests: XCTestCase {
timelineControllerFactory: TimelineControllerFactoryMock(.init()), timelineControllerFactory: TimelineControllerFactoryMock(.init()),
clientProxy: ClientProxyMock(.init())) clientProxy: ClientProxyMock(.init()))
let deferredState = deferFulfillment(viewModel.context.$viewState) { value in var deferredState = deferFulfillment(viewModel.context.$viewState) { value in
!value.canCurrentUserKick && !value.canCurrentUserBan !value.canCurrentUserKick && !value.canCurrentUserBan
} }
try await deferredState.fulfill() try await deferredState.fulfill()
let deferredAction = deferFulfillment(viewModel.actions) { action in deferredState = deferFulfillment(viewModel.context.$viewState) { value in
switch action { value.bindings.manageMemberViewModel != nil
case .displaySenderDetails(let userID):
return userID == RoomMemberProxyMock.mockAdmin.userID
default:
return false
}
} }
viewModel.context.send(viewAction: .tappedOnSenderDetails(userID: RoomMemberProxyMock.mockAdmin.userID)) viewModel.context.send(viewAction: .tappedOnSenderDetails(sender: .init(with: RoomMemberProxyMock.mockAdmin)))
try await deferredAction.fulfill() try await deferredState.fulfill()
XCTAssertNil(viewModel.context.manageMemberViewModel) XCTAssertEqual(viewModel.context.manageMemberViewModel?.state.permissions.canBan, false)
XCTAssertEqual(viewModel.context.manageMemberViewModel?.state.permissions.canKick, false)
XCTAssertEqual(viewModel.context.manageMemberViewModel?.state.isKickDisabled, true)
XCTAssertEqual(viewModel.context.manageMemberViewModel?.state.isBanUnbanDisabled, true)
XCTAssertEqual(viewModel.context.manageMemberViewModel?.id, RoomMemberProxyMock.mockAdmin.userID)
} }
func testShowDetailsForABannedUser() async throws { func testShowDetailsForABannedUser() async throws {
@@ -440,25 +441,25 @@ class TimelineViewModelTests: XCTestCase {
timelineControllerFactory: TimelineControllerFactoryMock(.init()), timelineControllerFactory: TimelineControllerFactoryMock(.init()),
clientProxy: ClientProxyMock(.init())) clientProxy: ClientProxyMock(.init()))
let deferredState = deferFulfillment(viewModel.context.$viewState) { value in var deferredState = deferFulfillment(viewModel.context.$viewState) { value in
value.canCurrentUserKick && value.canCurrentUserBan value.canCurrentUserKick && value.canCurrentUserBan
} }
try await deferredState.fulfill() try await deferredState.fulfill()
let deferredAction = deferFulfillment(viewModel.actions) { action in deferredState = deferFulfillment(viewModel.context.$viewState) { value in
switch action { value.bindings.manageMemberViewModel != nil
case .displaySenderDetails(let userID):
return userID == RoomMemberProxyMock.mockBanned[0].userID
default:
return false
}
} }
viewModel.context.send(viewAction: .tappedOnSenderDetails(userID: RoomMemberProxyMock.mockBanned[0].userID)) viewModel.context.send(viewAction: .tappedOnSenderDetails(sender: .init(with: RoomMemberProxyMock.mockBanned[0])))
try await deferredAction.fulfill() try await deferredState.fulfill()
XCTAssertNil(viewModel.context.manageMemberViewModel) XCTAssertEqual(viewModel.context.manageMemberViewModel?.state.permissions.canBan, true)
XCTAssertEqual(viewModel.context.manageMemberViewModel?.state.permissions.canKick, true)
XCTAssertEqual(viewModel.context.manageMemberViewModel?.state.isKickDisabled, true)
XCTAssertEqual(viewModel.context.manageMemberViewModel?.state.isBanUnbanDisabled, false)
XCTAssertEqual(viewModel.context.manageMemberViewModel?.state.isMemberBanned, true)
XCTAssertEqual(viewModel.context.manageMemberViewModel?.id, RoomMemberProxyMock.mockBanned[0].userID)
} }
// MARK: - Pins // MARK: - Pins
@@ -576,3 +577,12 @@ private extension TextRoomTimelineItem {
content: .init(body: "Hello, World!")) content: .init(body: "Hello, World!"))
} }
} }
private extension TimelineItemSender {
init(with proxy: RoomMemberProxyMock) {
self.init(id: proxy.userID,
displayName: proxy.displayName ?? "",
isDisplayNameAmbiguous: false,
avatarURL: proxy.avatarURL)
}
}