mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 01:20:12 +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 */; };
|
5D4643E485C179B2F485C519 /* MentionSuggestionItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FD0E68C42CA7DDCD4CAD68D /* MentionSuggestionItemView.swift */; };
|
||||||
5D53AE9342A4C06B704247ED /* MediaLoaderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A02406480C351B8C6E0682C /* MediaLoaderProtocol.swift */; };
|
5D53AE9342A4C06B704247ED /* MediaLoaderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A02406480C351B8C6E0682C /* MediaLoaderProtocol.swift */; };
|
||||||
5D70FAE4D2BF4553AFFFFE41 /* NotificationItemProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F7FE40EF7490A7E09D7BE6 /* NotificationItemProxy.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 */; };
|
5DD85A0FE3D85AEC3C7EFE36 /* DeveloperOptionsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7C7CFA6B2A62A685FF6CE3 /* DeveloperOptionsScreenCoordinator.swift */; };
|
||||||
5E0F2E612718BB4397A6D40A /* TextRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9E785D5137510481733A3E8 /* TextRoomTimelineView.swift */; };
|
5E0F2E612718BB4397A6D40A /* TextRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9E785D5137510481733A3E8 /* TextRoomTimelineView.swift */; };
|
||||||
5EE1D4E316D66943E97FDCF2 /* BloomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7BEB970F500BFB248443FA1 /* BloomView.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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainControllerTests.swift; sourceTree = "<group>"; };
|
||||||
@@ -3817,6 +3819,7 @@
|
|||||||
949B06577E5265373013DDAB /* View */ = {
|
949B06577E5265373013DDAB /* View */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
FC853F9B4FBE039D2C16EC6B /* RoomMembersListManageMemberSheet.swift */,
|
||||||
1B8E176484A89BAC389D4076 /* RoomMembersListScreen.swift */,
|
1B8E176484A89BAC389D4076 /* RoomMembersListScreen.swift */,
|
||||||
CC03209FDE8CE0810617BFFF /* RoomMembersListScreenMemberCell.swift */,
|
CC03209FDE8CE0810617BFFF /* RoomMembersListScreenMemberCell.swift */,
|
||||||
);
|
);
|
||||||
@@ -5882,6 +5885,7 @@
|
|||||||
6448F8D1D3CA4CD27BB4CADD /* RoomMemberProxy.swift in Sources */,
|
6448F8D1D3CA4CD27BB4CADD /* RoomMemberProxy.swift in Sources */,
|
||||||
92D9088B901CEBB1A99ECA4E /* RoomMemberProxyMock.swift in Sources */,
|
92D9088B901CEBB1A99ECA4E /* RoomMemberProxyMock.swift in Sources */,
|
||||||
F118DD449066E594F63C697D /* RoomMemberProxyProtocol.swift in Sources */,
|
F118DD449066E594F63C697D /* RoomMemberProxyProtocol.swift in Sources */,
|
||||||
|
5DD0EF30070DC0A82C5CCD33 /* RoomMembersListManageMemberSheet.swift in Sources */,
|
||||||
C08AAE7563E0722C9383F51C /* RoomMembersListScreen.swift in Sources */,
|
C08AAE7563E0722C9383F51C /* RoomMembersListScreen.swift in Sources */,
|
||||||
1C9BB74711E5F24C77B7FED0 /* RoomMembersListScreenCoordinator.swift in Sources */,
|
1C9BB74711E5F24C77B7FED0 /* RoomMembersListScreenCoordinator.swift in Sources */,
|
||||||
A975D60EA49F6AF73308809F /* RoomMembersListScreenMemberCell.swift in Sources */,
|
A975D60EA49F6AF73308809F /* RoomMembersListScreenMemberCell.swift in Sources */,
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
version = "1.7">
|
version = "1.7">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
buildImplicitDependencies = "YES">
|
buildImplicitDependencies = "YES"
|
||||||
|
runPostActionsOnFailure = "NO">
|
||||||
<BuildActionEntries>
|
<BuildActionEntries>
|
||||||
<BuildActionEntry
|
<BuildActionEntry
|
||||||
buildForTesting = "YES"
|
buildForTesting = "YES"
|
||||||
@@ -29,6 +30,12 @@
|
|||||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
codeCoverageEnabled = "YES"
|
codeCoverageEnabled = "YES"
|
||||||
onlyGenerateCoverageForSpecifiedTargets = "YES">
|
onlyGenerateCoverageForSpecifiedTargets = "YES">
|
||||||
|
<TestPlans>
|
||||||
|
<TestPlanReference
|
||||||
|
default = "YES"
|
||||||
|
reference = "container:PreviewTests/SupportingFiles/PreviewTests.xctestplan">
|
||||||
|
</TestPlanReference>
|
||||||
|
</TestPlans>
|
||||||
<MacroExpansion>
|
<MacroExpansion>
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
@@ -38,6 +45,10 @@
|
|||||||
ReferencedContainer = "container:ElementX.xcodeproj">
|
ReferencedContainer = "container:ElementX.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</MacroExpansion>
|
</MacroExpansion>
|
||||||
|
<Testables>
|
||||||
|
</Testables>
|
||||||
|
<CommandLineArguments>
|
||||||
|
</CommandLineArguments>
|
||||||
<CodeCoverageTargets>
|
<CodeCoverageTargets>
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
@@ -47,12 +58,6 @@
|
|||||||
ReferencedContainer = "container:ElementX.xcodeproj">
|
ReferencedContainer = "container:ElementX.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</CodeCoverageTargets>
|
</CodeCoverageTargets>
|
||||||
<TestPlans>
|
|
||||||
<TestPlanReference
|
|
||||||
reference = "container:PreviewTests/SupportingFiles/PreviewTests.xctestplan"
|
|
||||||
default = "YES">
|
|
||||||
</TestPlanReference>
|
|
||||||
</TestPlans>
|
|
||||||
</TestAction>
|
</TestAction>
|
||||||
<LaunchAction
|
<LaunchAction
|
||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
@@ -73,6 +78,8 @@
|
|||||||
ReferencedContainer = "container:ElementX.xcodeproj">
|
ReferencedContainer = "container:ElementX.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</MacroExpansion>
|
</MacroExpansion>
|
||||||
|
<CommandLineArguments>
|
||||||
|
</CommandLineArguments>
|
||||||
</LaunchAction>
|
</LaunchAction>
|
||||||
<ProfileAction
|
<ProfileAction
|
||||||
buildConfiguration = "Release"
|
buildConfiguration = "Release"
|
||||||
@@ -80,6 +87,8 @@
|
|||||||
savedToolIdentifier = ""
|
savedToolIdentifier = ""
|
||||||
useCustomWorkingDirectory = "NO"
|
useCustomWorkingDirectory = "NO"
|
||||||
debugDocumentVersioning = "YES">
|
debugDocumentVersioning = "YES">
|
||||||
|
<CommandLineArguments>
|
||||||
|
</CommandLineArguments>
|
||||||
<MacroExpansion>
|
<MacroExpansion>
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
|
|||||||
@@ -523,11 +523,14 @@
|
|||||||
"screen_room_member_details_unblock_alert_action" = "Unblock";
|
"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_alert_description" = "You'll be able to see all messages from them again.";
|
||||||
"screen_room_member_details_unblock_user" = "Unblock user";
|
"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" = "Remove member";
|
||||||
"screen_room_member_list_manage_member_remove_confirmation_ban" = "Remove and ban 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_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_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_manage_member_user_info" = "See user info";
|
||||||
"screen_room_member_list_mode_banned" = "Banned";
|
"screen_room_member_list_mode_banned" = "Banned";
|
||||||
"screen_room_member_list_mode_members" = "Members";
|
"screen_room_member_list_mode_members" = "Members";
|
||||||
@@ -536,6 +539,7 @@
|
|||||||
"screen_room_member_list_role_administrator" = "Admin";
|
"screen_room_member_list_role_administrator" = "Admin";
|
||||||
"screen_room_member_list_role_moderator" = "Moderator";
|
"screen_room_member_list_role_moderator" = "Moderator";
|
||||||
"screen_room_member_list_room_members_header_title" = "Room members";
|
"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_message_copied" = "Message copied";
|
||||||
"screen_room_no_permission_to_post" = "You do not have permission to post to this room";
|
"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";
|
"screen_room_notification_settings_allow_custom" = "Allow custom setting";
|
||||||
@@ -555,6 +559,15 @@
|
|||||||
"screen_room_reactions_show_more" = "Show more";
|
"screen_room_reactions_show_more" = "Show more";
|
||||||
"screen_room_retry_send_menu_send_again_action" = "Send again";
|
"screen_room_retry_send_menu_send_again_action" = "Send again";
|
||||||
"screen_room_retry_send_menu_title" = "Your message failed to send";
|
"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_add_reaction" = "Add emoji";
|
||||||
"screen_room_timeline_less_reactions" = "Show less";
|
"screen_room_timeline_less_reactions" = "Show less";
|
||||||
"screen_room_typing_many_members_first_component_ios" = "%1$@, %2$@ and ";
|
"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_invite_people_title" = "Invite people";
|
||||||
"screen_room_details_leave_conversation_title" = "Leave conversation";
|
"screen_room_details_leave_conversation_title" = "Leave conversation";
|
||||||
"screen_room_details_leave_room_title" = "Leave room";
|
"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_room_name_label" = "Room name";
|
||||||
"screen_room_details_security_title" = "Security";
|
"screen_room_details_security_title" = "Security";
|
||||||
"screen_room_details_topic_title" = "Topic";
|
"screen_room_details_topic_title" = "Topic";
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ final class AppSettings {
|
|||||||
case roomListFiltersEnabled
|
case roomListFiltersEnabled
|
||||||
case markAsUnreadEnabled
|
case markAsUnreadEnabled
|
||||||
case markAsFavouriteEnabled
|
case markAsFavouriteEnabled
|
||||||
|
case roomModerationEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier
|
private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier
|
||||||
@@ -289,6 +290,9 @@ final class AppSettings {
|
|||||||
@UserPreference(key: UserDefaultsKeys.markAsFavouriteEnabled, defaultValue: false, storageType: .userDefaults(store))
|
@UserPreference(key: UserDefaultsKeys.markAsFavouriteEnabled, defaultValue: false, storageType: .userDefaults(store))
|
||||||
var markAsFavouriteEnabled
|
var markAsFavouriteEnabled
|
||||||
|
|
||||||
|
@UserPreference(key: UserDefaultsKeys.roomModerationEnabled, defaultValue: false, storageType: .userDefaults(store))
|
||||||
|
var roomModerationEnabled
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// MARK: - Shared
|
// MARK: - Shared
|
||||||
|
|||||||
@@ -578,7 +578,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let params = RoomMembersListScreenCoordinatorParameters(mediaProvider: userSession.mediaProvider,
|
let params = RoomMembersListScreenCoordinatorParameters(mediaProvider: userSession.mediaProvider,
|
||||||
roomProxy: roomProxy)
|
roomProxy: roomProxy,
|
||||||
|
appSettings: appSettings)
|
||||||
let coordinator = RoomMembersListScreenCoordinator(parameters: params)
|
let coordinator = RoomMembersListScreenCoordinator(parameters: params)
|
||||||
|
|
||||||
coordinator.actions
|
coordinator.actions
|
||||||
|
|||||||
@@ -1249,6 +1249,8 @@ internal enum L10n {
|
|||||||
internal static var screenRoomDetailsNotificationModeDefault: String { return L10n.tr("Localizable", "screen_room_details_notification_mode_default") }
|
internal static var screenRoomDetailsNotificationModeDefault: String { return L10n.tr("Localizable", "screen_room_details_notification_mode_default") }
|
||||||
/// Notifications
|
/// Notifications
|
||||||
internal static var screenRoomDetailsNotificationTitle: String { return L10n.tr("Localizable", "screen_room_details_notification_title") }
|
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
|
/// Room name
|
||||||
internal static var screenRoomDetailsRoomNameLabel: String { return L10n.tr("Localizable", "screen_room_details_room_name_label") }
|
internal static var screenRoomDetailsRoomNameLabel: String { return L10n.tr("Localizable", "screen_room_details_room_name_label") }
|
||||||
/// Security
|
/// Security
|
||||||
@@ -1283,6 +1285,10 @@ internal enum L10n {
|
|||||||
internal static var screenRoomMemberDetailsUnblockAlertDescription: String { return L10n.tr("Localizable", "screen_room_member_details_unblock_alert_description") }
|
internal static var screenRoomMemberDetailsUnblockAlertDescription: String { return L10n.tr("Localizable", "screen_room_member_details_unblock_alert_description") }
|
||||||
/// Unblock user
|
/// Unblock user
|
||||||
internal static var screenRoomMemberDetailsUnblockUser: String { return L10n.tr("Localizable", "screen_room_member_details_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@"
|
/// Plural format key: "%#@COUNT@"
|
||||||
internal static func screenRoomMemberListHeaderTitle(_ p1: Int) -> String {
|
internal static func screenRoomMemberListHeaderTitle(_ p1: Int) -> String {
|
||||||
return L10n.tr("Localizable", "screen_room_member_list_header_title", p1)
|
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?
|
/// 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") }
|
internal static var screenRoomMemberListManageMemberRemoveConfirmationTitle: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_remove_confirmation_title") }
|
||||||
/// Unban
|
/// 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
|
/// See user info
|
||||||
internal static var screenRoomMemberListManageMemberUserInfo: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_user_info") }
|
internal static var screenRoomMemberListManageMemberUserInfo: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_user_info") }
|
||||||
/// Banned
|
/// Banned
|
||||||
@@ -1315,6 +1325,10 @@ internal enum L10n {
|
|||||||
internal static var screenRoomMemberListRoleModerator: String { return L10n.tr("Localizable", "screen_room_member_list_role_moderator") }
|
internal static var screenRoomMemberListRoleModerator: String { return L10n.tr("Localizable", "screen_room_member_list_role_moderator") }
|
||||||
/// Room members
|
/// Room members
|
||||||
internal static var screenRoomMemberListRoomMembersHeaderTitle: String { return L10n.tr("Localizable", "screen_room_member_list_room_members_header_title") }
|
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
|
/// Notify the whole room
|
||||||
internal static var screenRoomMentionsAtRoomSubtitle: String { return L10n.tr("Localizable", "screen_room_mentions_at_room_subtitle") }
|
internal static var screenRoomMentionsAtRoomSubtitle: String { return L10n.tr("Localizable", "screen_room_mentions_at_room_subtitle") }
|
||||||
/// Everyone
|
/// 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") }
|
internal static var screenRoomRetrySendMenuSendAgainAction: String { return L10n.tr("Localizable", "screen_room_retry_send_menu_send_again_action") }
|
||||||
/// Your message failed to send
|
/// Your message failed to send
|
||||||
internal static var screenRoomRetrySendMenuTitle: String { return L10n.tr("Localizable", "screen_room_retry_send_menu_title") }
|
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
|
/// Add emoji
|
||||||
internal static var screenRoomTimelineAddReaction: String { return L10n.tr("Localizable", "screen_room_timeline_add_reaction") }
|
internal static var screenRoomTimelineAddReaction: String { return L10n.tr("Localizable", "screen_room_timeline_add_reaction") }
|
||||||
/// Show less
|
/// Show less
|
||||||
|
|||||||
@@ -1750,6 +1750,11 @@ class RoomMemberProxyMock: RoomMemberProxyProtocol {
|
|||||||
set(value) { underlyingCanInviteUsers = value }
|
set(value) { underlyingCanInviteUsers = value }
|
||||||
}
|
}
|
||||||
var underlyingCanInviteUsers: Bool!
|
var underlyingCanInviteUsers: Bool!
|
||||||
|
var canKickUsers: Bool {
|
||||||
|
get { return underlyingCanKickUsers }
|
||||||
|
set(value) { underlyingCanKickUsers = value }
|
||||||
|
}
|
||||||
|
var underlyingCanKickUsers: Bool!
|
||||||
var canBanUsers: Bool {
|
var canBanUsers: Bool {
|
||||||
get { return underlyingCanBanUsers }
|
get { return underlyingCanBanUsers }
|
||||||
set(value) { underlyingCanBanUsers = value }
|
set(value) { underlyingCanBanUsers = value }
|
||||||
@@ -2338,6 +2343,69 @@ class RoomProxyMock: RoomProxyProtocol {
|
|||||||
return flagAsFavouriteReturnValue
|
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
|
//MARK: - canUserJoinCall
|
||||||
|
|
||||||
var canUserJoinCallUserIDCallsCount = 0
|
var canUserJoinCallUserIDCallsCount = 0
|
||||||
@@ -2389,6 +2457,11 @@ class RoomTimelineProviderMock: RoomTimelineProviderProtocol {
|
|||||||
set(value) { underlyingBackPaginationState = value }
|
set(value) { underlyingBackPaginationState = value }
|
||||||
}
|
}
|
||||||
var underlyingBackPaginationState: BackPaginationStatus!
|
var underlyingBackPaginationState: BackPaginationStatus!
|
||||||
|
var membershipChangePublisher: AnyPublisher<Void, Never> {
|
||||||
|
get { return underlyingMembershipChangePublisher }
|
||||||
|
set(value) { underlyingMembershipChangePublisher = value }
|
||||||
|
}
|
||||||
|
var underlyingMembershipChangePublisher: AnyPublisher<Void, Never>!
|
||||||
|
|
||||||
}
|
}
|
||||||
class SecureBackupControllerMock: SecureBackupControllerProtocol {
|
class SecureBackupControllerMock: SecureBackupControllerProtocol {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ struct RoomMemberProxyMockConfiguration {
|
|||||||
var powerLevel = 0
|
var powerLevel = 0
|
||||||
var role = RoomMemberRole.user
|
var role = RoomMemberRole.user
|
||||||
var canInviteUsers = false
|
var canInviteUsers = false
|
||||||
|
var canKickUsers = false
|
||||||
var canBanUsers = false
|
var canBanUsers = false
|
||||||
var canSendStateEvent: (StateEventType) -> Bool = { _ in true }
|
var canSendStateEvent: (StateEventType) -> Bool = { _ in true }
|
||||||
}
|
}
|
||||||
@@ -43,6 +44,7 @@ extension RoomMemberProxyMock {
|
|||||||
powerLevel = configuration.powerLevel
|
powerLevel = configuration.powerLevel
|
||||||
role = configuration.role
|
role = configuration.role
|
||||||
canInviteUsers = configuration.canInviteUsers
|
canInviteUsers = configuration.canInviteUsers
|
||||||
|
canKickUsers = configuration.canKickUsers
|
||||||
canBanUsers = configuration.canBanUsers
|
canBanUsers = configuration.canBanUsers
|
||||||
canSendStateEventTypeClosure = configuration.canSendStateEvent
|
canSendStateEventTypeClosure = configuration.canSendStateEvent
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
import Combine
|
import Combine
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
@MainActor
|
||||||
struct RoomProxyMockConfiguration {
|
struct RoomProxyMockConfiguration {
|
||||||
var id = UUID().uuidString
|
var id = UUID().uuidString
|
||||||
var name: String?
|
var name: String?
|
||||||
@@ -33,6 +34,10 @@ struct RoomProxyMockConfiguration {
|
|||||||
let mock = TimelineProxyMock()
|
let mock = TimelineProxyMock()
|
||||||
mock.underlyingActions = Empty(completeImmediately: false).eraseToAnyPublisher()
|
mock.underlyingActions = Empty(completeImmediately: false).eraseToAnyPublisher()
|
||||||
mock.timelineStartReached = false
|
mock.timelineStartReached = false
|
||||||
|
|
||||||
|
let timelineProvider = RoomTimelineProviderMock()
|
||||||
|
timelineProvider.underlyingMembershipChangePublisher = PassthroughSubject().eraseToAnyPublisher()
|
||||||
|
mock.underlyingTimelineProvider = timelineProvider
|
||||||
return mock
|
return mock
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -45,6 +50,7 @@ struct RoomProxyMockConfiguration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension RoomProxyMock {
|
extension RoomProxyMock {
|
||||||
|
@MainActor
|
||||||
convenience init(with configuration: RoomProxyMockConfiguration) {
|
convenience init(with configuration: RoomProxyMockConfiguration) {
|
||||||
self.init()
|
self.init()
|
||||||
|
|
||||||
@@ -84,6 +90,10 @@ extension RoomProxyMock {
|
|||||||
underlyingIsFavourite = false
|
underlyingIsFavourite = false
|
||||||
flagAsFavouriteReturnValue = .success(())
|
flagAsFavouriteReturnValue = .success(())
|
||||||
|
|
||||||
|
kickUserReturnValue = .success(())
|
||||||
|
banUserReturnValue = .success(())
|
||||||
|
unbanUserReturnValue = .success(())
|
||||||
|
|
||||||
let widgetDriver = ElementCallWidgetDriverMock()
|
let widgetDriver = ElementCallWidgetDriverMock()
|
||||||
widgetDriver.underlyingMessagePublisher = .init()
|
widgetDriver.underlyingMessagePublisher = .init()
|
||||||
widgetDriver.underlyingActions = PassthroughSubject().eraseToAnyPublisher()
|
widgetDriver.underlyingActions = PassthroughSubject().eraseToAnyPublisher()
|
||||||
|
|||||||
+3
-1
@@ -20,6 +20,7 @@ import SwiftUI
|
|||||||
struct RoomMembersListScreenCoordinatorParameters {
|
struct RoomMembersListScreenCoordinatorParameters {
|
||||||
let mediaProvider: MediaProviderProtocol
|
let mediaProvider: MediaProviderProtocol
|
||||||
let roomProxy: RoomProxyProtocol
|
let roomProxy: RoomProxyProtocol
|
||||||
|
let appSettings: AppSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RoomMembersListScreenCoordinatorAction {
|
enum RoomMembersListScreenCoordinatorAction {
|
||||||
@@ -40,7 +41,8 @@ final class RoomMembersListScreenCoordinator: CoordinatorProtocol {
|
|||||||
init(parameters: RoomMembersListScreenCoordinatorParameters) {
|
init(parameters: RoomMembersListScreenCoordinatorParameters) {
|
||||||
viewModel = RoomMembersListScreenViewModel(roomProxy: parameters.roomProxy,
|
viewModel = RoomMembersListScreenViewModel(roomProxy: parameters.roomProxy,
|
||||||
mediaProvider: parameters.mediaProvider,
|
mediaProvider: parameters.mediaProvider,
|
||||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||||
|
appSettings: parameters.appSettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
func start() {
|
func start() {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ struct RoomMembersListScreenViewState: BindableState {
|
|||||||
var bannedMembersCount: Int { bannedMembers.count }
|
var bannedMembersCount: Int { bannedMembers.count }
|
||||||
|
|
||||||
var canInviteUsers = false
|
var canInviteUsers = false
|
||||||
|
var canKickUsers = false
|
||||||
var canBanUsers = false
|
var canBanUsers = false
|
||||||
|
|
||||||
var bindings: RoomMembersListScreenViewStateBindings
|
var bindings: RoomMembersListScreenViewStateBindings
|
||||||
@@ -74,16 +75,26 @@ struct RoomMembersListScreenViewStateBindings {
|
|||||||
var searchQuery = ""
|
var searchQuery = ""
|
||||||
/// The current mode the screen is in.
|
/// The current mode the screen is in.
|
||||||
var mode: RoomMembersListScreenMode = .members
|
var mode: RoomMembersListScreenMode = .members
|
||||||
|
/// A selected member to kick, ban, promote etc.
|
||||||
|
var memberToManage: RoomMemberDetails?
|
||||||
|
|
||||||
/// Information describing the currently displayed alert.
|
/// Information describing the currently displayed alert.
|
||||||
var alertInfo: AlertInfo<RoomDetailsScreenErrorType>?
|
var alertInfo: AlertInfo<RoomMembersListScreenAlertType>?
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RoomMembersListScreenViewAction {
|
enum RoomMembersListScreenViewAction {
|
||||||
case selectMember(id: String)
|
case selectMember(RoomMemberDetails)
|
||||||
|
case showMemberDetails(RoomMemberDetails)
|
||||||
|
case kickMember(RoomMemberDetails)
|
||||||
|
case banMember(RoomMemberDetails)
|
||||||
|
case unbanMember(RoomMemberDetails)
|
||||||
case invite
|
case invite
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum RoomMembersListScreenAlertType: Hashable {
|
||||||
|
case unbanConfirmation(RoomMemberDetails)
|
||||||
|
}
|
||||||
|
|
||||||
private extension RoomMemberDetails {
|
private extension RoomMemberDetails {
|
||||||
func matches(searchQuery: String) -> Bool {
|
func matches(searchQuery: String) -> Bool {
|
||||||
guard !searchQuery.isEmpty else {
|
guard !searchQuery.isEmpty else {
|
||||||
|
|||||||
+112
-8
@@ -22,6 +22,7 @@ typealias RoomMembersListScreenViewModelType = StateStoreViewModel<RoomMembersLi
|
|||||||
class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMembersListScreenViewModelProtocol {
|
class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMembersListScreenViewModelProtocol {
|
||||||
private let roomProxy: RoomProxyProtocol
|
private let roomProxy: RoomProxyProtocol
|
||||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||||
|
private let appSettings: AppSettings
|
||||||
|
|
||||||
private var members: [RoomMemberProxyProtocol] = []
|
private var members: [RoomMemberProxyProtocol] = []
|
||||||
|
|
||||||
@@ -34,9 +35,11 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
|
|||||||
init(initialMode: RoomMembersListScreenMode = .members,
|
init(initialMode: RoomMembersListScreenMode = .members,
|
||||||
roomProxy: RoomProxyProtocol,
|
roomProxy: RoomProxyProtocol,
|
||||||
mediaProvider: MediaProviderProtocol,
|
mediaProvider: MediaProviderProtocol,
|
||||||
userIndicatorController: UserIndicatorControllerProtocol) {
|
userIndicatorController: UserIndicatorControllerProtocol,
|
||||||
|
appSettings: AppSettings) {
|
||||||
self.roomProxy = roomProxy
|
self.roomProxy = roomProxy
|
||||||
self.userIndicatorController = userIndicatorController
|
self.userIndicatorController = userIndicatorController
|
||||||
|
self.appSettings = appSettings
|
||||||
|
|
||||||
super.init(initialViewState: .init(joinedMembersCount: roomProxy.joinedMembersCount,
|
super.init(initialViewState: .init(joinedMembersCount: roomProxy.joinedMembersCount,
|
||||||
bindings: .init(mode: initialMode)),
|
bindings: .init(mode: initialMode)),
|
||||||
@@ -49,12 +52,16 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
|
|||||||
|
|
||||||
override func process(viewAction: RoomMembersListScreenViewAction) {
|
override func process(viewAction: RoomMembersListScreenViewAction) {
|
||||||
switch viewAction {
|
switch viewAction {
|
||||||
case .selectMember(let id):
|
case .selectMember(let member):
|
||||||
guard let member = members.first(where: { $0.userID == id }) else {
|
selectMember(member)
|
||||||
MXLog.error("Selected member \(id) not found")
|
case .showMemberDetails(let member):
|
||||||
return
|
showMemberDetails(member)
|
||||||
}
|
case .kickMember(let member):
|
||||||
actionsSubject.send(.selectMember(member))
|
Task { await kickMember(member) }
|
||||||
|
case .banMember(let member):
|
||||||
|
Task { await banMember(member) }
|
||||||
|
case .unbanMember(let member):
|
||||||
|
Task { await unbanMember(member) }
|
||||||
case .invite:
|
case .invite:
|
||||||
actionsSubject.send(.invite)
|
actionsSubject.send(.invite)
|
||||||
}
|
}
|
||||||
@@ -64,7 +71,7 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
|
|||||||
hideLoader()
|
hideLoader()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Members
|
||||||
|
|
||||||
private func setupMembers() {
|
private func setupMembers() {
|
||||||
Task {
|
Task {
|
||||||
@@ -80,11 +87,17 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
|
|||||||
self?.updateState(members: members)
|
self?.updateState(members: members)
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
roomProxy.timeline.timelineProvider.membershipChangePublisher.sink { [roomProxy] _ in
|
||||||
|
Task { await roomProxy.updateMembers() }
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateState(members: [RoomMemberProxyProtocol]) {
|
private func updateState(members: [RoomMemberProxyProtocol]) {
|
||||||
Task {
|
Task {
|
||||||
showLoader()
|
showLoader()
|
||||||
|
|
||||||
let members = members.sorted()
|
let members = members.sorted()
|
||||||
let roomMembersDetails = await buildMembersDetails(members: members)
|
let roomMembersDetails = await buildMembersDetails(members: members)
|
||||||
self.members = members
|
self.members = members
|
||||||
@@ -95,8 +108,13 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
|
|||||||
bindings: state.bindings)
|
bindings: state.bindings)
|
||||||
if let accountOwner = roomMembersDetails.accountOwner {
|
if let accountOwner = roomMembersDetails.accountOwner {
|
||||||
self.state.canInviteUsers = accountOwner.canInviteUsers
|
self.state.canInviteUsers = accountOwner.canInviteUsers
|
||||||
|
self.state.canKickUsers = accountOwner.canKickUsers
|
||||||
self.state.canBanUsers = accountOwner.canBanUsers
|
self.state.canBanUsers = accountOwner.canBanUsers
|
||||||
}
|
}
|
||||||
|
if state.bindings.mode == .banned, roomMembersDetails.bannedMembers.isEmpty {
|
||||||
|
state.bindings.mode = .members
|
||||||
|
}
|
||||||
|
|
||||||
hideLoader()
|
hideLoader()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,6 +152,76 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
|
|||||||
.value
|
.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 let userIndicatorID = UUID().uuidString
|
||||||
|
|
||||||
private func showLoader() {
|
private func showLoader() {
|
||||||
@@ -147,6 +235,22 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
|
|||||||
private func hideLoader() {
|
private func hideLoader() {
|
||||||
userIndicatorController.retractIndicatorWithId(userIndicatorID)
|
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 {
|
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))
|
.searchable(text: $context.searchQuery, placement: .navigationBarDrawer(displayMode: .always))
|
||||||
.compoundSearchField()
|
.compoundSearchField()
|
||||||
|
.autocorrectionDisabled()
|
||||||
.background(Color.compound.bgCanvasDefault.ignoresSafeArea())
|
.background(Color.compound.bgCanvasDefault.ignoresSafeArea())
|
||||||
.navigationTitle(L10n.commonPeople)
|
.navigationTitle(L10n.commonPeople)
|
||||||
.alert(item: $context.alertInfo)
|
.sheet(item: $context.memberToManage) {
|
||||||
.toolbar {
|
RoomMembersListManageMemberSheet(member: $0, context: context)
|
||||||
ToolbarItem(placement: .confirmationAction) {
|
|
||||||
inviteButton
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
.alert(item: $context.alertInfo)
|
||||||
|
.toolbar { toolbar }
|
||||||
.track(screen: .RoomMembers)
|
.track(screen: .RoomMembers)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,13 +91,13 @@ struct RoomMembersListScreen: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ToolbarContentBuilder
|
||||||
private var inviteButton: some View {
|
private var toolbar: some ToolbarContent {
|
||||||
if context.viewState.canInviteUsers {
|
ToolbarItem(placement: .confirmationAction) {
|
||||||
Button {
|
if context.viewState.canInviteUsers {
|
||||||
context.send(viewAction: .invite)
|
Button(L10n.actionInvite) {
|
||||||
} label: {
|
context.send(viewAction: .invite)
|
||||||
Text(L10n.actionInvite)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -162,6 +162,7 @@ struct RoomMembersListScreen_Previews: PreviewProvider, TestablePreview {
|
|||||||
return RoomMembersListScreenViewModel(initialMode: initialMode,
|
return RoomMembersListScreenViewModel(initialMode: initialMode,
|
||||||
roomProxy: RoomProxyMock(with: .init(name: "Some room", members: members)),
|
roomProxy: RoomProxyMock(with: .init(name: "Some room", members: members)),
|
||||||
mediaProvider: MockMediaProvider(),
|
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 {
|
var body: some View {
|
||||||
Button {
|
Button {
|
||||||
context.send(viewAction: .selectMember(id: member.id))
|
context.send(viewAction: .selectMember(member))
|
||||||
} label: {
|
} label: {
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
LoadableAvatarImage(url: avatarURL,
|
LoadableAvatarImage(url: avatarURL,
|
||||||
@@ -109,7 +109,8 @@ struct RoomMembersListMemberCell_Previews: PreviewProvider, TestablePreview {
|
|||||||
static let viewModel = RoomMembersListScreenViewModel(roomProxy: RoomProxyMock(with: .init(name: "Some room",
|
static let viewModel = RoomMembersListScreenViewModel(roomProxy: RoomProxyMock(with: .init(name: "Some room",
|
||||||
members: members)),
|
members: members)),
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||||
|
appSettings: ServiceLocator.shared.settings)
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
VStack(spacing: 12) {
|
VStack(spacing: 12) {
|
||||||
Section("Invited/Joined") {
|
Section("Invited/Joined") {
|
||||||
|
|||||||
+1
@@ -54,6 +54,7 @@ protocol DeveloperOptionsProtocol: AnyObject {
|
|||||||
|
|
||||||
var markAsUnreadEnabled: Bool { get set }
|
var markAsUnreadEnabled: Bool { get set }
|
||||||
var markAsFavouriteEnabled: Bool { get set }
|
var markAsFavouriteEnabled: Bool { get set }
|
||||||
|
var roomModerationEnabled: Bool { get set }
|
||||||
|
|
||||||
var elementCallBaseURL: URL { 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) {
|
Toggle(isOn: $context.shouldCollapseRoomStateEvents) {
|
||||||
Text("Collapse room state events")
|
Text("Collapse room state events")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import MatrixRustSDK
|
import MatrixRustSDK
|
||||||
|
|
||||||
struct RoomMemberDetails: Identifiable, Equatable {
|
struct RoomMemberDetails: Identifiable, Hashable {
|
||||||
let id: String
|
let id: String
|
||||||
let name: String?
|
let name: String?
|
||||||
let avatarURL: URL?
|
let avatarURL: URL?
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ final class RoomMemberProxy: RoomMemberProxyProtocol {
|
|||||||
lazy var powerLevel = Int(member.powerLevel())
|
lazy var powerLevel = Int(member.powerLevel())
|
||||||
lazy var role = member.suggestedRoleForPowerLevel()
|
lazy var role = member.suggestedRoleForPowerLevel()
|
||||||
lazy var canInviteUsers = member.canInvite()
|
lazy var canInviteUsers = member.canInvite()
|
||||||
|
lazy var canKickUsers = member.canKick()
|
||||||
lazy var canBanUsers = member.canBan()
|
lazy var canBanUsers = member.canBan()
|
||||||
|
|
||||||
func canSendStateEvent(type: StateEventType) -> Bool {
|
func canSendStateEvent(type: StateEventType) -> Bool {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ protocol RoomMemberProxyProtocol: AnyObject {
|
|||||||
var powerLevel: Int { get }
|
var powerLevel: Int { get }
|
||||||
var role: RoomMemberRole { get }
|
var role: RoomMemberRole { get }
|
||||||
var canInviteUsers: Bool { get }
|
var canInviteUsers: Bool { get }
|
||||||
|
var canKickUsers: Bool { get }
|
||||||
var canBanUsers: Bool { get }
|
var canBanUsers: Bool { get }
|
||||||
|
|
||||||
func ignoreUser() async -> Result<Void, RoomMemberProxyError>
|
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
|
// MARK: - Element Call
|
||||||
|
|
||||||
func canUserJoinCall(userID: String) async -> Result<Bool, RoomProxyError> {
|
func canUserJoinCall(userID: String) async -> Result<Bool, RoomProxyError> {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ enum RoomProxyError: Error, Equatable {
|
|||||||
case failedMarkingAsRead
|
case failedMarkingAsRead
|
||||||
case failedSendingTypingNotice
|
case failedSendingTypingNotice
|
||||||
case failedFlaggingAsFavourite
|
case failedFlaggingAsFavourite
|
||||||
|
case failedModeration
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RoomProxyAction {
|
enum RoomProxyAction {
|
||||||
@@ -120,6 +121,12 @@ protocol RoomProxyProtocol {
|
|||||||
|
|
||||||
func flagAsFavourite(_ isFavourite: Bool) async -> Result<Void, RoomProxyError>
|
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
|
// MARK: - Element Call
|
||||||
|
|
||||||
func canUserJoinCall(userID: String) async -> Result<Bool, RoomProxyError>
|
func canUserJoinCall(userID: String) async -> Result<Bool, RoomProxyError>
|
||||||
|
|||||||
@@ -38,6 +38,12 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol {
|
|||||||
.map { _, _ in () }
|
.map { _, _ in () }
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private let membershipChangeSubject = PassthroughSubject<Void, Never>()
|
||||||
|
var membershipChangePublisher: AnyPublisher<Void, Never> {
|
||||||
|
membershipChangeSubject
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
init(currentItems: [TimelineItem],
|
init(currentItems: [TimelineItem],
|
||||||
updatePublisher: AnyPublisher<[TimelineDiff], Never>,
|
updatePublisher: AnyPublisher<[TimelineDiff], Never>,
|
||||||
@@ -99,7 +105,13 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol {
|
|||||||
|
|
||||||
MXLog.verbose("Append \(items.map(\.debugIdentifier))")
|
MXLog.verbose("Append \(items.map(\.debugIdentifier))")
|
||||||
for (index, item) in items.enumerated() {
|
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:
|
case .clear:
|
||||||
MXLog.verbose("Clear all items")
|
MXLog.verbose("Clear all items")
|
||||||
@@ -129,6 +141,11 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol {
|
|||||||
|
|
||||||
MXLog.verbose("Push Back \(item.debugIdentifier)")
|
MXLog.verbose("Push Back \(item.debugIdentifier)")
|
||||||
let itemProxy = TimelineItemProxy(item: item)
|
let itemProxy = TimelineItemProxy(item: item)
|
||||||
|
|
||||||
|
if itemProxy.isMembershipChange {
|
||||||
|
membershipChangeSubject.send(())
|
||||||
|
}
|
||||||
|
|
||||||
changes.append(.insert(offset: Int(itemProxies.count), element: itemProxy, associatedWith: nil))
|
changes.append(.insert(offset: Int(itemProxies.count), element: itemProxy, associatedWith: nil))
|
||||||
case .pushFront:
|
case .pushFront:
|
||||||
guard let item = diff.pushFront() else { fatalError() }
|
guard let item = diff.pushFront() else { fatalError() }
|
||||||
@@ -197,6 +214,20 @@ private extension TimelineItemProxy {
|
|||||||
return .unknown(timelineID: String(item.uniqueId()))
|
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 {
|
private extension VirtualTimelineItem {
|
||||||
|
|||||||
@@ -22,7 +22,14 @@ import MatrixRustSDK
|
|||||||
@MainActor
|
@MainActor
|
||||||
// sourcery: AutoMockable
|
// sourcery: AutoMockable
|
||||||
protocol RoomTimelineProviderProtocol {
|
protocol RoomTimelineProviderProtocol {
|
||||||
|
/// A publisher that signals when ``itemProxies`` or ``backPaginationState`` are changed.
|
||||||
var updatePublisher: AnyPublisher<Void, Never> { get }
|
var updatePublisher: AnyPublisher<Void, Never> { get }
|
||||||
|
/// The current set of items in the timeline.
|
||||||
var itemProxies: [TimelineItemProxy] { get }
|
var itemProxies: [TimelineItemProxy] { get }
|
||||||
|
/// Whether the timeline is back paginating or not (or has reached the start of the room).
|
||||||
var backPaginationState: BackPaginationStatus { get }
|
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 navigationStackCoordinator = NavigationStackCoordinator()
|
||||||
let members: [RoomMemberProxyMock] = [.mockAlice, .mockBob, .mockCharlie]
|
let members: [RoomMemberProxyMock] = [.mockAlice, .mockBob, .mockCharlie]
|
||||||
let coordinator = RoomMembersListScreenCoordinator(parameters: .init(mediaProvider: MockMediaProvider(),
|
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)
|
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||||
return navigationStackCoordinator
|
return navigationStackCoordinator
|
||||||
case .roomMembersListScreenPendingInvites:
|
case .roomMembersListScreenPendingInvites:
|
||||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||||
let members: [RoomMemberProxyMock] = [.mockInvitedAlice, .mockBob, .mockCharlie]
|
let members: [RoomMemberProxyMock] = [.mockInvitedAlice, .mockBob, .mockCharlie]
|
||||||
let coordinator = RoomMembersListScreenCoordinator(parameters: .init(mediaProvider: MockMediaProvider(),
|
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)
|
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||||
return navigationStackCoordinator
|
return navigationStackCoordinator
|
||||||
case .roomNotificationSettingsDefaultSetting:
|
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
|
@testable import ElementX
|
||||||
|
|
||||||
|
@MainActor
|
||||||
final class CompletionSuggestionServiceTests: XCTestCase {
|
final class CompletionSuggestionServiceTests: XCTestCase {
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import XCTest
|
|||||||
@MainActor
|
@MainActor
|
||||||
class RoomMembersListScreenViewModelTests: XCTestCase {
|
class RoomMembersListScreenViewModelTests: XCTestCase {
|
||||||
var viewModel: RoomMembersListScreenViewModel!
|
var viewModel: RoomMembersListScreenViewModel!
|
||||||
|
var roomProxy: RoomProxyMock!
|
||||||
|
|
||||||
var context: RoomMembersListScreenViewModel.Context {
|
var context: RoomMembersListScreenViewModel.Context {
|
||||||
viewModel.context
|
viewModel.context
|
||||||
@@ -125,9 +126,50 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
|
|||||||
XCTAssertEqual(viewModel.state.visibleJoinedMembers.count, 0)
|
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]) {
|
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(),
|
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