mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-05 11:58:32 +00:00
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:
@@ -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?";
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
+39
-14
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+44
-14
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+18
-35
@@ -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
|
||||||
|
|||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:9de850c95f6dbdfad502994a33d0523226cebaa99f8e36564c5c30b3eade2237
|
||||||
|
size 195361
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:ba5ada0c14223f75f6f79112ce5b4741c8fc279289c7077e6b00a5d1545f823e
|
||||||
|
size 197461
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:5ef6e95930907faac60daa81166e91104dbe1821d79e847f8553ff7acaf5eda2
|
||||||
|
size 135322
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:c6b3cee316cfe4edf8fdaeac2fe0bf7d520b58f52138c504e3f55ebe3b68267c
|
||||||
|
size 140517
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:28206d20d834c4c4804045b9d47e4c95aa6ca16c0168f8267d98c814e2795b15
|
||||||
|
size 103571
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:b4a365e20cb2d85311a1db85f809e65ac0a46af5e9ae9eab7dcd6f31022724b4
|
||||||
|
size 105093
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:31eb3b34cd36134cfa8c787cc974c01d08eaeb53e8c67b4e023475ba9ead5c93
|
||||||
|
size 54438
|
||||||
+3
@@ -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]) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user