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
This commit is contained in:
Mauro Romito
2025-04-08 15:23:40 +02:00
committed by Mauro
parent 56476f8bb5
commit 0e7533145a
44 changed files with 760 additions and 344 deletions
+36 -4
View File
@@ -164,6 +164,7 @@
1B88BB631F7FC45A213BB554 /* TimelineItemSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55AEEF8142DF1B59DB40FB93 /* TimelineItemSender.swift */; }; 1B88BB631F7FC45A213BB554 /* TimelineItemSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55AEEF8142DF1B59DB40FB93 /* TimelineItemSender.swift */; };
1B8E30B35BF8F541C1318F19 /* SecureBackupScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40316EFFEAC7B206EE9A55AE /* SecureBackupScreenViewModelTests.swift */; }; 1B8E30B35BF8F541C1318F19 /* SecureBackupScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40316EFFEAC7B206EE9A55AE /* SecureBackupScreenViewModelTests.swift */; };
1BA04D05EBC6646958B1BE60 /* PlaceholderScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF34A2FD6797535C95AC918D /* PlaceholderScreenCoordinator.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 */; }; 1C409A26A99F0371C47AFA51 /* UserDiscoveryServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F615A00DB223FF3280204D2 /* UserDiscoveryServiceProtocol.swift */; };
1C598D3B785645AAC7B35760 /* ReportRoomScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 292EEE1F71DCC205C45728F7 /* ReportRoomScreenCoordinator.swift */; }; 1C598D3B785645AAC7B35760 /* ReportRoomScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 292EEE1F71DCC205C45728F7 /* ReportRoomScreenCoordinator.swift */; };
1C8BC70A18060677E295A846 /* ShareToMapsAppActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4481799F455B3DA243BDA2AC /* ShareToMapsAppActivity.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 */; }; 5038E69A5E6A89DE1A345E04 /* ShouldScrollOnKeyboardDidShow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832397B5C3D00A4BF52C5F0B /* ShouldScrollOnKeyboardDidShow.swift */; };
50539366B408780B232C1910 /* EstimatedWaveformView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD0FF64B0E6470F66F42E182 /* EstimatedWaveformView.swift */; }; 50539366B408780B232C1910 /* EstimatedWaveformView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD0FF64B0E6470F66F42E182 /* EstimatedWaveformView.swift */; };
5100F53E6884A15F9BA07CC3 /* AttributedStringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CA26F55123E36B50DB0B3A /* AttributedStringTests.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 */; }; 5139F4BD5A5DF6F8D11A9BDE /* NotificationPermissionsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46D0BA44B1838E65B507B277 /* NotificationPermissionsScreen.swift */; };
513AF15E0E84711B80D04B1B /* ReportRoomScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3E9684DCE6B66BD0B5DF67 /* ReportRoomScreenViewModelTests.swift */; }; 513AF15E0E84711B80D04B1B /* ReportRoomScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3E9684DCE6B66BD0B5DF67 /* ReportRoomScreenViewModelTests.swift */; };
518C93DC6516D3D018DE065F /* UNNotificationRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49E751D7EDB6043238111D90 /* UNNotificationRequest.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 */; }; 5D70FAE4D2BF4553AFFFFE41 /* NotificationItemProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F7FE40EF7490A7E09D7BE6 /* NotificationItemProxy.swift */; };
5D99F63CC88BB29383019FC6 /* ShareExtensionModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCAC01A97A43BE07B9E94E43 /* ShareExtensionModels.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 */; }; 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 */; }; 5DD85A0FE3D85AEC3C7EFE36 /* DeveloperOptionsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7C7CFA6B2A62A685FF6CE3 /* DeveloperOptionsScreenCoordinator.swift */; };
5DFC2A889D3B39DD47AC63A8 /* PillUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C537DE821FED94D23467B6C4 /* PillUtilities.swift */; }; 5DFC2A889D3B39DD47AC63A8 /* PillUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C537DE821FED94D23467B6C4 /* PillUtilities.swift */; };
5EC046E41755C095DAB1C3FF /* TimelineProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8C9BBB729C941BEE0E2A63 /* TimelineProviderProtocol.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 */; }; E481C8FDCB6C089963C95344 /* DeviceKit in Frameworks */ = {isa = PBXBuildFile; productRef = BC01130651CB23340B899032 /* DeviceKit */; };
E49F74BD93230BDEFFE5EA51 /* RoomNotificationSettingsScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D295F0081084F38DB20893 /* RoomNotificationSettingsScreenViewModelTests.swift */; }; E49F74BD93230BDEFFE5EA51 /* RoomNotificationSettingsScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D295F0081084F38DB20893 /* RoomNotificationSettingsScreenViewModelTests.swift */; };
E4B07FF075C99D04D9AF792D /* AppLockSetupPINScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = B410B32B72C90BF94E481F33 /* AppLockSetupPINScreenModels.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 */; }; E4F924DECC66389C1C810550 /* AuthenticationStartScreenBackgroundImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50D685B4DB38BB5BD87C956A /* AuthenticationStartScreenBackgroundImage.swift */; };
E58F1F3276E98A93F7D39219 /* RoomPollsHistoryScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1D8479BB704B7EF696F8ABE /* RoomPollsHistoryScreenCoordinator.swift */; }; E58F1F3276E98A93F7D39219 /* RoomPollsHistoryScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1D8479BB704B7EF696F8ABE /* RoomPollsHistoryScreenCoordinator.swift */; };
E5AB28123E2488F97E953AC0 /* CallNotificationRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1ED17433ADC77287F8904F9 /* CallNotificationRoomTimelineItem.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 */; }; F118DD449066E594F63C697D /* RoomMemberProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32B5E17028C02DFA7DDA3931 /* RoomMemberProxyProtocol.swift */; };
F12F6BED7B6D7EE4BEE55039 /* PlainMentionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AE78FA0011E07920AE83135 /* PlainMentionBuilder.swift */; }; F12F6BED7B6D7EE4BEE55039 /* PlainMentionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AE78FA0011E07920AE83135 /* PlainMentionBuilder.swift */; };
F18CA61A58C77C84F551B8E7 /* GeneratedMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57916A1578D8043BB0795441 /* GeneratedMocks.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 */; }; 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 */; }; F253AAB4C8F06208173C9C4A /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D52BAA5BADB06E5E8C295D /* Assets.swift */; };
F255083E18CDBFDF7E640FB1 /* Avatars.swift in Sources */ = {isa = PBXBuildFile; fileRef = C142248014E08E885E323E56 /* Avatars.swift */; }; F255083E18CDBFDF7E640FB1 /* Avatars.swift in Sources */ = {isa = PBXBuildFile; fileRef = C142248014E08E885E323E56 /* Avatars.swift */; };
F2D5C0E1351DA7BD16867629 /* TimelineStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD4823EAB4B4E8BAB4F6B8C /* TimelineStyle.swift */; }; F2D5C0E1351DA7BD16867629 /* TimelineStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD4823EAB4B4E8BAB4F6B8C /* TimelineStyle.swift */; };
F2E580C0FBFBEFFE9D69893B /* RoomPreviewProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 739077686814E4EA339B1C83 /* RoomPreviewProxyProtocol.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 */; }; F37629BAA5E8F50AAF2A131D /* SoftLogoutScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB7BAD55A4E2B8E5828CD64C /* SoftLogoutScreenViewModel.swift */; };
F38D32C1B0232AAFE6A0822C /* ExtensionLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A8571A8A071FB41778C016 /* ExtensionLogger.swift */; }; F38D32C1B0232AAFE6A0822C /* ExtensionLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A8571A8A071FB41778C016 /* ExtensionLogger.swift */; };
F3E2D3F7ACDED65A4E5CD8DE /* RoomMembersListScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECF79FB25E2D4BD6F50CE7C9 /* RoomMembersListScreenViewModel.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 = "<group>"; }; 1B065EC39C99C1303A101C1C /* WebRegistrationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRegistrationScreen.swift; sourceTree = "<group>"; };
1B10423B9102086A2D9BFCBA /* EventTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventTimelineItem.swift; sourceTree = "<group>"; }; 1B10423B9102086A2D9BFCBA /* EventTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventTimelineItem.swift; sourceTree = "<group>"; };
1B2AC540DE619B36832A5DB5 /* LocationRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRoomTimelineItem.swift; sourceTree = "<group>"; }; 1B2AC540DE619B36832A5DB5 /* LocationRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRoomTimelineItem.swift; sourceTree = "<group>"; };
1B30CD19ED6243FEDFBA8400 /* ManageRoomMemberSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageRoomMemberSheetView.swift; sourceTree = "<group>"; };
1B53D6C5C0D14B04D3AB3F6E /* PillAttachmentViewProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillAttachmentViewProvider.swift; sourceTree = "<group>"; }; 1B53D6C5C0D14B04D3AB3F6E /* PillAttachmentViewProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillAttachmentViewProvider.swift; sourceTree = "<group>"; };
1B564D748B67A156F413CD97 /* NotificationSettingsEditScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenModels.swift; sourceTree = "<group>"; }; 1B564D748B67A156F413CD97 /* NotificationSettingsEditScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenModels.swift; sourceTree = "<group>"; };
1B6E30BB748F3F480F077969 /* RoomMemberDetailsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsScreenModels.swift; sourceTree = "<group>"; }; 1B6E30BB748F3F480F077969 /* RoomMemberDetailsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsScreenModels.swift; sourceTree = "<group>"; };
@@ -1930,6 +1935,7 @@
7720ACAC6155AB7F9C70B546 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nb; path = nb.lproj/Localizable.stringsdict; sourceTree = "<group>"; }; 7720ACAC6155AB7F9C70B546 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nb; path = nb.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
7773CBFDBD458E0B7E270507 /* PillView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillView.swift; sourceTree = "<group>"; }; 7773CBFDBD458E0B7E270507 /* PillView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillView.swift; sourceTree = "<group>"; };
780258F1B9D15E30549FF4BE /* NotificationSettingsEditScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenViewModel.swift; sourceTree = "<group>"; }; 780258F1B9D15E30549FF4BE /* NotificationSettingsEditScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenViewModel.swift; sourceTree = "<group>"; };
787E84119E626E2F0E0BFBE8 /* ManageRoomMemberSheetModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageRoomMemberSheetModels.swift; sourceTree = "<group>"; };
78910787F967CBC6042A101E /* StartChatScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenViewModelProtocol.swift; sourceTree = "<group>"; }; 78910787F967CBC6042A101E /* StartChatScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenViewModelProtocol.swift; sourceTree = "<group>"; };
78913D6E120D46138E97C107 /* NavigationSplitCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationSplitCoordinatorTests.swift; sourceTree = "<group>"; }; 78913D6E120D46138E97C107 /* NavigationSplitCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationSplitCoordinatorTests.swift; sourceTree = "<group>"; };
7893780A1FD6E3F38B3E9049 /* UserIndicatorControllerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorControllerMock.swift; sourceTree = "<group>"; }; 7893780A1FD6E3F38B3E9049 /* UserIndicatorControllerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorControllerMock.swift; sourceTree = "<group>"; };
@@ -2105,6 +2111,7 @@
9A028783CFFF861C5E44FFB1 /* BadgeLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeLabel.swift; sourceTree = "<group>"; }; 9A028783CFFF861C5E44FFB1 /* BadgeLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeLabel.swift; sourceTree = "<group>"; };
9A1C33355FFB0F0953C35036 /* ClientBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientBuilder.swift; sourceTree = "<group>"; }; 9A1C33355FFB0F0953C35036 /* ClientBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientBuilder.swift; sourceTree = "<group>"; };
9A22A05E472533ED3C5A31B3 /* NavigationModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationModule.swift; sourceTree = "<group>"; }; 9A22A05E472533ED3C5A31B3 /* NavigationModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationModule.swift; sourceTree = "<group>"; };
9A34D9BCA1A7D9A56E1EAF1D /* ManageRoomMemberSheetViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageRoomMemberSheetViewModel.swift; sourceTree = "<group>"; };
9A68BCE6438873D2661D93D0 /* BugReportServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportServiceProtocol.swift; sourceTree = "<group>"; }; 9A68BCE6438873D2661D93D0 /* BugReportServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportServiceProtocol.swift; sourceTree = "<group>"; };
9AA3AF94A06D319BB37E52DA /* TimelineItemFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemFactoryTests.swift; sourceTree = "<group>"; }; 9AA3AF94A06D319BB37E52DA /* TimelineItemFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemFactoryTests.swift; sourceTree = "<group>"; };
9B06663F7858E45882E63471 /* StaticLocationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticLocationScreen.swift; sourceTree = "<group>"; }; 9B06663F7858E45882E63471 /* StaticLocationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticLocationScreen.swift; sourceTree = "<group>"; };
@@ -2352,6 +2359,7 @@
CA90BD288E5AE6BC643AFDDF /* TemplateScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenCoordinator.swift; sourceTree = "<group>"; }; CA90BD288E5AE6BC643AFDDF /* TemplateScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenCoordinator.swift; sourceTree = "<group>"; };
CACA846B3E3E9A521D98B178 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; }; CACA846B3E3E9A521D98B178 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
CAD9547E47C58930E2CE8306 /* CallScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallScreenViewModelTests.swift; sourceTree = "<group>"; }; CAD9547E47C58930E2CE8306 /* CallScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallScreenViewModelTests.swift; sourceTree = "<group>"; };
CB7B588A06911B455AC0B4C9 /* ManageRoomMemberSheetViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageRoomMemberSheetViewModelProtocol.swift; sourceTree = "<group>"; };
CBBCC6E74774E79B599625D0 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = "<group>"; }; CBBCC6E74774E79B599625D0 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = "<group>"; };
CBF9AEA706926DD0DA2B954C /* JoinedRoomSize+MemberCount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JoinedRoomSize+MemberCount.swift"; sourceTree = "<group>"; }; CBF9AEA706926DD0DA2B954C /* JoinedRoomSize+MemberCount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JoinedRoomSize+MemberCount.swift"; sourceTree = "<group>"; };
CC03209FDE8CE0810617BFFF /* RoomMembersListScreenMemberCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenMemberCell.swift; sourceTree = "<group>"; }; CC03209FDE8CE0810617BFFF /* RoomMembersListScreenMemberCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenMemberCell.swift; sourceTree = "<group>"; };
@@ -2409,6 +2417,7 @@
D7B18089ED50324583BB2FB7 /* EditRoomAddressScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditRoomAddressScreenViewModelProtocol.swift; sourceTree = "<group>"; }; D7B18089ED50324583BB2FB7 /* EditRoomAddressScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditRoomAddressScreenViewModelProtocol.swift; sourceTree = "<group>"; };
D7BB243B26D54EF1A0C422C0 /* NotificationContentBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationContentBuilder.swift; sourceTree = "<group>"; }; D7BB243B26D54EF1A0C422C0 /* NotificationContentBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationContentBuilder.swift; sourceTree = "<group>"; };
D7BEB970F500BFB248443FA1 /* BloomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BloomView.swift; sourceTree = "<group>"; }; D7BEB970F500BFB248443FA1 /* BloomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BloomView.swift; sourceTree = "<group>"; };
D80807B554CF9C524F98674F /* ManageRoomMemberSheetViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageRoomMemberSheetViewModelTests.swift; sourceTree = "<group>"; };
D879DC5515B1D42577F96C94 /* RoomSelectionScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSelectionScreen.swift; sourceTree = "<group>"; }; D879DC5515B1D42577F96C94 /* RoomSelectionScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSelectionScreen.swift; sourceTree = "<group>"; };
D8AA084E10B80D64449C02A9 /* SessionVerificationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationTests.swift; sourceTree = "<group>"; }; D8AA084E10B80D64449C02A9 /* SessionVerificationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationTests.swift; sourceTree = "<group>"; };
D8E60332509665C00179ACF6 /* MessageForwardingScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreenViewModel.swift; sourceTree = "<group>"; }; D8E60332509665C00179ACF6 /* MessageForwardingScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreenViewModel.swift; sourceTree = "<group>"; };
@@ -2572,7 +2581,6 @@
FBB0328F2887BF0A65BC5D49 /* NotificationSettingsEditScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreen.swift; sourceTree = "<group>"; }; FBB0328F2887BF0A65BC5D49 /* NotificationSettingsEditScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreen.swift; sourceTree = "<group>"; };
FBC776F301D374A3298C69DA /* AppCoordinatorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinatorProtocol.swift; sourceTree = "<group>"; }; FBC776F301D374A3298C69DA /* AppCoordinatorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinatorProtocol.swift; sourceTree = "<group>"; };
FC83F47D2173B7538AA72E0E /* RoomSummaryProviderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProviderMock.swift; sourceTree = "<group>"; }; FC83F47D2173B7538AA72E0E /* RoomSummaryProviderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProviderMock.swift; sourceTree = "<group>"; };
FC853F9B4FBE039D2C16EC6B /* RoomMembersListManageMemberSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListManageMemberSheet.swift; sourceTree = "<group>"; };
FC9044BE0E4A66F5B963E834 /* AudioFileEventsTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioFileEventsTimelineView.swift; sourceTree = "<group>"; }; FC9044BE0E4A66F5B963E834 /* AudioFileEventsTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioFileEventsTimelineView.swift; sourceTree = "<group>"; };
FCE93F0CBF0D96B77111C413 /* AppLockFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockFlowCoordinator.swift; sourceTree = "<group>"; }; FCE93F0CBF0D96B77111C413 /* AppLockFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockFlowCoordinator.swift; sourceTree = "<group>"; };
FD1275D9CE0FFBA6E8E85426 /* UserIndicatorController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorController.swift; sourceTree = "<group>"; }; FD1275D9CE0FFBA6E8E85426 /* UserIndicatorController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorController.swift; sourceTree = "<group>"; };
@@ -3490,6 +3498,17 @@
path = QRCodeLoginScreen; path = QRCodeLoginScreen;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
3D76DA5827DF9396AC90E7B4 /* ManageRoomMemberSheet */ = {
isa = PBXGroup;
children = (
787E84119E626E2F0E0BFBE8 /* ManageRoomMemberSheetModels.swift */,
9A34D9BCA1A7D9A56E1EAF1D /* ManageRoomMemberSheetViewModel.swift */,
CB7B588A06911B455AC0B4C9 /* ManageRoomMemberSheetViewModelProtocol.swift */,
40AE45E13BF744AA0E4EA0E4 /* View */,
);
path = ManageRoomMemberSheet;
sourceTree = "<group>";
};
3E1CCC4B607946CE90B4A827 /* DeclineAndBlockScreen */ = { 3E1CCC4B607946CE90B4A827 /* DeclineAndBlockScreen */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -3559,6 +3578,14 @@
); );
sourceTree = "<group>"; sourceTree = "<group>";
}; };
40AE45E13BF744AA0E4EA0E4 /* View */ = {
isa = PBXGroup;
children = (
1B30CD19ED6243FEDFBA8400 /* ManageRoomMemberSheetView.swift */,
);
path = View;
sourceTree = "<group>";
};
40D9A816C45E0278C29DF883 /* SupportingFiles */ = { 40D9A816C45E0278C29DF883 /* SupportingFiles */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -4287,6 +4314,7 @@
C070FD43DC6BF4E50217965A /* LocalizationTests.swift */, C070FD43DC6BF4E50217965A /* LocalizationTests.swift */,
3DC1943ADE6A62ED5129D7C8 /* LoggingTests.swift */, 3DC1943ADE6A62ED5129D7C8 /* LoggingTests.swift */,
5A43964330459965AF048A8C /* LoginScreenViewModelTests.swift */, 5A43964330459965AF048A8C /* LoginScreenViewModelTests.swift */,
D80807B554CF9C524F98674F /* ManageRoomMemberSheetViewModelTests.swift */,
1D262A26713C18BB70C82CA5 /* MapTilerURLBuilderTests.swift */, 1D262A26713C18BB70C82CA5 /* MapTilerURLBuilderTests.swift */,
F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */, F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */,
2D7A2C4A3A74F0D2FFE9356A /* MediaPlayerProviderTests.swift */, 2D7A2C4A3A74F0D2FFE9356A /* MediaPlayerProviderTests.swift */,
@@ -4878,7 +4906,6 @@
949B06577E5265373013DDAB /* View */ = { 949B06577E5265373013DDAB /* View */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
FC853F9B4FBE039D2C16EC6B /* RoomMembersListManageMemberSheet.swift */,
1B8E176484A89BAC389D4076 /* RoomMembersListScreen.swift */, 1B8E176484A89BAC389D4076 /* RoomMembersListScreen.swift */,
CC03209FDE8CE0810617BFFF /* RoomMembersListScreenMemberCell.swift */, CC03209FDE8CE0810617BFFF /* RoomMembersListScreenMemberCell.swift */,
); );
@@ -5776,6 +5803,7 @@
BF0415BE807CA2BCFC210008 /* KnockRequestsListScreen */, BF0415BE807CA2BCFC210008 /* KnockRequestsListScreen */,
948DD12A5533BE1BC260E437 /* LocationSharing */, 948DD12A5533BE1BC260E437 /* LocationSharing */,
73E032ADD008D63812791D97 /* LogViewerScreen */, 73E032ADD008D63812791D97 /* LogViewerScreen */,
3D76DA5827DF9396AC90E7B4 /* ManageRoomMemberSheet */,
26397A1EDB867FD573821532 /* MediaEventsTimelineScreen */, 26397A1EDB867FD573821532 /* MediaEventsTimelineScreen */,
87E2774157D9C4894BCFF3F8 /* MediaPickerScreen */, 87E2774157D9C4894BCFF3F8 /* MediaPickerScreen */,
23605DD08620BE6558242469 /* MediaUploadPreviewScreen */, 23605DD08620BE6558242469 /* MediaUploadPreviewScreen */,
@@ -6776,6 +6804,7 @@
0033481EE363E4914295F188 /* LocalizationTests.swift in Sources */, 0033481EE363E4914295F188 /* LocalizationTests.swift in Sources */,
149D1942DC005D0485FB8D93 /* LoggingTests.swift in Sources */, 149D1942DC005D0485FB8D93 /* LoggingTests.swift in Sources */,
7434A7F02D587A920B376A9A /* LoginScreenViewModelTests.swift in Sources */, 7434A7F02D587A920B376A9A /* LoginScreenViewModelTests.swift in Sources */,
1C1750C009F7214B967928BC /* ManageRoomMemberSheetViewModelTests.swift in Sources */,
3BEBDCB42BABFA3B456FECA7 /* MapTilerURLBuilderTests.swift in Sources */, 3BEBDCB42BABFA3B456FECA7 /* MapTilerURLBuilderTests.swift in Sources */,
2E43A3D221BE9587BC19C3F1 /* MatrixEntityRegexTests.swift in Sources */, 2E43A3D221BE9587BC19C3F1 /* MatrixEntityRegexTests.swift in Sources */,
4B978C09567387EF4366BD7A /* MediaLoaderTests.swift in Sources */, 4B978C09567387EF4366BD7A /* MediaLoaderTests.swift in Sources */,
@@ -7269,6 +7298,10 @@
A5B9EF45C7B8ACEB4954AE36 /* LoginScreenViewModelProtocol.swift in Sources */, A5B9EF45C7B8ACEB4954AE36 /* LoginScreenViewModelProtocol.swift in Sources */,
B10F7D5C237417DA160F4603 /* LongPressWithFeedback.swift in Sources */, B10F7D5C237417DA160F4603 /* LongPressWithFeedback.swift in Sources */,
B94368839BDB69172E28E245 /* MXLog.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 */, C1D0AB8222D7BAFC9AF9C8C0 /* MapLibreMapView.swift in Sources */,
C9BE065FA7D4E77E4C61CB69 /* MapLibreModels.swift in Sources */, C9BE065FA7D4E77E4C61CB69 /* MapLibreModels.swift in Sources */,
E2DDA49BD62F03F180A42E30 /* MapLibreStaticMapView.swift in Sources */, E2DDA49BD62F03F180A42E30 /* MapLibreStaticMapView.swift in Sources */,
@@ -7487,7 +7520,6 @@
6448F8D1D3CA4CD27BB4CADD /* RoomMemberProxy.swift in Sources */, 6448F8D1D3CA4CD27BB4CADD /* RoomMemberProxy.swift in Sources */,
92D9088B901CEBB1A99ECA4E /* RoomMemberProxyMock.swift in Sources */, 92D9088B901CEBB1A99ECA4E /* RoomMemberProxyMock.swift in Sources */,
F118DD449066E594F63C697D /* RoomMemberProxyProtocol.swift in Sources */, F118DD449066E594F63C697D /* RoomMemberProxyProtocol.swift in Sources */,
5DD0EF30070DC0A82C5CCD33 /* RoomMembersListManageMemberSheet.swift in Sources */,
C08AAE7563E0722C9383F51C /* RoomMembersListScreen.swift in Sources */, C08AAE7563E0722C9383F51C /* RoomMembersListScreen.swift in Sources */,
1C9BB74711E5F24C77B7FED0 /* RoomMembersListScreenCoordinator.swift in Sources */, 1C9BB74711E5F24C77B7FED0 /* RoomMembersListScreenCoordinator.swift in Sources */,
A975D60EA49F6AF73308809F /* RoomMembersListScreenMemberCell.swift in Sources */, A975D60EA49F6AF73308809F /* RoomMembersListScreenMemberCell.swift in Sources */,
@@ -399,9 +399,26 @@
"screen_advanced_settings_hide_invite_avatars_toggle_title" = "Hide avatars in room invite requests"; "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_hide_timeline_media_toggle_title" = "Hide media previews in timeline";
"screen_advanced_settings_moderation_and_safety_section_title" = "Moderation and Safety"; "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_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_message" = "Would you like to start a chat with %1$@?";
"screen_bottom_sheet_create_dm_title" = "Send invite?"; "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_description" = "Anyone can join this room";
"screen_create_room_room_access_section_anyone_option_title" = "Anyone"; "screen_create_room_room_access_section_anyone_option_title" = "Anyone";
"screen_create_room_room_access_section_header" = "Room Access"; "screen_create_room_room_access_section_header" = "Room Access";
@@ -886,26 +903,14 @@
"screen_room_member_details_unblock_user" = "Unblock user"; "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_subtitle" = "Use the web app to verify this user.";
"screen_room_member_details_verify_button_title" = "Verify %1$@"; "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_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_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_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_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_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_banned" = "Banned";
"screen_room_member_list_mode_members" = "Members"; "screen_room_member_list_mode_members" = "Members";
"screen_room_member_list_pending_header_title" = "Pending"; "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_administrator" = "Admin";
"screen_room_member_list_role_moderator" = "Moderator"; "screen_room_member_list_role_moderator" = "Moderator";
"screen_room_member_list_room_members_header_title" = "Room members"; "screen_room_member_list_room_members_header_title" = "Room members";
@@ -1134,9 +1139,10 @@
"troubleshoot_notifications_test_current_push_provider_failure" = "No push providers selected."; "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_success" = "Current push provider: %1$@.";
"troubleshoot_notifications_test_current_push_provider_title" = "Current push provider"; "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_description" = "Ensure that the application supports at least one push provider.";
"troubleshoot_notifications_test_detect_push_provider_failure" = "No push providers found."; "troubleshoot_notifications_test_detect_push_provider_failure" = "No push provider support found.";
"troubleshoot_notifications_test_detect_push_provider_title" = "Detect push providers"; "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_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_failure" = "The notification has not been clicked.";
"troubleshoot_notifications_test_display_notification_permission_failure" = "Cannot display the notification."; "troubleshoot_notifications_test_display_notification_permission_failure" = "Cannot display the notification.";
@@ -1237,7 +1243,7 @@
"screen_room_details_security_title" = "Security"; "screen_room_details_security_title" = "Security";
"screen_room_details_topic_title" = "Topic"; "screen_room_details_topic_title" = "Topic";
"screen_room_error_failed_processing_media" = "Failed processing media to upload, please try again."; "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_all_messages" = "All messages";
"screen_room_notification_settings_mode_mentions_and_keywords" = "Mentions and Keywords only"; "screen_room_notification_settings_mode_mentions_and_keywords" = "Mentions and Keywords only";
"screen_room_timeline_reactions_show_less" = "Show less"; "screen_room_timeline_reactions_show_less" = "Show less";
@@ -399,9 +399,26 @@
"screen_advanced_settings_hide_invite_avatars_toggle_title" = "Hide avatars in room invite requests"; "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_hide_timeline_media_toggle_title" = "Hide media previews in timeline";
"screen_advanced_settings_moderation_and_safety_section_title" = "Moderation and Safety"; "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_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_message" = "Would you like to start a chat with %1$@?";
"screen_bottom_sheet_create_dm_title" = "Send invite?"; "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_description" = "Anyone can join this room";
"screen_create_room_room_access_section_anyone_option_title" = "Anyone"; "screen_create_room_room_access_section_anyone_option_title" = "Anyone";
"screen_create_room_room_access_section_header" = "Room Access"; "screen_create_room_room_access_section_header" = "Room Access";
@@ -886,26 +903,14 @@
"screen_room_member_details_unblock_user" = "Unblock user"; "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_subtitle" = "Use the web app to verify this user.";
"screen_room_member_details_verify_button_title" = "Verify %1$@"; "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_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_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_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_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_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_banned" = "Banned";
"screen_room_member_list_mode_members" = "Members"; "screen_room_member_list_mode_members" = "Members";
"screen_room_member_list_pending_header_title" = "Pending"; "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_administrator" = "Admin";
"screen_room_member_list_role_moderator" = "Moderator"; "screen_room_member_list_role_moderator" = "Moderator";
"screen_room_member_list_room_members_header_title" = "Room members"; "screen_room_member_list_room_members_header_title" = "Room members";
@@ -1134,9 +1139,10 @@
"troubleshoot_notifications_test_current_push_provider_failure" = "No push providers selected."; "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_success" = "Current push provider: %1$@.";
"troubleshoot_notifications_test_current_push_provider_title" = "Current push provider"; "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_description" = "Ensure that the application supports at least one push provider.";
"troubleshoot_notifications_test_detect_push_provider_failure" = "No push providers found."; "troubleshoot_notifications_test_detect_push_provider_failure" = "No push provider support found.";
"troubleshoot_notifications_test_detect_push_provider_title" = "Detect push providers"; "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_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_failure" = "The notification has not been clicked.";
"troubleshoot_notifications_test_display_notification_permission_failure" = "Cannot display the notification."; "troubleshoot_notifications_test_display_notification_permission_failure" = "Cannot display the notification.";
@@ -1237,7 +1243,7 @@
"screen_room_details_security_title" = "Security"; "screen_room_details_security_title" = "Security";
"screen_room_details_topic_title" = "Topic"; "screen_room_details_topic_title" = "Topic";
"screen_room_error_failed_processing_media" = "Failed processing media to upload, please try again."; "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_all_messages" = "All messages";
"screen_room_notification_settings_mode_mentions_and_keywords" = "Mentions and Keywords only"; "screen_room_notification_settings_mode_mentions_and_keywords" = "Mentions and Keywords only";
"screen_room_timeline_reactions_show_less" = "Show less"; "screen_room_timeline_reactions_show_less" = "Show less";
+46 -32
View File
@@ -978,6 +978,16 @@ internal enum L10n {
internal static var screenAdvancedSettingsSharePresence: String { return L10n.tr("Localizable", "screen_advanced_settings_share_presence") } 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. /// 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") } 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. /// Enable option to view message source in the timeline.
internal static var screenAdvancedSettingsViewSourceDescription: String { return L10n.tr("Localizable", "screen_advanced_settings_view_source_description") } internal static var screenAdvancedSettingsViewSourceDescription: String { return L10n.tr("Localizable", "screen_advanced_settings_view_source_description") }
/// We won't record or profile any personal data /// 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") } internal static var screenBlockedUsersUnblockAlertTitle: String { return L10n.tr("Localizable", "screen_blocked_users_unblock_alert_title") }
/// Unblocking… /// Unblocking…
internal static var screenBlockedUsersUnblocking: String { return L10n.tr("Localizable", "screen_blocked_users_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 /// Send invite
internal static var screenBottomSheetCreateDmConfirmationButtonTitle: String { return L10n.tr("Localizable", "screen_bottom_sheet_create_dm_confirmation_button_title") } 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$@? /// Would you like to start a chat with %1$@?
@@ -1090,6 +1102,32 @@ internal enum L10n {
} }
/// Send invite? /// Send invite?
internal static var screenBottomSheetCreateDmTitle: String { return L10n.tr("Localizable", "screen_bottom_sheet_create_dm_title") } 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 /// Attach screenshot
internal static var screenBugReportAttachScreenshot: String { return L10n.tr("Localizable", "screen_bug_report_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. /// 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 { internal static func screenRoomMemberDetailsVerifyButtonTitle(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_room_member_details_verify_button_title", String(describing: p1)) 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. /// There are no banned users in this room.
internal static var screenRoomMemberListBannedEmpty: String { return L10n.tr("Localizable", "screen_room_member_list_banned_empty") } 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@" /// Plural format key: "%#@COUNT@"
internal static func screenRoomMemberListHeaderTitle(_ p1: Int) -> String { internal static func screenRoomMemberListHeaderTitle(_ p1: Int) -> String {
return L10n.tr("Localizable", "screen_room_member_list_header_title", p1) return L10n.tr("Localizable", "screen_room_member_list_header_title", p1)
} }
/// Remove /// Ban from room
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
internal static var screenRoomMemberListManageMemberRemoveConfirmationBan: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_remove_confirmation_ban") } internal static var screenRoomMemberListManageMemberRemoveConfirmationBan: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_remove_confirmation_ban") }
/// Only remove member /// Only remove member
internal static var screenRoomMemberListManageMemberRemoveConfirmationKick: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_remove_confirmation_kick") } 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 /// Unban
internal static var screenRoomMemberListManageMemberUnbanAction: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_unban_action") } 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. /// 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") } internal static var screenRoomMemberListManageMemberUnbanMessage: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_unban_message") }
/// Unban user /// Unban user
internal static var screenRoomMemberListManageMemberUnbanTitle: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_unban_title") } 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 /// Banned
internal static var screenRoomMemberListModeBanned: String { return L10n.tr("Localizable", "screen_room_member_list_mode_banned") } internal static var screenRoomMemberListModeBanned: String { return L10n.tr("Localizable", "screen_room_member_list_mode_banned") }
/// Members /// Members
internal static var screenRoomMemberListModeMembers: String { return L10n.tr("Localizable", "screen_room_member_list_mode_members") } internal static var screenRoomMemberListModeMembers: String { return L10n.tr("Localizable", "screen_room_member_list_mode_members") }
/// Pending /// Pending
internal static var screenRoomMemberListPendingHeaderTitle: String { return L10n.tr("Localizable", "screen_room_member_list_pending_header_title") } 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 /// Admin
internal static var screenRoomMemberListRoleAdministrator: String { return L10n.tr("Localizable", "screen_room_member_list_role_administrator") } internal static var screenRoomMemberListRoleAdministrator: String { return L10n.tr("Localizable", "screen_room_member_list_role_administrator") }
/// Moderator /// Moderator
@@ -2860,15 +2870,19 @@ internal enum L10n {
} }
/// Current push provider /// Current push provider
internal static var troubleshootNotificationsTestCurrentPushProviderTitle: String { return L10n.tr("Localizable", "troubleshoot_notifications_test_current_push_provider_title") } 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") } 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") } internal static var troubleshootNotificationsTestDetectPushProviderFailure: String { return L10n.tr("Localizable", "troubleshoot_notifications_test_detect_push_provider_failure") }
/// Plural format key: "%#@COUNT@" /// Plural format key: "%#@COUNT@"
internal static func troubleshootNotificationsTestDetectPushProviderSuccess(_ p1: Int) -> String { internal static func troubleshootNotificationsTestDetectPushProviderSuccess(_ p1: Int) -> String {
return L10n.tr("Localizable", "troubleshoot_notifications_test_detect_push_provider_success", p1) 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") } internal static var troubleshootNotificationsTestDetectPushProviderTitle: String { return L10n.tr("Localizable", "troubleshoot_notifications_test_detect_push_provider_title") }
/// Check that the application can display notification. /// Check that the application can display notification.
internal static var troubleshootNotificationsTestDisplayNotificationDescription: String { return L10n.tr("Localizable", "troubleshoot_notifications_test_display_notification_description") } internal static var troubleshootNotificationsTestDisplayNotificationDescription: String { return L10n.tr("Localizable", "troubleshoot_notifications_test_display_notification_description") }
@@ -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<ManageRoomMemberSheetViewAlertType>?
}
enum ManageRoomMemberSheetViewAlertType {
case kick
case ban
}
enum ManageRoomMemberSheetViewAction {
case kick
case ban
case displayDetails
}
@@ -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<ManageRoomMemberSheetViewState, ManageRoomMemberSheetViewAction>
class ManageRoomMemberSheetViewModel: ManageRoomMemberSheetViewModelType, ManageRoomMemberSheetViewModelProtocol {
private let roomProxy: JoinedRoomProxyProtocol
private let userIndicatorController: UserIndicatorControllerProtocol
private let analyticsService: AnalyticsService
private var actionsSubject: PassthroughSubject<ManageRoomMemberSheetViewModelAction, Never> = .init()
var actions: AnyPublisher<ManageRoomMemberSheetViewModelAction, Never> {
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<String> = .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 }
}
@@ -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<ManageRoomMemberSheetViewModelAction, Never> { get }
var context: ManageRoomMemberSheetViewModelType.Context { get }
}
@@ -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()))
}
}
@@ -80,7 +80,7 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType
self?.displayMediaPreview(mediaPreviewViewModel) self?.displayMediaPreview(mediaPreviewViewModel)
case .displayEmojiPicker, .displayReportContent, .displayCameraPicker, .displayMediaPicker, case .displayEmojiPicker, .displayReportContent, .displayCameraPicker, .displayMediaPicker,
.displayDocumentPicker, .displayLocationPicker, .displayPollForm, .displayMediaUploadPreviewScreen, .displayDocumentPicker, .displayLocationPicker, .displayPollForm, .displayMediaUploadPreviewScreen,
.tappedOnSenderDetails, .displayMessageForwarding, .displayLocation, .displayResolveSendFailure, .displaySenderDetails, .displayMessageForwarding, .displayLocation, .displayResolveSendFailure,
.composer, .hasScrolled, .viewInRoomTimeline: .composer, .hasScrolled, .viewInRoomTimeline:
break break
} }
@@ -102,7 +102,7 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType
self?.displayMediaPreview(mediaPreviewViewModel) self?.displayMediaPreview(mediaPreviewViewModel)
case .displayEmojiPicker, .displayReportContent, .displayCameraPicker, .displayMediaPicker, case .displayEmojiPicker, .displayReportContent, .displayCameraPicker, .displayMediaPicker,
.displayDocumentPicker, .displayLocationPicker, .displayPollForm, .displayMediaUploadPreviewScreen, .displayDocumentPicker, .displayLocationPicker, .displayPollForm, .displayMediaUploadPreviewScreen,
.tappedOnSenderDetails, .displayMessageForwarding, .displayLocation, .displayResolveSendFailure, .displaySenderDetails, .displayMessageForwarding, .displayLocation, .displayResolveSendFailure,
.composer, .hasScrolled, .viewInRoomTimeline: .composer, .hasScrolled, .viewInRoomTimeline:
break break
} }
@@ -78,7 +78,7 @@ final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol {
guard let self else { return } guard let self else { return }
switch action { switch action {
case .tappedOnSenderDetails(let userID): case .displaySenderDetails(let userID):
actionsSubject.send(.displayUser(userID: userID)) actionsSubject.send(.displayUser(userID: userID))
case .displayMessageForwarding(let forwardingItem): case .displayMessageForwarding(let forwardingItem):
actionsSubject.send(.displayMessageForwarding(forwardingItem: forwardingItem)) actionsSubject.send(.displayMessageForwarding(forwardingItem: forwardingItem))
@@ -28,6 +28,9 @@ struct PinnedEventsTimelineScreen: View {
.background(.compound.bgCanvasDefault) .background(.compound.bgCanvasDefault)
.interactiveDismissDisabled() .interactiveDismissDisabled()
.timelineMediaPreview(viewModel: $context.mediaPreviewViewModel) .timelineMediaPreview(viewModel: $context.mediaPreviewViewModel)
.sheet(item: $timelineContext.manageMemberViewModel) {
ManageRoomMemberSheetView(context: $0.context)
}
.sheet(item: $timelineContext.debugInfo) { TimelineItemDebugView(info: $0) } .sheet(item: $timelineContext.debugInfo) { TimelineItemDebugView(info: $0) }
.sheet(item: $timelineContext.actionMenuInfo) { info in .sheet(item: $timelineContext.actionMenuInfo) { info in
let actions = TimelineItemMenuActionProvider(timelineItem: info.item, let actions = TimelineItemMenuActionProvider(timelineItem: info.item,
@@ -80,31 +80,15 @@ struct RoomMembersListScreenViewStateBindings {
var searchQuery = "" var searchQuery = ""
/// The current mode the screen is in. /// The current mode the screen is in.
var mode: RoomMembersListScreenMode = .members var mode: RoomMembersListScreenMode = .members
/// A selected member to kick, ban, promote etc. /// A sheet model for the selected member to kick, ban, promote etc.
var memberToManage: RoomMembersListScreenManagementDetails? var manageMemeberViewModel: ManageRoomMemberSheetViewModel?
/// Information describing the currently displayed alert. /// Information describing the currently displayed alert.
var alertInfo: AlertInfo<RoomMembersListScreenAlertType>? var alertInfo: AlertInfo<RoomMembersListScreenAlertType>?
} }
/// 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 { enum RoomMembersListScreenViewAction {
case selectMember(RoomMemberDetails) case selectMember(RoomMemberDetails)
case showMemberDetails(RoomMemberDetails)
case kickMember(RoomMemberDetails)
case banMember(RoomMemberDetails)
case unbanMember(RoomMemberDetails) case unbanMember(RoomMemberDetails)
case invite case invite
} }
@@ -15,8 +15,10 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
private let roomProxy: JoinedRoomProxyProtocol private let roomProxy: JoinedRoomProxyProtocol
private let userIndicatorController: UserIndicatorControllerProtocol private let userIndicatorController: UserIndicatorControllerProtocol
private let analytics: AnalyticsService private let analytics: AnalyticsService
private let mediaProvider: MediaProviderProtocol
private var members: [RoomMemberProxyProtocol] = [] private var members: [RoomMemberProxyProtocol] = []
private var currentUserProxy: RoomMemberProxyProtocol?
private var actionsSubject: PassthroughSubject<RoomMembersListScreenViewModelAction, Never> = .init() private var actionsSubject: PassthroughSubject<RoomMembersListScreenViewModelAction, Never> = .init()
@@ -34,6 +36,7 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
self.roomProxy = roomProxy self.roomProxy = roomProxy
self.userIndicatorController = userIndicatorController self.userIndicatorController = userIndicatorController
self.analytics = analytics self.analytics = analytics
self.mediaProvider = mediaProvider
super.init(initialViewState: .init(joinedMembersCount: roomProxy.infoPublisher.value.joinedMembersCount, super.init(initialViewState: .init(joinedMembersCount: roomProxy.infoPublisher.value.joinedMembersCount,
bindings: .init(mode: initialMode)), bindings: .init(mode: initialMode)),
@@ -48,34 +51,6 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
switch viewAction { switch viewAction {
case .selectMember(let member): case .selectMember(let member):
selectMember(member) selectMember(member)
case .showMemberDetails(let member):
showMemberDetails(member)
case .kickMember(let member):
var reason: String?
let binding: Binding<String> = .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<String> = .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): case .unbanMember(let member):
Task { await unbanMember(member) } Task { await unbanMember(member) }
case .invite: case .invite:
@@ -118,6 +93,7 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
let members = members.sorted() let members = members.sorted()
let roomMembersDetails = await buildMembersDetails(members: members) let roomMembersDetails = await buildMembersDetails(members: members)
self.members = members self.members = members
self.currentUserProxy = members.first { $0.userID == roomProxy.ownUserID }
self.state = .init(joinedMembersCount: roomProxy.infoPublisher.value.joinedMembersCount, self.state = .init(joinedMembersCount: roomProxy.infoPublisher.value.joinedMembersCount,
joinedMembers: roomMembersDetails.joinedMembers, joinedMembers: roomMembersDetails.joinedMembers,
@@ -168,6 +144,12 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
} }
private func selectMember(_ member: RoomMemberDetails) { 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. 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), state.bindings.alertInfo = AlertInfo(id: .unbanConfirmation(member),
title: L10n.screenRoomMemberListManageMemberUnbanTitle, title: L10n.screenRoomMemberListManageMemberUnbanTitle,
@@ -179,16 +161,26 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
return return
} }
var actions = [RoomMembersListScreenManagementDetails.Action]() if state.canKickUsers || state.canBanUsers {
if state.canKickUsers, member.role != .administrator { let manageMemeberViewModel = ManageRoomMemberSheetViewModel(member: member,
actions.append(.kick) 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)
} }
if state.canBanUsers, member.role != .administrator {
actions.append(.ban)
} }
}
if !actions.isEmpty { .store(in: &cancellables)
state.bindings.memberToManage = .init(member: member, actions: actions) state.bindings.manageMemeberViewModel = manageMemeberViewModel
} else { } else {
showMemberDetails(member) showMemberDetails(member)
} }
@@ -200,46 +192,16 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
return return
} }
actionsSubject.send(.selectMember(member)) actionsSubject.send(.selectMember(member))
state.bindings.memberToManage = nil
} }
// MARK: - Member Management // 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 { private func unbanMember(_ member: RoomMemberDetails) async {
let indicatorTitle = L10n.screenRoomMemberListUnbanningUser(member.name ?? member.id) let indicatorTitle = L10n.screenRoomMemberListUnbanningUser(member.name ?? member.id)
showManageMemberIndicator(title: indicatorTitle) showManageMemberIndicator(title: indicatorTitle)
switch await roomProxy.unbanUser(member.id) { switch await roomProxy.unbanUser(member.id) {
case .success: case .success:
state.bindings.memberToManage = nil
hideManageMemberIndicator(title: indicatorTitle) hideManageMemberIndicator(title: indicatorTitle)
analytics.trackRoomModeration(action: .UnbanMember, role: nil) analytics.trackRoomModeration(action: .UnbanMember, role: nil)
case .failure: case .failure:
@@ -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)
}
}
@@ -47,8 +47,8 @@ struct RoomMembersListScreen: View {
.autocorrectionDisabled() .autocorrectionDisabled()
.background(Color.compound.bgCanvasDefault.ignoresSafeArea()) .background(Color.compound.bgCanvasDefault.ignoresSafeArea())
.navigationTitle(L10n.commonPeople) .navigationTitle(L10n.commonPeople)
.sheet(item: $context.memberToManage) { .sheet(item: $context.manageMemeberViewModel) {
RoomMembersListManageMemberSheet(member: $0.member, actions: $0.actions, context: context) ManageRoomMemberSheetView(context: $0.context)
} }
.alert(item: $context.alertInfo) .alert(item: $context.alertInfo)
.toolbar { toolbar } .toolbar { toolbar }
@@ -136,7 +136,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
actionsSubject.send(.presentPollForm(mode: mode)) actionsSubject.send(.presentPollForm(mode: mode))
case .displayMediaUploadPreviewScreen(let url): case .displayMediaUploadPreviewScreen(let url):
actionsSubject.send(.presentMediaUploadPreviewScreen(url)) actionsSubject.send(.presentMediaUploadPreviewScreen(url))
case .tappedOnSenderDetails(userID: let userID): case .displaySenderDetails(userID: let userID):
actionsSubject.send(.presentRoomMemberDetails(userID: userID)) actionsSubject.send(.presentRoomMemberDetails(userID: userID))
case .displayMessageForwarding(let forwardingItem): case .displayMessageForwarding(let forwardingItem):
actionsSubject.send(.presentMessageForwarding(forwardingItem: forwardingItem)) actionsSubject.send(.presentMessageForwarding(forwardingItem: forwardingItem))
@@ -66,6 +66,9 @@ struct RoomScreen: View {
.toolbarBackground(.visible, for: .navigationBar) // Fix the toolbar's background. .toolbarBackground(.visible, for: .navigationBar) // Fix the toolbar's background.
.overlay { loadingIndicator } .overlay { loadingIndicator }
.alert(item: $timelineContext.alertInfo) .alert(item: $timelineContext.alertInfo)
.sheet(item: $timelineContext.manageMemberViewModel) {
ManageRoomMemberSheetView(context: $0.context)
}
.sheet(item: $timelineContext.debugInfo) { TimelineItemDebugView(info: $0) } .sheet(item: $timelineContext.debugInfo) { TimelineItemDebugView(info: $0) }
.sheet(item: $timelineContext.actionMenuInfo) { info in .sheet(item: $timelineContext.actionMenuInfo) { info in
let actions = TimelineItemMenuActionProvider(timelineItem: info.item, let actions = TimelineItemMenuActionProvider(timelineItem: info.item,
@@ -19,7 +19,7 @@ enum TimelineViewModelAction {
case displayLocationPicker case displayLocationPicker
case displayPollForm(mode: PollFormMode) case displayPollForm(mode: PollFormMode)
case displayMediaUploadPreviewScreen(url: URL) case displayMediaUploadPreviewScreen(url: URL)
case tappedOnSenderDetails(userID: String) case displaySenderDetails(userID: String)
case displayMessageForwarding(forwardingItem: MessageForwardingItem) case displayMessageForwarding(forwardingItem: MessageForwardingItem)
case displayMediaPreview(TimelineMediaPreviewViewModel) case displayMediaPreview(TimelineMediaPreviewViewModel)
case displayLocation(body: String, geoURI: GeoURI, description: String?) case displayLocation(body: String, geoURI: GeoURI, description: String?)
@@ -99,6 +99,8 @@ struct TimelineViewState: BindableState {
var canCurrentUserRedactOthers = false var canCurrentUserRedactOthers = false
var canCurrentUserRedactSelf = false var canCurrentUserRedactSelf = false
var canCurrentUserPin = false var canCurrentUserPin = false
var canCurrentUserKick = false
var canCurrentUserBan = false
var isViewSourceEnabled: Bool var isViewSourceEnabled: Bool
var hideTimelineMedia: Bool var hideTimelineMedia: Bool
@@ -135,7 +137,7 @@ struct TimelineViewStateBindings {
/// Key is itemID, value is the collapsed state. /// Key is itemID, value is the collapsed state.
var reactionsCollapsed: [TimelineItemIdentifier: Bool] var reactionsCollapsed: [TimelineItemIdentifier: Bool]
var alertInfo: AlertInfo<RoomScreenAlertInfoType>? var alertInfo: AlertInfo<TimelineAlertInfoType>?
var debugInfo: TimelineItemDebugInfo? var debugInfo: TimelineItemDebugInfo?
@@ -144,6 +146,8 @@ struct TimelineViewStateBindings {
var reactionSummaryInfo: ReactionSummaryInfo? var reactionSummaryInfo: ReactionSummaryInfo?
var readReceiptsSummaryInfo: ReadReceiptSummaryInfo? var readReceiptsSummaryInfo: ReadReceiptSummaryInfo?
var manageMemberViewModel: ManageRoomMemberSheetViewModel?
} }
struct TimelineItemActionMenuInfo: Equatable, Identifiable { struct TimelineItemActionMenuInfo: Equatable, Identifiable {
@@ -172,7 +176,7 @@ struct ReadReceiptSummaryInfo: Identifiable {
let id: TimelineItemIdentifier let id: TimelineItemIdentifier
} }
enum RoomScreenAlertInfoType: Hashable { enum TimelineAlertInfoType: Hashable {
case audioRecodingPermissionError case audioRecodingPermissionError
case pollEndConfirmation(String) case pollEndConfirmation(String)
case sendingFailed case sendingFailed
@@ -42,6 +42,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
actionsSubject.eraseToAnyPublisher() actionsSubject.eraseToAnyPublisher()
} }
private var currentUserProxy: RoomMemberProxyProtocol?
private var paginateBackwardsTask: Task<Void, Never>? private var paginateBackwardsTask: Task<Void, Never>?
private var paginateForwardsTask: Task<Void, Never>? private var paginateForwardsTask: Task<Void, Never>?
@@ -173,7 +175,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
case .handleTimelineItemMenuAction(let itemID, let action): case .handleTimelineItemMenuAction(let itemID, let action):
timelineInteractionHandler.handleTimelineItemMenuAction(action, itemID: itemID) timelineInteractionHandler.handleTimelineItemMenuAction(action, itemID: itemID)
case .tappedOnSenderDetails(userID: let userID): case .tappedOnSenderDetails(userID: let userID):
actionsSubject.send(.tappedOnSenderDetails(userID: userID)) handleTappedOnSenderDetails(userID: userID)
case .displayEmojiPicker(let itemID): case .displayEmojiPicker(let itemID):
timelineInteractionHandler.displayEmojiPicker(for: itemID) timelineInteractionHandler.displayEmojiPicker(for: itemID)
case .displayReactionSummary(let itemID, let key): case .displayReactionSummary(let itemID, let key):
@@ -258,6 +260,41 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
// MARK: - Private // 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() { private func focusLive() {
timelineController.focusLive() timelineController.focusLive()
} }
@@ -348,6 +385,9 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
private func updateMembers(_ members: [RoomMemberProxyProtocol]) { private func updateMembers(_ members: [RoomMemberProxyProtocol]) {
state.members = members.reduce(into: [String: RoomMemberState]()) { dictionary, member in state.members = members.reduce(into: [String: RoomMemberState]()) { dictionary, member in
dictionary[member.userID] = RoomMemberState(displayName: member.displayName, avatarURL: member.avatarURL) 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 { } else {
state.canCurrentUserPin = false 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() { private func setupSubscriptions() {
@@ -898,7 +950,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
userIndicatorController.retractIndicatorWithId(Constants.focusTimelineToastIndicatorID) userIndicatorController.retractIndicatorWithId(Constants.focusTimelineToastIndicatorID)
} }
private func displayAlert(_ type: RoomScreenAlertInfoType) { private func displayAlert(_ type: TimelineAlertInfoType) {
switch type { switch type {
case .audioRecodingPermissionError: case .audioRecodingPermissionError:
state.bindings.alertInfo = .init(id: type, state.bindings.alertInfo = .init(id: type,
@@ -20,6 +20,7 @@ struct RoomMemberDetails: Identifiable, Hashable {
enum Role { case administrator, moderator, user } enum Role { case administrator, moderator, user }
let role: Role let role: Role
let powerLevel: Int
func matches(searchQuery: String) -> Bool { func matches(searchQuery: String) -> Bool {
guard !searchQuery.isEmpty else { return true } guard !searchQuery.isEmpty else { return true }
@@ -38,6 +39,7 @@ extension RoomMemberDetails {
isIgnored = proxy.isIgnored isIgnored = proxy.isIgnored
isBanned = proxy.membership == .ban isBanned = proxy.membership == .ban
role = .init(proxy.role) role = .init(proxy.role)
powerLevel = proxy.powerLevel
} }
} }
@@ -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 { func testMapLibreStaticMapView() async throws {
for (index, preview) in MapLibreStaticMapView_Previews._allPreviews.enumerated() { for (index, preview) in MapLibreStaticMapView_Previews._allPreviews.enumerated() {
try await assertSnapshots(matching: preview, step: index) 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 { func testRoomMembersListMemberCell() async throws {
for (index, preview) in RoomMembersListMemberCell_Previews._allPreviews.enumerated() { for (index, preview) in RoomMembersListMemberCell_Previews._allPreviews.enumerated() {
try await assertSnapshots(matching: preview, step: index) try await assertSnapshots(matching: preview, step: index)
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:838a77d7faa429fb769012c1361ddb6ec96aef85fbd3cbf913fdb934e6ed639d
size 195727
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9708620c69e77bc74e461e5a1f5bf5bc843781fe124bfaf3eaa2b24ff8020c64
size 197751
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b36e8a40c54853a34366ba352736da206a6659ae9291bba3d9f338da44f26f0a
size 135362
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1569bcd5c6e626315326b6275b6d069327af935ba9f8971aea27878be26beb8f
size 140550
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1f6d2a023376318b8d75646ac54d71117cd94f5b5b679747a550f9145d450e11
size 190338
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f3053a7e72c09435efb95e99e6c5ade86c7aa2b186f37c2b3a23207113bd0740
size 191560
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:94273ef44799d1d926c96451b8c93f41a4262300691f3dc61b93997b8ff49c02
size 129508
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:034aba123a2109820e7d15627ba68826769032a2de2045be4ee0bbdf8e12d76c
size 131928
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7a02e69172fc903b7efb96f4f9e9133a95b6c4db94004ca41b65f07279497e92
size 189632
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e215c8f632a632fb1f09d98e98fef71fe8fcf94503492078e60a929f4cea6946
size 191198
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:680f38ab808dcfd019e0e925ef187033036ff26287cdc495c6242563ed0466fa
size 129401
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4d4cfc4fe5d65b69f8930427a54fc55b83aa3f38dc15978c61f79f12776387e2
size 134646
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:69d3907f88971cdeb3024488bad0cc0e8023c8eb3f00770d35f1ceb706cee12e
size 93594
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:61569c78e79a50ab1f5430326df0007462d83ffd1b87e1bf75846df7fbebab17
size 94307
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:eb501a8fc0b0c2399a25c54d489aa3dd948a114fb015045de9c9204962f6510d
size 45831
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c8ceedc2eacd4a377c8b0e54c41bb621bc465660fd33bf5475bbd6ee55f699da
size 47942
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:481e986b2ab0b1bb7bf92fbbd14d1a58cf20e4497653d8b230aebb6a023139c1
size 196642
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:21f9d12fc11aaacafbb142dee763ac8d68447094aadb3d9c00910e7a5c11334d
size 199499
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:973bf92301cda4c122574b1b3d21ca7f30fcdcf427d821a819f4b02f1e7700d2
size 136277
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c6b7be6ae8cc2b73e04b709f5daed55b20df7258de7135f80539158aca860e94
size 145835
@@ -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)
}
}
@@ -143,7 +143,7 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
// Then the member's details should be shown. // Then the member's details should be shown.
try await memberDetailsAction.fulfill() try await memberDetailsAction.fulfill()
XCTAssertNil(context.memberToManage) XCTAssertNil(context.manageMemeberViewModel)
} }
func testSelectUserAsAdmin() async throws { func testSelectUserAsAdmin() async throws {
@@ -151,10 +151,10 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
setup(with: .allMembersAsAdmin) setup(with: .allMembersAsAdmin)
var deferred = deferFulfillment(context.$viewState) { !$0.visibleInvitedMembers.isEmpty && $0.canKickUsers && $0.canBanUsers } var deferred = deferFulfillment(context.$viewState) { !$0.visibleInvitedMembers.isEmpty && $0.canKickUsers && $0.canBanUsers }
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertNil(context.memberToManage) XCTAssertNil(context.manageMemeberViewModel)
// When tapping on a user in the list. // 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 { guard let user = viewModel.state.visibleJoinedMembers.first(where: { $0.member.role == .user && $0.member.id != RoomMemberProxyMock.mockMe.userID })?.member else {
XCTFail("Expected to find a regular user.") XCTFail("Expected to find a regular user.")
return return
@@ -163,8 +163,9 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
try await deferred.fulfill() try await deferred.fulfill()
// Then member management should be shown for that user. // Then member management should be shown for that user.
XCTAssertEqual(context.memberToManage?.member, user) XCTAssertEqual(context.manageMemeberViewModel?.state.member, user)
XCTAssertEqual(context.memberToManage?.actions, [.kick, .ban]) XCTAssertEqual(context.manageMemeberViewModel?.state.canKick, true)
XCTAssertEqual(context.manageMemeberViewModel?.state.canBan, true)
} }
func testSelectModeratorAsAdmin() async throws { func testSelectModeratorAsAdmin() async throws {
@@ -172,10 +173,10 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
setup(with: .allMembersAsAdmin) setup(with: .allMembersAsAdmin)
var deferred = deferFulfillment(context.$viewState) { !$0.visibleInvitedMembers.isEmpty && $0.canKickUsers && $0.canBanUsers } var deferred = deferFulfillment(context.$viewState) { !$0.visibleInvitedMembers.isEmpty && $0.canKickUsers && $0.canBanUsers }
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertNil(context.memberToManage) XCTAssertNil(context.manageMemeberViewModel)
// When tapping on a moderator in the list. // 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 { guard let moderator = viewModel.state.visibleJoinedMembers.first(where: { $0.member.role == .moderator })?.member else {
XCTFail("Expected to find a moderator.") XCTFail("Expected to find a moderator.")
return return
@@ -184,8 +185,9 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
try await deferred.fulfill() try await deferred.fulfill()
// Then member management should be shown for the moderator. // Then member management should be shown for the moderator.
XCTAssertEqual(context.memberToManage?.member, moderator) XCTAssertEqual(context.manageMemeberViewModel?.state.member, moderator)
XCTAssertEqual(context.memberToManage?.actions, [.kick, .ban]) XCTAssertEqual(context.manageMemeberViewModel?.state.canKick, true)
XCTAssertEqual(context.manageMemeberViewModel?.state.canBan, true)
} }
func testSelectAdminAsAdmin() async throws { func testSelectAdminAsAdmin() async throws {
@@ -204,7 +206,7 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
// Then the administrator's details should be shown. // Then the administrator's details should be shown.
try await memberDetailsAction.fulfill() try await memberDetailsAction.fulfill()
XCTAssertNil(context.memberToManage) XCTAssertNil(context.manageMemeberViewModel)
} }
func testSelectOwnMemberAsAdmin() async throws { func testSelectOwnMemberAsAdmin() async throws {
@@ -223,7 +225,7 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
// Then your member's details should be shown. // Then your member's details should be shown.
try await memberDetailsAction.fulfill() try await memberDetailsAction.fulfill()
XCTAssertNil(context.memberToManage) XCTAssertNil(context.manageMemeberViewModel)
} }
func testSelectBannedMember() async throws { func testSelectBannedMember() async throws {
@@ -243,50 +245,10 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
// Then an alert should be shown to unban the user. // Then an alert should be shown to unban the user.
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertNil(context.memberToManage) XCTAssertNil(context.manageMemeberViewModel)
XCTAssertNotNil(context.alertInfo) 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 { func testUnbanMember() async throws {
setup(with: .allMembersAsAdmin) setup(with: .allMembersAsAdmin)
let deferred = deferFulfillment(context.$viewState) { !$0.visibleJoinedMembers.isEmpty } let deferred = deferFulfillment(context.$viewState) { !$0.visibleJoinedMembers.isEmpty }
@@ -350,6 +350,117 @@ class TimelineViewModelTests: XCTestCase {
try await deferred.fulfill() 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 // MARK: - Pins
func testPinnedEvents() async throws { func testPinnedEvents() async throws {