From b3bff57ed9e9f65c2fc4d501f807d02193ad6725 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Tue, 8 Apr 2025 15:23:40 +0200 Subject: [PATCH] refactor: manage member sheet in timeline better implementation updated tests Update ElementX/Sources/Screens/Timeline/TimelineViewModel.swift Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> fix --- ElementX.xcodeproj/project.pbxproj | 40 +++++- .../en-US.lproj/Localizable.strings | 38 +++--- .../en.lproj/Localizable.strings | 38 +++--- ElementX/Sources/Generated/Strings.swift | 78 ++++++----- .../ManageRoomMemberSheetModels.swift | 33 +++++ .../ManageRoomMemberSheetViewModel.swift | 125 ++++++++++++++++++ ...nageRoomMemberSheetViewModelProtocol.swift | 14 ++ .../View/ManageRoomMemberSheetView.swift | 87 ++++++++++++ .../MediaEventsTimelineScreenViewModel.swift | 4 +- ...innedEventsTimelineScreenCoordinator.swift | 2 +- .../View/PinnedEventsTimelineScreen.swift | 3 + .../RoomMembersListScreenModels.swift | 20 +-- .../RoomMembersListScreenViewModel.swift | 98 +++++--------- .../RoomMembersListManageMemberSheet.swift | 96 -------------- .../View/RoomMembersListScreen.swift | 4 +- .../RoomScreen/RoomScreenCoordinator.swift | 2 +- .../Screens/RoomScreen/View/RoomScreen.swift | 3 + .../Screens/Timeline/TimelineModels.swift | 10 +- .../Screens/Timeline/TimelineViewModel.swift | 56 +++++++- .../Room/RoomMember/RoomMemberDetails.swift | 2 + .../Sources/GeneratedPreviewTests.swift | 12 +- ...MemberSheetView.All-Actions-iPad-en-GB.png | 3 + ...emberSheetView.All-Actions-iPad-pseudo.png | 3 + ...rSheetView.All-Actions-iPhone-16-en-GB.png | 3 + ...SheetView.All-Actions-iPhone-16-pseudo.png | 3 + ...oomMemberSheetView.Ban-Only-iPad-en-GB.png | 3 + ...omMemberSheetView.Ban-Only-iPad-pseudo.png | 3 + ...mberSheetView.Ban-Only-iPhone-16-en-GB.png | 3 + ...berSheetView.Ban-Only-iPhone-16-pseudo.png | 3 + ...omMemberSheetView.Kick-Only-iPad-en-GB.png | 3 + ...mMemberSheetView.Kick-Only-iPad-pseudo.png | 3 + ...berSheetView.Kick-Only-iPhone-16-en-GB.png | 3 + ...erSheetView.Kick-Only-iPhone-16-pseudo.png | 3 + ...istManageMemberSheet.Banned-iPad-en-GB.png | 3 - ...stManageMemberSheet.Banned-iPad-pseudo.png | 3 - ...nageMemberSheet.Banned-iPhone-16-en-GB.png | 3 - ...ageMemberSheet.Banned-iPhone-16-pseudo.png | 3 - ...istManageMemberSheet.Joined-iPad-en-GB.png | 3 - ...stManageMemberSheet.Joined-iPad-pseudo.png | 3 - ...nageMemberSheet.Joined-iPhone-16-en-GB.png | 3 - ...ageMemberSheet.Joined-iPhone-16-pseudo.png | 3 - .../ManageRoomMemberSheetViewModelTests.swift | 100 ++++++++++++++ .../RoomMembersListScreenViewModelTests.swift | 68 +++------- .../Sources/TimelineViewModelTests.swift | 111 ++++++++++++++++ 44 files changed, 760 insertions(+), 344 deletions(-) create mode 100644 ElementX/Sources/Screens/ManageRoomMemberSheet/ManageRoomMemberSheetModels.swift create mode 100644 ElementX/Sources/Screens/ManageRoomMemberSheet/ManageRoomMemberSheetViewModel.swift create mode 100644 ElementX/Sources/Screens/ManageRoomMemberSheet/ManageRoomMemberSheetViewModelProtocol.swift create mode 100644 ElementX/Sources/Screens/ManageRoomMemberSheet/View/ManageRoomMemberSheetView.swift delete mode 100644 ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListManageMemberSheet.swift create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.All-Actions-iPad-en-GB.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.All-Actions-iPad-pseudo.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.All-Actions-iPhone-16-en-GB.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.All-Actions-iPhone-16-pseudo.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Ban-Only-iPad-en-GB.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Ban-Only-iPad-pseudo.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Ban-Only-iPhone-16-en-GB.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Ban-Only-iPhone-16-pseudo.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Kick-Only-iPad-en-GB.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Kick-Only-iPad-pseudo.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Kick-Only-iPhone-16-en-GB.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Kick-Only-iPhone-16-pseudo.png delete mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/roomMembersListManageMemberSheet.Banned-iPad-en-GB.png delete mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/roomMembersListManageMemberSheet.Banned-iPad-pseudo.png delete mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/roomMembersListManageMemberSheet.Banned-iPhone-16-en-GB.png delete mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/roomMembersListManageMemberSheet.Banned-iPhone-16-pseudo.png delete mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/roomMembersListManageMemberSheet.Joined-iPad-en-GB.png delete mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/roomMembersListManageMemberSheet.Joined-iPad-pseudo.png delete mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/roomMembersListManageMemberSheet.Joined-iPhone-16-en-GB.png delete mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/roomMembersListManageMemberSheet.Joined-iPhone-16-pseudo.png create mode 100644 UnitTests/Sources/ManageRoomMemberSheetViewModelTests.swift diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 85e1c51f0..7d3f231e2 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -164,6 +164,7 @@ 1B88BB631F7FC45A213BB554 /* TimelineItemSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55AEEF8142DF1B59DB40FB93 /* TimelineItemSender.swift */; }; 1B8E30B35BF8F541C1318F19 /* SecureBackupScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40316EFFEAC7B206EE9A55AE /* SecureBackupScreenViewModelTests.swift */; }; 1BA04D05EBC6646958B1BE60 /* PlaceholderScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF34A2FD6797535C95AC918D /* PlaceholderScreenCoordinator.swift */; }; + 1C1750C009F7214B967928BC /* ManageRoomMemberSheetViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D80807B554CF9C524F98674F /* ManageRoomMemberSheetViewModelTests.swift */; }; 1C409A26A99F0371C47AFA51 /* UserDiscoveryServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F615A00DB223FF3280204D2 /* UserDiscoveryServiceProtocol.swift */; }; 1C598D3B785645AAC7B35760 /* ReportRoomScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 292EEE1F71DCC205C45728F7 /* ReportRoomScreenCoordinator.swift */; }; 1C8BC70A18060677E295A846 /* ShareToMapsAppActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4481799F455B3DA243BDA2AC /* ShareToMapsAppActivity.swift */; }; @@ -421,6 +422,7 @@ 5038E69A5E6A89DE1A345E04 /* ShouldScrollOnKeyboardDidShow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832397B5C3D00A4BF52C5F0B /* ShouldScrollOnKeyboardDidShow.swift */; }; 50539366B408780B232C1910 /* EstimatedWaveformView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD0FF64B0E6470F66F42E182 /* EstimatedWaveformView.swift */; }; 5100F53E6884A15F9BA07CC3 /* AttributedStringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CA26F55123E36B50DB0B3A /* AttributedStringTests.swift */; }; + 510C4EDF826CA9C6CEEC6C95 /* ManageRoomMemberSheetViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A34D9BCA1A7D9A56E1EAF1D /* ManageRoomMemberSheetViewModel.swift */; }; 5139F4BD5A5DF6F8D11A9BDE /* NotificationPermissionsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46D0BA44B1838E65B507B277 /* NotificationPermissionsScreen.swift */; }; 513AF15E0E84711B80D04B1B /* ReportRoomScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3E9684DCE6B66BD0B5DF67 /* ReportRoomScreenViewModelTests.swift */; }; 518C93DC6516D3D018DE065F /* UNNotificationRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49E751D7EDB6043238111D90 /* UNNotificationRequest.swift */; }; @@ -478,7 +480,6 @@ 5D70FAE4D2BF4553AFFFFE41 /* NotificationItemProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F7FE40EF7490A7E09D7BE6 /* NotificationItemProxy.swift */; }; 5D99F63CC88BB29383019FC6 /* ShareExtensionModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCAC01A97A43BE07B9E94E43 /* ShareExtensionModels.swift */; }; 5DB4334CBBA142376FF5FFEC /* preview_image.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 200626E8353AB2729444F991 /* preview_image.jpg */; }; - 5DD0EF30070DC0A82C5CCD33 /* RoomMembersListManageMemberSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC853F9B4FBE039D2C16EC6B /* RoomMembersListManageMemberSheet.swift */; }; 5DD85A0FE3D85AEC3C7EFE36 /* DeveloperOptionsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7C7CFA6B2A62A685FF6CE3 /* DeveloperOptionsScreenCoordinator.swift */; }; 5DFC2A889D3B39DD47AC63A8 /* PillUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C537DE821FED94D23467B6C4 /* PillUtilities.swift */; }; 5EC046E41755C095DAB1C3FF /* TimelineProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8C9BBB729C941BEE0E2A63 /* TimelineProviderProtocol.swift */; }; @@ -1145,6 +1146,7 @@ E481C8FDCB6C089963C95344 /* DeviceKit in Frameworks */ = {isa = PBXBuildFile; productRef = BC01130651CB23340B899032 /* DeviceKit */; }; E49F74BD93230BDEFFE5EA51 /* RoomNotificationSettingsScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D295F0081084F38DB20893 /* RoomNotificationSettingsScreenViewModelTests.swift */; }; E4B07FF075C99D04D9AF792D /* AppLockSetupPINScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = B410B32B72C90BF94E481F33 /* AppLockSetupPINScreenModels.swift */; }; + E4D261E237D5D45E6DF2D0F1 /* ManageRoomMemberSheetModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 787E84119E626E2F0E0BFBE8 /* ManageRoomMemberSheetModels.swift */; }; E4F924DECC66389C1C810550 /* AuthenticationStartScreenBackgroundImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50D685B4DB38BB5BD87C956A /* AuthenticationStartScreenBackgroundImage.swift */; }; E58F1F3276E98A93F7D39219 /* RoomPollsHistoryScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1D8479BB704B7EF696F8ABE /* RoomPollsHistoryScreenCoordinator.swift */; }; E5AB28123E2488F97E953AC0 /* CallNotificationRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1ED17433ADC77287F8904F9 /* CallNotificationRoomTimelineItem.swift */; }; @@ -1212,11 +1214,13 @@ F118DD449066E594F63C697D /* RoomMemberProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32B5E17028C02DFA7DDA3931 /* RoomMemberProxyProtocol.swift */; }; F12F6BED7B6D7EE4BEE55039 /* PlainMentionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AE78FA0011E07920AE83135 /* PlainMentionBuilder.swift */; }; F18CA61A58C77C84F551B8E7 /* GeneratedMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57916A1578D8043BB0795441 /* GeneratedMocks.swift */; }; + F1C68F64FC8A66B6B9510BF7 /* ManageRoomMemberSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B30CD19ED6243FEDFBA8400 /* ManageRoomMemberSheetView.swift */; }; F252C0EA49088801F4CA6006 /* landscape_test_image.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 96CE9D6642DD487D8CC90C9C /* landscape_test_image.jpg */; }; F253AAB4C8F06208173C9C4A /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D52BAA5BADB06E5E8C295D /* Assets.swift */; }; F255083E18CDBFDF7E640FB1 /* Avatars.swift in Sources */ = {isa = PBXBuildFile; fileRef = C142248014E08E885E323E56 /* Avatars.swift */; }; F2D5C0E1351DA7BD16867629 /* TimelineStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD4823EAB4B4E8BAB4F6B8C /* TimelineStyle.swift */; }; F2E580C0FBFBEFFE9D69893B /* RoomPreviewProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 739077686814E4EA339B1C83 /* RoomPreviewProxyProtocol.swift */; }; + F35FAD1B1B289E221A07D719 /* ManageRoomMemberSheetViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB7B588A06911B455AC0B4C9 /* ManageRoomMemberSheetViewModelProtocol.swift */; }; F37629BAA5E8F50AAF2A131D /* SoftLogoutScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB7BAD55A4E2B8E5828CD64C /* SoftLogoutScreenViewModel.swift */; }; F38D32C1B0232AAFE6A0822C /* ExtensionLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A8571A8A071FB41778C016 /* ExtensionLogger.swift */; }; F3E2D3F7ACDED65A4E5CD8DE /* RoomMembersListScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECF79FB25E2D4BD6F50CE7C9 /* RoomMembersListScreenViewModel.swift */; }; @@ -1489,6 +1493,7 @@ 1B065EC39C99C1303A101C1C /* WebRegistrationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRegistrationScreen.swift; sourceTree = ""; }; 1B10423B9102086A2D9BFCBA /* EventTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventTimelineItem.swift; sourceTree = ""; }; 1B2AC540DE619B36832A5DB5 /* LocationRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRoomTimelineItem.swift; sourceTree = ""; }; + 1B30CD19ED6243FEDFBA8400 /* ManageRoomMemberSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageRoomMemberSheetView.swift; sourceTree = ""; }; 1B53D6C5C0D14B04D3AB3F6E /* PillAttachmentViewProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillAttachmentViewProvider.swift; sourceTree = ""; }; 1B564D748B67A156F413CD97 /* NotificationSettingsEditScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenModels.swift; sourceTree = ""; }; 1B6E30BB748F3F480F077969 /* RoomMemberDetailsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsScreenModels.swift; sourceTree = ""; }; @@ -1930,6 +1935,7 @@ 7720ACAC6155AB7F9C70B546 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nb; path = nb.lproj/Localizable.stringsdict; sourceTree = ""; }; 7773CBFDBD458E0B7E270507 /* PillView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillView.swift; sourceTree = ""; }; 780258F1B9D15E30549FF4BE /* NotificationSettingsEditScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenViewModel.swift; sourceTree = ""; }; + 787E84119E626E2F0E0BFBE8 /* ManageRoomMemberSheetModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageRoomMemberSheetModels.swift; sourceTree = ""; }; 78910787F967CBC6042A101E /* StartChatScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenViewModelProtocol.swift; sourceTree = ""; }; 78913D6E120D46138E97C107 /* NavigationSplitCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationSplitCoordinatorTests.swift; sourceTree = ""; }; 7893780A1FD6E3F38B3E9049 /* UserIndicatorControllerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorControllerMock.swift; sourceTree = ""; }; @@ -2105,6 +2111,7 @@ 9A028783CFFF861C5E44FFB1 /* BadgeLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeLabel.swift; sourceTree = ""; }; 9A1C33355FFB0F0953C35036 /* ClientBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientBuilder.swift; sourceTree = ""; }; 9A22A05E472533ED3C5A31B3 /* NavigationModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationModule.swift; sourceTree = ""; }; + 9A34D9BCA1A7D9A56E1EAF1D /* ManageRoomMemberSheetViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageRoomMemberSheetViewModel.swift; sourceTree = ""; }; 9A68BCE6438873D2661D93D0 /* BugReportServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportServiceProtocol.swift; sourceTree = ""; }; 9AA3AF94A06D319BB37E52DA /* TimelineItemFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemFactoryTests.swift; sourceTree = ""; }; 9B06663F7858E45882E63471 /* StaticLocationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticLocationScreen.swift; sourceTree = ""; }; @@ -2352,6 +2359,7 @@ CA90BD288E5AE6BC643AFDDF /* TemplateScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenCoordinator.swift; sourceTree = ""; }; CACA846B3E3E9A521D98B178 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; CAD9547E47C58930E2CE8306 /* CallScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallScreenViewModelTests.swift; sourceTree = ""; }; + CB7B588A06911B455AC0B4C9 /* ManageRoomMemberSheetViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageRoomMemberSheetViewModelProtocol.swift; sourceTree = ""; }; CBBCC6E74774E79B599625D0 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; CBF9AEA706926DD0DA2B954C /* JoinedRoomSize+MemberCount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JoinedRoomSize+MemberCount.swift"; sourceTree = ""; }; CC03209FDE8CE0810617BFFF /* RoomMembersListScreenMemberCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenMemberCell.swift; sourceTree = ""; }; @@ -2409,6 +2417,7 @@ D7B18089ED50324583BB2FB7 /* EditRoomAddressScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditRoomAddressScreenViewModelProtocol.swift; sourceTree = ""; }; D7BB243B26D54EF1A0C422C0 /* NotificationContentBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationContentBuilder.swift; sourceTree = ""; }; D7BEB970F500BFB248443FA1 /* BloomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BloomView.swift; sourceTree = ""; }; + D80807B554CF9C524F98674F /* ManageRoomMemberSheetViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageRoomMemberSheetViewModelTests.swift; sourceTree = ""; }; D879DC5515B1D42577F96C94 /* RoomSelectionScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSelectionScreen.swift; sourceTree = ""; }; D8AA084E10B80D64449C02A9 /* SessionVerificationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationTests.swift; sourceTree = ""; }; D8E60332509665C00179ACF6 /* MessageForwardingScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreenViewModel.swift; sourceTree = ""; }; @@ -2572,7 +2581,6 @@ FBB0328F2887BF0A65BC5D49 /* NotificationSettingsEditScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreen.swift; sourceTree = ""; }; FBC776F301D374A3298C69DA /* AppCoordinatorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinatorProtocol.swift; sourceTree = ""; }; FC83F47D2173B7538AA72E0E /* RoomSummaryProviderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProviderMock.swift; sourceTree = ""; }; - FC853F9B4FBE039D2C16EC6B /* RoomMembersListManageMemberSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListManageMemberSheet.swift; sourceTree = ""; }; FC9044BE0E4A66F5B963E834 /* AudioFileEventsTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioFileEventsTimelineView.swift; sourceTree = ""; }; FCE93F0CBF0D96B77111C413 /* AppLockFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockFlowCoordinator.swift; sourceTree = ""; }; FD1275D9CE0FFBA6E8E85426 /* UserIndicatorController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorController.swift; sourceTree = ""; }; @@ -3490,6 +3498,17 @@ path = QRCodeLoginScreen; sourceTree = ""; }; + 3D76DA5827DF9396AC90E7B4 /* ManageRoomMemberSheet */ = { + isa = PBXGroup; + children = ( + 787E84119E626E2F0E0BFBE8 /* ManageRoomMemberSheetModels.swift */, + 9A34D9BCA1A7D9A56E1EAF1D /* ManageRoomMemberSheetViewModel.swift */, + CB7B588A06911B455AC0B4C9 /* ManageRoomMemberSheetViewModelProtocol.swift */, + 40AE45E13BF744AA0E4EA0E4 /* View */, + ); + path = ManageRoomMemberSheet; + sourceTree = ""; + }; 3E1CCC4B607946CE90B4A827 /* DeclineAndBlockScreen */ = { isa = PBXGroup; children = ( @@ -3559,6 +3578,14 @@ ); sourceTree = ""; }; + 40AE45E13BF744AA0E4EA0E4 /* View */ = { + isa = PBXGroup; + children = ( + 1B30CD19ED6243FEDFBA8400 /* ManageRoomMemberSheetView.swift */, + ); + path = View; + sourceTree = ""; + }; 40D9A816C45E0278C29DF883 /* SupportingFiles */ = { isa = PBXGroup; children = ( @@ -4287,6 +4314,7 @@ C070FD43DC6BF4E50217965A /* LocalizationTests.swift */, 3DC1943ADE6A62ED5129D7C8 /* LoggingTests.swift */, 5A43964330459965AF048A8C /* LoginScreenViewModelTests.swift */, + D80807B554CF9C524F98674F /* ManageRoomMemberSheetViewModelTests.swift */, 1D262A26713C18BB70C82CA5 /* MapTilerURLBuilderTests.swift */, F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */, 2D7A2C4A3A74F0D2FFE9356A /* MediaPlayerProviderTests.swift */, @@ -4878,7 +4906,6 @@ 949B06577E5265373013DDAB /* View */ = { isa = PBXGroup; children = ( - FC853F9B4FBE039D2C16EC6B /* RoomMembersListManageMemberSheet.swift */, 1B8E176484A89BAC389D4076 /* RoomMembersListScreen.swift */, CC03209FDE8CE0810617BFFF /* RoomMembersListScreenMemberCell.swift */, ); @@ -5776,6 +5803,7 @@ BF0415BE807CA2BCFC210008 /* KnockRequestsListScreen */, 948DD12A5533BE1BC260E437 /* LocationSharing */, 73E032ADD008D63812791D97 /* LogViewerScreen */, + 3D76DA5827DF9396AC90E7B4 /* ManageRoomMemberSheet */, 26397A1EDB867FD573821532 /* MediaEventsTimelineScreen */, 87E2774157D9C4894BCFF3F8 /* MediaPickerScreen */, 23605DD08620BE6558242469 /* MediaUploadPreviewScreen */, @@ -6776,6 +6804,7 @@ 0033481EE363E4914295F188 /* LocalizationTests.swift in Sources */, 149D1942DC005D0485FB8D93 /* LoggingTests.swift in Sources */, 7434A7F02D587A920B376A9A /* LoginScreenViewModelTests.swift in Sources */, + 1C1750C009F7214B967928BC /* ManageRoomMemberSheetViewModelTests.swift in Sources */, 3BEBDCB42BABFA3B456FECA7 /* MapTilerURLBuilderTests.swift in Sources */, 2E43A3D221BE9587BC19C3F1 /* MatrixEntityRegexTests.swift in Sources */, 4B978C09567387EF4366BD7A /* MediaLoaderTests.swift in Sources */, @@ -7269,6 +7298,10 @@ A5B9EF45C7B8ACEB4954AE36 /* LoginScreenViewModelProtocol.swift in Sources */, B10F7D5C237417DA160F4603 /* LongPressWithFeedback.swift in Sources */, B94368839BDB69172E28E245 /* MXLog.swift in Sources */, + E4D261E237D5D45E6DF2D0F1 /* ManageRoomMemberSheetModels.swift in Sources */, + F1C68F64FC8A66B6B9510BF7 /* ManageRoomMemberSheetView.swift in Sources */, + 510C4EDF826CA9C6CEEC6C95 /* ManageRoomMemberSheetViewModel.swift in Sources */, + F35FAD1B1B289E221A07D719 /* ManageRoomMemberSheetViewModelProtocol.swift in Sources */, C1D0AB8222D7BAFC9AF9C8C0 /* MapLibreMapView.swift in Sources */, C9BE065FA7D4E77E4C61CB69 /* MapLibreModels.swift in Sources */, E2DDA49BD62F03F180A42E30 /* MapLibreStaticMapView.swift in Sources */, @@ -7487,7 +7520,6 @@ 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 */, diff --git a/ElementX/Resources/Localizations/en-US.lproj/Localizable.strings b/ElementX/Resources/Localizations/en-US.lproj/Localizable.strings index 6de75d8ab..628601e06 100644 --- a/ElementX/Resources/Localizations/en-US.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en-US.lproj/Localizable.strings @@ -399,9 +399,26 @@ "screen_advanced_settings_hide_invite_avatars_toggle_title" = "Hide avatars in room invite requests"; "screen_advanced_settings_hide_timeline_media_toggle_title" = "Hide media previews in timeline"; "screen_advanced_settings_moderation_and_safety_section_title" = "Moderation and Safety"; +"screen_advanced_settings_show_media_timeline_always_hide" = "Always hide"; +"screen_advanced_settings_show_media_timeline_always_show" = "Always show"; +"screen_advanced_settings_show_media_timeline_private_rooms" = "In private rooms"; +"screen_advanced_settings_show_media_timeline_subtitle" = "A hidden media can always be shown by tapping on it"; +"screen_advanced_settings_show_media_timeline_title" = "Show media in timeline"; +"screen_bottom_sheet,manage_room_member_remove" = "Remove from room"; "screen_bottom_sheet_create_dm_confirmation_button_title" = "Send invite"; "screen_bottom_sheet_create_dm_message" = "Would you like to start a chat with %1$@?"; "screen_bottom_sheet_create_dm_title" = "Send invite?"; +"screen_bottom_sheet_manage_room_member_ban" = "Ban from room"; +"screen_bottom_sheet_manage_room_member_ban_member_confirmation_action" = "Ban"; +"screen_bottom_sheet_manage_room_member_ban_member_confirmation_description" = "They won’t be able to join this room again if invited."; +"screen_bottom_sheet_manage_room_member_ban_member_confirmation_title" = "Are you sure you want to ban this member?"; +"screen_bottom_sheet_manage_room_member_banning_user" = "Banning %1$@"; +"screen_bottom_sheet_manage_room_member_kick_member_confirmation_action" = "Remove"; +"screen_bottom_sheet_manage_room_member_kick_member_confirmation_description" = "They will be able to join this room again if invited."; +"screen_bottom_sheet_manage_room_member_kick_member_confirmation_title" = "Are you sure you want to remove this member?"; +"screen_bottom_sheet_manage_room_member_member_user_info" = "View profile"; +"screen_bottom_sheet_manage_room_member_remove_confirmation_title" = "Remove member and ban from joining in the future?"; +"screen_bottom_sheet_manage_room_member_removing_user" = "Removing %1$@…"; "screen_create_room_room_access_section_anyone_option_description" = "Anyone can join this room"; "screen_create_room_room_access_section_anyone_option_title" = "Anyone"; "screen_create_room_room_access_section_header" = "Room Access"; @@ -886,26 +903,14 @@ "screen_room_member_details_unblock_user" = "Unblock user"; "screen_room_member_details_verify_button_subtitle" = "Use the web app to verify this user."; "screen_room_member_details_verify_button_title" = "Verify %1$@"; -"screen_room_member_list_ban_member_confirmation_action" = "Ban"; -"screen_room_member_list_ban_member_confirmation_description" = "They won’t be able to join this room again if invited."; -"screen_room_member_list_ban_member_confirmation_title" = "Are you sure you want to ban this member?"; "screen_room_member_list_banned_empty" = "There are no banned users in this room."; -"screen_room_member_list_banning_user" = "Banning %1$@"; -"screen_room_member_list_kick_member_confirmation_action" = "Remove"; -"screen_room_member_list_kick_member_confirmation_description" = "They will be able to join this room again if invited."; -"screen_room_member_list_kick_member_confirmation_title" = "Are you sure you want to remove this member?"; -"screen_room_member_list_manage_member_ban" = "Remove and ban member"; -"screen_room_member_list_manage_member_remove" = "Remove from room"; "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_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" = "View profile"; "screen_room_member_list_mode_banned" = "Banned"; "screen_room_member_list_mode_members" = "Members"; "screen_room_member_list_pending_header_title" = "Pending"; -"screen_room_member_list_removing_user" = "Removing %1$@…"; "screen_room_member_list_role_administrator" = "Admin"; "screen_room_member_list_role_moderator" = "Moderator"; "screen_room_member_list_room_members_header_title" = "Room members"; @@ -1134,9 +1139,10 @@ "troubleshoot_notifications_test_current_push_provider_failure" = "No push providers selected."; "troubleshoot_notifications_test_current_push_provider_success" = "Current push provider: %1$@."; "troubleshoot_notifications_test_current_push_provider_title" = "Current push provider"; -"troubleshoot_notifications_test_detect_push_provider_description" = "Ensure that the application has at least one push provider."; -"troubleshoot_notifications_test_detect_push_provider_failure" = "No push providers found."; -"troubleshoot_notifications_test_detect_push_provider_title" = "Detect push providers"; +"troubleshoot_notifications_test_detect_push_provider_description" = "Ensure that the application supports at least one push provider."; +"troubleshoot_notifications_test_detect_push_provider_failure" = "No push provider support found."; +"troubleshoot_notifications_test_detect_push_provider_success_2" = "The application was built with support for: %1$@"; +"troubleshoot_notifications_test_detect_push_provider_title" = "Push provider support"; "troubleshoot_notifications_test_display_notification_description" = "Check that the application can display notification."; "troubleshoot_notifications_test_display_notification_failure" = "The notification has not been clicked."; "troubleshoot_notifications_test_display_notification_permission_failure" = "Cannot display the notification."; @@ -1237,7 +1243,7 @@ "screen_room_details_security_title" = "Security"; "screen_room_details_topic_title" = "Topic"; "screen_room_error_failed_processing_media" = "Failed processing media to upload, please try again."; -"screen_room_member_list_manage_member_remove_confirmation_ban" = "Remove and ban member"; +"screen_room_member_list_manage_member_remove_confirmation_ban" = "Ban from room"; "screen_room_notification_settings_mode_all_messages" = "All messages"; "screen_room_notification_settings_mode_mentions_and_keywords" = "Mentions and Keywords only"; "screen_room_timeline_reactions_show_less" = "Show less"; diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index 8a1e82500..fc6ddda30 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -399,9 +399,26 @@ "screen_advanced_settings_hide_invite_avatars_toggle_title" = "Hide avatars in room invite requests"; "screen_advanced_settings_hide_timeline_media_toggle_title" = "Hide media previews in timeline"; "screen_advanced_settings_moderation_and_safety_section_title" = "Moderation and Safety"; +"screen_advanced_settings_show_media_timeline_always_hide" = "Always hide"; +"screen_advanced_settings_show_media_timeline_always_show" = "Always show"; +"screen_advanced_settings_show_media_timeline_private_rooms" = "In private rooms"; +"screen_advanced_settings_show_media_timeline_subtitle" = "A hidden media can always be shown by tapping on it"; +"screen_advanced_settings_show_media_timeline_title" = "Show media in timeline"; +"screen_bottom_sheet,manage_room_member_remove" = "Remove from room"; "screen_bottom_sheet_create_dm_confirmation_button_title" = "Send invite"; "screen_bottom_sheet_create_dm_message" = "Would you like to start a chat with %1$@?"; "screen_bottom_sheet_create_dm_title" = "Send invite?"; +"screen_bottom_sheet_manage_room_member_ban" = "Ban from room"; +"screen_bottom_sheet_manage_room_member_ban_member_confirmation_action" = "Ban"; +"screen_bottom_sheet_manage_room_member_ban_member_confirmation_description" = "They won’t be able to join this room again if invited."; +"screen_bottom_sheet_manage_room_member_ban_member_confirmation_title" = "Are you sure you want to ban this member?"; +"screen_bottom_sheet_manage_room_member_banning_user" = "Banning %1$@"; +"screen_bottom_sheet_manage_room_member_kick_member_confirmation_action" = "Remove"; +"screen_bottom_sheet_manage_room_member_kick_member_confirmation_description" = "They will be able to join this room again if invited."; +"screen_bottom_sheet_manage_room_member_kick_member_confirmation_title" = "Are you sure you want to remove this member?"; +"screen_bottom_sheet_manage_room_member_member_user_info" = "View profile"; +"screen_bottom_sheet_manage_room_member_remove_confirmation_title" = "Remove member and ban from joining in the future?"; +"screen_bottom_sheet_manage_room_member_removing_user" = "Removing %1$@…"; "screen_create_room_room_access_section_anyone_option_description" = "Anyone can join this room"; "screen_create_room_room_access_section_anyone_option_title" = "Anyone"; "screen_create_room_room_access_section_header" = "Room Access"; @@ -886,26 +903,14 @@ "screen_room_member_details_unblock_user" = "Unblock user"; "screen_room_member_details_verify_button_subtitle" = "Use the web app to verify this user."; "screen_room_member_details_verify_button_title" = "Verify %1$@"; -"screen_room_member_list_ban_member_confirmation_action" = "Ban"; -"screen_room_member_list_ban_member_confirmation_description" = "They won’t be able to join this room again if invited."; -"screen_room_member_list_ban_member_confirmation_title" = "Are you sure you want to ban this member?"; "screen_room_member_list_banned_empty" = "There are no banned users in this room."; -"screen_room_member_list_banning_user" = "Banning %1$@"; -"screen_room_member_list_kick_member_confirmation_action" = "Remove"; -"screen_room_member_list_kick_member_confirmation_description" = "They will be able to join this room again if invited."; -"screen_room_member_list_kick_member_confirmation_title" = "Are you sure you want to remove this member?"; -"screen_room_member_list_manage_member_ban" = "Remove and ban member"; -"screen_room_member_list_manage_member_remove" = "Remove from room"; "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_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" = "View profile"; "screen_room_member_list_mode_banned" = "Banned"; "screen_room_member_list_mode_members" = "Members"; "screen_room_member_list_pending_header_title" = "Pending"; -"screen_room_member_list_removing_user" = "Removing %1$@…"; "screen_room_member_list_role_administrator" = "Admin"; "screen_room_member_list_role_moderator" = "Moderator"; "screen_room_member_list_room_members_header_title" = "Room members"; @@ -1134,9 +1139,10 @@ "troubleshoot_notifications_test_current_push_provider_failure" = "No push providers selected."; "troubleshoot_notifications_test_current_push_provider_success" = "Current push provider: %1$@."; "troubleshoot_notifications_test_current_push_provider_title" = "Current push provider"; -"troubleshoot_notifications_test_detect_push_provider_description" = "Ensure that the application has at least one push provider."; -"troubleshoot_notifications_test_detect_push_provider_failure" = "No push providers found."; -"troubleshoot_notifications_test_detect_push_provider_title" = "Detect push providers"; +"troubleshoot_notifications_test_detect_push_provider_description" = "Ensure that the application supports at least one push provider."; +"troubleshoot_notifications_test_detect_push_provider_failure" = "No push provider support found."; +"troubleshoot_notifications_test_detect_push_provider_success_2" = "The application was built with support for: %1$@"; +"troubleshoot_notifications_test_detect_push_provider_title" = "Push provider support"; "troubleshoot_notifications_test_display_notification_description" = "Check that the application can display notification."; "troubleshoot_notifications_test_display_notification_failure" = "The notification has not been clicked."; "troubleshoot_notifications_test_display_notification_permission_failure" = "Cannot display the notification."; @@ -1237,7 +1243,7 @@ "screen_room_details_security_title" = "Security"; "screen_room_details_topic_title" = "Topic"; "screen_room_error_failed_processing_media" = "Failed processing media to upload, please try again."; -"screen_room_member_list_manage_member_remove_confirmation_ban" = "Remove and ban member"; +"screen_room_member_list_manage_member_remove_confirmation_ban" = "Ban from room"; "screen_room_notification_settings_mode_all_messages" = "All messages"; "screen_room_notification_settings_mode_mentions_and_keywords" = "Mentions and Keywords only"; "screen_room_timeline_reactions_show_less" = "Show less"; diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index a4403ee41..f5299f082 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -978,6 +978,16 @@ internal enum L10n { internal static var screenAdvancedSettingsSharePresence: String { return L10n.tr("Localizable", "screen_advanced_settings_share_presence") } /// If turned off, you won’t be able to send or receive read receipts or typing notifications. internal static var screenAdvancedSettingsSharePresenceDescription: String { return L10n.tr("Localizable", "screen_advanced_settings_share_presence_description") } + /// Always hide + internal static var screenAdvancedSettingsShowMediaTimelineAlwaysHide: String { return L10n.tr("Localizable", "screen_advanced_settings_show_media_timeline_always_hide") } + /// Always show + internal static var screenAdvancedSettingsShowMediaTimelineAlwaysShow: String { return L10n.tr("Localizable", "screen_advanced_settings_show_media_timeline_always_show") } + /// In private rooms + internal static var screenAdvancedSettingsShowMediaTimelinePrivateRooms: String { return L10n.tr("Localizable", "screen_advanced_settings_show_media_timeline_private_rooms") } + /// A hidden media can always be shown by tapping on it + internal static var screenAdvancedSettingsShowMediaTimelineSubtitle: String { return L10n.tr("Localizable", "screen_advanced_settings_show_media_timeline_subtitle") } + /// Show media in timeline + internal static var screenAdvancedSettingsShowMediaTimelineTitle: String { return L10n.tr("Localizable", "screen_advanced_settings_show_media_timeline_title") } /// Enable option to view message source in the timeline. internal static var screenAdvancedSettingsViewSourceDescription: String { return L10n.tr("Localizable", "screen_advanced_settings_view_source_description") } /// We won't record or profile any personal data @@ -1082,6 +1092,8 @@ internal enum L10n { internal static var screenBlockedUsersUnblockAlertTitle: String { return L10n.tr("Localizable", "screen_blocked_users_unblock_alert_title") } /// Unblocking… internal static var screenBlockedUsersUnblocking: String { return L10n.tr("Localizable", "screen_blocked_users_unblocking") } + /// Remove from room + internal static var screenBottomSheetManageRoomMemberRemove: String { return L10n.tr("Localizable", "screen_bottom_sheet,manage_room_member_remove") } /// Send invite internal static var screenBottomSheetCreateDmConfirmationButtonTitle: String { return L10n.tr("Localizable", "screen_bottom_sheet_create_dm_confirmation_button_title") } /// Would you like to start a chat with %1$@? @@ -1090,6 +1102,32 @@ internal enum L10n { } /// Send invite? internal static var screenBottomSheetCreateDmTitle: String { return L10n.tr("Localizable", "screen_bottom_sheet_create_dm_title") } + /// Ban from room + internal static var screenBottomSheetManageRoomMemberBan: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_ban") } + /// Ban + internal static var screenBottomSheetManageRoomMemberBanMemberConfirmationAction: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_ban_member_confirmation_action") } + /// They won’t be able to join this room again if invited. + internal static var screenBottomSheetManageRoomMemberBanMemberConfirmationDescription: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_ban_member_confirmation_description") } + /// Are you sure you want to ban this member? + internal static var screenBottomSheetManageRoomMemberBanMemberConfirmationTitle: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_ban_member_confirmation_title") } + /// Banning %1$@ + internal static func screenBottomSheetManageRoomMemberBanningUser(_ p1: Any) -> String { + return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_banning_user", String(describing: p1)) + } + /// Remove + internal static var screenBottomSheetManageRoomMemberKickMemberConfirmationAction: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_kick_member_confirmation_action") } + /// They will be able to join this room again if invited. + internal static var screenBottomSheetManageRoomMemberKickMemberConfirmationDescription: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_kick_member_confirmation_description") } + /// Are you sure you want to remove this member? + internal static var screenBottomSheetManageRoomMemberKickMemberConfirmationTitle: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_kick_member_confirmation_title") } + /// View profile + internal static var screenBottomSheetManageRoomMemberMemberUserInfo: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_member_user_info") } + /// Remove member and ban from joining in the future? + internal static var screenBottomSheetManageRoomMemberRemoveConfirmationTitle: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_remove_confirmation_title") } + /// Removing %1$@… + internal static func screenBottomSheetManageRoomMemberRemovingUser(_ p1: Any) -> String { + return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_removing_user", String(describing: p1)) + } /// Attach screenshot internal static var screenBugReportAttachScreenshot: String { return L10n.tr("Localizable", "screen_bug_report_attach_screenshot") } /// You may contact me if you have any follow up questions. @@ -2066,56 +2104,28 @@ internal enum L10n { internal static func screenRoomMemberDetailsVerifyButtonTitle(_ p1: Any) -> String { return L10n.tr("Localizable", "screen_room_member_details_verify_button_title", String(describing: p1)) } - /// Ban - internal static var screenRoomMemberListBanMemberConfirmationAction: String { return L10n.tr("Localizable", "screen_room_member_list_ban_member_confirmation_action") } - /// They won’t be able to join this room again if invited. - internal static var screenRoomMemberListBanMemberConfirmationDescription: String { return L10n.tr("Localizable", "screen_room_member_list_ban_member_confirmation_description") } - /// Are you sure you want to ban this member? - internal static var screenRoomMemberListBanMemberConfirmationTitle: String { return L10n.tr("Localizable", "screen_room_member_list_ban_member_confirmation_title") } /// There are no banned users in this room. internal static var screenRoomMemberListBannedEmpty: String { return L10n.tr("Localizable", "screen_room_member_list_banned_empty") } - /// 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) } - /// Remove - internal static var screenRoomMemberListKickMemberConfirmationAction: String { return L10n.tr("Localizable", "screen_room_member_list_kick_member_confirmation_action") } - /// They will be able to join this room again if invited. - internal static var screenRoomMemberListKickMemberConfirmationDescription: String { return L10n.tr("Localizable", "screen_room_member_list_kick_member_confirmation_description") } - /// Are you sure you want to remove this member? - internal static var screenRoomMemberListKickMemberConfirmationTitle: String { return L10n.tr("Localizable", "screen_room_member_list_kick_member_confirmation_title") } - /// Remove and ban member - internal static var screenRoomMemberListManageMemberBan: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_ban") } - /// Remove from room - internal static var screenRoomMemberListManageMemberRemove: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_remove") } - /// Remove and ban member + /// Ban from room internal static var screenRoomMemberListManageMemberRemoveConfirmationBan: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_remove_confirmation_ban") } /// Only remove member internal static var screenRoomMemberListManageMemberRemoveConfirmationKick: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_remove_confirmation_kick") } - /// 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 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") } - /// View profile - internal static var screenRoomMemberListManageMemberUserInfo: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_user_info") } /// Banned internal static var screenRoomMemberListModeBanned: String { return L10n.tr("Localizable", "screen_room_member_list_mode_banned") } /// Members internal static var screenRoomMemberListModeMembers: String { return L10n.tr("Localizable", "screen_room_member_list_mode_members") } /// Pending internal static var screenRoomMemberListPendingHeaderTitle: String { return L10n.tr("Localizable", "screen_room_member_list_pending_header_title") } - /// Removing %1$@… - internal static func screenRoomMemberListRemovingUser(_ p1: Any) -> String { - return L10n.tr("Localizable", "screen_room_member_list_removing_user", String(describing: p1)) - } /// Admin internal static var screenRoomMemberListRoleAdministrator: String { return L10n.tr("Localizable", "screen_room_member_list_role_administrator") } /// Moderator @@ -2860,15 +2870,19 @@ internal enum L10n { } /// Current push provider internal static var troubleshootNotificationsTestCurrentPushProviderTitle: String { return L10n.tr("Localizable", "troubleshoot_notifications_test_current_push_provider_title") } - /// Ensure that the application has at least one push provider. + /// Ensure that the application supports at least one push provider. internal static var troubleshootNotificationsTestDetectPushProviderDescription: String { return L10n.tr("Localizable", "troubleshoot_notifications_test_detect_push_provider_description") } - /// No push providers found. + /// No push provider support found. internal static var troubleshootNotificationsTestDetectPushProviderFailure: String { return L10n.tr("Localizable", "troubleshoot_notifications_test_detect_push_provider_failure") } /// Plural format key: "%#@COUNT@" internal static func troubleshootNotificationsTestDetectPushProviderSuccess(_ p1: Int) -> String { return L10n.tr("Localizable", "troubleshoot_notifications_test_detect_push_provider_success", p1) } - /// Detect push providers + /// The application was built with support for: %1$@ + internal static func troubleshootNotificationsTestDetectPushProviderSuccess2(_ p1: Any) -> String { + return L10n.tr("Localizable", "troubleshoot_notifications_test_detect_push_provider_success_2", String(describing: p1)) + } + /// Push provider support internal static var troubleshootNotificationsTestDetectPushProviderTitle: String { return L10n.tr("Localizable", "troubleshoot_notifications_test_detect_push_provider_title") } /// Check that the application can display notification. internal static var troubleshootNotificationsTestDisplayNotificationDescription: String { return L10n.tr("Localizable", "troubleshoot_notifications_test_display_notification_description") } diff --git a/ElementX/Sources/Screens/ManageRoomMemberSheet/ManageRoomMemberSheetModels.swift b/ElementX/Sources/Screens/ManageRoomMemberSheet/ManageRoomMemberSheetModels.swift new file mode 100644 index 000000000..928fb0083 --- /dev/null +++ b/ElementX/Sources/Screens/ManageRoomMemberSheet/ManageRoomMemberSheetModels.swift @@ -0,0 +1,33 @@ +// +// Copyright 2025 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. +// + +enum ManageRoomMemberSheetViewModelAction: Equatable { + case dismiss(shouldShowDetails: Bool) +} + +struct ManageRoomMemberSheetViewState: BindableState { + let member: RoomMemberDetails + let canKick: Bool + let canBan: Bool + + var bindings = ManageRoomMemberSheetViewStateBindings() +} + +struct ManageRoomMemberSheetViewStateBindings { + var alertInfo: AlertInfo? +} + +enum ManageRoomMemberSheetViewAlertType { + case kick + case ban +} + +enum ManageRoomMemberSheetViewAction { + case kick + case ban + case displayDetails +} diff --git a/ElementX/Sources/Screens/ManageRoomMemberSheet/ManageRoomMemberSheetViewModel.swift b/ElementX/Sources/Screens/ManageRoomMemberSheet/ManageRoomMemberSheetViewModel.swift new file mode 100644 index 000000000..2bdcd629e --- /dev/null +++ b/ElementX/Sources/Screens/ManageRoomMemberSheet/ManageRoomMemberSheetViewModel.swift @@ -0,0 +1,125 @@ +// +// Copyright 2025 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. +// + +import Combine +import Foundation +import SwiftUI + +typealias ManageRoomMemberSheetViewModelType = StateStoreViewModel + +class ManageRoomMemberSheetViewModel: ManageRoomMemberSheetViewModelType, ManageRoomMemberSheetViewModelProtocol { + private let roomProxy: JoinedRoomProxyProtocol + private let userIndicatorController: UserIndicatorControllerProtocol + private let analyticsService: AnalyticsService + + private var actionsSubject: PassthroughSubject = .init() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(member: RoomMemberDetails, + canKick: Bool, + canBan: Bool, + roomProxy: JoinedRoomProxyProtocol, + userIndicatorController: UserIndicatorControllerProtocol, + analyticsService: AnalyticsService, + mediaProvider: MediaProviderProtocol) { + self.userIndicatorController = userIndicatorController + self.roomProxy = roomProxy + self.analyticsService = analyticsService + super.init(initialViewState: .init(member: member, canKick: canKick, canBan: canBan), mediaProvider: mediaProvider) + } + + override func process(viewAction: ManageRoomMemberSheetViewAction) { + switch viewAction { + case .kick: + displayAlert(.kick) + case .ban: + displayAlert(.ban) + case .displayDetails: + actionsSubject.send(.dismiss(shouldShowDetails: true)) + } + } + + private func displayAlert(_ alertType: ManageRoomMemberSheetViewAlertType) { + let member = state.member + var reason: String? + let binding: Binding = .init(get: { reason ?? "" }, + set: { reason = $0.isBlank ? nil : $0 }) + switch alertType { + case .kick: + state.bindings.alertInfo = .init(id: alertType, + title: L10n.screenBottomSheetManageRoomMemberKickMemberConfirmationTitle, + message: L10n.screenBottomSheetManageRoomMemberKickMemberConfirmationDescription, + primaryButton: .init(title: L10n.actionCancel, role: .cancel) { }, + secondaryButton: .init(title: L10n.screenBottomSheetManageRoomMemberKickMemberConfirmationAction) { [weak self] in Task { await self?.kickMember(member, reason: reason) } }, + textFields: [.init(placeholder: L10n.commonReason, + text: binding, + autoCapitalization: .sentences, + autoCorrectionDisabled: false)]) + case .ban: + state.bindings.alertInfo = .init(id: alertType, + title: L10n.screenBottomSheetManageRoomMemberBanMemberConfirmationTitle, + message: L10n.screenBottomSheetManageRoomMemberBanMemberConfirmationDescription, + primaryButton: .init(title: L10n.actionCancel, role: .cancel) { }, + secondaryButton: .init(title: L10n.screenBottomSheetManageRoomMemberBanMemberConfirmationAction) { [weak self] in Task { await self?.banMember(member, reason: reason) } }, + textFields: [.init(placeholder: L10n.commonReason, + text: binding, + autoCapitalization: .sentences, + autoCorrectionDisabled: false)]) + } + } + + private func kickMember(_ member: RoomMemberDetails, reason: String?) async { + let indicatorTitle = L10n.screenBottomSheetManageRoomMemberRemovingUser(member.name ?? member.id) + showManageMemberIndicator(title: indicatorTitle) + + switch await roomProxy.kickUser(member.id, reason: reason) { + case .success: + hideManageMemberIndicator(title: indicatorTitle) + analyticsService.trackRoomModeration(action: .KickMember, role: nil) + actionsSubject.send(.dismiss(shouldShowDetails: false)) + case .failure: + showManageMemberFailure(title: indicatorTitle) + } + } + + private func banMember(_ member: RoomMemberDetails, reason: String?) async { + let indicatorTitle = L10n.screenBottomSheetManageRoomMemberBanningUser(member.name ?? member.id) + showManageMemberIndicator(title: indicatorTitle) + + switch await roomProxy.banUser(member.id, reason: reason) { + case .success: + hideManageMemberIndicator(title: indicatorTitle) + analyticsService.trackRoomModeration(action: .BanMember, role: nil) + actionsSubject.send(.dismiss(shouldShowDetails: false)) + case .failure: + showManageMemberFailure(title: indicatorTitle) + } + } + + private func showManageMemberIndicator(title: String) { + userIndicatorController.submitIndicator(UserIndicator(id: title, + type: .toast(progress: .indeterminate), + 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")) + } +} + +extension ManageRoomMemberSheetViewModel: Identifiable { + var id: String { state.member.id } +} diff --git a/ElementX/Sources/Screens/ManageRoomMemberSheet/ManageRoomMemberSheetViewModelProtocol.swift b/ElementX/Sources/Screens/ManageRoomMemberSheet/ManageRoomMemberSheetViewModelProtocol.swift new file mode 100644 index 000000000..20c7a37f3 --- /dev/null +++ b/ElementX/Sources/Screens/ManageRoomMemberSheet/ManageRoomMemberSheetViewModelProtocol.swift @@ -0,0 +1,14 @@ +// +// Copyright 2025 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. +// + +import Combine + +@MainActor +protocol ManageRoomMemberSheetViewModelProtocol { + var actions: AnyPublisher { get } + var context: ManageRoomMemberSheetViewModelType.Context { get } +} diff --git a/ElementX/Sources/Screens/ManageRoomMemberSheet/View/ManageRoomMemberSheetView.swift b/ElementX/Sources/Screens/ManageRoomMemberSheet/View/ManageRoomMemberSheetView.swift new file mode 100644 index 000000000..9c6a5e46a --- /dev/null +++ b/ElementX/Sources/Screens/ManageRoomMemberSheet/View/ManageRoomMemberSheetView.swift @@ -0,0 +1,87 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. +// + +import Compound +import SwiftUI + +struct ManageRoomMemberSheetView: View { + @ObservedObject var context: ManageRoomMemberSheetViewModelType.Context + + var body: some View { + Form { + AvatarHeaderView(member: context.viewState.member, + avatarSize: .user(on: .memberDetails), + mediaProvider: context.mediaProvider) { + EmptyView() + } + + Section { + ListRow(label: .default(title: L10n.screenBottomSheetManageRoomMemberMemberUserInfo, + icon: \.userProfileSolid), + kind: .navigationLink { + context.send(viewAction: .displayDetails) + }) + } + + Section { + if context.viewState.canKick { + ListRow(label: .default(title: L10n.screenBottomSheetManageRoomMemberRemove, + icon: \.close, + role: .destructive), + kind: .button { + context.send(viewAction: .kick) + }) + } + + if context.viewState.canBan { + ListRow(label: .default(title: L10n.screenBottomSheetManageRoomMemberBan, + icon: \.block, + role: .destructive), + kind: .button { + context.send(viewAction: .ban) + }) + } + } + } + .compoundList() + .scrollBounceBehavior(.basedOnSize) + .presentationDragIndicator(.visible) + .presentationDetents([.large, .fraction(0.67)]) // Maybe find a way to use the ideal height somehow? + .alert(item: $context.alertInfo) + } +} + +struct ManageRoomMemberSheetView_Previews: PreviewProvider, TestablePreview { + static let allActionsViewModel = ManageRoomMemberSheetViewModel.mock() + + static let kickOnlyViewModel = ManageRoomMemberSheetViewModel.mock(canBan: false) + + static let banOnlyViewModel = ManageRoomMemberSheetViewModel.mock(canKick: false) + + static var previews: some View { + ManageRoomMemberSheetView(context: allActionsViewModel.context) + .previewDisplayName("All Actions") + ManageRoomMemberSheetView(context: kickOnlyViewModel.context) + .previewDisplayName("Kick Only") + ManageRoomMemberSheetView(context: banOnlyViewModel.context) + .previewDisplayName("Ban Only") + } +} + +private extension ManageRoomMemberSheetViewModel { + static func mock(canKick: Bool = true, + canBan: Bool = true) -> ManageRoomMemberSheetViewModel { + let member = RoomMemberDetails(withProxy: RoomMemberProxyMock.mockDan) + return ManageRoomMemberSheetViewModel(member: member, + canKick: canKick, + canBan: canBan, + roomProxy: JoinedRoomProxyMock(.init()), + userIndicatorController: UserIndicatorControllerMock(), + analyticsService: ServiceLocator.shared.analytics, + mediaProvider: MediaProviderMock(configuration: .init())) + } +} diff --git a/ElementX/Sources/Screens/MediaEventsTimelineScreen/MediaEventsTimelineScreenViewModel.swift b/ElementX/Sources/Screens/MediaEventsTimelineScreen/MediaEventsTimelineScreenViewModel.swift index bca3a031f..ff19c183c 100644 --- a/ElementX/Sources/Screens/MediaEventsTimelineScreen/MediaEventsTimelineScreenViewModel.swift +++ b/ElementX/Sources/Screens/MediaEventsTimelineScreen/MediaEventsTimelineScreenViewModel.swift @@ -80,7 +80,7 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType self?.displayMediaPreview(mediaPreviewViewModel) case .displayEmojiPicker, .displayReportContent, .displayCameraPicker, .displayMediaPicker, .displayDocumentPicker, .displayLocationPicker, .displayPollForm, .displayMediaUploadPreviewScreen, - .tappedOnSenderDetails, .displayMessageForwarding, .displayLocation, .displayResolveSendFailure, + .displaySenderDetails, .displayMessageForwarding, .displayLocation, .displayResolveSendFailure, .composer, .hasScrolled, .viewInRoomTimeline: break } @@ -102,7 +102,7 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType self?.displayMediaPreview(mediaPreviewViewModel) case .displayEmojiPicker, .displayReportContent, .displayCameraPicker, .displayMediaPicker, .displayDocumentPicker, .displayLocationPicker, .displayPollForm, .displayMediaUploadPreviewScreen, - .tappedOnSenderDetails, .displayMessageForwarding, .displayLocation, .displayResolveSendFailure, + .displaySenderDetails, .displayMessageForwarding, .displayLocation, .displayResolveSendFailure, .composer, .hasScrolled, .viewInRoomTimeline: break } diff --git a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/PinnedEventsTimelineScreenCoordinator.swift b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/PinnedEventsTimelineScreenCoordinator.swift index 133d78368..61ea85487 100644 --- a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/PinnedEventsTimelineScreenCoordinator.swift +++ b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/PinnedEventsTimelineScreenCoordinator.swift @@ -78,7 +78,7 @@ final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol { guard let self else { return } switch action { - case .tappedOnSenderDetails(let userID): + case .displaySenderDetails(let userID): actionsSubject.send(.displayUser(userID: userID)) case .displayMessageForwarding(let forwardingItem): actionsSubject.send(.displayMessageForwarding(forwardingItem: forwardingItem)) diff --git a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift index 185c85be0..3a8935952 100644 --- a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift +++ b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift @@ -28,6 +28,9 @@ struct PinnedEventsTimelineScreen: View { .background(.compound.bgCanvasDefault) .interactiveDismissDisabled() .timelineMediaPreview(viewModel: $context.mediaPreviewViewModel) + .sheet(item: $timelineContext.manageMemberViewModel) { + ManageRoomMemberSheetView(context: $0.context) + } .sheet(item: $timelineContext.debugInfo) { TimelineItemDebugView(info: $0) } .sheet(item: $timelineContext.actionMenuInfo) { info in let actions = TimelineItemMenuActionProvider(timelineItem: info.item, diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenModels.swift b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenModels.swift index 5f15e3c66..8dda1f39b 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenModels.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenModels.swift @@ -80,31 +80,15 @@ 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: RoomMembersListScreenManagementDetails? + /// A sheet model for the selected member to kick, ban, promote etc. + var manageMemeberViewModel: ManageRoomMemberSheetViewModel? /// Information describing the currently displayed alert. var alertInfo: AlertInfo? } -/// Information about managing a particular room member. -struct RoomMembersListScreenManagementDetails: Identifiable { - var id: String { member.id } - - /// The member that is being managed. - let member: RoomMemberDetails - - /// A management action that can be performed on the member. - enum Action { case kick, ban } - /// The management actions available for `member`. - let actions: [Action] -} - enum RoomMembersListScreenViewAction { case selectMember(RoomMemberDetails) - case showMemberDetails(RoomMemberDetails) - case kickMember(RoomMemberDetails) - case banMember(RoomMemberDetails) case unbanMember(RoomMemberDetails) case invite } diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift index e17a2cded..1b3b17c42 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift @@ -15,8 +15,10 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe private let roomProxy: JoinedRoomProxyProtocol private let userIndicatorController: UserIndicatorControllerProtocol private let analytics: AnalyticsService + private let mediaProvider: MediaProviderProtocol private var members: [RoomMemberProxyProtocol] = [] + private var currentUserProxy: RoomMemberProxyProtocol? private var actionsSubject: PassthroughSubject = .init() @@ -34,6 +36,7 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe self.roomProxy = roomProxy self.userIndicatorController = userIndicatorController self.analytics = analytics + self.mediaProvider = mediaProvider super.init(initialViewState: .init(joinedMembersCount: roomProxy.infoPublisher.value.joinedMembersCount, bindings: .init(mode: initialMode)), @@ -48,34 +51,6 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe switch viewAction { case .selectMember(let member): selectMember(member) - case .showMemberDetails(let member): - showMemberDetails(member) - case .kickMember(let member): - var reason: String? - let binding: Binding = .init(get: { reason ?? "" }, - set: { reason = $0.isBlank ? nil : $0 }) - state.bindings.alertInfo = .init(id: .kickConfirmation, - title: L10n.screenRoomMemberListKickMemberConfirmationTitle, - message: L10n.screenRoomMemberListKickMemberConfirmationDescription, - primaryButton: .init(title: L10n.actionCancel, role: .cancel) { }, - secondaryButton: .init(title: L10n.screenRoomMemberListKickMemberConfirmationAction) { [weak self] in Task { await self?.kickMember(member, reason: reason) } }, - textFields: [.init(placeholder: L10n.commonReason, - text: binding, - autoCapitalization: .sentences, - autoCorrectionDisabled: false)]) - case .banMember(let member): - var reason: String? - let binding: Binding = .init(get: { reason ?? "" }, - set: { reason = $0.isBlank ? nil : $0 }) - state.bindings.alertInfo = .init(id: .banConfirmation, - title: L10n.screenRoomMemberListBanMemberConfirmationTitle, - message: L10n.screenRoomMemberListBanMemberConfirmationDescription, - primaryButton: .init(title: L10n.actionCancel, role: .cancel) { }, - secondaryButton: .init(title: L10n.screenRoomMemberListBanMemberConfirmationAction) { [weak self] in Task { await self?.banMember(member, reason: reason) } }, - textFields: [.init(placeholder: L10n.commonReason, - text: binding, - autoCapitalization: .sentences, - autoCorrectionDisabled: false)]) case .unbanMember(let member): Task { await unbanMember(member) } case .invite: @@ -118,6 +93,7 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe let members = members.sorted() let roomMembersDetails = await buildMembersDetails(members: members) self.members = members + self.currentUserProxy = members.first { $0.userID == roomProxy.ownUserID } self.state = .init(joinedMembersCount: roomProxy.infoPublisher.value.joinedMembersCount, joinedMembers: roomMembersDetails.joinedMembers, @@ -168,6 +144,12 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe } private func selectMember(_ member: RoomMemberDetails) { + guard let currentUserProxy, + currentUserProxy.powerLevel > member.powerLevel else { + showMemberDetails(member) + return + } + if member.isBanned { // No need to check canBan here, banned users are only shown when it is true. state.bindings.alertInfo = AlertInfo(id: .unbanConfirmation(member), title: L10n.screenRoomMemberListManageMemberUnbanTitle, @@ -179,16 +161,26 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe return } - var actions = [RoomMembersListScreenManagementDetails.Action]() - if state.canKickUsers, member.role != .administrator { - actions.append(.kick) - } - if state.canBanUsers, member.role != .administrator { - actions.append(.ban) - } - - if !actions.isEmpty { - state.bindings.memberToManage = .init(member: member, actions: actions) + if state.canKickUsers || state.canBanUsers { + let manageMemeberViewModel = ManageRoomMemberSheetViewModel(member: member, + canKick: state.canKickUsers, + canBan: state.canBanUsers, + roomProxy: roomProxy, + userIndicatorController: userIndicatorController, + analyticsService: analytics, + mediaProvider: mediaProvider) + manageMemeberViewModel.actions.sink { [weak self] action in + guard let self else { return } + switch action { + case .dismiss(let shouldShowDetails): + state.bindings.manageMemeberViewModel = nil + if shouldShowDetails { + showMemberDetails(member) + } + } + } + .store(in: &cancellables) + state.bindings.manageMemeberViewModel = manageMemeberViewModel } else { showMemberDetails(member) } @@ -200,46 +192,16 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe return } actionsSubject.send(.selectMember(member)) - state.bindings.memberToManage = nil } // MARK: - Member Management - private func kickMember(_ member: RoomMemberDetails, reason: String?) async { - let indicatorTitle = L10n.screenRoomMemberListRemovingUser(member.name ?? member.id) - showManageMemberIndicator(title: indicatorTitle) - - switch await roomProxy.kickUser(member.id, reason: reason) { - case .success: - state.bindings.memberToManage = nil - hideManageMemberIndicator(title: indicatorTitle) - analytics.trackRoomModeration(action: .KickMember, role: nil) - case .failure: - showManageMemberFailure(title: indicatorTitle) - } - } - - private func banMember(_ member: RoomMemberDetails, reason: String?) async { - let indicatorTitle = L10n.screenRoomMemberListBanningUser(member.name ?? member.id) - showManageMemberIndicator(title: indicatorTitle) - - switch await roomProxy.banUser(member.id, reason: reason) { - case .success: - state.bindings.memberToManage = nil - hideManageMemberIndicator(title: indicatorTitle) - analytics.trackRoomModeration(action: .BanMember, role: nil) - 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) analytics.trackRoomModeration(action: .UnbanMember, role: nil) case .failure: diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListManageMemberSheet.swift b/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListManageMemberSheet.swift deleted file mode 100644 index 7632c9099..000000000 --- a/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListManageMemberSheet.swift +++ /dev/null @@ -1,96 +0,0 @@ -// -// Copyright 2024 New Vector Ltd. -// -// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial -// Please see LICENSE files in the repository root for full details. -// - -import Compound -import SwiftUI - -struct RoomMembersListManageMemberSheet: View { - let member: RoomMemberDetails - let actions: [RoomMembersListScreenManagementDetails.Action] - - @ObservedObject var context: RoomMembersListScreenViewModel.Context - - var body: some View { - Form { - AvatarHeaderView(member: member, - avatarSize: .user(on: .memberDetails), - mediaProvider: context.mediaProvider) { - EmptyView() - } - - Section { - ListRow(label: .default(title: L10n.screenRoomMemberListManageMemberUserInfo, - icon: \.userProfileSolid), - kind: .button { - context.send(viewAction: .showMemberDetails(member)) - }) - - if actions.contains(.kick) { - ListRow(label: .default(title: L10n.screenRoomMemberListManageMemberRemove, - icon: \.close), - kind: .button { - context.send(viewAction: .kickMember(member)) - }) - } - - if actions.contains(.ban) { - ListRow(label: .default(title: L10n.screenRoomMemberListManageMemberBan, - icon: \.block, - role: .destructive), - kind: .button { - context.send(viewAction: .banMember(member)) - }) - } - } - } - .compoundList() - .scrollBounceBehavior(.basedOnSize) - .presentationDragIndicator(.visible) - .presentationDetents([.large, .fraction(0.54)]) // Maybe find a way to use the ideal height somehow? - } -} - -struct RoomMembersListManageMemberSheet_Previews: PreviewProvider, TestablePreview { - static let viewModel = RoomMembersListScreenViewModel.mock - - static var previews: some View { - RoomMembersListManageMemberSheet(member: .init(withProxy: RoomMemberProxyMock.mockDan), - actions: [.kick, .ban], - context: viewModel.context) - .previewDisplayName("Joined") - - RoomMembersListManageMemberSheet(member: .init(withProxy: RoomMemberProxyMock.mockBanned[3]), - actions: [], - 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), - actions: [.kick, .ban], - context: viewModel.context) - } - .previewDisplayName("Sheet") - } -} - -private extension RoomMembersListScreenViewModel { - static var mock: RoomMembersListScreenViewModel { - RoomMembersListScreenViewModel(initialMode: .members, - clientProxy: ClientProxyMock(.init()), - roomProxy: JoinedRoomProxyMock(.init(members: .allMembersAsAdmin)), - mediaProvider: MediaProviderMock(configuration: .init()), - userIndicatorController: ServiceLocator.shared.userIndicatorController, - analytics: ServiceLocator.shared.analytics) - } -} diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreen.swift b/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreen.swift index 216ba778d..308df1b64 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreen.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreen.swift @@ -47,8 +47,8 @@ struct RoomMembersListScreen: View { .autocorrectionDisabled() .background(Color.compound.bgCanvasDefault.ignoresSafeArea()) .navigationTitle(L10n.commonPeople) - .sheet(item: $context.memberToManage) { - RoomMembersListManageMemberSheet(member: $0.member, actions: $0.actions, context: context) + .sheet(item: $context.manageMemeberViewModel) { + ManageRoomMemberSheetView(context: $0.context) } .alert(item: $context.alertInfo) .toolbar { toolbar } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift index d14fbebd3..f2a2c9e02 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift @@ -136,7 +136,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol { actionsSubject.send(.presentPollForm(mode: mode)) case .displayMediaUploadPreviewScreen(let url): actionsSubject.send(.presentMediaUploadPreviewScreen(url)) - case .tappedOnSenderDetails(userID: let userID): + case .displaySenderDetails(userID: let userID): actionsSubject.send(.presentRoomMemberDetails(userID: userID)) case .displayMessageForwarding(let forwardingItem): actionsSubject.send(.presentMessageForwarding(forwardingItem: forwardingItem)) diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift index 4b7659849..116b53280 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -66,6 +66,9 @@ struct RoomScreen: View { .toolbarBackground(.visible, for: .navigationBar) // Fix the toolbar's background. .overlay { loadingIndicator } .alert(item: $timelineContext.alertInfo) + .sheet(item: $timelineContext.manageMemberViewModel) { + ManageRoomMemberSheetView(context: $0.context) + } .sheet(item: $timelineContext.debugInfo) { TimelineItemDebugView(info: $0) } .sheet(item: $timelineContext.actionMenuInfo) { info in let actions = TimelineItemMenuActionProvider(timelineItem: info.item, diff --git a/ElementX/Sources/Screens/Timeline/TimelineModels.swift b/ElementX/Sources/Screens/Timeline/TimelineModels.swift index 8f07b25f3..a1cddae55 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineModels.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineModels.swift @@ -19,7 +19,7 @@ enum TimelineViewModelAction { case displayLocationPicker case displayPollForm(mode: PollFormMode) case displayMediaUploadPreviewScreen(url: URL) - case tappedOnSenderDetails(userID: String) + case displaySenderDetails(userID: String) case displayMessageForwarding(forwardingItem: MessageForwardingItem) case displayMediaPreview(TimelineMediaPreviewViewModel) case displayLocation(body: String, geoURI: GeoURI, description: String?) @@ -99,6 +99,8 @@ struct TimelineViewState: BindableState { var canCurrentUserRedactOthers = false var canCurrentUserRedactSelf = false var canCurrentUserPin = false + var canCurrentUserKick = false + var canCurrentUserBan = false var isViewSourceEnabled: Bool var hideTimelineMedia: Bool @@ -135,7 +137,7 @@ struct TimelineViewStateBindings { /// Key is itemID, value is the collapsed state. var reactionsCollapsed: [TimelineItemIdentifier: Bool] - var alertInfo: AlertInfo? + var alertInfo: AlertInfo? var debugInfo: TimelineItemDebugInfo? @@ -144,6 +146,8 @@ struct TimelineViewStateBindings { var reactionSummaryInfo: ReactionSummaryInfo? var readReceiptsSummaryInfo: ReadReceiptSummaryInfo? + + var manageMemberViewModel: ManageRoomMemberSheetViewModel? } struct TimelineItemActionMenuInfo: Equatable, Identifiable { @@ -172,7 +176,7 @@ struct ReadReceiptSummaryInfo: Identifiable { let id: TimelineItemIdentifier } -enum RoomScreenAlertInfoType: Hashable { +enum TimelineAlertInfoType: Hashable { case audioRecodingPermissionError case pollEndConfirmation(String) case sendingFailed diff --git a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift index c8a75c07a..d18223410 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift @@ -42,6 +42,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { actionsSubject.eraseToAnyPublisher() } + private var currentUserProxy: RoomMemberProxyProtocol? + private var paginateBackwardsTask: Task? private var paginateForwardsTask: Task? @@ -173,7 +175,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { case .handleTimelineItemMenuAction(let itemID, let action): timelineInteractionHandler.handleTimelineItemMenuAction(action, itemID: itemID) case .tappedOnSenderDetails(userID: let userID): - actionsSubject.send(.tappedOnSenderDetails(userID: userID)) + handleTappedOnSenderDetails(userID: userID) case .displayEmojiPicker(let itemID): timelineInteractionHandler.displayEmojiPicker(for: itemID) case .displayReactionSummary(let itemID, let key): @@ -258,6 +260,41 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { // MARK: - Private + private func handleTappedOnSenderDetails(userID: String) { + // We also need to make sure the user is in the joined state, otherwise we just show the details. + guard let memberProxy = roomProxy.membersPublisher.value.first(where: { $0.userID == userID && $0.membership == .join }), + let currentUserProxy, + currentUserProxy.powerLevel > memberProxy.powerLevel else { + actionsSubject.send(.displaySenderDetails(userID: userID)) + return + } + + if state.canCurrentUserBan || state.canCurrentUserKick { + let member = RoomMemberDetails(withProxy: memberProxy) + let manageMemeberViewModel = ManageRoomMemberSheetViewModel(member: member, + canKick: state.canCurrentUserKick, + canBan: state.canCurrentUserBan, + roomProxy: roomProxy, + userIndicatorController: userIndicatorController, + analyticsService: analyticsService, + mediaProvider: mediaProvider) + manageMemeberViewModel.actions.sink { [weak self] action in + guard let self else { return } + switch action { + case .dismiss(let shouldShowDetails): + state.bindings.manageMemberViewModel = nil + if shouldShowDetails { + actionsSubject.send(.displaySenderDetails(userID: userID)) + } + } + } + .store(in: &cancellables) + state.bindings.manageMemberViewModel = manageMemeberViewModel + } else { + actionsSubject.send(.displaySenderDetails(userID: userID)) + } + } + private func focusLive() { timelineController.focusLive() } @@ -348,6 +385,9 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { private func updateMembers(_ members: [RoomMemberProxyProtocol]) { state.members = members.reduce(into: [String: RoomMemberState]()) { dictionary, member in dictionary[member.userID] = RoomMemberState(displayName: member.displayName, avatarURL: member.avatarURL) + if member.userID == roomProxy.ownUserID { + currentUserProxy = member + } } } @@ -369,6 +409,18 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { } else { state.canCurrentUserPin = false } + + if case let .success(value) = await roomProxy.canUserKick(userID: roomProxy.ownUserID) { + state.canCurrentUserKick = value + } else { + state.canCurrentUserKick = false + } + + if case let .success(value) = await roomProxy.canUserBan(userID: roomProxy.ownUserID) { + state.canCurrentUserBan = value + } else { + state.canCurrentUserBan = false + } } private func setupSubscriptions() { @@ -898,7 +950,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { userIndicatorController.retractIndicatorWithId(Constants.focusTimelineToastIndicatorID) } - private func displayAlert(_ type: RoomScreenAlertInfoType) { + private func displayAlert(_ type: TimelineAlertInfoType) { switch type { case .audioRecodingPermissionError: state.bindings.alertInfo = .init(id: type, diff --git a/ElementX/Sources/Services/Room/RoomMember/RoomMemberDetails.swift b/ElementX/Sources/Services/Room/RoomMember/RoomMemberDetails.swift index b76cb1d2b..9f8918586 100644 --- a/ElementX/Sources/Services/Room/RoomMember/RoomMemberDetails.swift +++ b/ElementX/Sources/Services/Room/RoomMember/RoomMemberDetails.swift @@ -20,6 +20,7 @@ struct RoomMemberDetails: Identifiable, Hashable { enum Role { case administrator, moderator, user } let role: Role + let powerLevel: Int func matches(searchQuery: String) -> Bool { guard !searchQuery.isEmpty else { return true } @@ -38,6 +39,7 @@ extension RoomMemberDetails { isIgnored = proxy.isIgnored isBanned = proxy.membership == .ban role = .init(proxy.role) + powerLevel = proxy.powerLevel } } diff --git a/PreviewTests/Sources/GeneratedPreviewTests.swift b/PreviewTests/Sources/GeneratedPreviewTests.swift index 537e3136d..bec7294ef 100644 --- a/PreviewTests/Sources/GeneratedPreviewTests.swift +++ b/PreviewTests/Sources/GeneratedPreviewTests.swift @@ -401,6 +401,12 @@ extension PreviewTests { } } + func testManageRoomMemberSheetView() async throws { + for (index, preview) in ManageRoomMemberSheetView_Previews._allPreviews.enumerated() { + try await assertSnapshots(matching: preview, step: index) + } + } + func testMapLibreStaticMapView() async throws { for (index, preview) in MapLibreStaticMapView_Previews._allPreviews.enumerated() { try await assertSnapshots(matching: preview, step: index) @@ -707,12 +713,6 @@ extension PreviewTests { } } - func testRoomMembersListManageMemberSheet() async throws { - for (index, preview) in RoomMembersListManageMemberSheet_Previews._allPreviews.enumerated() { - try await assertSnapshots(matching: preview, step: index) - } - } - func testRoomMembersListMemberCell() async throws { for (index, preview) in RoomMembersListMemberCell_Previews._allPreviews.enumerated() { try await assertSnapshots(matching: preview, step: index) diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.All-Actions-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.All-Actions-iPad-en-GB.png new file mode 100644 index 000000000..00f17a84c --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.All-Actions-iPad-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:838a77d7faa429fb769012c1361ddb6ec96aef85fbd3cbf913fdb934e6ed639d +size 195727 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.All-Actions-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.All-Actions-iPad-pseudo.png new file mode 100644 index 000000000..a522d9b5c --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.All-Actions-iPad-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9708620c69e77bc74e461e5a1f5bf5bc843781fe124bfaf3eaa2b24ff8020c64 +size 197751 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.All-Actions-iPhone-16-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.All-Actions-iPhone-16-en-GB.png new file mode 100644 index 000000000..79ecfa20d --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.All-Actions-iPhone-16-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b36e8a40c54853a34366ba352736da206a6659ae9291bba3d9f338da44f26f0a +size 135362 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.All-Actions-iPhone-16-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.All-Actions-iPhone-16-pseudo.png new file mode 100644 index 000000000..21b619f38 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.All-Actions-iPhone-16-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1569bcd5c6e626315326b6275b6d069327af935ba9f8971aea27878be26beb8f +size 140550 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Ban-Only-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Ban-Only-iPad-en-GB.png new file mode 100644 index 000000000..b51f5fa19 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Ban-Only-iPad-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f6d2a023376318b8d75646ac54d71117cd94f5b5b679747a550f9145d450e11 +size 190338 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Ban-Only-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Ban-Only-iPad-pseudo.png new file mode 100644 index 000000000..22f7b365d --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Ban-Only-iPad-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3053a7e72c09435efb95e99e6c5ade86c7aa2b186f37c2b3a23207113bd0740 +size 191560 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Ban-Only-iPhone-16-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Ban-Only-iPhone-16-en-GB.png new file mode 100644 index 000000000..97286bda1 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Ban-Only-iPhone-16-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:94273ef44799d1d926c96451b8c93f41a4262300691f3dc61b93997b8ff49c02 +size 129508 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Ban-Only-iPhone-16-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Ban-Only-iPhone-16-pseudo.png new file mode 100644 index 000000000..c931d0ec5 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Ban-Only-iPhone-16-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:034aba123a2109820e7d15627ba68826769032a2de2045be4ee0bbdf8e12d76c +size 131928 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Kick-Only-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Kick-Only-iPad-en-GB.png new file mode 100644 index 000000000..16fb5c16c --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Kick-Only-iPad-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7a02e69172fc903b7efb96f4f9e9133a95b6c4db94004ca41b65f07279497e92 +size 189632 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Kick-Only-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Kick-Only-iPad-pseudo.png new file mode 100644 index 000000000..94e44da29 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Kick-Only-iPad-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e215c8f632a632fb1f09d98e98fef71fe8fcf94503492078e60a929f4cea6946 +size 191198 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Kick-Only-iPhone-16-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Kick-Only-iPhone-16-en-GB.png new file mode 100644 index 000000000..19d2d4240 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Kick-Only-iPhone-16-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:680f38ab808dcfd019e0e925ef187033036ff26287cdc495c6242563ed0466fa +size 129401 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Kick-Only-iPhone-16-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Kick-Only-iPhone-16-pseudo.png new file mode 100644 index 000000000..8cca553eb --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/manageRoomMemberSheetView.Kick-Only-iPhone-16-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4d4cfc4fe5d65b69f8930427a54fc55b83aa3f38dc15978c61f79f12776387e2 +size 134646 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomMembersListManageMemberSheet.Banned-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomMembersListManageMemberSheet.Banned-iPad-en-GB.png deleted file mode 100644 index ca2809f56..000000000 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomMembersListManageMemberSheet.Banned-iPad-en-GB.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:69d3907f88971cdeb3024488bad0cc0e8023c8eb3f00770d35f1ceb706cee12e -size 93594 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomMembersListManageMemberSheet.Banned-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomMembersListManageMemberSheet.Banned-iPad-pseudo.png deleted file mode 100644 index 8f924ccc3..000000000 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomMembersListManageMemberSheet.Banned-iPad-pseudo.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:61569c78e79a50ab1f5430326df0007462d83ffd1b87e1bf75846df7fbebab17 -size 94307 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomMembersListManageMemberSheet.Banned-iPhone-16-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomMembersListManageMemberSheet.Banned-iPhone-16-en-GB.png deleted file mode 100644 index 641b52ca2..000000000 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomMembersListManageMemberSheet.Banned-iPhone-16-en-GB.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:eb501a8fc0b0c2399a25c54d489aa3dd948a114fb015045de9c9204962f6510d -size 45831 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomMembersListManageMemberSheet.Banned-iPhone-16-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomMembersListManageMemberSheet.Banned-iPhone-16-pseudo.png deleted file mode 100644 index 7acc5f8a7..000000000 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomMembersListManageMemberSheet.Banned-iPhone-16-pseudo.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c8ceedc2eacd4a377c8b0e54c41bb621bc465660fd33bf5475bbd6ee55f699da -size 47942 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomMembersListManageMemberSheet.Joined-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomMembersListManageMemberSheet.Joined-iPad-en-GB.png deleted file mode 100644 index 8b07195af..000000000 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomMembersListManageMemberSheet.Joined-iPad-en-GB.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:481e986b2ab0b1bb7bf92fbbd14d1a58cf20e4497653d8b230aebb6a023139c1 -size 196642 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomMembersListManageMemberSheet.Joined-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomMembersListManageMemberSheet.Joined-iPad-pseudo.png deleted file mode 100644 index c1b553902..000000000 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomMembersListManageMemberSheet.Joined-iPad-pseudo.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:21f9d12fc11aaacafbb142dee763ac8d68447094aadb3d9c00910e7a5c11334d -size 199499 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomMembersListManageMemberSheet.Joined-iPhone-16-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomMembersListManageMemberSheet.Joined-iPhone-16-en-GB.png deleted file mode 100644 index 48027aec2..000000000 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomMembersListManageMemberSheet.Joined-iPhone-16-en-GB.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:973bf92301cda4c122574b1b3d21ca7f30fcdcf427d821a819f4b02f1e7700d2 -size 136277 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomMembersListManageMemberSheet.Joined-iPhone-16-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomMembersListManageMemberSheet.Joined-iPhone-16-pseudo.png deleted file mode 100644 index f3e2958fd..000000000 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomMembersListManageMemberSheet.Joined-iPhone-16-pseudo.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c6b7be6ae8cc2b73e04b709f5daed55b20df7258de7135f80539158aca860e94 -size 145835 diff --git a/UnitTests/Sources/ManageRoomMemberSheetViewModelTests.swift b/UnitTests/Sources/ManageRoomMemberSheetViewModelTests.swift new file mode 100644 index 000000000..398528c70 --- /dev/null +++ b/UnitTests/Sources/ManageRoomMemberSheetViewModelTests.swift @@ -0,0 +1,100 @@ +// +// Copyright 2025 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. +// + +import XCTest + +@testable import ElementX + +@MainActor +class ManageRoomMemberSheetViewModelTests: XCTestCase { + private var viewModel: ManageRoomMemberSheetViewModel! + private var context: ManageRoomMemberSheetViewModel.Context! { + viewModel.context + } + + func testKick() async throws { + let testReason = "Kick Test" + let roomProxy = JoinedRoomProxyMock(.init(members: [RoomMemberProxyMock.mockAdmin, RoomMemberProxyMock.mockAlice])) + let expectation = XCTestExpectation(description: "Kick member") + roomProxy.kickUserReasonClosure = { userID, reason in + defer { expectation.fulfill() } + XCTAssertEqual(userID, RoomMemberProxyMock.mockAlice.userID) + XCTAssertEqual(reason, testReason) + return .success(()) + } + + viewModel = ManageRoomMemberSheetViewModel(member: RoomMemberDetails(withProxy: RoomMemberProxyMock.mockAlice), + canKick: true, + canBan: true, + roomProxy: roomProxy, + userIndicatorController: UserIndicatorControllerMock(), + analyticsService: ServiceLocator.shared.analytics, + mediaProvider: MediaProviderMock(configuration: .init())) + + let deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil } + let deferredAction = deferFulfillment(viewModel.actions) { action in + action == .dismiss(shouldShowDetails: false) + } + context.send(viewAction: .kick) + try await deferred.fulfill() + + context.alertInfo?.textFields?[0].text.wrappedValue = testReason + context.alertInfo?.secondaryButton?.action?() + await fulfillment(of: [expectation]) + try await deferredAction.fulfill() + } + + func testBan() async throws { + let testReason = "Ban Test" + let roomProxy = JoinedRoomProxyMock(.init(members: [RoomMemberProxyMock.mockAdmin, RoomMemberProxyMock.mockAlice])) + let expectation = XCTestExpectation(description: "Ban member") + roomProxy.banUserReasonClosure = { userID, reason in + defer { expectation.fulfill() } + XCTAssertEqual(userID, RoomMemberProxyMock.mockAlice.userID) + XCTAssertEqual(reason, testReason) + return .success(()) + } + + viewModel = ManageRoomMemberSheetViewModel(member: RoomMemberDetails(withProxy: RoomMemberProxyMock.mockAlice), + canKick: true, + canBan: true, + roomProxy: roomProxy, + userIndicatorController: UserIndicatorControllerMock(), + analyticsService: ServiceLocator.shared.analytics, + mediaProvider: MediaProviderMock(configuration: .init())) + + let deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil } + context.send(viewAction: .ban) + try await deferred.fulfill() + + let deferredAction = deferFulfillment(viewModel.actions) { action in + action == .dismiss(shouldShowDetails: false) + } + context.alertInfo?.textFields?[0].text.wrappedValue = testReason + context.alertInfo?.secondaryButton?.action?() + await fulfillment(of: [expectation]) + try await deferredAction.fulfill() + } + + func testDisplayDetails() async throws { + let roomProxy = JoinedRoomProxyMock(.init(members: [RoomMemberProxyMock.mockAdmin, RoomMemberProxyMock.mockAlice])) + viewModel = ManageRoomMemberSheetViewModel(member: RoomMemberDetails(withProxy: RoomMemberProxyMock.mockAlice), + canKick: true, + canBan: true, + roomProxy: roomProxy, + userIndicatorController: UserIndicatorControllerMock(), + analyticsService: ServiceLocator.shared.analytics, + mediaProvider: MediaProviderMock(configuration: .init())) + + let deferredAction = deferFulfillment(viewModel.actions) { action in + action == .dismiss(shouldShowDetails: true) + } + context.send(viewAction: .displayDetails) + try await deferredAction.fulfill() + XCTAssertNil(context.alertInfo) + } +} diff --git a/UnitTests/Sources/RoomMembersListScreenViewModelTests.swift b/UnitTests/Sources/RoomMembersListScreenViewModelTests.swift index 3ae5633e5..a3694e908 100644 --- a/UnitTests/Sources/RoomMembersListScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomMembersListScreenViewModelTests.swift @@ -143,7 +143,7 @@ class RoomMembersListScreenViewModelTests: XCTestCase { // Then the member's details should be shown. try await memberDetailsAction.fulfill() - XCTAssertNil(context.memberToManage) + XCTAssertNil(context.manageMemeberViewModel) } func testSelectUserAsAdmin() async throws { @@ -151,10 +151,10 @@ class RoomMembersListScreenViewModelTests: XCTestCase { setup(with: .allMembersAsAdmin) var deferred = deferFulfillment(context.$viewState) { !$0.visibleInvitedMembers.isEmpty && $0.canKickUsers && $0.canBanUsers } try await deferred.fulfill() - XCTAssertNil(context.memberToManage) - + XCTAssertNil(context.manageMemeberViewModel) + // When tapping on a user in the list. - deferred = deferFulfillment(context.$viewState) { $0.bindings.memberToManage != nil } + deferred = deferFulfillment(context.$viewState) { $0.bindings.manageMemeberViewModel != nil } guard let user = viewModel.state.visibleJoinedMembers.first(where: { $0.member.role == .user && $0.member.id != RoomMemberProxyMock.mockMe.userID })?.member else { XCTFail("Expected to find a regular user.") return @@ -163,8 +163,9 @@ class RoomMembersListScreenViewModelTests: XCTestCase { try await deferred.fulfill() // Then member management should be shown for that user. - XCTAssertEqual(context.memberToManage?.member, user) - XCTAssertEqual(context.memberToManage?.actions, [.kick, .ban]) + XCTAssertEqual(context.manageMemeberViewModel?.state.member, user) + XCTAssertEqual(context.manageMemeberViewModel?.state.canKick, true) + XCTAssertEqual(context.manageMemeberViewModel?.state.canBan, true) } func testSelectModeratorAsAdmin() async throws { @@ -172,10 +173,10 @@ class RoomMembersListScreenViewModelTests: XCTestCase { setup(with: .allMembersAsAdmin) var deferred = deferFulfillment(context.$viewState) { !$0.visibleInvitedMembers.isEmpty && $0.canKickUsers && $0.canBanUsers } try await deferred.fulfill() - XCTAssertNil(context.memberToManage) + XCTAssertNil(context.manageMemeberViewModel) // When tapping on a moderator in the list. - deferred = deferFulfillment(context.$viewState) { $0.bindings.memberToManage != nil } + deferred = deferFulfillment(context.$viewState) { $0.bindings.manageMemeberViewModel != nil } guard let moderator = viewModel.state.visibleJoinedMembers.first(where: { $0.member.role == .moderator })?.member else { XCTFail("Expected to find a moderator.") return @@ -184,8 +185,9 @@ class RoomMembersListScreenViewModelTests: XCTestCase { try await deferred.fulfill() // Then member management should be shown for the moderator. - XCTAssertEqual(context.memberToManage?.member, moderator) - XCTAssertEqual(context.memberToManage?.actions, [.kick, .ban]) + XCTAssertEqual(context.manageMemeberViewModel?.state.member, moderator) + XCTAssertEqual(context.manageMemeberViewModel?.state.canKick, true) + XCTAssertEqual(context.manageMemeberViewModel?.state.canBan, true) } func testSelectAdminAsAdmin() async throws { @@ -204,7 +206,7 @@ class RoomMembersListScreenViewModelTests: XCTestCase { // Then the administrator's details should be shown. try await memberDetailsAction.fulfill() - XCTAssertNil(context.memberToManage) + XCTAssertNil(context.manageMemeberViewModel) } func testSelectOwnMemberAsAdmin() async throws { @@ -223,7 +225,7 @@ class RoomMembersListScreenViewModelTests: XCTestCase { // Then your member's details should be shown. try await memberDetailsAction.fulfill() - XCTAssertNil(context.memberToManage) + XCTAssertNil(context.manageMemeberViewModel) } func testSelectBannedMember() async throws { @@ -243,50 +245,10 @@ class RoomMembersListScreenViewModelTests: XCTestCase { // Then an alert should be shown to unban the user. try await deferred.fulfill() - XCTAssertNil(context.memberToManage) + XCTAssertNil(context.manageMemeberViewModel) XCTAssertNotNil(context.alertInfo) } - func testKickMember() async throws { - setup(with: .allMembersAsAdmin) - var deferred = deferFulfillment(context.$viewState) { !$0.visibleJoinedMembers.isEmpty } - try await deferred.fulfill() - - let expectation = XCTestExpectation(description: "Ban member") - roomProxy.kickUserReasonClosure = { _, _ in - defer { expectation.fulfill() } - return .success(()) - } - - context.send(viewAction: .kickMember(viewModel.state.visibleJoinedMembers[0].member)) - - deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil } - try await deferred.fulfill() - - context.alertInfo?.secondaryButton?.action?() - await fulfillment(of: [expectation]) - } - - func testBanMember() async throws { - setup(with: .allMembersAsAdmin) - var deferred = deferFulfillment(context.$viewState) { !$0.visibleJoinedMembers.isEmpty } - try await deferred.fulfill() - - let expectation = XCTestExpectation(description: "Ban member") - roomProxy.banUserReasonClosure = { _, _ in - defer { expectation.fulfill() } - return .success(()) - } - - context.send(viewAction: .banMember(viewModel.state.visibleJoinedMembers[0].member)) - - deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil } - try await deferred.fulfill() - - context.alertInfo?.secondaryButton?.action?() - await fulfillment(of: [expectation]) - } - func testUnbanMember() async throws { setup(with: .allMembersAsAdmin) let deferred = deferFulfillment(context.$viewState) { !$0.visibleJoinedMembers.isEmpty } diff --git a/UnitTests/Sources/TimelineViewModelTests.swift b/UnitTests/Sources/TimelineViewModelTests.swift index 8d1c38a3b..1f7cc2617 100644 --- a/UnitTests/Sources/TimelineViewModelTests.swift +++ b/UnitTests/Sources/TimelineViewModelTests.swift @@ -350,6 +350,117 @@ class TimelineViewModelTests: XCTestCase { try await deferred.fulfill() } + func testShowManageUserAsAdmin() async throws { + let viewModel = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "", + members: [RoomMemberProxyMock.mockAdmin, + RoomMemberProxyMock.mockAlice], + ownUserID: RoomMemberProxyMock.mockAdmin.userID)), + timelineController: MockTimelineController(), + mediaProvider: MediaProviderMock(configuration: .init()), + mediaPlayerProvider: MediaPlayerProviderMock(), + voiceMessageMediaManager: VoiceMessageMediaManagerMock(), + userIndicatorController: userIndicatorControllerMock, + appMediator: AppMediatorMock.default, + appSettings: ServiceLocator.shared.settings, + analyticsService: ServiceLocator.shared.analytics, + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + timelineControllerFactory: TimelineControllerFactoryMock(.init()), + clientProxy: ClientProxyMock(.init())) + + var deferred = deferFulfillment(viewModel.context.$viewState) { value in + value.canCurrentUserKick && value.canCurrentUserBan + } + + try await deferred.fulfill() + + deferred = deferFulfillment(viewModel.context.$viewState) { value in + value.bindings.manageMemberViewModel != nil + } + + viewModel.context.send(viewAction: .tappedOnSenderDetails(userID: RoomMemberProxyMock.mockAlice.userID)) + try await deferred.fulfill() + + XCTAssertEqual(viewModel.context.manageMemberViewModel?.state.member.id, RoomMemberProxyMock.mockAlice.userID) + XCTAssertEqual(viewModel.context.manageMemberViewModel?.state.canBan, true) + XCTAssertEqual(viewModel.context.manageMemberViewModel?.state.canKick, true) + } + + func testShowDetailsForAnAdmin() async throws { + let viewModel = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "", + members: [RoomMemberProxyMock.mockAdmin, + RoomMemberProxyMock.mockAlice], + ownUserID: RoomMemberProxyMock.mockAlice.userID)), + timelineController: MockTimelineController(), + mediaProvider: MediaProviderMock(configuration: .init()), + mediaPlayerProvider: MediaPlayerProviderMock(), + voiceMessageMediaManager: VoiceMessageMediaManagerMock(), + userIndicatorController: userIndicatorControllerMock, + appMediator: AppMediatorMock.default, + appSettings: ServiceLocator.shared.settings, + analyticsService: ServiceLocator.shared.analytics, + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + timelineControllerFactory: TimelineControllerFactoryMock(.init()), + clientProxy: ClientProxyMock(.init())) + + let deferredState = deferFulfillment(viewModel.context.$viewState) { value in + !value.canCurrentUserKick && !value.canCurrentUserBan + } + + try await deferredState.fulfill() + + let deferredAction = deferFulfillment(viewModel.actions) { action in + switch action { + case .displaySenderDetails(let userID): + return userID == RoomMemberProxyMock.mockAdmin.userID + default: + return false + } + } + + viewModel.context.send(viewAction: .tappedOnSenderDetails(userID: RoomMemberProxyMock.mockAdmin.userID)) + try await deferredAction.fulfill() + + XCTAssertNil(viewModel.context.manageMemberViewModel) + } + + func testShowDetailsForABannedUser() async throws { + let viewModel = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "", + members: [RoomMemberProxyMock.mockAdmin, + RoomMemberProxyMock.mockBanned[0]], + ownUserID: RoomMemberProxyMock.mockAdmin.userID)), + timelineController: MockTimelineController(), + mediaProvider: MediaProviderMock(configuration: .init()), + mediaPlayerProvider: MediaPlayerProviderMock(), + voiceMessageMediaManager: VoiceMessageMediaManagerMock(), + userIndicatorController: userIndicatorControllerMock, + appMediator: AppMediatorMock.default, + appSettings: ServiceLocator.shared.settings, + analyticsService: ServiceLocator.shared.analytics, + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), + timelineControllerFactory: TimelineControllerFactoryMock(.init()), + clientProxy: ClientProxyMock(.init())) + + let deferredState = deferFulfillment(viewModel.context.$viewState) { value in + value.canCurrentUserKick && value.canCurrentUserBan + } + + try await deferredState.fulfill() + + let deferredAction = deferFulfillment(viewModel.actions) { action in + switch action { + case .displaySenderDetails(let userID): + return userID == RoomMemberProxyMock.mockBanned[0].userID + default: + return false + } + } + + viewModel.context.send(viewAction: .tappedOnSenderDetails(userID: RoomMemberProxyMock.mockBanned[0].userID)) + try await deferredAction.fulfill() + + XCTAssertNil(viewModel.context.manageMemberViewModel) + } + // MARK: - Pins func testPinnedEvents() async throws {