diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index 6ccfb9ebf..ad5a026c3 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -425,7 +425,7 @@ "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_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_title" = "Anyone"; "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_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_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_notice" = "Enter a domain address."; "screen_change_server_subtitle" = "What is the address of your server?"; diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index e4c60e2d9..e1f31f3c1 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -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") } /// 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") } - /// Unbanning $1%@ + /// Unbanning %1$@ internal static func screenBottomSheetManageRoomMemberUnbanningUser(_ p1: Any) -> String { 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 { 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 internal static var screenChangeServerFormHeader: String { return L10n.tr("Localizable", "screen_change_server_form_header") } /// Enter a domain address. diff --git a/ElementX/Sources/Other/SwiftUI/Views/AvatarHeaderView.swift b/ElementX/Sources/Other/SwiftUI/Views/AvatarHeaderView.swift index 25ec8e1b4..1145f8701 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/AvatarHeaderView.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/AvatarHeaderView.swift @@ -110,6 +110,23 @@ struct AvatarHeaderView: View { 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 { HStack(spacing: 8) { ForEach(badges, id: \.self) { badge in diff --git a/ElementX/Sources/Screens/ManageRoomMemberSheet/ManageRoomMemberSheetModels.swift b/ElementX/Sources/Screens/ManageRoomMemberSheet/ManageRoomMemberSheetModels.swift index 928fb0083..772c2cdf4 100644 --- a/ElementX/Sources/Screens/ManageRoomMemberSheet/ManageRoomMemberSheetModels.swift +++ b/ElementX/Sources/Screens/ManageRoomMemberSheet/ManageRoomMemberSheetModels.swift @@ -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 +} diff --git a/ElementX/Sources/Screens/ManageRoomMemberSheet/ManageRoomMemberSheetViewModel.swift b/ElementX/Sources/Screens/ManageRoomMemberSheet/ManageRoomMemberSheetViewModel.swift index 2bdcd629e..3bafa0747 100644 --- a/ElementX/Sources/Screens/ManageRoomMemberSheet/ManageRoomMemberSheetViewModel.swift +++ b/ElementX/Sources/Screens/ManageRoomMemberSheet/ManageRoomMemberSheetViewModel.swift @@ -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 = .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 + } } diff --git a/ElementX/Sources/Screens/ManageRoomMemberSheet/View/ManageRoomMemberSheetView.swift b/ElementX/Sources/Screens/ManageRoomMemberSheet/View/ManageRoomMemberSheetView.swift index 9c6a5e46a..23ff1de5e 100644 --- a/ElementX/Sources/Screens/ManageRoomMemberSheet/View/ManageRoomMemberSheetView.swift +++ b/ElementX/Sources/Screens/ManageRoomMemberSheet/View/ManageRoomMemberSheetView.swift @@ -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 = 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, diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenModels.swift b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenModels.swift index 8dda1f39b..f24364087 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenModels.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenModels.swift @@ -89,7 +89,6 @@ struct RoomMembersListScreenViewStateBindings { enum RoomMembersListScreenViewAction { case selectMember(RoomMemberDetails) - case unbanMember(RoomMemberDetails) case invite } diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift index 1b3b17c42..f9de111b4 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift @@ -51,8 +51,6 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe switch viewAction { case .selectMember(let member): selectMember(member) - case .unbanMember(let member): - Task { await unbanMember(member) } case .invite: actionsSubject.send(.invite) } @@ -144,46 +142,31 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe } private func selectMember(_ member: RoomMemberDetails) { - guard let currentUserProxy, - currentUserProxy.powerLevel > member.powerLevel else { + guard currentUserProxy?.userID != member.id 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, - 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(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) - } + let manageMemeberViewModel = ManageRoomMemberSheetViewModel(memberDetails: .memberDetails(roomMember: member), + permissions: .init(canKick: state.canKickUsers, + canBan: state.canBanUsers, + ownPowerLevel: currentUserProxy?.powerLevel ?? 0), + 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) } + .store(in: &cancellables) + state.bindings.manageMemeberViewModel = manageMemeberViewModel } private func showMemberDetails(_ member: RoomMemberDetails) { diff --git a/ElementX/Sources/Screens/Timeline/TimelineModels.swift b/ElementX/Sources/Screens/Timeline/TimelineModels.swift index c9951510a..1996a1279 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineModels.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineModels.swift @@ -55,7 +55,7 @@ enum TimelineViewAction { case displayTimelineItemMenu(itemID: TimelineItemIdentifier) case handleTimelineItemMenuAction(itemID: TimelineItemIdentifier, action: TimelineItemMenuAction) - case tappedOnSenderDetails(userID: String) + case tappedOnSenderDetails(sender: TimelineItemSender) case displayReactionSummary(itemID: TimelineItemIdentifier, key: String) case displayEmojiPicker(itemID: TimelineItemIdentifier) case displayReadReceipts(itemID: TimelineItemIdentifier) diff --git a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift index 88a22bf9a..61f0ad903 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift @@ -183,8 +183,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { timelineInteractionHandler.displayTimelineItemActionMenu(for: itemID) case .handleTimelineItemMenuAction(let itemID, let action): timelineInteractionHandler.handleTimelineItemMenuAction(action, itemID: itemID) - case .tappedOnSenderDetails(let userID): - handleTappedOnSenderDetails(userID: userID) + case .tappedOnSenderDetails(let sender): + handleTappedOnSenderDetails(sender: sender) case .displayEmojiPicker(let itemID): timelineInteractionHandler.displayEmojiPicker(for: itemID) case .displayReactionSummary(let itemID, let key): @@ -271,39 +271,34 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { // MARK: - Private - private func handleTappedOnSenderDetails(userID: String) { - // We also need to make sure the user is in the joined state, otherwise we just show the details. - guard let memberProxy = roomProxy.membersPublisher.value.first(where: { $0.userID == userID && $0.membership == .join }), - let currentUserProxy, - currentUserProxy.powerLevel > memberProxy.powerLevel else { - actionsSubject.send(.displaySenderDetails(userID: userID)) - return + private func handleTappedOnSenderDetails(sender: TimelineItemSender) { + let memberDetails: ManageRoomMemberDetails = if let memberProxy = roomProxy.membersPublisher.value.first(where: { $0.userID == sender.id }) { + .memberDetails(roomMember: .init(withProxy: memberProxy)) + } else { + .loadingMemberDetails(sender: sender) } - if state.canCurrentUserBan || state.canCurrentUserKick { - let member = RoomMemberDetails(withProxy: memberProxy) - let manageMemeberViewModel = ManageRoomMemberSheetViewModel(member: member, - canKick: state.canCurrentUserKick, - canBan: state.canCurrentUserBan, - roomProxy: roomProxy, - userIndicatorController: userIndicatorController, - analyticsService: analyticsService, - mediaProvider: mediaProvider) - manageMemeberViewModel.actions.sink { [weak self] action in - guard let self else { return } - switch action { - case .dismiss(let shouldShowDetails): - state.bindings.manageMemberViewModel = nil - if shouldShowDetails { - actionsSubject.send(.displaySenderDetails(userID: userID)) - } + let viewModel = ManageRoomMemberSheetViewModel(memberDetails: memberDetails, + permissions: .init(canKick: state.canCurrentUserKick, + canBan: state.canCurrentUserBan, + ownPowerLevel: currentUserProxy?.powerLevel ?? 0), + roomProxy: roomProxy, + userIndicatorController: userIndicatorController, + analyticsService: analyticsService, + mediaProvider: mediaProvider) + + viewModel.actions.sink { [weak self] action in + guard let self else { return } + switch action { + case .dismiss(let shouldShowDetails): + state.bindings.manageMemberViewModel = nil + if shouldShowDetails { + 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() { diff --git a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift index 344c9bd55..8a47b0322 100644 --- a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift +++ b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift @@ -95,7 +95,7 @@ struct TimelineItemBubbledStylerView: View { // sender info are read inside the `TimelineAccessibilityModifier` .accessibilityHidden(true) .onTapGesture { - context.send(viewAction: .tappedOnSenderDetails(userID: timelineItem.sender.id)) + context.send(viewAction: .tappedOnSenderDetails(sender: timelineItem.sender)) } .padding(.top, 8) } diff --git a/ElementX/Sources/Services/Room/RoomMember/RoomMemberDetails.swift b/ElementX/Sources/Services/Room/RoomMember/RoomMemberDetails.swift index 9f8918586..230177bd4 100644 --- a/ElementX/Sources/Services/Room/RoomMember/RoomMemberDetails.swift +++ b/ElementX/Sources/Services/Room/RoomMember/RoomMemberDetails.swift @@ -17,7 +17,8 @@ struct RoomMemberDetails: Identifiable, Hashable { var isInvited: Bool var isIgnored: Bool var isBanned: Bool - + var isActive: Bool + enum Role { case administrator, moderator, user } let role: Role let powerLevel: Int @@ -34,7 +35,7 @@ extension RoomMemberDetails { name = proxy.displayName avatarURL = proxy.avatarURL permalink = proxy.permalink - + isActive = proxy.isActive isInvited = proxy.membership == .invite isIgnored = proxy.isIgnored isBanned = proxy.membership == .ban diff --git a/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxyProtocol.swift b/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxyProtocol.swift index 842059cb9..48129339c 100644 --- a/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxyProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxyProtocol.swift @@ -31,7 +31,7 @@ protocol RoomMemberProxyProtocol: AnyObject { extension RoomMemberProxyProtocol { /// The member is active in the room (joined or invited). var isActive: Bool { - membership == .join || membership == .invite + membership == .join || membership == .invite || membership == .knock } var permalink: URL? { diff --git a/ElementX/Sources/Services/Users/UserProfileProxy.swift b/ElementX/Sources/Services/Users/UserProfileProxy.swift index a0035d2fb..a8af95bc4 100644 --- a/ElementX/Sources/Services/Users/UserProfileProxy.swift +++ b/ElementX/Sources/Services/Users/UserProfileProxy.swift @@ -25,6 +25,12 @@ struct UserProfileProxy: Equatable, Hashable { avatarURL = member.isBanned ? nil : member.avatarURL } + init(sender: TimelineItemSender) { + userID = sender.id + displayName = sender.displayName + avatarURL = sender.avatarURL + } + init(sdkUserProfile: MatrixRustSDK.UserProfile) { userID = sdkUserProfile.userId displayName = sdkUserProfile.displayName diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.All-Actions-Disabled-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.All-Actions-Disabled-iPad-en-GB.png new file mode 100644 index 000000000..9d9442277 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.All-Actions-Disabled-iPad-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9de850c95f6dbdfad502994a33d0523226cebaa99f8e36564c5c30b3eade2237 +size 195361 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.All-Actions-Disabled-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.All-Actions-Disabled-iPad-pseudo.png new file mode 100644 index 000000000..d96055dec --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.All-Actions-Disabled-iPad-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ba5ada0c14223f75f6f79112ce5b4741c8fc279289c7077e6b00a5d1545f823e +size 197461 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.All-Actions-Disabled-iPhone-16-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.All-Actions-Disabled-iPhone-16-en-GB.png new file mode 100644 index 000000000..7dc592c03 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.All-Actions-Disabled-iPhone-16-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5ef6e95930907faac60daa81166e91104dbe1821d79e847f8553ff7acaf5eda2 +size 135322 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.All-Actions-Disabled-iPhone-16-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.All-Actions-Disabled-iPhone-16-pseudo.png new file mode 100644 index 000000000..10f0c1575 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.All-Actions-Disabled-iPhone-16-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c6b3cee316cfe4edf8fdaeac2fe0bf7d520b58f52138c504e3f55ebe3b68267c +size 140517 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Unban-Only-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Unban-Only-iPad-en-GB.png new file mode 100644 index 000000000..7b8b5ef53 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Unban-Only-iPad-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:28206d20d834c4c4804045b9d47e4c95aa6ca16c0168f8267d98c814e2795b15 +size 103571 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Unban-Only-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Unban-Only-iPad-pseudo.png new file mode 100644 index 000000000..18fe4f9b2 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Unban-Only-iPad-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b4a365e20cb2d85311a1db85f809e65ac0a46af5e9ae9eab7dcd6f31022724b4 +size 105093 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Unban-Only-iPhone-16-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Unban-Only-iPhone-16-en-GB.png new file mode 100644 index 000000000..9543beff4 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Unban-Only-iPhone-16-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31eb3b34cd36134cfa8c787cc974c01d08eaeb53e8c67b4e023475ba9ead5c93 +size 54438 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Unban-Only-iPhone-16-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Unban-Only-iPhone-16-pseudo.png new file mode 100644 index 000000000..2071bc792 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Unban-Only-iPhone-16-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ebd80833a565d8f865096dc772bd58ef81523e9e219ad600f06f3b5251ba4dfc +size 58583 diff --git a/UnitTests/Sources/ManageRoomMemberSheetViewModelTests.swift b/UnitTests/Sources/ManageRoomMemberSheetViewModelTests.swift index 398528c70..bbb97de47 100644 --- a/UnitTests/Sources/ManageRoomMemberSheetViewModelTests.swift +++ b/UnitTests/Sources/ManageRoomMemberSheetViewModelTests.swift @@ -27,9 +27,8 @@ class ManageRoomMemberSheetViewModelTests: XCTestCase { return .success(()) } - viewModel = ManageRoomMemberSheetViewModel(member: RoomMemberDetails(withProxy: RoomMemberProxyMock.mockAlice), - canKick: true, - canBan: true, + viewModel = ManageRoomMemberSheetViewModel(memberDetails: .memberDetails(roomMember: .init(withProxy: RoomMemberProxyMock.mockAlice)), + permissions: .init(canKick: true, canBan: true, ownPowerLevel: RoomMemberProxyMock.mockAdmin.powerLevel), roomProxy: roomProxy, userIndicatorController: UserIndicatorControllerMock(), analyticsService: ServiceLocator.shared.analytics, @@ -59,9 +58,8 @@ class ManageRoomMemberSheetViewModelTests: XCTestCase { return .success(()) } - viewModel = ManageRoomMemberSheetViewModel(member: RoomMemberDetails(withProxy: RoomMemberProxyMock.mockAlice), - canKick: true, - canBan: true, + viewModel = ManageRoomMemberSheetViewModel(memberDetails: .memberDetails(roomMember: .init(withProxy: RoomMemberProxyMock.mockAlice)), + permissions: .init(canKick: true, canBan: true, ownPowerLevel: RoomMemberProxyMock.mockAdmin.powerLevel), roomProxy: roomProxy, userIndicatorController: UserIndicatorControllerMock(), analyticsService: ServiceLocator.shared.analytics, @@ -82,9 +80,8 @@ class ManageRoomMemberSheetViewModelTests: XCTestCase { func testDisplayDetails() async throws { let roomProxy = JoinedRoomProxyMock(.init(members: [RoomMemberProxyMock.mockAdmin, RoomMemberProxyMock.mockAlice])) - viewModel = ManageRoomMemberSheetViewModel(member: RoomMemberDetails(withProxy: RoomMemberProxyMock.mockAlice), - canKick: true, - canBan: true, + viewModel = ManageRoomMemberSheetViewModel(memberDetails: .memberDetails(roomMember: .init(withProxy: RoomMemberProxyMock.mockAlice)), + permissions: .init(canKick: true, canBan: true, ownPowerLevel: RoomMemberProxyMock.mockAdmin.powerLevel), roomProxy: roomProxy, userIndicatorController: UserIndicatorControllerMock(), analyticsService: ServiceLocator.shared.analytics, diff --git a/UnitTests/Sources/RoomMembersListScreenViewModelTests.swift b/UnitTests/Sources/RoomMembersListScreenViewModelTests.swift index a3694e908..6bb05abdb 100644 --- a/UnitTests/Sources/RoomMembersListScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomMembersListScreenViewModelTests.swift @@ -18,6 +18,11 @@ class RoomMembersListScreenViewModelTests: XCTestCase { viewModel.context } + override func tearDown() { + viewModel = nil + roomProxy = nil + } + func testJoinedMembers() async throws { setup(with: [.mockAlice, .mockBob]) @@ -130,11 +135,11 @@ class RoomMembersListScreenViewModelTests: XCTestCase { func testSelectUserAsUser() async throws { // Given the room list viewed as a regular user. setup(with: .allMembers) - let deferred = deferFulfillment(context.$viewState) { !$0.visibleInvitedMembers.isEmpty } + var deferred = deferFulfillment(context.$viewState) { !$0.visibleInvitedMembers.isEmpty } try await deferred.fulfill() // 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 { XCTFail("Expected to find a regular user.") return @@ -142,8 +147,11 @@ class RoomMembersListScreenViewModelTests: XCTestCase { context.send(viewAction: .selectMember(user)) // Then the member's details should be shown. - try await memberDetailsAction.fulfill() - XCTAssertNil(context.manageMemeberViewModel) + try await deferred.fulfill() + 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 { @@ -163,9 +171,12 @@ class RoomMembersListScreenViewModelTests: XCTestCase { try await deferred.fulfill() // Then member management should be shown for that user. - XCTAssertEqual(context.manageMemeberViewModel?.state.member, user) - XCTAssertEqual(context.manageMemeberViewModel?.state.canKick, true) - XCTAssertEqual(context.manageMemeberViewModel?.state.canBan, true) + XCTAssertEqual(context.manageMemeberViewModel?.state.memberDetails.id, user.id) + XCTAssertEqual(context.manageMemeberViewModel?.state.permissions.canKick, 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 { @@ -185,19 +196,22 @@ class RoomMembersListScreenViewModelTests: XCTestCase { try await deferred.fulfill() // Then member management should be shown for the moderator. - XCTAssertEqual(context.manageMemeberViewModel?.state.member, moderator) - XCTAssertEqual(context.manageMemeberViewModel?.state.canKick, true) - XCTAssertEqual(context.manageMemeberViewModel?.state.canBan, true) + XCTAssertEqual(context.manageMemeberViewModel?.state.memberDetails.id, moderator.id) + XCTAssertEqual(context.manageMemeberViewModel?.state.permissions.canKick, 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 { // Given the room list viewed as an admin. 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() // 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 { XCTFail("Expected to find another admin.") return @@ -205,8 +219,13 @@ class RoomMembersListScreenViewModelTests: XCTestCase { context.send(viewAction: .selectMember(admin)) // Then the administrator's details should be shown. - try await memberDetailsAction.fulfill() - XCTAssertNil(context.manageMemeberViewModel) + try await deferred.fulfill() + 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 { @@ -231,12 +250,12 @@ class RoomMembersListScreenViewModelTests: XCTestCase { func testSelectBannedMember() async throws { // Given the room list viewed as an admin. 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() XCTAssertNil(context.alertInfo) // 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 { XCTFail("Expected to find a banned user.") return @@ -245,21 +264,12 @@ class RoomMembersListScreenViewModelTests: XCTestCase { // Then an alert should be shown to unban the user. try await deferred.fulfill() - XCTAssertNil(context.manageMemeberViewModel) - XCTAssertNotNil(context.alertInfo) - } - - func testUnbanMember() async throws { - setup(with: .allMembersAsAdmin) - 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) + XCTAssertEqual(context.manageMemeberViewModel?.state.memberDetails.id, bannedMember.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, false) + XCTAssertEqual(context.manageMemeberViewModel?.state.isMemberBanned, true) } private func setup(with members: [RoomMemberProxyMock]) { diff --git a/UnitTests/Sources/TimelineViewModelTests.swift b/UnitTests/Sources/TimelineViewModelTests.swift index 4920e6256..9fdb9cade 100644 --- a/UnitTests/Sources/TimelineViewModelTests.swift +++ b/UnitTests/Sources/TimelineViewModelTests.swift @@ -377,12 +377,14 @@ class TimelineViewModelTests: XCTestCase { 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() - XCTAssertEqual(viewModel.context.manageMemberViewModel?.state.member.id, RoomMemberProxyMock.mockAlice.userID) - XCTAssertEqual(viewModel.context.manageMemberViewModel?.state.canBan, true) - XCTAssertEqual(viewModel.context.manageMemberViewModel?.state.canKick, true) + XCTAssertEqual(viewModel.context.manageMemberViewModel?.id, RoomMemberProxyMock.mockAlice.userID) + XCTAssertEqual(viewModel.context.manageMemberViewModel?.state.permissions.canBan, 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 { @@ -402,25 +404,24 @@ class TimelineViewModelTests: XCTestCase { timelineControllerFactory: TimelineControllerFactoryMock(.init()), clientProxy: ClientProxyMock(.init())) - let deferredState = deferFulfillment(viewModel.context.$viewState) { value in + var deferredState = deferFulfillment(viewModel.context.$viewState) { value in !value.canCurrentUserKick && !value.canCurrentUserBan } try await deferredState.fulfill() - let deferredAction = deferFulfillment(viewModel.actions) { action in - switch action { - case .displaySenderDetails(let userID): - return userID == RoomMemberProxyMock.mockAdmin.userID - default: - return false - } + deferredState = deferFulfillment(viewModel.context.$viewState) { value in + value.bindings.manageMemberViewModel != nil } - viewModel.context.send(viewAction: .tappedOnSenderDetails(userID: RoomMemberProxyMock.mockAdmin.userID)) - try await deferredAction.fulfill() + viewModel.context.send(viewAction: .tappedOnSenderDetails(sender: .init(with: RoomMemberProxyMock.mockAdmin))) + 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 { @@ -440,25 +441,25 @@ class TimelineViewModelTests: XCTestCase { timelineControllerFactory: TimelineControllerFactoryMock(.init()), clientProxy: ClientProxyMock(.init())) - let deferredState = deferFulfillment(viewModel.context.$viewState) { value in + var deferredState = deferFulfillment(viewModel.context.$viewState) { value in value.canCurrentUserKick && value.canCurrentUserBan } try await deferredState.fulfill() - let deferredAction = deferFulfillment(viewModel.actions) { action in - switch action { - case .displaySenderDetails(let userID): - return userID == RoomMemberProxyMock.mockBanned[0].userID - default: - return false - } + deferredState = deferFulfillment(viewModel.context.$viewState) { value in + value.bindings.manageMemberViewModel != nil } - viewModel.context.send(viewAction: .tappedOnSenderDetails(userID: RoomMemberProxyMock.mockBanned[0].userID)) - try await deferredAction.fulfill() + viewModel.context.send(viewAction: .tappedOnSenderDetails(sender: .init(with: RoomMemberProxyMock.mockBanned[0]))) + 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 @@ -576,3 +577,12 @@ private extension TextRoomTimelineItem { 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) + } +}