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 */; };
1B8E30B35BF8F541C1318F19 /* SecureBackupScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40316EFFEAC7B206EE9A55AE /* SecureBackupScreenViewModelTests.swift */; };
1BA04D05EBC6646958B1BE60 /* PlaceholderScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF34A2FD6797535C95AC918D /* PlaceholderScreenCoordinator.swift */; };
1C1750C009F7214B967928BC /* ManageRoomMemberSheetViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D80807B554CF9C524F98674F /* ManageRoomMemberSheetViewModelTests.swift */; };
1C409A26A99F0371C47AFA51 /* UserDiscoveryServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F615A00DB223FF3280204D2 /* UserDiscoveryServiceProtocol.swift */; };
1C598D3B785645AAC7B35760 /* ReportRoomScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 292EEE1F71DCC205C45728F7 /* ReportRoomScreenCoordinator.swift */; };
1C8BC70A18060677E295A846 /* ShareToMapsAppActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4481799F455B3DA243BDA2AC /* ShareToMapsAppActivity.swift */; };
@@ -421,6 +422,7 @@
5038E69A5E6A89DE1A345E04 /* ShouldScrollOnKeyboardDidShow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832397B5C3D00A4BF52C5F0B /* ShouldScrollOnKeyboardDidShow.swift */; };
50539366B408780B232C1910 /* EstimatedWaveformView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD0FF64B0E6470F66F42E182 /* EstimatedWaveformView.swift */; };
5100F53E6884A15F9BA07CC3 /* AttributedStringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CA26F55123E36B50DB0B3A /* AttributedStringTests.swift */; };
510C4EDF826CA9C6CEEC6C95 /* ManageRoomMemberSheetViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A34D9BCA1A7D9A56E1EAF1D /* ManageRoomMemberSheetViewModel.swift */; };
5139F4BD5A5DF6F8D11A9BDE /* NotificationPermissionsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46D0BA44B1838E65B507B277 /* NotificationPermissionsScreen.swift */; };
513AF15E0E84711B80D04B1B /* ReportRoomScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3E9684DCE6B66BD0B5DF67 /* ReportRoomScreenViewModelTests.swift */; };
518C93DC6516D3D018DE065F /* UNNotificationRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49E751D7EDB6043238111D90 /* UNNotificationRequest.swift */; };
@@ -478,7 +480,6 @@
5D70FAE4D2BF4553AFFFFE41 /* NotificationItemProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F7FE40EF7490A7E09D7BE6 /* NotificationItemProxy.swift */; };
5D99F63CC88BB29383019FC6 /* ShareExtensionModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCAC01A97A43BE07B9E94E43 /* ShareExtensionModels.swift */; };
5DB4334CBBA142376FF5FFEC /* preview_image.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 200626E8353AB2729444F991 /* preview_image.jpg */; };
5DD0EF30070DC0A82C5CCD33 /* RoomMembersListManageMemberSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC853F9B4FBE039D2C16EC6B /* RoomMembersListManageMemberSheet.swift */; };
5DD85A0FE3D85AEC3C7EFE36 /* DeveloperOptionsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7C7CFA6B2A62A685FF6CE3 /* DeveloperOptionsScreenCoordinator.swift */; };
5DFC2A889D3B39DD47AC63A8 /* PillUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C537DE821FED94D23467B6C4 /* PillUtilities.swift */; };
5EC046E41755C095DAB1C3FF /* TimelineProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8C9BBB729C941BEE0E2A63 /* TimelineProviderProtocol.swift */; };
@@ -1145,6 +1146,7 @@
E481C8FDCB6C089963C95344 /* DeviceKit in Frameworks */ = {isa = PBXBuildFile; productRef = BC01130651CB23340B899032 /* DeviceKit */; };
E49F74BD93230BDEFFE5EA51 /* RoomNotificationSettingsScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D295F0081084F38DB20893 /* RoomNotificationSettingsScreenViewModelTests.swift */; };
E4B07FF075C99D04D9AF792D /* AppLockSetupPINScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = B410B32B72C90BF94E481F33 /* AppLockSetupPINScreenModels.swift */; };
E4D261E237D5D45E6DF2D0F1 /* ManageRoomMemberSheetModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 787E84119E626E2F0E0BFBE8 /* ManageRoomMemberSheetModels.swift */; };
E4F924DECC66389C1C810550 /* AuthenticationStartScreenBackgroundImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50D685B4DB38BB5BD87C956A /* AuthenticationStartScreenBackgroundImage.swift */; };
E58F1F3276E98A93F7D39219 /* RoomPollsHistoryScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1D8479BB704B7EF696F8ABE /* RoomPollsHistoryScreenCoordinator.swift */; };
E5AB28123E2488F97E953AC0 /* CallNotificationRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1ED17433ADC77287F8904F9 /* CallNotificationRoomTimelineItem.swift */; };
@@ -1212,11 +1214,13 @@
F118DD449066E594F63C697D /* RoomMemberProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32B5E17028C02DFA7DDA3931 /* RoomMemberProxyProtocol.swift */; };
F12F6BED7B6D7EE4BEE55039 /* PlainMentionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AE78FA0011E07920AE83135 /* PlainMentionBuilder.swift */; };
F18CA61A58C77C84F551B8E7 /* GeneratedMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57916A1578D8043BB0795441 /* GeneratedMocks.swift */; };
F1C68F64FC8A66B6B9510BF7 /* ManageRoomMemberSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B30CD19ED6243FEDFBA8400 /* ManageRoomMemberSheetView.swift */; };
F252C0EA49088801F4CA6006 /* landscape_test_image.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 96CE9D6642DD487D8CC90C9C /* landscape_test_image.jpg */; };
F253AAB4C8F06208173C9C4A /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D52BAA5BADB06E5E8C295D /* Assets.swift */; };
F255083E18CDBFDF7E640FB1 /* Avatars.swift in Sources */ = {isa = PBXBuildFile; fileRef = C142248014E08E885E323E56 /* Avatars.swift */; };
F2D5C0E1351DA7BD16867629 /* TimelineStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD4823EAB4B4E8BAB4F6B8C /* TimelineStyle.swift */; };
F2E580C0FBFBEFFE9D69893B /* RoomPreviewProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 739077686814E4EA339B1C83 /* RoomPreviewProxyProtocol.swift */; };
F35FAD1B1B289E221A07D719 /* ManageRoomMemberSheetViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB7B588A06911B455AC0B4C9 /* ManageRoomMemberSheetViewModelProtocol.swift */; };
F37629BAA5E8F50AAF2A131D /* SoftLogoutScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB7BAD55A4E2B8E5828CD64C /* SoftLogoutScreenViewModel.swift */; };
F38D32C1B0232AAFE6A0822C /* ExtensionLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A8571A8A071FB41778C016 /* ExtensionLogger.swift */; };
F3E2D3F7ACDED65A4E5CD8DE /* RoomMembersListScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECF79FB25E2D4BD6F50CE7C9 /* RoomMembersListScreenViewModel.swift */; };
@@ -1489,6 +1493,7 @@
1B065EC39C99C1303A101C1C /* WebRegistrationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRegistrationScreen.swift; sourceTree = "<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>"; };
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>"; };
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>"; };
@@ -1930,6 +1935,7 @@
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>"; };
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>"; };
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>"; };
@@ -2105,6 +2111,7 @@
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>"; };
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>"; };
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>"; };
@@ -2352,6 +2359,7 @@
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>"; };
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>"; };
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>"; };
@@ -2409,6 +2417,7 @@
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>"; };
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>"; };
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>"; };
@@ -2572,7 +2581,6 @@
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>"; };
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>"; };
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>"; };
@@ -3490,6 +3498,17 @@
path = QRCodeLoginScreen;
sourceTree = "<group>";
};
3D76DA5827DF9396AC90E7B4 /* ManageRoomMemberSheet */ = {
isa = PBXGroup;
children = (
787E84119E626E2F0E0BFBE8 /* ManageRoomMemberSheetModels.swift */,
9A34D9BCA1A7D9A56E1EAF1D /* ManageRoomMemberSheetViewModel.swift */,
CB7B588A06911B455AC0B4C9 /* ManageRoomMemberSheetViewModelProtocol.swift */,
40AE45E13BF744AA0E4EA0E4 /* View */,
);
path = ManageRoomMemberSheet;
sourceTree = "<group>";
};
3E1CCC4B607946CE90B4A827 /* DeclineAndBlockScreen */ = {
isa = PBXGroup;
children = (
@@ -3559,6 +3578,14 @@
);
sourceTree = "<group>";
};
40AE45E13BF744AA0E4EA0E4 /* View */ = {
isa = PBXGroup;
children = (
1B30CD19ED6243FEDFBA8400 /* ManageRoomMemberSheetView.swift */,
);
path = View;
sourceTree = "<group>";
};
40D9A816C45E0278C29DF883 /* SupportingFiles */ = {
isa = PBXGroup;
children = (
@@ -4287,6 +4314,7 @@
C070FD43DC6BF4E50217965A /* LocalizationTests.swift */,
3DC1943ADE6A62ED5129D7C8 /* LoggingTests.swift */,
5A43964330459965AF048A8C /* LoginScreenViewModelTests.swift */,
D80807B554CF9C524F98674F /* ManageRoomMemberSheetViewModelTests.swift */,
1D262A26713C18BB70C82CA5 /* MapTilerURLBuilderTests.swift */,
F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */,
2D7A2C4A3A74F0D2FFE9356A /* MediaPlayerProviderTests.swift */,
@@ -4878,7 +4906,6 @@
949B06577E5265373013DDAB /* View */ = {
isa = PBXGroup;
children = (
FC853F9B4FBE039D2C16EC6B /* RoomMembersListManageMemberSheet.swift */,
1B8E176484A89BAC389D4076 /* RoomMembersListScreen.swift */,
CC03209FDE8CE0810617BFFF /* RoomMembersListScreenMemberCell.swift */,
);
@@ -5776,6 +5803,7 @@
BF0415BE807CA2BCFC210008 /* KnockRequestsListScreen */,
948DD12A5533BE1BC260E437 /* LocationSharing */,
73E032ADD008D63812791D97 /* LogViewerScreen */,
3D76DA5827DF9396AC90E7B4 /* ManageRoomMemberSheet */,
26397A1EDB867FD573821532 /* MediaEventsTimelineScreen */,
87E2774157D9C4894BCFF3F8 /* MediaPickerScreen */,
23605DD08620BE6558242469 /* MediaUploadPreviewScreen */,
@@ -6776,6 +6804,7 @@
0033481EE363E4914295F188 /* LocalizationTests.swift in Sources */,
149D1942DC005D0485FB8D93 /* LoggingTests.swift in Sources */,
7434A7F02D587A920B376A9A /* LoginScreenViewModelTests.swift in Sources */,
1C1750C009F7214B967928BC /* ManageRoomMemberSheetViewModelTests.swift in Sources */,
3BEBDCB42BABFA3B456FECA7 /* MapTilerURLBuilderTests.swift in Sources */,
2E43A3D221BE9587BC19C3F1 /* MatrixEntityRegexTests.swift in Sources */,
4B978C09567387EF4366BD7A /* MediaLoaderTests.swift in Sources */,
@@ -7269,6 +7298,10 @@
A5B9EF45C7B8ACEB4954AE36 /* LoginScreenViewModelProtocol.swift in Sources */,
B10F7D5C237417DA160F4603 /* LongPressWithFeedback.swift in Sources */,
B94368839BDB69172E28E245 /* MXLog.swift in Sources */,
E4D261E237D5D45E6DF2D0F1 /* ManageRoomMemberSheetModels.swift in Sources */,
F1C68F64FC8A66B6B9510BF7 /* ManageRoomMemberSheetView.swift in Sources */,
510C4EDF826CA9C6CEEC6C95 /* ManageRoomMemberSheetViewModel.swift in Sources */,
F35FAD1B1B289E221A07D719 /* ManageRoomMemberSheetViewModelProtocol.swift in Sources */,
C1D0AB8222D7BAFC9AF9C8C0 /* MapLibreMapView.swift in Sources */,
C9BE065FA7D4E77E4C61CB69 /* MapLibreModels.swift in Sources */,
E2DDA49BD62F03F180A42E30 /* MapLibreStaticMapView.swift in Sources */,
@@ -7487,7 +7520,6 @@
6448F8D1D3CA4CD27BB4CADD /* RoomMemberProxy.swift in Sources */,
92D9088B901CEBB1A99ECA4E /* RoomMemberProxyMock.swift in Sources */,
F118DD449066E594F63C697D /* RoomMemberProxyProtocol.swift in Sources */,
5DD0EF30070DC0A82C5CCD33 /* RoomMembersListManageMemberSheet.swift in Sources */,
C08AAE7563E0722C9383F51C /* RoomMembersListScreen.swift in Sources */,
1C9BB74711E5F24C77B7FED0 /* RoomMembersListScreenCoordinator.swift in Sources */,
A975D60EA49F6AF73308809F /* RoomMembersListScreenMemberCell.swift in Sources */,
@@ -399,9 +399,26 @@
"screen_advanced_settings_hide_invite_avatars_toggle_title" = "Hide avatars in room invite requests";
"screen_advanced_settings_hide_timeline_media_toggle_title" = "Hide media previews in timeline";
"screen_advanced_settings_moderation_and_safety_section_title" = "Moderation and Safety";
"screen_advanced_settings_show_media_timeline_always_hide" = "Always hide";
"screen_advanced_settings_show_media_timeline_always_show" = "Always show";
"screen_advanced_settings_show_media_timeline_private_rooms" = "In private rooms";
"screen_advanced_settings_show_media_timeline_subtitle" = "A hidden media can always be shown by tapping on it";
"screen_advanced_settings_show_media_timeline_title" = "Show media in timeline";
"screen_bottom_sheet,manage_room_member_remove" = "Remove from room";
"screen_bottom_sheet_create_dm_confirmation_button_title" = "Send invite";
"screen_bottom_sheet_create_dm_message" = "Would you like to start a chat with %1$@?";
"screen_bottom_sheet_create_dm_title" = "Send invite?";
"screen_bottom_sheet_manage_room_member_ban" = "Ban from room";
"screen_bottom_sheet_manage_room_member_ban_member_confirmation_action" = "Ban";
"screen_bottom_sheet_manage_room_member_ban_member_confirmation_description" = "They won’t be able to join this room again if invited.";
"screen_bottom_sheet_manage_room_member_ban_member_confirmation_title" = "Are you sure you want to ban this member?";
"screen_bottom_sheet_manage_room_member_banning_user" = "Banning %1$@";
"screen_bottom_sheet_manage_room_member_kick_member_confirmation_action" = "Remove";
"screen_bottom_sheet_manage_room_member_kick_member_confirmation_description" = "They will be able to join this room again if invited.";
"screen_bottom_sheet_manage_room_member_kick_member_confirmation_title" = "Are you sure you want to remove this member?";
"screen_bottom_sheet_manage_room_member_member_user_info" = "View profile";
"screen_bottom_sheet_manage_room_member_remove_confirmation_title" = "Remove member and ban from joining in the future?";
"screen_bottom_sheet_manage_room_member_removing_user" = "Removing %1$@…";
"screen_create_room_room_access_section_anyone_option_description" = "Anyone can join this room";
"screen_create_room_room_access_section_anyone_option_title" = "Anyone";
"screen_create_room_room_access_section_header" = "Room Access";
@@ -886,26 +903,14 @@
"screen_room_member_details_unblock_user" = "Unblock user";
"screen_room_member_details_verify_button_subtitle" = "Use the web app to verify this user.";
"screen_room_member_details_verify_button_title" = "Verify %1$@";
"screen_room_member_list_ban_member_confirmation_action" = "Ban";
"screen_room_member_list_ban_member_confirmation_description" = "They won’t be able to join this room again if invited.";
"screen_room_member_list_ban_member_confirmation_title" = "Are you sure you want to ban this member?";
"screen_room_member_list_banned_empty" = "There are no banned users in this room.";
"screen_room_member_list_banning_user" = "Banning %1$@";
"screen_room_member_list_kick_member_confirmation_action" = "Remove";
"screen_room_member_list_kick_member_confirmation_description" = "They will be able to join this room again if invited.";
"screen_room_member_list_kick_member_confirmation_title" = "Are you sure you want to remove this member?";
"screen_room_member_list_manage_member_ban" = "Remove and ban member";
"screen_room_member_list_manage_member_remove" = "Remove from room";
"screen_room_member_list_manage_member_remove_confirmation_kick" = "Only remove member";
"screen_room_member_list_manage_member_remove_confirmation_title" = "Remove member and ban from joining in the future?";
"screen_room_member_list_manage_member_unban_action" = "Unban";
"screen_room_member_list_manage_member_unban_message" = "They will be able to join this room again if invited.";
"screen_room_member_list_manage_member_unban_title" = "Unban user";
"screen_room_member_list_manage_member_user_info" = "View profile";
"screen_room_member_list_mode_banned" = "Banned";
"screen_room_member_list_mode_members" = "Members";
"screen_room_member_list_pending_header_title" = "Pending";
"screen_room_member_list_removing_user" = "Removing %1$@…";
"screen_room_member_list_role_administrator" = "Admin";
"screen_room_member_list_role_moderator" = "Moderator";
"screen_room_member_list_room_members_header_title" = "Room members";
@@ -1134,9 +1139,10 @@
"troubleshoot_notifications_test_current_push_provider_failure" = "No push providers selected.";
"troubleshoot_notifications_test_current_push_provider_success" = "Current push provider: %1$@.";
"troubleshoot_notifications_test_current_push_provider_title" = "Current push provider";
"troubleshoot_notifications_test_detect_push_provider_description" = "Ensure that the application has at least one push provider.";
"troubleshoot_notifications_test_detect_push_provider_failure" = "No push providers found.";
"troubleshoot_notifications_test_detect_push_provider_title" = "Detect push providers";
"troubleshoot_notifications_test_detect_push_provider_description" = "Ensure that the application supports at least one push provider.";
"troubleshoot_notifications_test_detect_push_provider_failure" = "No push provider support found.";
"troubleshoot_notifications_test_detect_push_provider_success_2" = "The application was built with support for: %1$@";
"troubleshoot_notifications_test_detect_push_provider_title" = "Push provider support";
"troubleshoot_notifications_test_display_notification_description" = "Check that the application can display notification.";
"troubleshoot_notifications_test_display_notification_failure" = "The notification has not been clicked.";
"troubleshoot_notifications_test_display_notification_permission_failure" = "Cannot display the notification.";
@@ -1237,7 +1243,7 @@
"screen_room_details_security_title" = "Security";
"screen_room_details_topic_title" = "Topic";
"screen_room_error_failed_processing_media" = "Failed processing media to upload, please try again.";
"screen_room_member_list_manage_member_remove_confirmation_ban" = "Remove and ban member";
"screen_room_member_list_manage_member_remove_confirmation_ban" = "Ban from room";
"screen_room_notification_settings_mode_all_messages" = "All messages";
"screen_room_notification_settings_mode_mentions_and_keywords" = "Mentions and Keywords only";
"screen_room_timeline_reactions_show_less" = "Show less";
@@ -399,9 +399,26 @@
"screen_advanced_settings_hide_invite_avatars_toggle_title" = "Hide avatars in room invite requests";
"screen_advanced_settings_hide_timeline_media_toggle_title" = "Hide media previews in timeline";
"screen_advanced_settings_moderation_and_safety_section_title" = "Moderation and Safety";
"screen_advanced_settings_show_media_timeline_always_hide" = "Always hide";
"screen_advanced_settings_show_media_timeline_always_show" = "Always show";
"screen_advanced_settings_show_media_timeline_private_rooms" = "In private rooms";
"screen_advanced_settings_show_media_timeline_subtitle" = "A hidden media can always be shown by tapping on it";
"screen_advanced_settings_show_media_timeline_title" = "Show media in timeline";
"screen_bottom_sheet,manage_room_member_remove" = "Remove from room";
"screen_bottom_sheet_create_dm_confirmation_button_title" = "Send invite";
"screen_bottom_sheet_create_dm_message" = "Would you like to start a chat with %1$@?";
"screen_bottom_sheet_create_dm_title" = "Send invite?";
"screen_bottom_sheet_manage_room_member_ban" = "Ban from room";
"screen_bottom_sheet_manage_room_member_ban_member_confirmation_action" = "Ban";
"screen_bottom_sheet_manage_room_member_ban_member_confirmation_description" = "They won’t be able to join this room again if invited.";
"screen_bottom_sheet_manage_room_member_ban_member_confirmation_title" = "Are you sure you want to ban this member?";
"screen_bottom_sheet_manage_room_member_banning_user" = "Banning %1$@";
"screen_bottom_sheet_manage_room_member_kick_member_confirmation_action" = "Remove";
"screen_bottom_sheet_manage_room_member_kick_member_confirmation_description" = "They will be able to join this room again if invited.";
"screen_bottom_sheet_manage_room_member_kick_member_confirmation_title" = "Are you sure you want to remove this member?";
"screen_bottom_sheet_manage_room_member_member_user_info" = "View profile";
"screen_bottom_sheet_manage_room_member_remove_confirmation_title" = "Remove member and ban from joining in the future?";
"screen_bottom_sheet_manage_room_member_removing_user" = "Removing %1$@…";
"screen_create_room_room_access_section_anyone_option_description" = "Anyone can join this room";
"screen_create_room_room_access_section_anyone_option_title" = "Anyone";
"screen_create_room_room_access_section_header" = "Room Access";
@@ -886,26 +903,14 @@
"screen_room_member_details_unblock_user" = "Unblock user";
"screen_room_member_details_verify_button_subtitle" = "Use the web app to verify this user.";
"screen_room_member_details_verify_button_title" = "Verify %1$@";
"screen_room_member_list_ban_member_confirmation_action" = "Ban";
"screen_room_member_list_ban_member_confirmation_description" = "They won’t be able to join this room again if invited.";
"screen_room_member_list_ban_member_confirmation_title" = "Are you sure you want to ban this member?";
"screen_room_member_list_banned_empty" = "There are no banned users in this room.";
"screen_room_member_list_banning_user" = "Banning %1$@";
"screen_room_member_list_kick_member_confirmation_action" = "Remove";
"screen_room_member_list_kick_member_confirmation_description" = "They will be able to join this room again if invited.";
"screen_room_member_list_kick_member_confirmation_title" = "Are you sure you want to remove this member?";
"screen_room_member_list_manage_member_ban" = "Remove and ban member";
"screen_room_member_list_manage_member_remove" = "Remove from room";
"screen_room_member_list_manage_member_remove_confirmation_kick" = "Only remove member";
"screen_room_member_list_manage_member_remove_confirmation_title" = "Remove member and ban from joining in the future?";
"screen_room_member_list_manage_member_unban_action" = "Unban";
"screen_room_member_list_manage_member_unban_message" = "They will be able to join this room again if invited.";
"screen_room_member_list_manage_member_unban_title" = "Unban user";
"screen_room_member_list_manage_member_user_info" = "View profile";
"screen_room_member_list_mode_banned" = "Banned";
"screen_room_member_list_mode_members" = "Members";
"screen_room_member_list_pending_header_title" = "Pending";
"screen_room_member_list_removing_user" = "Removing %1$@…";
"screen_room_member_list_role_administrator" = "Admin";
"screen_room_member_list_role_moderator" = "Moderator";
"screen_room_member_list_room_members_header_title" = "Room members";
@@ -1134,9 +1139,10 @@
"troubleshoot_notifications_test_current_push_provider_failure" = "No push providers selected.";
"troubleshoot_notifications_test_current_push_provider_success" = "Current push provider: %1$@.";
"troubleshoot_notifications_test_current_push_provider_title" = "Current push provider";
"troubleshoot_notifications_test_detect_push_provider_description" = "Ensure that the application has at least one push provider.";
"troubleshoot_notifications_test_detect_push_provider_failure" = "No push providers found.";
"troubleshoot_notifications_test_detect_push_provider_title" = "Detect push providers";
"troubleshoot_notifications_test_detect_push_provider_description" = "Ensure that the application supports at least one push provider.";
"troubleshoot_notifications_test_detect_push_provider_failure" = "No push provider support found.";
"troubleshoot_notifications_test_detect_push_provider_success_2" = "The application was built with support for: %1$@";
"troubleshoot_notifications_test_detect_push_provider_title" = "Push provider support";
"troubleshoot_notifications_test_display_notification_description" = "Check that the application can display notification.";
"troubleshoot_notifications_test_display_notification_failure" = "The notification has not been clicked.";
"troubleshoot_notifications_test_display_notification_permission_failure" = "Cannot display the notification.";
@@ -1237,7 +1243,7 @@
"screen_room_details_security_title" = "Security";
"screen_room_details_topic_title" = "Topic";
"screen_room_error_failed_processing_media" = "Failed processing media to upload, please try again.";
"screen_room_member_list_manage_member_remove_confirmation_ban" = "Remove and ban member";
"screen_room_member_list_manage_member_remove_confirmation_ban" = "Ban from room";
"screen_room_notification_settings_mode_all_messages" = "All messages";
"screen_room_notification_settings_mode_mentions_and_keywords" = "Mentions and Keywords only";
"screen_room_timeline_reactions_show_less" = "Show less";
+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") }
/// If turned off, you won’t be able to send or receive read receipts or typing notifications.
internal static var screenAdvancedSettingsSharePresenceDescription: String { return L10n.tr("Localizable", "screen_advanced_settings_share_presence_description") }
/// Always hide
internal static var screenAdvancedSettingsShowMediaTimelineAlwaysHide: String { return L10n.tr("Localizable", "screen_advanced_settings_show_media_timeline_always_hide") }
/// Always show
internal static var screenAdvancedSettingsShowMediaTimelineAlwaysShow: String { return L10n.tr("Localizable", "screen_advanced_settings_show_media_timeline_always_show") }
/// In private rooms
internal static var screenAdvancedSettingsShowMediaTimelinePrivateRooms: String { return L10n.tr("Localizable", "screen_advanced_settings_show_media_timeline_private_rooms") }
/// A hidden media can always be shown by tapping on it
internal static var screenAdvancedSettingsShowMediaTimelineSubtitle: String { return L10n.tr("Localizable", "screen_advanced_settings_show_media_timeline_subtitle") }
/// Show media in timeline
internal static var screenAdvancedSettingsShowMediaTimelineTitle: String { return L10n.tr("Localizable", "screen_advanced_settings_show_media_timeline_title") }
/// Enable option to view message source in the timeline.
internal static var screenAdvancedSettingsViewSourceDescription: String { return L10n.tr("Localizable", "screen_advanced_settings_view_source_description") }
/// We won't record or profile any personal data
@@ -1082,6 +1092,8 @@ internal enum L10n {
internal static var screenBlockedUsersUnblockAlertTitle: String { return L10n.tr("Localizable", "screen_blocked_users_unblock_alert_title") }
/// Unblocking…
internal static var screenBlockedUsersUnblocking: String { return L10n.tr("Localizable", "screen_blocked_users_unblocking") }
/// Remove from room
internal static var screenBottomSheetManageRoomMemberRemove: String { return L10n.tr("Localizable", "screen_bottom_sheet,manage_room_member_remove") }
/// Send invite
internal static var screenBottomSheetCreateDmConfirmationButtonTitle: String { return L10n.tr("Localizable", "screen_bottom_sheet_create_dm_confirmation_button_title") }
/// Would you like to start a chat with %1$@?
@@ -1090,6 +1102,32 @@ internal enum L10n {
}
/// Send invite?
internal static var screenBottomSheetCreateDmTitle: String { return L10n.tr("Localizable", "screen_bottom_sheet_create_dm_title") }
/// Ban from room
internal static var screenBottomSheetManageRoomMemberBan: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_ban") }
/// Ban
internal static var screenBottomSheetManageRoomMemberBanMemberConfirmationAction: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_ban_member_confirmation_action") }
/// They won’t be able to join this room again if invited.
internal static var screenBottomSheetManageRoomMemberBanMemberConfirmationDescription: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_ban_member_confirmation_description") }
/// Are you sure you want to ban this member?
internal static var screenBottomSheetManageRoomMemberBanMemberConfirmationTitle: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_ban_member_confirmation_title") }
/// Banning %1$@
internal static func screenBottomSheetManageRoomMemberBanningUser(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_banning_user", String(describing: p1))
}
/// Remove
internal static var screenBottomSheetManageRoomMemberKickMemberConfirmationAction: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_kick_member_confirmation_action") }
/// They will be able to join this room again if invited.
internal static var screenBottomSheetManageRoomMemberKickMemberConfirmationDescription: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_kick_member_confirmation_description") }
/// Are you sure you want to remove this member?
internal static var screenBottomSheetManageRoomMemberKickMemberConfirmationTitle: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_kick_member_confirmation_title") }
/// View profile
internal static var screenBottomSheetManageRoomMemberMemberUserInfo: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_member_user_info") }
/// Remove member and ban from joining in the future?
internal static var screenBottomSheetManageRoomMemberRemoveConfirmationTitle: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_remove_confirmation_title") }
/// Removing %1$@…
internal static func screenBottomSheetManageRoomMemberRemovingUser(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_removing_user", String(describing: p1))
}
/// Attach screenshot
internal static var screenBugReportAttachScreenshot: String { return L10n.tr("Localizable", "screen_bug_report_attach_screenshot") }
/// You may contact me if you have any follow up questions.
@@ -2066,56 +2104,28 @@ internal enum L10n {
internal static func screenRoomMemberDetailsVerifyButtonTitle(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_room_member_details_verify_button_title", String(describing: p1))
}
/// Ban
internal static var screenRoomMemberListBanMemberConfirmationAction: String { return L10n.tr("Localizable", "screen_room_member_list_ban_member_confirmation_action") }
/// They won’t be able to join this room again if invited.
internal static var screenRoomMemberListBanMemberConfirmationDescription: String { return L10n.tr("Localizable", "screen_room_member_list_ban_member_confirmation_description") }
/// Are you sure you want to ban this member?
internal static var screenRoomMemberListBanMemberConfirmationTitle: String { return L10n.tr("Localizable", "screen_room_member_list_ban_member_confirmation_title") }
/// There are no banned users in this room.
internal static var screenRoomMemberListBannedEmpty: String { return L10n.tr("Localizable", "screen_room_member_list_banned_empty") }
/// Banning %1$@
internal static func screenRoomMemberListBanningUser(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_room_member_list_banning_user", String(describing: p1))
}
/// Plural format key: "%#@COUNT@"
internal static func screenRoomMemberListHeaderTitle(_ p1: Int) -> String {
return L10n.tr("Localizable", "screen_room_member_list_header_title", p1)
}
/// Remove
internal static var screenRoomMemberListKickMemberConfirmationAction: String { return L10n.tr("Localizable", "screen_room_member_list_kick_member_confirmation_action") }
/// They will be able to join this room again if invited.
internal static var screenRoomMemberListKickMemberConfirmationDescription: String { return L10n.tr("Localizable", "screen_room_member_list_kick_member_confirmation_description") }
/// Are you sure you want to remove this member?
internal static var screenRoomMemberListKickMemberConfirmationTitle: String { return L10n.tr("Localizable", "screen_room_member_list_kick_member_confirmation_title") }
/// Remove and ban member
internal static var screenRoomMemberListManageMemberBan: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_ban") }
/// Remove from room
internal static var screenRoomMemberListManageMemberRemove: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_remove") }
/// Remove and ban member
/// Ban from room
internal static var screenRoomMemberListManageMemberRemoveConfirmationBan: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_remove_confirmation_ban") }
/// Only remove member
internal static var screenRoomMemberListManageMemberRemoveConfirmationKick: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_remove_confirmation_kick") }
/// Remove member and ban from joining in the future?
internal static var screenRoomMemberListManageMemberRemoveConfirmationTitle: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_remove_confirmation_title") }
/// Unban
internal static var screenRoomMemberListManageMemberUnbanAction: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_unban_action") }
/// They will be able to join this room again if invited.
internal static var screenRoomMemberListManageMemberUnbanMessage: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_unban_message") }
/// Unban user
internal static var screenRoomMemberListManageMemberUnbanTitle: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_unban_title") }
/// View profile
internal static var screenRoomMemberListManageMemberUserInfo: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_user_info") }
/// Banned
internal static var screenRoomMemberListModeBanned: String { return L10n.tr("Localizable", "screen_room_member_list_mode_banned") }
/// Members
internal static var screenRoomMemberListModeMembers: String { return L10n.tr("Localizable", "screen_room_member_list_mode_members") }
/// Pending
internal static var screenRoomMemberListPendingHeaderTitle: String { return L10n.tr("Localizable", "screen_room_member_list_pending_header_title") }
/// Removing %1$@…
internal static func screenRoomMemberListRemovingUser(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_room_member_list_removing_user", String(describing: p1))
}
/// Admin
internal static var screenRoomMemberListRoleAdministrator: String { return L10n.tr("Localizable", "screen_room_member_list_role_administrator") }
/// Moderator
@@ -2860,15 +2870,19 @@ internal enum L10n {
}
/// Current push provider
internal static var troubleshootNotificationsTestCurrentPushProviderTitle: String { return L10n.tr("Localizable", "troubleshoot_notifications_test_current_push_provider_title") }
/// Ensure that the application has at least one push provider.
/// Ensure that the application supports at least one push provider.
internal static var troubleshootNotificationsTestDetectPushProviderDescription: String { return L10n.tr("Localizable", "troubleshoot_notifications_test_detect_push_provider_description") }
/// No push providers found.
/// No push provider support found.
internal static var troubleshootNotificationsTestDetectPushProviderFailure: String { return L10n.tr("Localizable", "troubleshoot_notifications_test_detect_push_provider_failure") }
/// Plural format key: "%#@COUNT@"
internal static func troubleshootNotificationsTestDetectPushProviderSuccess(_ p1: Int) -> String {
return L10n.tr("Localizable", "troubleshoot_notifications_test_detect_push_provider_success", p1)
}
/// Detect push providers
/// The application was built with support for: %1$@
internal static func troubleshootNotificationsTestDetectPushProviderSuccess2(_ p1: Any) -> String {
return L10n.tr("Localizable", "troubleshoot_notifications_test_detect_push_provider_success_2", String(describing: p1))
}
/// Push provider support
internal static var troubleshootNotificationsTestDetectPushProviderTitle: String { return L10n.tr("Localizable", "troubleshoot_notifications_test_detect_push_provider_title") }
/// Check that the application can display notification.
internal static var troubleshootNotificationsTestDisplayNotificationDescription: String { return L10n.tr("Localizable", "troubleshoot_notifications_test_display_notification_description") }
@@ -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)
case .displayEmojiPicker, .displayReportContent, .displayCameraPicker, .displayMediaPicker,
.displayDocumentPicker, .displayLocationPicker, .displayPollForm, .displayMediaUploadPreviewScreen,
.tappedOnSenderDetails, .displayMessageForwarding, .displayLocation, .displayResolveSendFailure,
.displaySenderDetails, .displayMessageForwarding, .displayLocation, .displayResolveSendFailure,
.composer, .hasScrolled, .viewInRoomTimeline:
break
}
@@ -102,7 +102,7 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType
self?.displayMediaPreview(mediaPreviewViewModel)
case .displayEmojiPicker, .displayReportContent, .displayCameraPicker, .displayMediaPicker,
.displayDocumentPicker, .displayLocationPicker, .displayPollForm, .displayMediaUploadPreviewScreen,
.tappedOnSenderDetails, .displayMessageForwarding, .displayLocation, .displayResolveSendFailure,
.displaySenderDetails, .displayMessageForwarding, .displayLocation, .displayResolveSendFailure,
.composer, .hasScrolled, .viewInRoomTimeline:
break
}
@@ -78,7 +78,7 @@ final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol {
guard let self else { return }
switch action {
case .tappedOnSenderDetails(let userID):
case .displaySenderDetails(let userID):
actionsSubject.send(.displayUser(userID: userID))
case .displayMessageForwarding(let forwardingItem):
actionsSubject.send(.displayMessageForwarding(forwardingItem: forwardingItem))
@@ -28,6 +28,9 @@ struct PinnedEventsTimelineScreen: View {
.background(.compound.bgCanvasDefault)
.interactiveDismissDisabled()
.timelineMediaPreview(viewModel: $context.mediaPreviewViewModel)
.sheet(item: $timelineContext.manageMemberViewModel) {
ManageRoomMemberSheetView(context: $0.context)
}
.sheet(item: $timelineContext.debugInfo) { TimelineItemDebugView(info: $0) }
.sheet(item: $timelineContext.actionMenuInfo) { info in
let actions = TimelineItemMenuActionProvider(timelineItem: info.item,
@@ -80,31 +80,15 @@ struct RoomMembersListScreenViewStateBindings {
var searchQuery = ""
/// The current mode the screen is in.
var mode: RoomMembersListScreenMode = .members
/// A selected member to kick, ban, promote etc.
var memberToManage: RoomMembersListScreenManagementDetails?
/// A sheet model for the selected member to kick, ban, promote etc.
var manageMemeberViewModel: ManageRoomMemberSheetViewModel?
/// Information describing the currently displayed alert.
var alertInfo: AlertInfo<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 {
case selectMember(RoomMemberDetails)
case showMemberDetails(RoomMemberDetails)
case kickMember(RoomMemberDetails)
case banMember(RoomMemberDetails)
case unbanMember(RoomMemberDetails)
case invite
}
@@ -15,8 +15,10 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
private let roomProxy: JoinedRoomProxyProtocol
private let userIndicatorController: UserIndicatorControllerProtocol
private let analytics: AnalyticsService
private let mediaProvider: MediaProviderProtocol
private var members: [RoomMemberProxyProtocol] = []
private var currentUserProxy: RoomMemberProxyProtocol?
private var actionsSubject: PassthroughSubject<RoomMembersListScreenViewModelAction, Never> = .init()
@@ -34,6 +36,7 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
self.roomProxy = roomProxy
self.userIndicatorController = userIndicatorController
self.analytics = analytics
self.mediaProvider = mediaProvider
super.init(initialViewState: .init(joinedMembersCount: roomProxy.infoPublisher.value.joinedMembersCount,
bindings: .init(mode: initialMode)),
@@ -48,34 +51,6 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
switch viewAction {
case .selectMember(let member):
selectMember(member)
case .showMemberDetails(let member):
showMemberDetails(member)
case .kickMember(let member):
var reason: String?
let binding: Binding<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):
Task { await unbanMember(member) }
case .invite:
@@ -118,6 +93,7 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
let members = members.sorted()
let roomMembersDetails = await buildMembersDetails(members: members)
self.members = members
self.currentUserProxy = members.first { $0.userID == roomProxy.ownUserID }
self.state = .init(joinedMembersCount: roomProxy.infoPublisher.value.joinedMembersCount,
joinedMembers: roomMembersDetails.joinedMembers,
@@ -168,6 +144,12 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
}
private func selectMember(_ member: RoomMemberDetails) {
guard let currentUserProxy,
currentUserProxy.powerLevel > member.powerLevel else {
showMemberDetails(member)
return
}
if member.isBanned { // No need to check canBan here, banned users are only shown when it is true.
state.bindings.alertInfo = AlertInfo(id: .unbanConfirmation(member),
title: L10n.screenRoomMemberListManageMemberUnbanTitle,
@@ -179,16 +161,26 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
return
}
var actions = [RoomMembersListScreenManagementDetails.Action]()
if state.canKickUsers, member.role != .administrator {
actions.append(.kick)
}
if state.canBanUsers, member.role != .administrator {
actions.append(.ban)
}
if !actions.isEmpty {
state.bindings.memberToManage = .init(member: member, actions: actions)
if state.canKickUsers || state.canBanUsers {
let manageMemeberViewModel = ManageRoomMemberSheetViewModel(member: member,
canKick: state.canKickUsers,
canBan: state.canBanUsers,
roomProxy: roomProxy,
userIndicatorController: userIndicatorController,
analyticsService: analytics,
mediaProvider: mediaProvider)
manageMemeberViewModel.actions.sink { [weak self] action in
guard let self else { return }
switch action {
case .dismiss(let shouldShowDetails):
state.bindings.manageMemeberViewModel = nil
if shouldShowDetails {
showMemberDetails(member)
}
}
}
.store(in: &cancellables)
state.bindings.manageMemeberViewModel = manageMemeberViewModel
} else {
showMemberDetails(member)
}
@@ -200,46 +192,16 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
return
}
actionsSubject.send(.selectMember(member))
state.bindings.memberToManage = nil
}
// MARK: - Member Management
private func kickMember(_ member: RoomMemberDetails, reason: String?) async {
let indicatorTitle = L10n.screenRoomMemberListRemovingUser(member.name ?? member.id)
showManageMemberIndicator(title: indicatorTitle)
switch await roomProxy.kickUser(member.id, reason: reason) {
case .success:
state.bindings.memberToManage = nil
hideManageMemberIndicator(title: indicatorTitle)
analytics.trackRoomModeration(action: .KickMember, role: nil)
case .failure:
showManageMemberFailure(title: indicatorTitle)
}
}
private func banMember(_ member: RoomMemberDetails, reason: String?) async {
let indicatorTitle = L10n.screenRoomMemberListBanningUser(member.name ?? member.id)
showManageMemberIndicator(title: indicatorTitle)
switch await roomProxy.banUser(member.id, reason: reason) {
case .success:
state.bindings.memberToManage = nil
hideManageMemberIndicator(title: indicatorTitle)
analytics.trackRoomModeration(action: .BanMember, role: nil)
case .failure:
showManageMemberFailure(title: indicatorTitle)
}
}
private func unbanMember(_ member: RoomMemberDetails) async {
let indicatorTitle = L10n.screenRoomMemberListUnbanningUser(member.name ?? member.id)
showManageMemberIndicator(title: indicatorTitle)
switch await roomProxy.unbanUser(member.id) {
case .success:
state.bindings.memberToManage = nil
hideManageMemberIndicator(title: indicatorTitle)
analytics.trackRoomModeration(action: .UnbanMember, role: nil)
case .failure:
@@ -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()
.background(Color.compound.bgCanvasDefault.ignoresSafeArea())
.navigationTitle(L10n.commonPeople)
.sheet(item: $context.memberToManage) {
RoomMembersListManageMemberSheet(member: $0.member, actions: $0.actions, context: context)
.sheet(item: $context.manageMemeberViewModel) {
ManageRoomMemberSheetView(context: $0.context)
}
.alert(item: $context.alertInfo)
.toolbar { toolbar }
@@ -136,7 +136,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
actionsSubject.send(.presentPollForm(mode: mode))
case .displayMediaUploadPreviewScreen(let url):
actionsSubject.send(.presentMediaUploadPreviewScreen(url))
case .tappedOnSenderDetails(userID: let userID):
case .displaySenderDetails(userID: let userID):
actionsSubject.send(.presentRoomMemberDetails(userID: userID))
case .displayMessageForwarding(let forwardingItem):
actionsSubject.send(.presentMessageForwarding(forwardingItem: forwardingItem))
@@ -66,6 +66,9 @@ struct RoomScreen: View {
.toolbarBackground(.visible, for: .navigationBar) // Fix the toolbar's background.
.overlay { loadingIndicator }
.alert(item: $timelineContext.alertInfo)
.sheet(item: $timelineContext.manageMemberViewModel) {
ManageRoomMemberSheetView(context: $0.context)
}
.sheet(item: $timelineContext.debugInfo) { TimelineItemDebugView(info: $0) }
.sheet(item: $timelineContext.actionMenuInfo) { info in
let actions = TimelineItemMenuActionProvider(timelineItem: info.item,
@@ -19,7 +19,7 @@ enum TimelineViewModelAction {
case displayLocationPicker
case displayPollForm(mode: PollFormMode)
case displayMediaUploadPreviewScreen(url: URL)
case tappedOnSenderDetails(userID: String)
case displaySenderDetails(userID: String)
case displayMessageForwarding(forwardingItem: MessageForwardingItem)
case displayMediaPreview(TimelineMediaPreviewViewModel)
case displayLocation(body: String, geoURI: GeoURI, description: String?)
@@ -99,6 +99,8 @@ struct TimelineViewState: BindableState {
var canCurrentUserRedactOthers = false
var canCurrentUserRedactSelf = false
var canCurrentUserPin = false
var canCurrentUserKick = false
var canCurrentUserBan = false
var isViewSourceEnabled: Bool
var hideTimelineMedia: Bool
@@ -135,7 +137,7 @@ struct TimelineViewStateBindings {
/// Key is itemID, value is the collapsed state.
var reactionsCollapsed: [TimelineItemIdentifier: Bool]
var alertInfo: AlertInfo<RoomScreenAlertInfoType>?
var alertInfo: AlertInfo<TimelineAlertInfoType>?
var debugInfo: TimelineItemDebugInfo?
@@ -144,6 +146,8 @@ struct TimelineViewStateBindings {
var reactionSummaryInfo: ReactionSummaryInfo?
var readReceiptsSummaryInfo: ReadReceiptSummaryInfo?
var manageMemberViewModel: ManageRoomMemberSheetViewModel?
}
struct TimelineItemActionMenuInfo: Equatable, Identifiable {
@@ -172,7 +176,7 @@ struct ReadReceiptSummaryInfo: Identifiable {
let id: TimelineItemIdentifier
}
enum RoomScreenAlertInfoType: Hashable {
enum TimelineAlertInfoType: Hashable {
case audioRecodingPermissionError
case pollEndConfirmation(String)
case sendingFailed
@@ -42,6 +42,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
actionsSubject.eraseToAnyPublisher()
}
private var currentUserProxy: RoomMemberProxyProtocol?
private var paginateBackwardsTask: Task<Void, Never>?
private var paginateForwardsTask: Task<Void, Never>?
@@ -173,7 +175,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
case .handleTimelineItemMenuAction(let itemID, let action):
timelineInteractionHandler.handleTimelineItemMenuAction(action, itemID: itemID)
case .tappedOnSenderDetails(userID: let userID):
actionsSubject.send(.tappedOnSenderDetails(userID: userID))
handleTappedOnSenderDetails(userID: userID)
case .displayEmojiPicker(let itemID):
timelineInteractionHandler.displayEmojiPicker(for: itemID)
case .displayReactionSummary(let itemID, let key):
@@ -258,6 +260,41 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
// MARK: - Private
private func handleTappedOnSenderDetails(userID: String) {
// We also need to make sure the user is in the joined state, otherwise we just show the details.
guard let memberProxy = roomProxy.membersPublisher.value.first(where: { $0.userID == userID && $0.membership == .join }),
let currentUserProxy,
currentUserProxy.powerLevel > memberProxy.powerLevel else {
actionsSubject.send(.displaySenderDetails(userID: userID))
return
}
if state.canCurrentUserBan || state.canCurrentUserKick {
let member = RoomMemberDetails(withProxy: memberProxy)
let manageMemeberViewModel = ManageRoomMemberSheetViewModel(member: member,
canKick: state.canCurrentUserKick,
canBan: state.canCurrentUserBan,
roomProxy: roomProxy,
userIndicatorController: userIndicatorController,
analyticsService: analyticsService,
mediaProvider: mediaProvider)
manageMemeberViewModel.actions.sink { [weak self] action in
guard let self else { return }
switch action {
case .dismiss(let shouldShowDetails):
state.bindings.manageMemberViewModel = nil
if shouldShowDetails {
actionsSubject.send(.displaySenderDetails(userID: userID))
}
}
}
.store(in: &cancellables)
state.bindings.manageMemberViewModel = manageMemeberViewModel
} else {
actionsSubject.send(.displaySenderDetails(userID: userID))
}
}
private func focusLive() {
timelineController.focusLive()
}
@@ -348,6 +385,9 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
private func updateMembers(_ members: [RoomMemberProxyProtocol]) {
state.members = members.reduce(into: [String: RoomMemberState]()) { dictionary, member in
dictionary[member.userID] = RoomMemberState(displayName: member.displayName, avatarURL: member.avatarURL)
if member.userID == roomProxy.ownUserID {
currentUserProxy = member
}
}
}
@@ -369,6 +409,18 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
} else {
state.canCurrentUserPin = false
}
if case let .success(value) = await roomProxy.canUserKick(userID: roomProxy.ownUserID) {
state.canCurrentUserKick = value
} else {
state.canCurrentUserKick = false
}
if case let .success(value) = await roomProxy.canUserBan(userID: roomProxy.ownUserID) {
state.canCurrentUserBan = value
} else {
state.canCurrentUserBan = false
}
}
private func setupSubscriptions() {
@@ -898,7 +950,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
userIndicatorController.retractIndicatorWithId(Constants.focusTimelineToastIndicatorID)
}
private func displayAlert(_ type: RoomScreenAlertInfoType) {
private func displayAlert(_ type: TimelineAlertInfoType) {
switch type {
case .audioRecodingPermissionError:
state.bindings.alertInfo = .init(id: type,
@@ -20,6 +20,7 @@ struct RoomMemberDetails: Identifiable, Hashable {
enum Role { case administrator, moderator, user }
let role: Role
let powerLevel: Int
func matches(searchQuery: String) -> Bool {
guard !searchQuery.isEmpty else { return true }
@@ -38,6 +39,7 @@ extension RoomMemberDetails {
isIgnored = proxy.isIgnored
isBanned = proxy.membership == .ban
role = .init(proxy.role)
powerLevel = proxy.powerLevel
}
}
@@ -401,6 +401,12 @@ extension PreviewTests {
}
}
func testManageRoomMemberSheetView() async throws {
for (index, preview) in ManageRoomMemberSheetView_Previews._allPreviews.enumerated() {
try await assertSnapshots(matching: preview, step: index)
}
}
func testMapLibreStaticMapView() async throws {
for (index, preview) in MapLibreStaticMapView_Previews._allPreviews.enumerated() {
try await assertSnapshots(matching: preview, step: index)
@@ -707,12 +713,6 @@ extension PreviewTests {
}
}
func testRoomMembersListManageMemberSheet() async throws {
for (index, preview) in RoomMembersListManageMemberSheet_Previews._allPreviews.enumerated() {
try await assertSnapshots(matching: preview, step: index)
}
}
func testRoomMembersListMemberCell() async throws {
for (index, preview) in RoomMembersListMemberCell_Previews._allPreviews.enumerated() {
try await assertSnapshots(matching: preview, step: index)
@@ -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.
try await memberDetailsAction.fulfill()
XCTAssertNil(context.memberToManage)
XCTAssertNil(context.manageMemeberViewModel)
}
func testSelectUserAsAdmin() async throws {
@@ -151,10 +151,10 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
setup(with: .allMembersAsAdmin)
var deferred = deferFulfillment(context.$viewState) { !$0.visibleInvitedMembers.isEmpty && $0.canKickUsers && $0.canBanUsers }
try await deferred.fulfill()
XCTAssertNil(context.memberToManage)
XCTAssertNil(context.manageMemeberViewModel)
// When tapping on a user in the list.
deferred = deferFulfillment(context.$viewState) { $0.bindings.memberToManage != nil }
deferred = deferFulfillment(context.$viewState) { $0.bindings.manageMemeberViewModel != nil }
guard let user = viewModel.state.visibleJoinedMembers.first(where: { $0.member.role == .user && $0.member.id != RoomMemberProxyMock.mockMe.userID })?.member else {
XCTFail("Expected to find a regular user.")
return
@@ -163,8 +163,9 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
try await deferred.fulfill()
// Then member management should be shown for that user.
XCTAssertEqual(context.memberToManage?.member, user)
XCTAssertEqual(context.memberToManage?.actions, [.kick, .ban])
XCTAssertEqual(context.manageMemeberViewModel?.state.member, user)
XCTAssertEqual(context.manageMemeberViewModel?.state.canKick, true)
XCTAssertEqual(context.manageMemeberViewModel?.state.canBan, true)
}
func testSelectModeratorAsAdmin() async throws {
@@ -172,10 +173,10 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
setup(with: .allMembersAsAdmin)
var deferred = deferFulfillment(context.$viewState) { !$0.visibleInvitedMembers.isEmpty && $0.canKickUsers && $0.canBanUsers }
try await deferred.fulfill()
XCTAssertNil(context.memberToManage)
XCTAssertNil(context.manageMemeberViewModel)
// When tapping on a moderator in the list.
deferred = deferFulfillment(context.$viewState) { $0.bindings.memberToManage != nil }
deferred = deferFulfillment(context.$viewState) { $0.bindings.manageMemeberViewModel != nil }
guard let moderator = viewModel.state.visibleJoinedMembers.first(where: { $0.member.role == .moderator })?.member else {
XCTFail("Expected to find a moderator.")
return
@@ -184,8 +185,9 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
try await deferred.fulfill()
// Then member management should be shown for the moderator.
XCTAssertEqual(context.memberToManage?.member, moderator)
XCTAssertEqual(context.memberToManage?.actions, [.kick, .ban])
XCTAssertEqual(context.manageMemeberViewModel?.state.member, moderator)
XCTAssertEqual(context.manageMemeberViewModel?.state.canKick, true)
XCTAssertEqual(context.manageMemeberViewModel?.state.canBan, true)
}
func testSelectAdminAsAdmin() async throws {
@@ -204,7 +206,7 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
// Then the administrator's details should be shown.
try await memberDetailsAction.fulfill()
XCTAssertNil(context.memberToManage)
XCTAssertNil(context.manageMemeberViewModel)
}
func testSelectOwnMemberAsAdmin() async throws {
@@ -223,7 +225,7 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
// Then your member's details should be shown.
try await memberDetailsAction.fulfill()
XCTAssertNil(context.memberToManage)
XCTAssertNil(context.manageMemeberViewModel)
}
func testSelectBannedMember() async throws {
@@ -243,50 +245,10 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
// Then an alert should be shown to unban the user.
try await deferred.fulfill()
XCTAssertNil(context.memberToManage)
XCTAssertNil(context.manageMemeberViewModel)
XCTAssertNotNil(context.alertInfo)
}
func testKickMember() async throws {
setup(with: .allMembersAsAdmin)
var deferred = deferFulfillment(context.$viewState) { !$0.visibleJoinedMembers.isEmpty }
try await deferred.fulfill()
let expectation = XCTestExpectation(description: "Ban member")
roomProxy.kickUserReasonClosure = { _, _ in
defer { expectation.fulfill() }
return .success(())
}
context.send(viewAction: .kickMember(viewModel.state.visibleJoinedMembers[0].member))
deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil }
try await deferred.fulfill()
context.alertInfo?.secondaryButton?.action?()
await fulfillment(of: [expectation])
}
func testBanMember() async throws {
setup(with: .allMembersAsAdmin)
var deferred = deferFulfillment(context.$viewState) { !$0.visibleJoinedMembers.isEmpty }
try await deferred.fulfill()
let expectation = XCTestExpectation(description: "Ban member")
roomProxy.banUserReasonClosure = { _, _ in
defer { expectation.fulfill() }
return .success(())
}
context.send(viewAction: .banMember(viewModel.state.visibleJoinedMembers[0].member))
deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil }
try await deferred.fulfill()
context.alertInfo?.secondaryButton?.action?()
await fulfillment(of: [expectation])
}
func testUnbanMember() async throws {
setup(with: .allMembersAsAdmin)
let deferred = deferFulfillment(context.$viewState) { !$0.visibleJoinedMembers.isEmpty }
@@ -350,6 +350,117 @@ class TimelineViewModelTests: XCTestCase {
try await deferred.fulfill()
}
func testShowManageUserAsAdmin() async throws {
let viewModel = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "",
members: [RoomMemberProxyMock.mockAdmin,
RoomMemberProxyMock.mockAlice],
ownUserID: RoomMemberProxyMock.mockAdmin.userID)),
timelineController: MockTimelineController(),
mediaProvider: MediaProviderMock(configuration: .init()),
mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
userIndicatorController: userIndicatorControllerMock,
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
clientProxy: ClientProxyMock(.init()))
var deferred = deferFulfillment(viewModel.context.$viewState) { value in
value.canCurrentUserKick && value.canCurrentUserBan
}
try await deferred.fulfill()
deferred = deferFulfillment(viewModel.context.$viewState) { value in
value.bindings.manageMemberViewModel != nil
}
viewModel.context.send(viewAction: .tappedOnSenderDetails(userID: RoomMemberProxyMock.mockAlice.userID))
try await deferred.fulfill()
XCTAssertEqual(viewModel.context.manageMemberViewModel?.state.member.id, RoomMemberProxyMock.mockAlice.userID)
XCTAssertEqual(viewModel.context.manageMemberViewModel?.state.canBan, true)
XCTAssertEqual(viewModel.context.manageMemberViewModel?.state.canKick, true)
}
func testShowDetailsForAnAdmin() async throws {
let viewModel = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "",
members: [RoomMemberProxyMock.mockAdmin,
RoomMemberProxyMock.mockAlice],
ownUserID: RoomMemberProxyMock.mockAlice.userID)),
timelineController: MockTimelineController(),
mediaProvider: MediaProviderMock(configuration: .init()),
mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
userIndicatorController: userIndicatorControllerMock,
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
clientProxy: ClientProxyMock(.init()))
let deferredState = deferFulfillment(viewModel.context.$viewState) { value in
!value.canCurrentUserKick && !value.canCurrentUserBan
}
try await deferredState.fulfill()
let deferredAction = deferFulfillment(viewModel.actions) { action in
switch action {
case .displaySenderDetails(let userID):
return userID == RoomMemberProxyMock.mockAdmin.userID
default:
return false
}
}
viewModel.context.send(viewAction: .tappedOnSenderDetails(userID: RoomMemberProxyMock.mockAdmin.userID))
try await deferredAction.fulfill()
XCTAssertNil(viewModel.context.manageMemberViewModel)
}
func testShowDetailsForABannedUser() async throws {
let viewModel = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "",
members: [RoomMemberProxyMock.mockAdmin,
RoomMemberProxyMock.mockBanned[0]],
ownUserID: RoomMemberProxyMock.mockAdmin.userID)),
timelineController: MockTimelineController(),
mediaProvider: MediaProviderMock(configuration: .init()),
mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
userIndicatorController: userIndicatorControllerMock,
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
clientProxy: ClientProxyMock(.init()))
let deferredState = deferFulfillment(viewModel.context.$viewState) { value in
value.canCurrentUserKick && value.canCurrentUserBan
}
try await deferredState.fulfill()
let deferredAction = deferFulfillment(viewModel.actions) { action in
switch action {
case .displaySenderDetails(let userID):
return userID == RoomMemberProxyMock.mockBanned[0].userID
default:
return false
}
}
viewModel.context.send(viewAction: .tappedOnSenderDetails(userID: RoomMemberProxyMock.mockBanned[0].userID))
try await deferredAction.fulfill()
XCTAssertNil(viewModel.context.manageMemberViewModel)
}
// MARK: - Pins
func testPinnedEvents() async throws {