mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-10-31 16:24:11 +00:00
Kick/ban room members (#2501)
* Add a temporary membershipChangePublisher on the timeline. * Kick/Ban users from a room. * Unit tests. * Disable autocorrection on the members list search field.
This commit is contained in:
@@ -364,6 +364,7 @@
|
||||
5D4643E485C179B2F485C519 /* MentionSuggestionItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FD0E68C42CA7DDCD4CAD68D /* MentionSuggestionItemView.swift */; };
|
||||
5D53AE9342A4C06B704247ED /* MediaLoaderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A02406480C351B8C6E0682C /* MediaLoaderProtocol.swift */; };
|
||||
5D70FAE4D2BF4553AFFFFE41 /* NotificationItemProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F7FE40EF7490A7E09D7BE6 /* NotificationItemProxy.swift */; };
|
||||
5DD0EF30070DC0A82C5CCD33 /* RoomMembersListManageMemberSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC853F9B4FBE039D2C16EC6B /* RoomMembersListManageMemberSheet.swift */; };
|
||||
5DD85A0FE3D85AEC3C7EFE36 /* DeveloperOptionsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7C7CFA6B2A62A685FF6CE3 /* DeveloperOptionsScreenCoordinator.swift */; };
|
||||
5E0F2E612718BB4397A6D40A /* TextRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9E785D5137510481733A3E8 /* TextRoomTimelineView.swift */; };
|
||||
5EE1D4E316D66943E97FDCF2 /* BloomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7BEB970F500BFB248443FA1 /* BloomView.swift */; };
|
||||
@@ -2008,6 +2009,7 @@
|
||||
FBB0328F2887BF0A65BC5D49 /* NotificationSettingsEditScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreen.swift; sourceTree = "<group>"; };
|
||||
FBC776F301D374A3298C69DA /* AppCoordinatorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinatorProtocol.swift; sourceTree = "<group>"; };
|
||||
FC2D505742FDA21FCDC4C18A /* AudioRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRoomTimelineView.swift; sourceTree = "<group>"; };
|
||||
FC853F9B4FBE039D2C16EC6B /* RoomMembersListManageMemberSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListManageMemberSheet.swift; sourceTree = "<group>"; };
|
||||
FCE93F0CBF0D96B77111C413 /* AppLockFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockFlowCoordinator.swift; sourceTree = "<group>"; };
|
||||
FD1275D9CE0FFBA6E8E85426 /* UserIndicatorController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorController.swift; sourceTree = "<group>"; };
|
||||
FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainControllerTests.swift; sourceTree = "<group>"; };
|
||||
@@ -3817,6 +3819,7 @@
|
||||
949B06577E5265373013DDAB /* View */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FC853F9B4FBE039D2C16EC6B /* RoomMembersListManageMemberSheet.swift */,
|
||||
1B8E176484A89BAC389D4076 /* RoomMembersListScreen.swift */,
|
||||
CC03209FDE8CE0810617BFFF /* RoomMembersListScreenMemberCell.swift */,
|
||||
);
|
||||
@@ -5882,6 +5885,7 @@
|
||||
6448F8D1D3CA4CD27BB4CADD /* RoomMemberProxy.swift in Sources */,
|
||||
92D9088B901CEBB1A99ECA4E /* RoomMemberProxyMock.swift in Sources */,
|
||||
F118DD449066E594F63C697D /* RoomMemberProxyProtocol.swift in Sources */,
|
||||
5DD0EF30070DC0A82C5CCD33 /* RoomMembersListManageMemberSheet.swift in Sources */,
|
||||
C08AAE7563E0722C9383F51C /* RoomMembersListScreen.swift in Sources */,
|
||||
1C9BB74711E5F24C77B7FED0 /* RoomMembersListScreenCoordinator.swift in Sources */,
|
||||
A975D60EA49F6AF73308809F /* RoomMembersListScreenMemberCell.swift in Sources */,
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
buildImplicitDependencies = "YES"
|
||||
runPostActionsOnFailure = "NO">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
@@ -29,6 +30,12 @@
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
codeCoverageEnabled = "YES"
|
||||
onlyGenerateCoverageForSpecifiedTargets = "YES">
|
||||
<TestPlans>
|
||||
<TestPlanReference
|
||||
default = "YES"
|
||||
reference = "container:PreviewTests/SupportingFiles/PreviewTests.xctestplan">
|
||||
</TestPlanReference>
|
||||
</TestPlans>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
@@ -38,6 +45,10 @@
|
||||
ReferencedContainer = "container:ElementX.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
</Testables>
|
||||
<CommandLineArguments>
|
||||
</CommandLineArguments>
|
||||
<CodeCoverageTargets>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
@@ -47,12 +58,6 @@
|
||||
ReferencedContainer = "container:ElementX.xcodeproj">
|
||||
</BuildableReference>
|
||||
</CodeCoverageTargets>
|
||||
<TestPlans>
|
||||
<TestPlanReference
|
||||
reference = "container:PreviewTests/SupportingFiles/PreviewTests.xctestplan"
|
||||
default = "YES">
|
||||
</TestPlanReference>
|
||||
</TestPlans>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
@@ -73,6 +78,8 @@
|
||||
ReferencedContainer = "container:ElementX.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<CommandLineArguments>
|
||||
</CommandLineArguments>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
@@ -80,6 +87,8 @@
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<CommandLineArguments>
|
||||
</CommandLineArguments>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
|
||||
@@ -523,11 +523,14 @@
|
||||
"screen_room_member_details_unblock_alert_action" = "Unblock";
|
||||
"screen_room_member_details_unblock_alert_description" = "You'll be able to see all messages from them again.";
|
||||
"screen_room_member_details_unblock_user" = "Unblock user";
|
||||
"screen_room_member_list_banning_user" = "Banning %1$@";
|
||||
"screen_room_member_list_manage_member_remove" = "Remove member";
|
||||
"screen_room_member_list_manage_member_remove_confirmation_ban" = "Remove and ban member";
|
||||
"screen_room_member_list_manage_member_remove_confirmation_kick" = "Only remove member";
|
||||
"screen_room_member_list_manage_member_remove_confirmation_title" = "Remove member and ban from joining in the future?";
|
||||
"screen_room_member_list_manage_member_unban" = "Unban";
|
||||
"screen_room_member_list_manage_member_unban_action" = "Unban";
|
||||
"screen_room_member_list_manage_member_unban_message" = "They will be able to join this room again if invited.";
|
||||
"screen_room_member_list_manage_member_unban_title" = "Unban user";
|
||||
"screen_room_member_list_manage_member_user_info" = "See user info";
|
||||
"screen_room_member_list_mode_banned" = "Banned";
|
||||
"screen_room_member_list_mode_members" = "Members";
|
||||
@@ -536,6 +539,7 @@
|
||||
"screen_room_member_list_role_administrator" = "Admin";
|
||||
"screen_room_member_list_role_moderator" = "Moderator";
|
||||
"screen_room_member_list_room_members_header_title" = "Room members";
|
||||
"screen_room_member_list_unbanning_user" = "Unbanning %1$@";
|
||||
"screen_room_message_copied" = "Message copied";
|
||||
"screen_room_no_permission_to_post" = "You do not have permission to post to this room";
|
||||
"screen_room_notification_settings_allow_custom" = "Allow custom setting";
|
||||
@@ -555,6 +559,15 @@
|
||||
"screen_room_reactions_show_more" = "Show more";
|
||||
"screen_room_retry_send_menu_send_again_action" = "Send again";
|
||||
"screen_room_retry_send_menu_title" = "Your message failed to send";
|
||||
"screen_room_roles_and_permissions_admins" = "Admins";
|
||||
"screen_room_roles_and_permissions_member_moderation" = "Member moderation";
|
||||
"screen_room_roles_and_permissions_messages_and_content" = "Messages and content";
|
||||
"screen_room_roles_and_permissions_moderators" = "Moderators";
|
||||
"screen_room_roles_and_permissions_permissions_header" = "Permissions";
|
||||
"screen_room_roles_and_permissions_reset" = "Reset roles and permissions";
|
||||
"screen_room_roles_and_permissions_roles_header" = "Roles";
|
||||
"screen_room_roles_and_permissions_room_details" = "Room details";
|
||||
"screen_room_roles_and_permissions_title" = "Roles and permissions";
|
||||
"screen_room_timeline_add_reaction" = "Add emoji";
|
||||
"screen_room_timeline_less_reactions" = "Show less";
|
||||
"screen_room_typing_many_members_first_component_ios" = "%1$@, %2$@ and ";
|
||||
@@ -726,6 +739,7 @@
|
||||
"screen_room_details_invite_people_title" = "Invite people";
|
||||
"screen_room_details_leave_conversation_title" = "Leave conversation";
|
||||
"screen_room_details_leave_room_title" = "Leave room";
|
||||
"screen_room_details_roles_and_permissions" = "Roles and permissions";
|
||||
"screen_room_details_room_name_label" = "Room name";
|
||||
"screen_room_details_security_title" = "Security";
|
||||
"screen_room_details_topic_title" = "Topic";
|
||||
|
||||
@@ -50,6 +50,7 @@ final class AppSettings {
|
||||
case roomListFiltersEnabled
|
||||
case markAsUnreadEnabled
|
||||
case markAsFavouriteEnabled
|
||||
case roomModerationEnabled
|
||||
}
|
||||
|
||||
private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier
|
||||
@@ -289,6 +290,9 @@ final class AppSettings {
|
||||
@UserPreference(key: UserDefaultsKeys.markAsFavouriteEnabled, defaultValue: false, storageType: .userDefaults(store))
|
||||
var markAsFavouriteEnabled
|
||||
|
||||
@UserPreference(key: UserDefaultsKeys.roomModerationEnabled, defaultValue: false, storageType: .userDefaults(store))
|
||||
var roomModerationEnabled
|
||||
|
||||
#endif
|
||||
|
||||
// MARK: - Shared
|
||||
|
||||
@@ -578,7 +578,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
}
|
||||
|
||||
let params = RoomMembersListScreenCoordinatorParameters(mediaProvider: userSession.mediaProvider,
|
||||
roomProxy: roomProxy)
|
||||
roomProxy: roomProxy,
|
||||
appSettings: appSettings)
|
||||
let coordinator = RoomMembersListScreenCoordinator(parameters: params)
|
||||
|
||||
coordinator.actions
|
||||
|
||||
@@ -1249,6 +1249,8 @@ internal enum L10n {
|
||||
internal static var screenRoomDetailsNotificationModeDefault: String { return L10n.tr("Localizable", "screen_room_details_notification_mode_default") }
|
||||
/// Notifications
|
||||
internal static var screenRoomDetailsNotificationTitle: String { return L10n.tr("Localizable", "screen_room_details_notification_title") }
|
||||
/// Roles and permissions
|
||||
internal static var screenRoomDetailsRolesAndPermissions: String { return L10n.tr("Localizable", "screen_room_details_roles_and_permissions") }
|
||||
/// Room name
|
||||
internal static var screenRoomDetailsRoomNameLabel: String { return L10n.tr("Localizable", "screen_room_details_room_name_label") }
|
||||
/// Security
|
||||
@@ -1283,6 +1285,10 @@ internal enum L10n {
|
||||
internal static var screenRoomMemberDetailsUnblockAlertDescription: String { return L10n.tr("Localizable", "screen_room_member_details_unblock_alert_description") }
|
||||
/// Unblock user
|
||||
internal static var screenRoomMemberDetailsUnblockUser: String { return L10n.tr("Localizable", "screen_room_member_details_unblock_user") }
|
||||
/// Banning %1$@
|
||||
internal static func screenRoomMemberListBanningUser(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "screen_room_member_list_banning_user", String(describing: p1))
|
||||
}
|
||||
/// Plural format key: "%#@COUNT@"
|
||||
internal static func screenRoomMemberListHeaderTitle(_ p1: Int) -> String {
|
||||
return L10n.tr("Localizable", "screen_room_member_list_header_title", p1)
|
||||
@@ -1296,7 +1302,11 @@ internal enum L10n {
|
||||
/// Remove member and ban from joining in the future?
|
||||
internal static var screenRoomMemberListManageMemberRemoveConfirmationTitle: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_remove_confirmation_title") }
|
||||
/// Unban
|
||||
internal static var screenRoomMemberListManageMemberUnban: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_unban") }
|
||||
internal static var screenRoomMemberListManageMemberUnbanAction: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_unban_action") }
|
||||
/// They will be able to join this room again if invited.
|
||||
internal static var screenRoomMemberListManageMemberUnbanMessage: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_unban_message") }
|
||||
/// Unban user
|
||||
internal static var screenRoomMemberListManageMemberUnbanTitle: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_unban_title") }
|
||||
/// See user info
|
||||
internal static var screenRoomMemberListManageMemberUserInfo: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_user_info") }
|
||||
/// Banned
|
||||
@@ -1315,6 +1325,10 @@ internal enum L10n {
|
||||
internal static var screenRoomMemberListRoleModerator: String { return L10n.tr("Localizable", "screen_room_member_list_role_moderator") }
|
||||
/// Room members
|
||||
internal static var screenRoomMemberListRoomMembersHeaderTitle: String { return L10n.tr("Localizable", "screen_room_member_list_room_members_header_title") }
|
||||
/// Unbanning %1$@
|
||||
internal static func screenRoomMemberListUnbanningUser(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "screen_room_member_list_unbanning_user", String(describing: p1))
|
||||
}
|
||||
/// Notify the whole room
|
||||
internal static var screenRoomMentionsAtRoomSubtitle: String { return L10n.tr("Localizable", "screen_room_mentions_at_room_subtitle") }
|
||||
/// Everyone
|
||||
@@ -1361,6 +1375,24 @@ internal enum L10n {
|
||||
internal static var screenRoomRetrySendMenuSendAgainAction: String { return L10n.tr("Localizable", "screen_room_retry_send_menu_send_again_action") }
|
||||
/// Your message failed to send
|
||||
internal static var screenRoomRetrySendMenuTitle: String { return L10n.tr("Localizable", "screen_room_retry_send_menu_title") }
|
||||
/// Admins
|
||||
internal static var screenRoomRolesAndPermissionsAdmins: String { return L10n.tr("Localizable", "screen_room_roles_and_permissions_admins") }
|
||||
/// Member moderation
|
||||
internal static var screenRoomRolesAndPermissionsMemberModeration: String { return L10n.tr("Localizable", "screen_room_roles_and_permissions_member_moderation") }
|
||||
/// Messages and content
|
||||
internal static var screenRoomRolesAndPermissionsMessagesAndContent: String { return L10n.tr("Localizable", "screen_room_roles_and_permissions_messages_and_content") }
|
||||
/// Moderators
|
||||
internal static var screenRoomRolesAndPermissionsModerators: String { return L10n.tr("Localizable", "screen_room_roles_and_permissions_moderators") }
|
||||
/// Permissions
|
||||
internal static var screenRoomRolesAndPermissionsPermissionsHeader: String { return L10n.tr("Localizable", "screen_room_roles_and_permissions_permissions_header") }
|
||||
/// Reset roles and permissions
|
||||
internal static var screenRoomRolesAndPermissionsReset: String { return L10n.tr("Localizable", "screen_room_roles_and_permissions_reset") }
|
||||
/// Roles
|
||||
internal static var screenRoomRolesAndPermissionsRolesHeader: String { return L10n.tr("Localizable", "screen_room_roles_and_permissions_roles_header") }
|
||||
/// Room details
|
||||
internal static var screenRoomRolesAndPermissionsRoomDetails: String { return L10n.tr("Localizable", "screen_room_roles_and_permissions_room_details") }
|
||||
/// Roles and permissions
|
||||
internal static var screenRoomRolesAndPermissionsTitle: String { return L10n.tr("Localizable", "screen_room_roles_and_permissions_title") }
|
||||
/// Add emoji
|
||||
internal static var screenRoomTimelineAddReaction: String { return L10n.tr("Localizable", "screen_room_timeline_add_reaction") }
|
||||
/// Show less
|
||||
|
||||
@@ -1750,6 +1750,11 @@ class RoomMemberProxyMock: RoomMemberProxyProtocol {
|
||||
set(value) { underlyingCanInviteUsers = value }
|
||||
}
|
||||
var underlyingCanInviteUsers: Bool!
|
||||
var canKickUsers: Bool {
|
||||
get { return underlyingCanKickUsers }
|
||||
set(value) { underlyingCanKickUsers = value }
|
||||
}
|
||||
var underlyingCanKickUsers: Bool!
|
||||
var canBanUsers: Bool {
|
||||
get { return underlyingCanBanUsers }
|
||||
set(value) { underlyingCanBanUsers = value }
|
||||
@@ -2338,6 +2343,69 @@ class RoomProxyMock: RoomProxyProtocol {
|
||||
return flagAsFavouriteReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - kickUser
|
||||
|
||||
var kickUserCallsCount = 0
|
||||
var kickUserCalled: Bool {
|
||||
return kickUserCallsCount > 0
|
||||
}
|
||||
var kickUserReceivedUserID: String?
|
||||
var kickUserReceivedInvocations: [String] = []
|
||||
var kickUserReturnValue: Result<Void, RoomProxyError>!
|
||||
var kickUserClosure: ((String) async -> Result<Void, RoomProxyError>)?
|
||||
|
||||
func kickUser(_ userID: String) async -> Result<Void, RoomProxyError> {
|
||||
kickUserCallsCount += 1
|
||||
kickUserReceivedUserID = userID
|
||||
kickUserReceivedInvocations.append(userID)
|
||||
if let kickUserClosure = kickUserClosure {
|
||||
return await kickUserClosure(userID)
|
||||
} else {
|
||||
return kickUserReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - banUser
|
||||
|
||||
var banUserCallsCount = 0
|
||||
var banUserCalled: Bool {
|
||||
return banUserCallsCount > 0
|
||||
}
|
||||
var banUserReceivedUserID: String?
|
||||
var banUserReceivedInvocations: [String] = []
|
||||
var banUserReturnValue: Result<Void, RoomProxyError>!
|
||||
var banUserClosure: ((String) async -> Result<Void, RoomProxyError>)?
|
||||
|
||||
func banUser(_ userID: String) async -> Result<Void, RoomProxyError> {
|
||||
banUserCallsCount += 1
|
||||
banUserReceivedUserID = userID
|
||||
banUserReceivedInvocations.append(userID)
|
||||
if let banUserClosure = banUserClosure {
|
||||
return await banUserClosure(userID)
|
||||
} else {
|
||||
return banUserReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - unbanUser
|
||||
|
||||
var unbanUserCallsCount = 0
|
||||
var unbanUserCalled: Bool {
|
||||
return unbanUserCallsCount > 0
|
||||
}
|
||||
var unbanUserReceivedUserID: String?
|
||||
var unbanUserReceivedInvocations: [String] = []
|
||||
var unbanUserReturnValue: Result<Void, RoomProxyError>!
|
||||
var unbanUserClosure: ((String) async -> Result<Void, RoomProxyError>)?
|
||||
|
||||
func unbanUser(_ userID: String) async -> Result<Void, RoomProxyError> {
|
||||
unbanUserCallsCount += 1
|
||||
unbanUserReceivedUserID = userID
|
||||
unbanUserReceivedInvocations.append(userID)
|
||||
if let unbanUserClosure = unbanUserClosure {
|
||||
return await unbanUserClosure(userID)
|
||||
} else {
|
||||
return unbanUserReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - canUserJoinCall
|
||||
|
||||
var canUserJoinCallUserIDCallsCount = 0
|
||||
@@ -2389,6 +2457,11 @@ class RoomTimelineProviderMock: RoomTimelineProviderProtocol {
|
||||
set(value) { underlyingBackPaginationState = value }
|
||||
}
|
||||
var underlyingBackPaginationState: BackPaginationStatus!
|
||||
var membershipChangePublisher: AnyPublisher<Void, Never> {
|
||||
get { return underlyingMembershipChangePublisher }
|
||||
set(value) { underlyingMembershipChangePublisher = value }
|
||||
}
|
||||
var underlyingMembershipChangePublisher: AnyPublisher<Void, Never>!
|
||||
|
||||
}
|
||||
class SecureBackupControllerMock: SecureBackupControllerProtocol {
|
||||
|
||||
@@ -27,6 +27,7 @@ struct RoomMemberProxyMockConfiguration {
|
||||
var powerLevel = 0
|
||||
var role = RoomMemberRole.user
|
||||
var canInviteUsers = false
|
||||
var canKickUsers = false
|
||||
var canBanUsers = false
|
||||
var canSendStateEvent: (StateEventType) -> Bool = { _ in true }
|
||||
}
|
||||
@@ -43,6 +44,7 @@ extension RoomMemberProxyMock {
|
||||
powerLevel = configuration.powerLevel
|
||||
role = configuration.role
|
||||
canInviteUsers = configuration.canInviteUsers
|
||||
canKickUsers = configuration.canKickUsers
|
||||
canBanUsers = configuration.canBanUsers
|
||||
canSendStateEventTypeClosure = configuration.canSendStateEvent
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
struct RoomProxyMockConfiguration {
|
||||
var id = UUID().uuidString
|
||||
var name: String?
|
||||
@@ -33,6 +34,10 @@ struct RoomProxyMockConfiguration {
|
||||
let mock = TimelineProxyMock()
|
||||
mock.underlyingActions = Empty(completeImmediately: false).eraseToAnyPublisher()
|
||||
mock.timelineStartReached = false
|
||||
|
||||
let timelineProvider = RoomTimelineProviderMock()
|
||||
timelineProvider.underlyingMembershipChangePublisher = PassthroughSubject().eraseToAnyPublisher()
|
||||
mock.underlyingTimelineProvider = timelineProvider
|
||||
return mock
|
||||
}()
|
||||
|
||||
@@ -45,6 +50,7 @@ struct RoomProxyMockConfiguration {
|
||||
}
|
||||
|
||||
extension RoomProxyMock {
|
||||
@MainActor
|
||||
convenience init(with configuration: RoomProxyMockConfiguration) {
|
||||
self.init()
|
||||
|
||||
@@ -84,6 +90,10 @@ extension RoomProxyMock {
|
||||
underlyingIsFavourite = false
|
||||
flagAsFavouriteReturnValue = .success(())
|
||||
|
||||
kickUserReturnValue = .success(())
|
||||
banUserReturnValue = .success(())
|
||||
unbanUserReturnValue = .success(())
|
||||
|
||||
let widgetDriver = ElementCallWidgetDriverMock()
|
||||
widgetDriver.underlyingMessagePublisher = .init()
|
||||
widgetDriver.underlyingActions = PassthroughSubject().eraseToAnyPublisher()
|
||||
|
||||
+3
-1
@@ -20,6 +20,7 @@ import SwiftUI
|
||||
struct RoomMembersListScreenCoordinatorParameters {
|
||||
let mediaProvider: MediaProviderProtocol
|
||||
let roomProxy: RoomProxyProtocol
|
||||
let appSettings: AppSettings
|
||||
}
|
||||
|
||||
enum RoomMembersListScreenCoordinatorAction {
|
||||
@@ -40,7 +41,8 @@ final class RoomMembersListScreenCoordinator: CoordinatorProtocol {
|
||||
init(parameters: RoomMembersListScreenCoordinatorParameters) {
|
||||
viewModel = RoomMembersListScreenViewModel(roomProxy: parameters.roomProxy,
|
||||
mediaProvider: parameters.mediaProvider,
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
appSettings: parameters.appSettings)
|
||||
}
|
||||
|
||||
func start() {
|
||||
|
||||
@@ -38,6 +38,7 @@ struct RoomMembersListScreenViewState: BindableState {
|
||||
var bannedMembersCount: Int { bannedMembers.count }
|
||||
|
||||
var canInviteUsers = false
|
||||
var canKickUsers = false
|
||||
var canBanUsers = false
|
||||
|
||||
var bindings: RoomMembersListScreenViewStateBindings
|
||||
@@ -74,16 +75,26 @@ struct RoomMembersListScreenViewStateBindings {
|
||||
var searchQuery = ""
|
||||
/// The current mode the screen is in.
|
||||
var mode: RoomMembersListScreenMode = .members
|
||||
/// A selected member to kick, ban, promote etc.
|
||||
var memberToManage: RoomMemberDetails?
|
||||
|
||||
/// Information describing the currently displayed alert.
|
||||
var alertInfo: AlertInfo<RoomDetailsScreenErrorType>?
|
||||
var alertInfo: AlertInfo<RoomMembersListScreenAlertType>?
|
||||
}
|
||||
|
||||
enum RoomMembersListScreenViewAction {
|
||||
case selectMember(id: String)
|
||||
case selectMember(RoomMemberDetails)
|
||||
case showMemberDetails(RoomMemberDetails)
|
||||
case kickMember(RoomMemberDetails)
|
||||
case banMember(RoomMemberDetails)
|
||||
case unbanMember(RoomMemberDetails)
|
||||
case invite
|
||||
}
|
||||
|
||||
enum RoomMembersListScreenAlertType: Hashable {
|
||||
case unbanConfirmation(RoomMemberDetails)
|
||||
}
|
||||
|
||||
private extension RoomMemberDetails {
|
||||
func matches(searchQuery: String) -> Bool {
|
||||
guard !searchQuery.isEmpty else {
|
||||
|
||||
+112
-8
@@ -22,6 +22,7 @@ typealias RoomMembersListScreenViewModelType = StateStoreViewModel<RoomMembersLi
|
||||
class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMembersListScreenViewModelProtocol {
|
||||
private let roomProxy: RoomProxyProtocol
|
||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||
private let appSettings: AppSettings
|
||||
|
||||
private var members: [RoomMemberProxyProtocol] = []
|
||||
|
||||
@@ -34,9 +35,11 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
|
||||
init(initialMode: RoomMembersListScreenMode = .members,
|
||||
roomProxy: RoomProxyProtocol,
|
||||
mediaProvider: MediaProviderProtocol,
|
||||
userIndicatorController: UserIndicatorControllerProtocol) {
|
||||
userIndicatorController: UserIndicatorControllerProtocol,
|
||||
appSettings: AppSettings) {
|
||||
self.roomProxy = roomProxy
|
||||
self.userIndicatorController = userIndicatorController
|
||||
self.appSettings = appSettings
|
||||
|
||||
super.init(initialViewState: .init(joinedMembersCount: roomProxy.joinedMembersCount,
|
||||
bindings: .init(mode: initialMode)),
|
||||
@@ -49,12 +52,16 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
|
||||
|
||||
override func process(viewAction: RoomMembersListScreenViewAction) {
|
||||
switch viewAction {
|
||||
case .selectMember(let id):
|
||||
guard let member = members.first(where: { $0.userID == id }) else {
|
||||
MXLog.error("Selected member \(id) not found")
|
||||
return
|
||||
}
|
||||
actionsSubject.send(.selectMember(member))
|
||||
case .selectMember(let member):
|
||||
selectMember(member)
|
||||
case .showMemberDetails(let member):
|
||||
showMemberDetails(member)
|
||||
case .kickMember(let member):
|
||||
Task { await kickMember(member) }
|
||||
case .banMember(let member):
|
||||
Task { await banMember(member) }
|
||||
case .unbanMember(let member):
|
||||
Task { await unbanMember(member) }
|
||||
case .invite:
|
||||
actionsSubject.send(.invite)
|
||||
}
|
||||
@@ -64,7 +71,7 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
|
||||
hideLoader()
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
// MARK: - Members
|
||||
|
||||
private func setupMembers() {
|
||||
Task {
|
||||
@@ -80,11 +87,17 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
|
||||
self?.updateState(members: members)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
roomProxy.timeline.timelineProvider.membershipChangePublisher.sink { [roomProxy] _ in
|
||||
Task { await roomProxy.updateMembers() }
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private func updateState(members: [RoomMemberProxyProtocol]) {
|
||||
Task {
|
||||
showLoader()
|
||||
|
||||
let members = members.sorted()
|
||||
let roomMembersDetails = await buildMembersDetails(members: members)
|
||||
self.members = members
|
||||
@@ -95,8 +108,13 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
|
||||
bindings: state.bindings)
|
||||
if let accountOwner = roomMembersDetails.accountOwner {
|
||||
self.state.canInviteUsers = accountOwner.canInviteUsers
|
||||
self.state.canKickUsers = accountOwner.canKickUsers
|
||||
self.state.canBanUsers = accountOwner.canBanUsers
|
||||
}
|
||||
if state.bindings.mode == .banned, roomMembersDetails.bannedMembers.isEmpty {
|
||||
state.bindings.mode = .members
|
||||
}
|
||||
|
||||
hideLoader()
|
||||
}
|
||||
}
|
||||
@@ -134,6 +152,76 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
|
||||
.value
|
||||
}
|
||||
|
||||
private func selectMember(_ member: RoomMemberDetails) {
|
||||
if appSettings.roomModerationEnabled, state.canKickUsers || state.canBanUsers {
|
||||
if member.isBanned {
|
||||
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) { })
|
||||
} else {
|
||||
state.bindings.memberToManage = member
|
||||
}
|
||||
} else {
|
||||
showMemberDetails(member)
|
||||
}
|
||||
}
|
||||
|
||||
private func showMemberDetails(_ member: RoomMemberDetails) {
|
||||
guard let member = members.first(where: { $0.userID == member.id }) else {
|
||||
MXLog.error("Selected member \(member.id) not found")
|
||||
return
|
||||
}
|
||||
actionsSubject.send(.selectMember(member))
|
||||
state.bindings.memberToManage = nil
|
||||
}
|
||||
|
||||
// MARK: - Member Management
|
||||
|
||||
private func kickMember(_ member: RoomMemberDetails) async {
|
||||
let indicatorTitle = L10n.screenRoomMemberListRemovingUser(member.name ?? member.id)
|
||||
showManageMemberIndicator(title: indicatorTitle)
|
||||
|
||||
switch await roomProxy.kickUser(member.id) {
|
||||
case .success:
|
||||
state.bindings.memberToManage = nil
|
||||
hideManageMemberIndicator(title: indicatorTitle)
|
||||
case .failure:
|
||||
showManageMemberFailure(title: indicatorTitle)
|
||||
}
|
||||
}
|
||||
|
||||
private func banMember(_ member: RoomMemberDetails) async {
|
||||
let indicatorTitle = L10n.screenRoomMemberListBanningUser(member.name ?? member.id)
|
||||
showManageMemberIndicator(title: indicatorTitle)
|
||||
|
||||
switch await roomProxy.banUser(member.id) {
|
||||
case .success:
|
||||
state.bindings.memberToManage = nil
|
||||
hideManageMemberIndicator(title: indicatorTitle)
|
||||
case .failure:
|
||||
showManageMemberFailure(title: indicatorTitle)
|
||||
}
|
||||
}
|
||||
|
||||
private func unbanMember(_ member: RoomMemberDetails) async {
|
||||
let indicatorTitle = L10n.screenRoomMemberListUnbanningUser(member.name ?? member.id)
|
||||
showManageMemberIndicator(title: indicatorTitle)
|
||||
|
||||
switch await roomProxy.unbanUser(member.id) {
|
||||
case .success:
|
||||
state.bindings.memberToManage = nil
|
||||
hideManageMemberIndicator(title: indicatorTitle)
|
||||
case .failure:
|
||||
showManageMemberFailure(title: indicatorTitle)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Indicators
|
||||
|
||||
private let userIndicatorID = UUID().uuidString
|
||||
|
||||
private func showLoader() {
|
||||
@@ -147,6 +235,22 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
|
||||
private func hideLoader() {
|
||||
userIndicatorController.retractIndicatorWithId(userIndicatorID)
|
||||
}
|
||||
|
||||
private func showManageMemberIndicator(title: String) {
|
||||
userIndicatorController.submitIndicator(UserIndicator(id: title,
|
||||
type: .toast(progress: .indeterminate),
|
||||
title: title,
|
||||
persistent: true))
|
||||
}
|
||||
|
||||
private func hideManageMemberIndicator(title: String) {
|
||||
userIndicatorController.retractIndicatorWithId(title)
|
||||
}
|
||||
|
||||
private func showManageMemberFailure(title: String) {
|
||||
userIndicatorController.retractIndicatorWithId(title)
|
||||
userIndicatorController.submitIndicator(UserIndicator(title: L10n.commonFailed, iconName: "xmark"))
|
||||
}
|
||||
}
|
||||
|
||||
private struct RoomMembersDetails {
|
||||
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
//
|
||||
// Copyright 2024 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Compound
|
||||
import SwiftUI
|
||||
|
||||
struct RoomMembersListManageMemberSheet: View {
|
||||
let member: RoomMemberDetails
|
||||
let context: RoomMembersListScreenViewModel.Context
|
||||
|
||||
@State private var isPresentingRemoveConfirmation = false
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
AvatarHeaderView(member: member,
|
||||
avatarSize: .user(on: .memberDetails),
|
||||
imageProvider: context.imageProvider) {
|
||||
EmptyView()
|
||||
}
|
||||
|
||||
Section {
|
||||
ListRow(label: .default(title: L10n.screenRoomMemberListManageMemberUserInfo,
|
||||
icon: \.info),
|
||||
kind: .button {
|
||||
context.send(viewAction: .showMemberDetails(member))
|
||||
})
|
||||
|
||||
if !member.isBanned {
|
||||
ListRow(label: .default(title: L10n.screenRoomMemberListManageMemberRemove,
|
||||
icon: \.block,
|
||||
role: .destructive),
|
||||
kind: .button {
|
||||
isPresentingRemoveConfirmation = true
|
||||
})
|
||||
} else {
|
||||
// Theoretically we shouldn't reach this branch but just in case we do.
|
||||
ListRow(label: .default(title: L10n.screenRoomMemberListManageMemberUnbanAction,
|
||||
icon: \.block,
|
||||
role: .destructive),
|
||||
kind: .button {
|
||||
context.send(viewAction: .unbanMember(member))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
.compoundList()
|
||||
.scrollBounceBehavior(.basedOnSize)
|
||||
.presentationDragIndicator(.visible)
|
||||
.presentationDetents([.large, .fraction(0.5)]) // TODO: Use the ideal height somehow?
|
||||
.confirmationDialog(L10n.screenRoomMemberListManageMemberRemoveConfirmationTitle,
|
||||
isPresented: $isPresentingRemoveConfirmation,
|
||||
titleVisibility: .visible) {
|
||||
if context.viewState.canKickUsers {
|
||||
Button(L10n.screenRoomMemberListManageMemberRemoveConfirmationKick) {
|
||||
context.send(viewAction: .kickMember(member))
|
||||
}
|
||||
}
|
||||
if context.viewState.canBanUsers {
|
||||
Button(L10n.screenRoomMemberListManageMemberRemoveConfirmationBan, role: .destructive) {
|
||||
context.send(viewAction: .banMember(member))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RoomMembersListManageMemberSheet_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = RoomMembersListScreenViewModel.mock
|
||||
|
||||
static var previews: some View {
|
||||
RoomMembersListManageMemberSheet(member: .init(withProxy: RoomMemberProxyMock.mockDan),
|
||||
context: viewModel.context)
|
||||
.previewDisplayName("Joined")
|
||||
|
||||
RoomMembersListManageMemberSheet(member: .init(withProxy: RoomMemberProxyMock.mockBanned[3]),
|
||||
context: viewModel.context)
|
||||
.previewDisplayName("Banned")
|
||||
}
|
||||
}
|
||||
|
||||
struct RoomMembersListManageMemberSheetLive_Previews: PreviewProvider {
|
||||
static let viewModel = RoomMembersListScreenViewModel.mock
|
||||
|
||||
static var previews: some View {
|
||||
Color.clear
|
||||
.sheet(isPresented: .constant(true)) {
|
||||
RoomMembersListManageMemberSheet(member: .init(withProxy: RoomMemberProxyMock.mockDan),
|
||||
context: viewModel.context)
|
||||
}
|
||||
.previewDisplayName("Sheet")
|
||||
}
|
||||
}
|
||||
|
||||
private extension RoomMembersListScreenViewModel {
|
||||
static var mock: RoomMembersListScreenViewModel {
|
||||
RoomMembersListScreenViewModel(initialMode: .members,
|
||||
roomProxy: RoomProxyMock(with: .init()),
|
||||
mediaProvider: MockMediaProvider(),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
}
|
||||
}
|
||||
@@ -43,14 +43,14 @@ struct RoomMembersListScreen: View {
|
||||
}
|
||||
.searchable(text: $context.searchQuery, placement: .navigationBarDrawer(displayMode: .always))
|
||||
.compoundSearchField()
|
||||
.autocorrectionDisabled()
|
||||
.background(Color.compound.bgCanvasDefault.ignoresSafeArea())
|
||||
.navigationTitle(L10n.commonPeople)
|
||||
.alert(item: $context.alertInfo)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
inviteButton
|
||||
}
|
||||
.sheet(item: $context.memberToManage) {
|
||||
RoomMembersListManageMemberSheet(member: $0, context: context)
|
||||
}
|
||||
.alert(item: $context.alertInfo)
|
||||
.toolbar { toolbar }
|
||||
.track(screen: .RoomMembers)
|
||||
}
|
||||
|
||||
@@ -91,13 +91,13 @@ struct RoomMembersListScreen: View {
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var inviteButton: some View {
|
||||
if context.viewState.canInviteUsers {
|
||||
Button {
|
||||
context.send(viewAction: .invite)
|
||||
} label: {
|
||||
Text(L10n.actionInvite)
|
||||
@ToolbarContentBuilder
|
||||
private var toolbar: some ToolbarContent {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
if context.viewState.canInviteUsers {
|
||||
Button(L10n.actionInvite) {
|
||||
context.send(viewAction: .invite)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,6 +162,7 @@ struct RoomMembersListScreen_Previews: PreviewProvider, TestablePreview {
|
||||
return RoomMembersListScreenViewModel(initialMode: initialMode,
|
||||
roomProxy: RoomProxyMock(with: .init(name: "Some room", members: members)),
|
||||
mediaProvider: MockMediaProvider(),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -22,7 +22,7 @@ struct RoomMembersListScreenMemberCell: View {
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
context.send(viewAction: .selectMember(id: member.id))
|
||||
context.send(viewAction: .selectMember(member))
|
||||
} label: {
|
||||
HStack(spacing: 8) {
|
||||
LoadableAvatarImage(url: avatarURL,
|
||||
@@ -109,7 +109,8 @@ struct RoomMembersListMemberCell_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = RoomMembersListScreenViewModel(roomProxy: RoomProxyMock(with: .init(name: "Some room",
|
||||
members: members)),
|
||||
mediaProvider: MockMediaProvider(),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
static var previews: some View {
|
||||
VStack(spacing: 12) {
|
||||
Section("Invited/Joined") {
|
||||
|
||||
+1
@@ -54,6 +54,7 @@ protocol DeveloperOptionsProtocol: AnyObject {
|
||||
|
||||
var markAsUnreadEnabled: Bool { get set }
|
||||
var markAsFavouriteEnabled: Bool { get set }
|
||||
var roomModerationEnabled: Bool { get set }
|
||||
|
||||
var elementCallBaseURL: URL { get set }
|
||||
}
|
||||
|
||||
+4
-1
@@ -32,7 +32,10 @@ struct DeveloperOptionsScreen: View {
|
||||
}
|
||||
}
|
||||
|
||||
Section("Timeline") {
|
||||
Section("Room") {
|
||||
Toggle(isOn: $context.roomModerationEnabled) {
|
||||
Text("Moderation")
|
||||
}
|
||||
Toggle(isOn: $context.shouldCollapseRoomStateEvents) {
|
||||
Text("Collapse room state events")
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
struct RoomMemberDetails: Identifiable, Equatable {
|
||||
struct RoomMemberDetails: Identifiable, Hashable {
|
||||
let id: String
|
||||
let name: String?
|
||||
let avatarURL: URL?
|
||||
|
||||
@@ -46,6 +46,7 @@ final class RoomMemberProxy: RoomMemberProxyProtocol {
|
||||
lazy var powerLevel = Int(member.powerLevel())
|
||||
lazy var role = member.suggestedRoleForPowerLevel()
|
||||
lazy var canInviteUsers = member.canInvite()
|
||||
lazy var canKickUsers = member.canKick()
|
||||
lazy var canBanUsers = member.canBan()
|
||||
|
||||
func canSendStateEvent(type: StateEventType) -> Bool {
|
||||
|
||||
@@ -34,6 +34,7 @@ protocol RoomMemberProxyProtocol: AnyObject {
|
||||
var powerLevel: Int { get }
|
||||
var role: RoomMemberRole { get }
|
||||
var canInviteUsers: Bool { get }
|
||||
var canKickUsers: Bool { get }
|
||||
var canBanUsers: Bool { get }
|
||||
|
||||
func ignoreUser() async -> Result<Void, RoomMemberProxyError>
|
||||
|
||||
@@ -416,6 +416,38 @@ class RoomProxy: RoomProxyProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Moderation
|
||||
|
||||
func kickUser(_ userID: String) async -> Result<Void, RoomProxyError> {
|
||||
do {
|
||||
try await room.kickUser(userId: userID, reason: nil)
|
||||
return .success(())
|
||||
} catch {
|
||||
MXLog.error("Failed kicking \(userID) with error: \(error)")
|
||||
return .failure(.failedModeration)
|
||||
}
|
||||
}
|
||||
|
||||
func banUser(_ userID: String) async -> Result<Void, RoomProxyError> {
|
||||
do {
|
||||
try await room.banUser(userId: userID, reason: nil)
|
||||
return .success(())
|
||||
} catch {
|
||||
MXLog.error("Failed banning \(userID) with error: \(error)")
|
||||
return .failure(.failedModeration)
|
||||
}
|
||||
}
|
||||
|
||||
func unbanUser(_ userID: String) async -> Result<Void, RoomProxyError> {
|
||||
do {
|
||||
try await room.unbanUser(userId: userID, reason: nil)
|
||||
return .success(())
|
||||
} catch {
|
||||
MXLog.error("Failed unbanning \(userID) with error: \(error)")
|
||||
return .failure(.failedModeration)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Element Call
|
||||
|
||||
func canUserJoinCall(userID: String) async -> Result<Bool, RoomProxyError> {
|
||||
|
||||
@@ -36,6 +36,7 @@ enum RoomProxyError: Error, Equatable {
|
||||
case failedMarkingAsRead
|
||||
case failedSendingTypingNotice
|
||||
case failedFlaggingAsFavourite
|
||||
case failedModeration
|
||||
}
|
||||
|
||||
enum RoomProxyAction {
|
||||
@@ -120,6 +121,12 @@ protocol RoomProxyProtocol {
|
||||
|
||||
func flagAsFavourite(_ isFavourite: Bool) async -> Result<Void, RoomProxyError>
|
||||
|
||||
// MARK: - Moderation
|
||||
|
||||
func kickUser(_ userID: String) async -> Result<Void, RoomProxyError>
|
||||
func banUser(_ userID: String) async -> Result<Void, RoomProxyError>
|
||||
func unbanUser(_ userID: String) async -> Result<Void, RoomProxyError>
|
||||
|
||||
// MARK: - Element Call
|
||||
|
||||
func canUserJoinCall(userID: String) async -> Result<Bool, RoomProxyError>
|
||||
|
||||
@@ -38,6 +38,12 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol {
|
||||
.map { _, _ in () }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
private let membershipChangeSubject = PassthroughSubject<Void, Never>()
|
||||
var membershipChangePublisher: AnyPublisher<Void, Never> {
|
||||
membershipChangeSubject
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(currentItems: [TimelineItem],
|
||||
updatePublisher: AnyPublisher<[TimelineDiff], Never>,
|
||||
@@ -99,7 +105,13 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol {
|
||||
|
||||
MXLog.verbose("Append \(items.map(\.debugIdentifier))")
|
||||
for (index, item) in items.enumerated() {
|
||||
changes.append(.insert(offset: Int(itemProxies.count) + index, element: TimelineItemProxy(item: item), associatedWith: nil))
|
||||
let itemProxy = TimelineItemProxy(item: item)
|
||||
|
||||
if itemProxy.isMembershipChange {
|
||||
membershipChangeSubject.send(())
|
||||
}
|
||||
|
||||
changes.append(.insert(offset: Int(itemProxies.count) + index, element: itemProxy, associatedWith: nil))
|
||||
}
|
||||
case .clear:
|
||||
MXLog.verbose("Clear all items")
|
||||
@@ -129,6 +141,11 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol {
|
||||
|
||||
MXLog.verbose("Push Back \(item.debugIdentifier)")
|
||||
let itemProxy = TimelineItemProxy(item: item)
|
||||
|
||||
if itemProxy.isMembershipChange {
|
||||
membershipChangeSubject.send(())
|
||||
}
|
||||
|
||||
changes.append(.insert(offset: Int(itemProxies.count), element: itemProxy, associatedWith: nil))
|
||||
case .pushFront:
|
||||
guard let item = diff.pushFront() else { fatalError() }
|
||||
@@ -197,6 +214,20 @@ private extension TimelineItemProxy {
|
||||
return .unknown(timelineID: String(item.uniqueId()))
|
||||
}
|
||||
}
|
||||
|
||||
var isMembershipChange: Bool {
|
||||
switch self {
|
||||
case .event(let eventTimelineItemProxy):
|
||||
switch eventTimelineItemProxy.content.kind() {
|
||||
case .roomMembership:
|
||||
true
|
||||
default:
|
||||
false
|
||||
}
|
||||
case .virtual, .unknown:
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension VirtualTimelineItem {
|
||||
|
||||
@@ -22,7 +22,14 @@ import MatrixRustSDK
|
||||
@MainActor
|
||||
// sourcery: AutoMockable
|
||||
protocol RoomTimelineProviderProtocol {
|
||||
/// A publisher that signals when ``itemProxies`` or ``backPaginationState`` are changed.
|
||||
var updatePublisher: AnyPublisher<Void, Never> { get }
|
||||
/// The current set of items in the timeline.
|
||||
var itemProxies: [TimelineItemProxy] { get }
|
||||
/// Whether the timeline is back paginating or not (or has reached the start of the room).
|
||||
var backPaginationState: BackPaginationStatus { get }
|
||||
/// A publisher that signals when changes to the room's membership have occurred through `/sync`.
|
||||
///
|
||||
/// This is temporary and will be replace by a subscription on the room itself.
|
||||
var membershipChangePublisher: AnyPublisher<Void, Never> { get }
|
||||
}
|
||||
|
||||
@@ -699,14 +699,16 @@ class MockScreen: Identifiable {
|
||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||
let members: [RoomMemberProxyMock] = [.mockAlice, .mockBob, .mockCharlie]
|
||||
let coordinator = RoomMembersListScreenCoordinator(parameters: .init(mediaProvider: MockMediaProvider(),
|
||||
roomProxy: RoomProxyMock(with: .init(name: "test", members: members))))
|
||||
roomProxy: RoomProxyMock(with: .init(name: "test", members: members)),
|
||||
appSettings: ServiceLocator.shared.settings))
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
return navigationStackCoordinator
|
||||
case .roomMembersListScreenPendingInvites:
|
||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||
let members: [RoomMemberProxyMock] = [.mockInvitedAlice, .mockBob, .mockCharlie]
|
||||
let coordinator = RoomMembersListScreenCoordinator(parameters: .init(mediaProvider: MockMediaProvider(),
|
||||
roomProxy: RoomProxyMock(with: .init(name: "test", members: members))))
|
||||
roomProxy: RoomProxyMock(with: .init(name: "test", members: members)),
|
||||
appSettings: ServiceLocator.shared.settings))
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
return navigationStackCoordinator
|
||||
case .roomNotificationSettingsDefaultSetting:
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7448e79e299c5ee48583c1e4b93fa9fe337adbf316f835097c06e4240779ccc4
|
||||
size 96198
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7bb98cc5979ae429fbf7de2858e4dd957780c710574727746407de518c028b7b
|
||||
size 140127
|
||||
@@ -19,6 +19,7 @@ import XCTest
|
||||
|
||||
@testable import ElementX
|
||||
|
||||
@MainActor
|
||||
final class CompletionSuggestionServiceTests: XCTestCase {
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import XCTest
|
||||
@MainActor
|
||||
class RoomMembersListScreenViewModelTests: XCTestCase {
|
||||
var viewModel: RoomMembersListScreenViewModel!
|
||||
var roomProxy: RoomProxyMock!
|
||||
|
||||
var context: RoomMembersListScreenViewModel.Context {
|
||||
viewModel.context
|
||||
@@ -125,9 +126,50 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
|
||||
XCTAssertEqual(viewModel.state.visibleJoinedMembers.count, 0)
|
||||
}
|
||||
|
||||
func testKickMember() async throws {
|
||||
setup(with: .allMembers)
|
||||
var deferred = deferFulfillment(context.$viewState) { !$0.visibleJoinedMembers.isEmpty }
|
||||
try await deferred.fulfill()
|
||||
|
||||
context.send(viewAction: .kickMember(viewModel.state.visibleJoinedMembers[0]))
|
||||
|
||||
// Calling the mock won't actually change any view state, so sleep instead.
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
|
||||
XCTAssert(roomProxy.kickUserCalled)
|
||||
}
|
||||
|
||||
func testBanMember() async throws {
|
||||
setup(with: .allMembers)
|
||||
var deferred = deferFulfillment(context.$viewState) { !$0.visibleJoinedMembers.isEmpty }
|
||||
try await deferred.fulfill()
|
||||
|
||||
context.send(viewAction: .banMember(viewModel.state.visibleJoinedMembers[0]))
|
||||
|
||||
// Calling the mock won't actually change any view state, so sleep instead.
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
|
||||
XCTAssert(roomProxy.banUserCalled)
|
||||
}
|
||||
|
||||
func testUnbanMember() async throws {
|
||||
setup(with: .allMembers)
|
||||
var deferred = deferFulfillment(context.$viewState) { !$0.visibleJoinedMembers.isEmpty }
|
||||
try await deferred.fulfill()
|
||||
|
||||
context.send(viewAction: .unbanMember(viewModel.state.visibleJoinedMembers[0]))
|
||||
|
||||
// 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]) {
|
||||
viewModel = .init(roomProxy: RoomProxyMock(with: .init(name: "test", members: members)),
|
||||
roomProxy = RoomProxyMock(with: .init(name: "test", members: members))
|
||||
viewModel = .init(roomProxy: roomProxy,
|
||||
mediaProvider: MockMediaProvider(),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Add support for kicking and banning room members.
|
||||
Reference in New Issue
Block a user