User verification state indicators (#3793)

* Introduce a `UserIdentityProxy` and have it combine upstream methods into an easy to digest  `UserIdentityVerificationState`. Use it in a dedicated `VerificationBadge` UI component

* Show a DMs counterpart verification state in the room header

* Show a warning on the room details `People` entry when there are identity verification state violations on any of the members.

* Show verification badges in the room member list

* Show a withdraw verification section on the room member details for users that have pinning violations.

* Remove the verification section from the profile screen as there's no reliable way to keep it up to date
- the underlying Rust SDK Olm Machine can be rebuilt without notice which would break any existing user identity change streams.

* Update preview test snapshots
This commit is contained in:
Stefan Ceriu
2025-02-18 08:37:34 +02:00
committed by GitHub
parent b71c93dfaa
commit f77faee981
101 changed files with 722 additions and 305 deletions
+17 -5
View File
@@ -106,6 +106,7 @@
126CBCF5B0145FA1377C1316 /* Tracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B574805B9812C111D6215D /* Tracing.swift */; }; 126CBCF5B0145FA1377C1316 /* Tracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B574805B9812C111D6215D /* Tracing.swift */; };
126EE01D8BEAEF26105D83C5 /* RoomDetailsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A5FEF17ED7E6176D922D4F /* RoomDetailsScreen.swift */; }; 126EE01D8BEAEF26105D83C5 /* RoomDetailsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A5FEF17ED7E6176D922D4F /* RoomDetailsScreen.swift */; };
128FFD8A3D85845F9A927F47 /* PollRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF8548D48512127CCC17C520 /* PollRoomTimelineView.swift */; }; 128FFD8A3D85845F9A927F47 /* PollRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF8548D48512127CCC17C520 /* PollRoomTimelineView.swift */; };
12AF926E783E40BFB32A2D84 /* UserIdentityProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8536B40F578BA7FDB23D97B1 /* UserIdentityProxyMock.swift */; };
12C867E85E6D12EEDFD0B127 /* CustomStringConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96C4762F8D6112E43117DB2F /* CustomStringConvertible.swift */; }; 12C867E85E6D12EEDFD0B127 /* CustomStringConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96C4762F8D6112E43117DB2F /* CustomStringConvertible.swift */; };
12CCA59536EDD99A3272CF77 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC3F82523D6F48B926D6AF68 /* AppSettings.swift */; }; 12CCA59536EDD99A3272CF77 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC3F82523D6F48B926D6AF68 /* AppSettings.swift */; };
12CD8B5CC30A05061228BF9E /* TimelineItemMenuActionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E6065FC6BC4A1B4C629E08 /* TimelineItemMenuActionProvider.swift */; }; 12CD8B5CC30A05061228BF9E /* TimelineItemMenuActionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E6065FC6BC4A1B4C629E08 /* TimelineItemMenuActionProvider.swift */; };
@@ -188,6 +189,7 @@
238D561CA231339C6D4D06F3 /* ClientBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A1C33355FFB0F0953C35036 /* ClientBuilder.swift */; }; 238D561CA231339C6D4D06F3 /* ClientBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A1C33355FFB0F0953C35036 /* ClientBuilder.swift */; };
241CDEFE23819867D9B39066 /* RoomChangePermissionsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE75941583A033A9EDC9FE0 /* RoomChangePermissionsScreenViewModel.swift */; }; 241CDEFE23819867D9B39066 /* RoomChangePermissionsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE75941583A033A9EDC9FE0 /* RoomChangePermissionsScreenViewModel.swift */; };
244407B18B2F2D6466BA5961 /* RoomChangeRolesScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82DFA1B7B088D033E0794B82 /* RoomChangeRolesScreenCoordinator.swift */; }; 244407B18B2F2D6466BA5961 /* RoomChangeRolesScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82DFA1B7B088D033E0794B82 /* RoomChangeRolesScreenCoordinator.swift */; };
2447FADEF13225BB6227B977 /* VerificationBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1D97BAF04AA150C0EF03021 /* VerificationBadge.swift */; };
244CB93DD7390379D905AFA8 /* DeactivateAccountScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E49D10BFA7E4D70A947888C /* DeactivateAccountScreen.swift */; }; 244CB93DD7390379D905AFA8 /* DeactivateAccountScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E49D10BFA7E4D70A947888C /* DeactivateAccountScreen.swift */; };
24A1BBADAC43DC3F3A7347DA /* AnalyticsPromptScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = E53BFB7E4F329621C844E8C3 /* AnalyticsPromptScreen.swift */; }; 24A1BBADAC43DC3F3A7347DA /* AnalyticsPromptScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = E53BFB7E4F329621C844E8C3 /* AnalyticsPromptScreen.swift */; };
24A75F72EEB7561B82D726FD /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2141693488CE5446BB391964 /* Date.swift */; }; 24A75F72EEB7561B82D726FD /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2141693488CE5446BB391964 /* Date.swift */; };
@@ -401,6 +403,7 @@
4E4EF97B9F9CEFAC726BA72F /* TimelineProviderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EACAFB3F3E017060F9F1C5 /* TimelineProviderMock.swift */; }; 4E4EF97B9F9CEFAC726BA72F /* TimelineProviderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EACAFB3F3E017060F9F1C5 /* TimelineProviderMock.swift */; };
4E8A2A2CFEB212F14E49E1A1 /* AppLockSetupSettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5484457C81B325660901B161 /* AppLockSetupSettingsScreen.swift */; }; 4E8A2A2CFEB212F14E49E1A1 /* AppLockSetupSettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5484457C81B325660901B161 /* AppLockSetupSettingsScreen.swift */; };
4E945AD6862C403F74E57755 /* RoomTimelineItemFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 105B2A8426404EF66F00CFDB /* RoomTimelineItemFactory.swift */; }; 4E945AD6862C403F74E57755 /* RoomTimelineItemFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 105B2A8426404EF66F00CFDB /* RoomTimelineItemFactory.swift */; };
4E9782D683660463804C2DC3 /* UserIdentityProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F793C422BDACE0C60C774F4 /* UserIdentityProxyProtocol.swift */; };
4EA1CE0E88EA68E862FF0EA2 /* NotificationSettingsEditScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B564D748B67A156F413CD97 /* NotificationSettingsEditScreenModels.swift */; }; 4EA1CE0E88EA68E862FF0EA2 /* NotificationSettingsEditScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B564D748B67A156F413CD97 /* NotificationSettingsEditScreenModels.swift */; };
4EAC427267424192964B16B3 /* AppSettingsHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13BE9781699FB510E9263192 /* AppSettingsHook.swift */; }; 4EAC427267424192964B16B3 /* AppSettingsHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13BE9781699FB510E9263192 /* AppSettingsHook.swift */; };
4ED764A24F2A715C25CF07F1 /* TimelineMediaPreviewFileExportPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30856520F3263D0E195710D7 /* TimelineMediaPreviewFileExportPicker.swift */; }; 4ED764A24F2A715C25CF07F1 /* TimelineMediaPreviewFileExportPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30856520F3263D0E195710D7 /* TimelineMediaPreviewFileExportPicker.swift */; };
@@ -619,6 +622,7 @@
7A0D335D38ECA095A575B4F7 /* TimelineStyler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DB0E533508094156D8024C3 /* TimelineStyler.swift */; }; 7A0D335D38ECA095A575B4F7 /* TimelineStyler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DB0E533508094156D8024C3 /* TimelineStyler.swift */; };
7A170A5A4A352954BB2A1B96 /* AuthenticationStartScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24E8C8817F59BEC7E358EB78 /* AuthenticationStartScreen.swift */; }; 7A170A5A4A352954BB2A1B96 /* AuthenticationStartScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24E8C8817F59BEC7E358EB78 /* AuthenticationStartScreen.swift */; };
7A25D6926A2C01DB8D0D67A5 /* BadgeLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A028783CFFF861C5E44FFB1 /* BadgeLabel.swift */; }; 7A25D6926A2C01DB8D0D67A5 /* BadgeLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A028783CFFF861C5E44FFB1 /* BadgeLabel.swift */; };
7A495A5F3E5522DD7928CF8F /* UserIdentityProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 964093C7CA8823CAB7FFD88E /* UserIdentityProxy.swift */; };
7A642EE5F1ADC5D520F21924 /* MediaProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB16E7FE59A947CA441531 /* MediaProviderProtocol.swift */; }; 7A642EE5F1ADC5D520F21924 /* MediaProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB16E7FE59A947CA441531 /* MediaProviderProtocol.swift */; };
7A71AEF419904209BB8C2833 /* UserAgentBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F2529D434C750ED78ADF1ED /* UserAgentBuilder.swift */; }; 7A71AEF419904209BB8C2833 /* UserAgentBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F2529D434C750ED78ADF1ED /* UserAgentBuilder.swift */; };
7A8B264506D3DDABC01B4EEB /* AppMediator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B53AC78E49A297AC1D72A7CF /* AppMediator.swift */; }; 7A8B264506D3DDABC01B4EEB /* AppMediator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B53AC78E49A297AC1D72A7CF /* AppMediator.swift */; };
@@ -993,7 +997,6 @@
C76892321558E75101E68ED6 /* ReadableFrameModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 398817652FA8ABAE0A31AC6D /* ReadableFrameModifier.swift */; }; C76892321558E75101E68ED6 /* ReadableFrameModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 398817652FA8ABAE0A31AC6D /* ReadableFrameModifier.swift */; };
C7774720A4B2E34693E3227C /* RoomNotificationSettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8896CDD20CA2D87EA3B848A1 /* RoomNotificationSettingsScreen.swift */; }; C7774720A4B2E34693E3227C /* RoomNotificationSettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8896CDD20CA2D87EA3B848A1 /* RoomNotificationSettingsScreen.swift */; };
C7ABEBECDC513F7887DACF66 /* ProgressMaskModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68010886142843705E342645 /* ProgressMaskModifier.swift */; }; C7ABEBECDC513F7887DACF66 /* ProgressMaskModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68010886142843705E342645 /* ProgressMaskModifier.swift */; };
C7B07EBA0F12B5912DA9BB97 /* UserIdentitySDKMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8DE9D0D480D087D0F676B52 /* UserIdentitySDKMock.swift */; };
C7F20DBF873CC72FB482E326 /* test_rotated_image.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 723B055A57857BFF0F18D9CB /* test_rotated_image.jpg */; }; C7F20DBF873CC72FB482E326 /* test_rotated_image.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 723B055A57857BFF0F18D9CB /* test_rotated_image.jpg */; };
C80E06ED97CE52704A46C148 /* ClientBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A1C33355FFB0F0953C35036 /* ClientBuilder.swift */; }; C80E06ED97CE52704A46C148 /* ClientBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A1C33355FFB0F0953C35036 /* ClientBuilder.swift */; };
C85C7A201E4CFDA477ACEBEB /* AppLockSetupSettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8610C1D21565C950BCA6A454 /* AppLockSetupSettingsScreenViewModelProtocol.swift */; }; C85C7A201E4CFDA477ACEBEB /* AppLockSetupSettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8610C1D21565C950BCA6A454 /* AppLockSetupSettingsScreenViewModelProtocol.swift */; };
@@ -1414,6 +1417,7 @@
0F5567A7EF6F2AB9473236F6 /* DocumentPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentPicker.swift; sourceTree = "<group>"; }; 0F5567A7EF6F2AB9473236F6 /* DocumentPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentPicker.swift; sourceTree = "<group>"; };
0F569CFB77E0D40BD82203D9 /* AuthenticationClientBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationClientBuilder.swift; sourceTree = "<group>"; }; 0F569CFB77E0D40BD82203D9 /* AuthenticationClientBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationClientBuilder.swift; sourceTree = "<group>"; };
0F64447FF544298A6A3BEF85 /* NotificationSettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsScreenModels.swift; sourceTree = "<group>"; }; 0F64447FF544298A6A3BEF85 /* NotificationSettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsScreenModels.swift; sourceTree = "<group>"; };
0F793C422BDACE0C60C774F4 /* UserIdentityProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIdentityProxyProtocol.swift; sourceTree = "<group>"; };
0FA60F848D1C14F873F9621A /* RoomMemberDetailsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsScreenCoordinator.swift; sourceTree = "<group>"; }; 0FA60F848D1C14F873F9621A /* RoomMemberDetailsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsScreenCoordinator.swift; sourceTree = "<group>"; };
105429F29096729EDD3152CF /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/SAS.strings; sourceTree = "<group>"; }; 105429F29096729EDD3152CF /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/SAS.strings; sourceTree = "<group>"; };
1059E2AE7878CF7820592637 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; }; 1059E2AE7878CF7820592637 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
@@ -1967,6 +1971,7 @@
8512B82404B1751D0BCC82D2 /* MediaEventsTimelineScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaEventsTimelineScreenCoordinator.swift; sourceTree = "<group>"; }; 8512B82404B1751D0BCC82D2 /* MediaEventsTimelineScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaEventsTimelineScreenCoordinator.swift; sourceTree = "<group>"; };
85149F56BA333619900E2410 /* UserDetailsEditScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDetailsEditScreenViewModelProtocol.swift; sourceTree = "<group>"; }; 85149F56BA333619900E2410 /* UserDetailsEditScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDetailsEditScreenViewModelProtocol.swift; sourceTree = "<group>"; };
851B95BB98649B8E773D6790 /* AppLockService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockService.swift; sourceTree = "<group>"; }; 851B95BB98649B8E773D6790 /* AppLockService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockService.swift; sourceTree = "<group>"; };
8536B40F578BA7FDB23D97B1 /* UserIdentityProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIdentityProxyMock.swift; sourceTree = "<group>"; };
8544F7058D31DBEB8DBFF0F5 /* NotificationSettingsEditScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenViewModelTests.swift; sourceTree = "<group>"; }; 8544F7058D31DBEB8DBFF0F5 /* NotificationSettingsEditScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenViewModelTests.swift; sourceTree = "<group>"; };
854BCEAF2A832176FAACD2CB /* SplashScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenCoordinator.swift; sourceTree = "<group>"; }; 854BCEAF2A832176FAACD2CB /* SplashScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenCoordinator.swift; sourceTree = "<group>"; };
85666E40F7E817809B4FD787 /* ComposerToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerToolbar.swift; sourceTree = "<group>"; }; 85666E40F7E817809B4FD787 /* ComposerToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerToolbar.swift; sourceTree = "<group>"; };
@@ -2047,6 +2052,7 @@
955336CBD5ED73C792D1F580 /* EncryptionAuthenticity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionAuthenticity.swift; sourceTree = "<group>"; }; 955336CBD5ED73C792D1F580 /* EncryptionAuthenticity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionAuthenticity.swift; sourceTree = "<group>"; };
95A2E4BD7C0CAD25EF924A4C /* GeneratedPreviewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneratedPreviewTests.swift; sourceTree = "<group>"; }; 95A2E4BD7C0CAD25EF924A4C /* GeneratedPreviewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneratedPreviewTests.swift; sourceTree = "<group>"; };
95BAC0F6C9644336E9567EE6 /* NSRegularExpresion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSRegularExpresion.swift; sourceTree = "<group>"; }; 95BAC0F6C9644336E9567EE6 /* NSRegularExpresion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSRegularExpresion.swift; sourceTree = "<group>"; };
964093C7CA8823CAB7FFD88E /* UserIdentityProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIdentityProxy.swift; sourceTree = "<group>"; };
969694F67E844FCA51F7E051 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 969694F67E844FCA51F7E051 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/InfoPlist.strings; sourceTree = "<group>"; };
96C4762F8D6112E43117DB2F /* CustomStringConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomStringConvertible.swift; sourceTree = "<group>"; }; 96C4762F8D6112E43117DB2F /* CustomStringConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomStringConvertible.swift; sourceTree = "<group>"; };
96CE9D6642DD487D8CC90C9C /* landscape_test_image.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = landscape_test_image.jpg; sourceTree = "<group>"; }; 96CE9D6642DD487D8CC90C9C /* landscape_test_image.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = landscape_test_image.jpg; sourceTree = "<group>"; };
@@ -2340,6 +2346,7 @@
D196116D2DD3F2757D45FCB7 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/SAS.strings; sourceTree = "<group>"; }; D196116D2DD3F2757D45FCB7 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/SAS.strings; sourceTree = "<group>"; };
D1BC84BA0AF11C2128D58ABD /* Common.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Common.swift; sourceTree = "<group>"; }; D1BC84BA0AF11C2128D58ABD /* Common.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Common.swift; sourceTree = "<group>"; };
D1D8479BB704B7EF696F8ABE /* RoomPollsHistoryScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreenCoordinator.swift; sourceTree = "<group>"; }; D1D8479BB704B7EF696F8ABE /* RoomPollsHistoryScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreenCoordinator.swift; sourceTree = "<group>"; };
D1D97BAF04AA150C0EF03021 /* VerificationBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerificationBadge.swift; sourceTree = "<group>"; };
D263254AFE5B7993FFBBF324 /* NSE.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NSE.entitlements; sourceTree = "<group>"; }; D263254AFE5B7993FFBBF324 /* NSE.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NSE.entitlements; sourceTree = "<group>"; };
D26813CCE39221FE30BF22CD /* PlatformViewVersionPredicate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlatformViewVersionPredicate.swift; sourceTree = "<group>"; }; D26813CCE39221FE30BF22CD /* PlatformViewVersionPredicate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlatformViewVersionPredicate.swift; sourceTree = "<group>"; };
D28F7A6CEEA4A2815B0F0F55 /* SettingsFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsFlowCoordinator.swift; sourceTree = "<group>"; }; D28F7A6CEEA4A2815B0F0F55 /* SettingsFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsFlowCoordinator.swift; sourceTree = "<group>"; };
@@ -2444,7 +2451,6 @@
E8A1F98AE670377B20679FF5 /* MediaPlayerProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayerProvider.swift; sourceTree = "<group>"; }; E8A1F98AE670377B20679FF5 /* MediaPlayerProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayerProvider.swift; sourceTree = "<group>"; };
E8AE4B3273BA189FDCD4055C /* UserIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicator.swift; sourceTree = "<group>"; }; E8AE4B3273BA189FDCD4055C /* UserIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicator.swift; sourceTree = "<group>"; };
E8CA187FE656EE5A3F6C7DE5 /* UIFont+AttributedStringBuilder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIFont+AttributedStringBuilder.m"; sourceTree = "<group>"; }; E8CA187FE656EE5A3F6C7DE5 /* UIFont+AttributedStringBuilder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIFont+AttributedStringBuilder.m"; sourceTree = "<group>"; };
E8DE9D0D480D087D0F676B52 /* UserIdentitySDKMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIdentitySDKMock.swift; sourceTree = "<group>"; };
E96ED747FF90332EA1333C22 /* RoomTimelineItemFixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemFixtures.swift; sourceTree = "<group>"; }; E96ED747FF90332EA1333C22 /* RoomTimelineItemFixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemFixtures.swift; sourceTree = "<group>"; };
E992D7B8BE54B2AB454613AF /* XCUIElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCUIElement.swift; sourceTree = "<group>"; }; E992D7B8BE54B2AB454613AF /* XCUIElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCUIElement.swift; sourceTree = "<group>"; };
E9A3D3CFA199FA7897364547 /* CallInviteRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallInviteRoomTimelineItem.swift; sourceTree = "<group>"; }; E9A3D3CFA199FA7897364547 /* CallInviteRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallInviteRoomTimelineItem.swift; sourceTree = "<group>"; };
@@ -2707,6 +2713,7 @@
children = ( children = (
693E16574C6F7F9FA1015A8C /* Search.swift */, 693E16574C6F7F9FA1015A8C /* Search.swift */,
832397B5C3D00A4BF52C5F0B /* ShouldScrollOnKeyboardDidShow.swift */, 832397B5C3D00A4BF52C5F0B /* ShouldScrollOnKeyboardDidShow.swift */,
D1D97BAF04AA150C0EF03021 /* VerificationBadge.swift */,
E2DA161C142B7AB8CC40F752 /* Animation */, E2DA161C142B7AB8CC40F752 /* Animation */,
CE2FBFD64A89F5DBE4EB30DB /* Layout */, CE2FBFD64A89F5DBE4EB30DB /* Layout */,
E6E1D07163F8752D62DA4A93 /* Styles */, E6E1D07163F8752D62DA4A93 /* Styles */,
@@ -3215,6 +3222,7 @@
B0F5CC38803B8382D2C63222 /* TimelineControllerFactoryMock.swift */, B0F5CC38803B8382D2C63222 /* TimelineControllerFactoryMock.swift */,
62EACAFB3F3E017060F9F1C5 /* TimelineProviderMock.swift */, 62EACAFB3F3E017060F9F1C5 /* TimelineProviderMock.swift */,
17A8AA0DFA06012A9DAB951E /* TimelineProxyMock.swift */, 17A8AA0DFA06012A9DAB951E /* TimelineProxyMock.swift */,
8536B40F578BA7FDB23D97B1 /* UserIdentityProxyMock.swift */,
7893780A1FD6E3F38B3E9049 /* UserIndicatorControllerMock.swift */, 7893780A1FD6E3F38B3E9049 /* UserIndicatorControllerMock.swift */,
AAD01F7FC2BBAC7351948595 /* UserProfile+Mock.swift */, AAD01F7FC2BBAC7351948595 /* UserProfile+Mock.swift */,
F4469F6AE311BDC439B3A5EC /* UserSessionMock.swift */, F4469F6AE311BDC439B3A5EC /* UserSessionMock.swift */,
@@ -5652,6 +5660,8 @@
D071F86CD47582B9196C9D16 /* UserDiscoverySection.swift */, D071F86CD47582B9196C9D16 /* UserDiscoverySection.swift */,
65AAD845E53B0C8B5E0812C2 /* UserDiscoveryService.swift */, 65AAD845E53B0C8B5E0812C2 /* UserDiscoveryService.swift */,
7F615A00DB223FF3280204D2 /* UserDiscoveryServiceProtocol.swift */, 7F615A00DB223FF3280204D2 /* UserDiscoveryServiceProtocol.swift */,
964093C7CA8823CAB7FFD88E /* UserIdentityProxy.swift */,
0F793C422BDACE0C60C774F4 /* UserIdentityProxyProtocol.swift */,
51C454AE59914B551A6D02C0 /* UserProfileProxy.swift */, 51C454AE59914B551A6D02C0 /* UserProfileProxy.swift */,
); );
path = Users; path = Users;
@@ -5717,7 +5727,6 @@
children = ( children = (
8EAF4A49F3ACD8BB8B0D2371 /* ClientSDKMock.swift */, 8EAF4A49F3ACD8BB8B0D2371 /* ClientSDKMock.swift */,
5EFB1D29B0870AFB6A56E9B8 /* IdentityResetHandleSDKMock.swift */, 5EFB1D29B0870AFB6A56E9B8 /* IdentityResetHandleSDKMock.swift */,
E8DE9D0D480D087D0F676B52 /* UserIdentitySDKMock.swift */,
); );
path = SDK; path = SDK;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -7624,7 +7633,9 @@
828EA5009557C2B9DCD4CA0F /* UserDiscoverySection.swift in Sources */, 828EA5009557C2B9DCD4CA0F /* UserDiscoverySection.swift in Sources */,
044DD8F80231BC30570F7965 /* UserDiscoveryService.swift in Sources */, 044DD8F80231BC30570F7965 /* UserDiscoveryService.swift in Sources */,
1C409A26A99F0371C47AFA51 /* UserDiscoveryServiceProtocol.swift in Sources */, 1C409A26A99F0371C47AFA51 /* UserDiscoveryServiceProtocol.swift in Sources */,
C7B07EBA0F12B5912DA9BB97 /* UserIdentitySDKMock.swift in Sources */, 7A495A5F3E5522DD7928CF8F /* UserIdentityProxy.swift in Sources */,
12AF926E783E40BFB32A2D84 /* UserIdentityProxyMock.swift in Sources */,
4E9782D683660463804C2DC3 /* UserIdentityProxyProtocol.swift in Sources */,
988BA75A182738150894A23F /* UserIndicator.swift in Sources */, 988BA75A182738150894A23F /* UserIndicator.swift in Sources */,
C4E0D03DF88242697545A9B7 /* UserIndicatorController.swift in Sources */, C4E0D03DF88242697545A9B7 /* UserIndicatorController.swift in Sources */,
3467FEE8210D301FF1B77001 /* UserIndicatorControllerMock.swift in Sources */, 3467FEE8210D301FF1B77001 /* UserIndicatorControllerMock.swift in Sources */,
@@ -7651,6 +7662,7 @@
7E91BAC17963ED41208F489B /* UserSessionStore.swift in Sources */, 7E91BAC17963ED41208F489B /* UserSessionStore.swift in Sources */,
79D57E9AE03A2DC689D14EA2 /* UserSessionStoreMock.swift in Sources */, 79D57E9AE03A2DC689D14EA2 /* UserSessionStoreMock.swift in Sources */,
AC69B6DF15FC451AB2945036 /* UserSessionStoreProtocol.swift in Sources */, AC69B6DF15FC451AB2945036 /* UserSessionStoreProtocol.swift in Sources */,
2447FADEF13225BB6227B977 /* VerificationBadge.swift in Sources */,
5C33976A720B64094CBC56B1 /* VideoMediaEventsTimelineView.swift in Sources */, 5C33976A720B64094CBC56B1 /* VideoMediaEventsTimelineView.swift in Sources */,
F07D88421A9BC4D03D4A5055 /* VideoRoomTimelineItem.swift in Sources */, F07D88421A9BC4D03D4A5055 /* VideoRoomTimelineItem.swift in Sources */,
1A83DD22F3E6F76B13B6E2F9 /* VideoRoomTimelineItemContent.swift in Sources */, 1A83DD22F3E6F76B13B6E2F9 /* VideoRoomTimelineItemContent.swift in Sources */,
@@ -8509,7 +8521,7 @@
repositoryURL = "https://github.com/element-hq/matrix-rust-components-swift"; repositoryURL = "https://github.com/element-hq/matrix-rust-components-swift";
requirement = { requirement = {
kind = exactVersion; kind = exactVersion;
version = 25.02.07; version = 25.02.11;
}; };
}; };
701C7BEF8F70F7A83E852DCC /* XCRemoteSwiftPackageReference "GZIP" */ = { 701C7BEF8F70F7A83E852DCC /* XCRemoteSwiftPackageReference "GZIP" */ = {
@@ -149,8 +149,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/element-hq/matrix-rust-components-swift", "location" : "https://github.com/element-hq/matrix-rust-components-swift",
"state" : { "state" : {
"revision" : "bc819f09ac66bbe1adc2fde2afeb7ab023d1b909", "revision" : "cc010fc6971370d1df2c0eb67cc5cfd577465b62",
"version" : "25.2.7" "version" : "25.2.11"
} }
}, },
{ {
@@ -889,8 +889,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
} }
private func presentRoomMembersList() { private func presentRoomMembersList() {
let parameters = RoomMembersListScreenCoordinatorParameters(mediaProvider: userSession.mediaProvider, let parameters = RoomMembersListScreenCoordinatorParameters(clientProxy: userSession.clientProxy,
roomProxy: roomProxy, roomProxy: roomProxy,
mediaProvider: userSession.mediaProvider,
userIndicatorController: userIndicatorController, userIndicatorController: userIndicatorController,
analytics: analytics) analytics: analytics)
let coordinator = RoomMembersListScreenCoordinator(parameters: parameters) let coordinator = RoomMembersListScreenCoordinator(parameters: parameters)
@@ -1275,8 +1276,6 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
stateMachine.tryEvent(.startChildFlow(roomID: roomID, via: [], entryPoint: .room)) stateMachine.tryEvent(.startChildFlow(roomID: roomID, via: [], entryPoint: .room))
case .startCall(let roomID): case .startCall(let roomID):
Task { await self.presentCallScreen(roomID: roomID) } Task { await self.presentCallScreen(roomID: roomID) }
case .verifyUser(let userID):
actionsSubject.send(.verifyUser(userID: userID))
case .dismiss: case .dismiss:
break // Not supported when pushed. break // Not supported when pushed.
} }
@@ -923,8 +923,6 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
stateMachine.processEvent(.selectRoom(roomID: roomID, via: [], entryPoint: .room)) stateMachine.processEvent(.selectRoom(roomID: roomID, via: [], entryPoint: .room))
case .startCall(let roomID): case .startCall(let roomID):
Task { await self.presentCallScreen(roomID: roomID, notifyOtherParticipants: false) } Task { await self.presentCallScreen(roomID: roomID, notifyOtherParticipants: false) }
case .verifyUser(let userID):
presentSessionVerificationScreen(flow: .userIntiator(userID: userID))
case .dismiss: case .dismiss:
navigationSplitCoordinator.setSheetCoordinator(nil) navigationSplitCoordinator.setSheetCoordinator(nil)
} }
+1 -1
View File
@@ -95,6 +95,6 @@ extension ClientProxyMock {
return await .joined(JoinedRoomProxyMock(.init(id: room.id, name: room.name))) return await .joined(JoinedRoomProxyMock(.init(id: room.id, name: room.name)))
} }
userIdentityForReturnValue = .success(UserIdentitySDKMock(configuration: .init())) userIdentityForReturnValue = .success(UserIdentityProxyMock(configuration: .init()))
} }
} }
@@ -4860,13 +4860,13 @@ class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable {
var userIdentityForReceivedUserID: String? var userIdentityForReceivedUserID: String?
var userIdentityForReceivedInvocations: [String] = [] var userIdentityForReceivedInvocations: [String] = []
var userIdentityForUnderlyingReturnValue: Result<UserIdentity?, ClientProxyError>! var userIdentityForUnderlyingReturnValue: Result<UserIdentityProxyProtocol?, ClientProxyError>!
var userIdentityForReturnValue: Result<UserIdentity?, ClientProxyError>! { var userIdentityForReturnValue: Result<UserIdentityProxyProtocol?, ClientProxyError>! {
get { get {
if Thread.isMainThread { if Thread.isMainThread {
return userIdentityForUnderlyingReturnValue return userIdentityForUnderlyingReturnValue
} else { } else {
var returnValue: Result<UserIdentity?, ClientProxyError>? = nil var returnValue: Result<UserIdentityProxyProtocol?, ClientProxyError>? = nil
DispatchQueue.main.sync { DispatchQueue.main.sync {
returnValue = userIdentityForUnderlyingReturnValue returnValue = userIdentityForUnderlyingReturnValue
} }
@@ -4884,9 +4884,9 @@ class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable {
} }
} }
} }
var userIdentityForClosure: ((String) async -> Result<UserIdentity?, ClientProxyError>)? var userIdentityForClosure: ((String) async -> Result<UserIdentityProxyProtocol?, ClientProxyError>)?
func userIdentity(for userID: String) async -> Result<UserIdentity?, ClientProxyError> { func userIdentity(for userID: String) async -> Result<UserIdentityProxyProtocol?, ClientProxyError> {
userIdentityForCallsCount += 1 userIdentityForCallsCount += 1
userIdentityForReceivedUserID = userID userIdentityForReceivedUserID = userID
DispatchQueue.main.async { DispatchQueue.main.async {
@@ -16511,6 +16511,14 @@ class UserDiscoveryServiceMock: UserDiscoveryServiceProtocol, @unchecked Sendabl
} }
} }
} }
class UserIdentityProxyMock: UserIdentityProxyProtocol, @unchecked Sendable {
var verificationState: UserIdentityVerificationState {
get { return underlyingVerificationState }
set(value) { underlyingVerificationState = value }
}
var underlyingVerificationState: UserIdentityVerificationState!
}
class UserIndicatorControllerMock: UserIndicatorControllerProtocol, @unchecked Sendable { class UserIndicatorControllerMock: UserIndicatorControllerProtocol, @unchecked Sendable {
var window: UIWindow? var window: UIWindow?
var alertInfo: AlertInfo<UUID>? var alertInfo: AlertInfo<UUID>?
@@ -22876,6 +22876,71 @@ open class UserIdentitySDKMock: MatrixRustSDK.UserIdentity, @unchecked Sendable
fileprivate var pointer: UnsafeMutableRawPointer! fileprivate var pointer: UnsafeMutableRawPointer!
//MARK: - hasVerificationViolation
var hasVerificationViolationUnderlyingCallsCount = 0
open var hasVerificationViolationCallsCount: Int {
get {
if Thread.isMainThread {
return hasVerificationViolationUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = hasVerificationViolationUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
hasVerificationViolationUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
hasVerificationViolationUnderlyingCallsCount = newValue
}
}
}
}
open var hasVerificationViolationCalled: Bool {
return hasVerificationViolationCallsCount > 0
}
var hasVerificationViolationUnderlyingReturnValue: Bool!
open var hasVerificationViolationReturnValue: Bool! {
get {
if Thread.isMainThread {
return hasVerificationViolationUnderlyingReturnValue
} else {
var returnValue: Bool? = nil
DispatchQueue.main.sync {
returnValue = hasVerificationViolationUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
hasVerificationViolationUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
hasVerificationViolationUnderlyingReturnValue = newValue
}
}
}
}
open var hasVerificationViolationClosure: (() -> Bool)?
open override func hasVerificationViolation() -> Bool {
hasVerificationViolationCallsCount += 1
if let hasVerificationViolationClosure = hasVerificationViolationClosure {
return hasVerificationViolationClosure()
} else {
return hasVerificationViolationReturnValue
}
}
//MARK: - isVerified //MARK: - isVerified
var isVerifiedUnderlyingCallsCount = 0 var isVerifiedUnderlyingCallsCount = 0
@@ -23046,6 +23111,71 @@ open class UserIdentitySDKMock: MatrixRustSDK.UserIdentity, @unchecked Sendable
try await pinClosure?() try await pinClosure?()
} }
//MARK: - wasPreviouslyVerified
var wasPreviouslyVerifiedUnderlyingCallsCount = 0
open var wasPreviouslyVerifiedCallsCount: Int {
get {
if Thread.isMainThread {
return wasPreviouslyVerifiedUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = wasPreviouslyVerifiedUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
wasPreviouslyVerifiedUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
wasPreviouslyVerifiedUnderlyingCallsCount = newValue
}
}
}
}
open var wasPreviouslyVerifiedCalled: Bool {
return wasPreviouslyVerifiedCallsCount > 0
}
var wasPreviouslyVerifiedUnderlyingReturnValue: Bool!
open var wasPreviouslyVerifiedReturnValue: Bool! {
get {
if Thread.isMainThread {
return wasPreviouslyVerifiedUnderlyingReturnValue
} else {
var returnValue: Bool? = nil
DispatchQueue.main.sync {
returnValue = wasPreviouslyVerifiedUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
wasPreviouslyVerifiedUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
wasPreviouslyVerifiedUnderlyingReturnValue = newValue
}
}
}
}
open var wasPreviouslyVerifiedClosure: (() -> Bool)?
open override func wasPreviouslyVerified() -> Bool {
wasPreviouslyVerifiedCallsCount += 1
if let wasPreviouslyVerifiedClosure = wasPreviouslyVerifiedClosure {
return wasPreviouslyVerifiedClosure()
} else {
return wasPreviouslyVerifiedReturnValue
}
}
//MARK: - withdrawVerification //MARK: - withdrawVerification
open var withdrawVerificationThrowableError: Error? open var withdrawVerificationThrowableError: Error?
@@ -1,21 +1,18 @@
// //
// Copyright 2024 New Vector Ltd. // Copyright 2025 New Vector Ltd.
// //
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details. // Please see LICENSE files in the repository root for full details.
// //
import Foundation extension UserIdentityProxyMock {
import MatrixRustSDK
extension UserIdentitySDKMock {
struct Configuration { struct Configuration {
var isVerified = false var verificationState: UserIdentityVerificationState = .notVerified
} }
convenience init(configuration: Configuration) { convenience init(configuration: Configuration) {
self.init() self.init()
isVerifiedReturnValue = configuration.isVerified underlyingVerificationState = configuration.verificationState
} }
} }
@@ -0,0 +1,37 @@
//
// 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 Compound
import SwiftUI
struct VerificationBadge: View {
let verificationState: UserIdentityVerificationState
var body: some View {
switch verificationState {
case .verified:
CompoundIcon(\.verified, size: .small, relativeTo: .compound.bodyMD)
.foregroundStyle(.compound.iconSuccessPrimary)
case .verificationViolation:
CompoundIcon(\.infoSolid, size: .small, relativeTo: .compound.bodyMD)
.foregroundStyle(.compound.iconCriticalPrimary)
case .notVerified:
EmptyView()
}
}
}
struct VerificationBadge_Previews: PreviewProvider, TestablePreview {
static var previews: some View {
VStack(spacing: 16.0) {
VerificationBadge(verificationState: .notVerified)
VerificationBadge(verificationState: .verificationViolation)
VerificationBadge(verificationState: .verified)
}
.previewLayout(.sizeThatFits)
}
}
@@ -12,17 +12,25 @@ import SwiftUI
struct RoomHeaderView: View { struct RoomHeaderView: View {
let roomName: String let roomName: String
let roomAvatar: RoomAvatar let roomAvatar: RoomAvatar
var dmRecipientVerificationState: UserIdentityVerificationState?
let mediaProvider: MediaProviderProtocol? let mediaProvider: MediaProviderProtocol?
var body: some View { var body: some View {
HStack(spacing: 12) { HStack(spacing: 8) {
avatarImage avatarImage
.accessibilityHidden(true) .accessibilityHidden(true)
HStack(spacing: 4) {
Text(roomName) Text(roomName)
.lineLimit(1) .lineLimit(1)
.font(.compound.bodyLGSemibold) .font(.compound.bodyLGSemibold)
.accessibilityIdentifier(A11yIdentifiers.roomScreen.name) .accessibilityIdentifier(A11yIdentifiers.roomScreen.name)
if let dmRecipientVerificationState {
VerificationBadge(verificationState: dmRecipientVerificationState)
}
}
} }
// Take up as much space as possible, with a leading alignment for use in the principal toolbar position. // Take up as much space as possible, with a leading alignment for use in the principal toolbar position.
.frame(idealWidth: .greatestFiniteMagnitude, maxWidth: .infinity, alignment: .leading) .frame(idealWidth: .greatestFiniteMagnitude, maxWidth: .infinity, alignment: .leading)
@@ -38,20 +46,23 @@ struct RoomHeaderView: View {
struct RoomHeaderView_Previews: PreviewProvider, TestablePreview { struct RoomHeaderView_Previews: PreviewProvider, TestablePreview {
static var previews: some View { static var previews: some View {
RoomHeaderView(roomName: "Some Room name", VStack(spacing: 8) {
roomAvatar: .room(id: "1", makeHeader(avatarURL: nil, verificationState: .notVerified)
name: "Some Room Name", makeHeader(avatarURL: .mockMXCAvatar, verificationState: .notVerified)
avatarURL: .mockMXCAvatar), makeHeader(avatarURL: .mockMXCAvatar, verificationState: .verified)
mediaProvider: MediaProviderMock(configuration: .init())) makeHeader(avatarURL: .mockMXCAvatar, verificationState: .verificationViolation)
}
.previewLayout(.sizeThatFits) .previewLayout(.sizeThatFits)
.padding() }
static func makeHeader(avatarURL: URL?,
verificationState: UserIdentityVerificationState) -> some View {
RoomHeaderView(roomName: "Some Room name", RoomHeaderView(roomName: "Some Room name",
roomAvatar: .room(id: "1", roomAvatar: .room(id: "1",
name: "Some Room Name", name: "Some Room Name",
avatarURL: nil), avatarURL: avatarURL),
dmRecipientVerificationState: verificationState,
mediaProvider: MediaProviderMock(configuration: .init())) mediaProvider: MediaProviderMock(configuration: .init()))
.previewLayout(.sizeThatFits)
.padding() .padding()
} }
} }
@@ -39,7 +39,10 @@ struct RoomDetailsScreenViewState: BindableState {
var topic: AttributedString? var topic: AttributedString?
var topicSummary: AttributedString? var topicSummary: AttributedString?
var joinedMembersCount: Int var joinedMembersCount: Int
var hasMemberIdentityVerificationStateViolations = false
var isProcessingIgnoreRequest = false var isProcessingIgnoreRequest = false
var canInviteUsers = false var canInviteUsers = false
var canEditRoomName = false var canEditRoomName = false
@@ -200,6 +200,12 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
.throttle(for: .milliseconds(100), scheduler: DispatchQueue.main, latest: true) .throttle(for: .milliseconds(100), scheduler: DispatchQueue.main, latest: true)
.weakAssign(to: \.state.knockRequestsCount, on: self) .weakAssign(to: \.state.knockRequestsCount, on: self)
.store(in: &cancellables) .store(in: &cancellables)
roomProxy.membersPublisher.combineLatest(roomProxy.identityStatusChangesPublisher)
.sink { _ in
Task { await self.updateMemberIdentityVerificationStates() }
}
.store(in: &cancellables)
} }
private func updateRoomInfo(_ roomInfo: RoomInfoProxy) { private func updateRoomInfo(_ roomInfo: RoomInfoProxy) {
@@ -239,6 +245,24 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
await roomProxy.updateMembers() await roomProxy.updateMembers()
} }
private func updateMemberIdentityVerificationStates() async {
guard roomProxy.isEncrypted else {
// We don't care about identity statuses on non-encrypted rooms
return
}
for member in roomProxy.membersPublisher.value {
if case let .success(identity) = await clientProxy.userIdentity(for: member.userID) {
if identity?.verificationState == .verificationViolation {
state.hasMemberIdentityVerificationStateViolations = true
return
}
}
}
state.hasMemberIdentityVerificationStateViolations = false
}
private func updatePowerLevelPermissions() async { private func updatePowerLevelPermissions() async {
state.canEditRoomName = await (try? roomProxy.canUser(userID: roomProxy.ownUserID, sendStateEvent: .roomName).get()) == true state.canEditRoomName = await (try? roomProxy.canUser(userID: roomProxy.ownUserID, sendStateEvent: .roomName).get()) == true
state.canEditRoomTopic = await (try? roomProxy.canUser(userID: roomProxy.ownUserID, sendStateEvent: .roomTopic).get()) == true state.canEditRoomTopic = await (try? roomProxy.canUser(userID: roomProxy.ownUserID, sendStateEvent: .roomTopic).get()) == true
@@ -198,13 +198,22 @@ struct RoomDetailsScreen: View {
private var peopleSection: some View { private var peopleSection: some View {
Section { Section {
ListRow(label: .default(title: L10n.commonPeople, if context.viewState.hasMemberIdentityVerificationStateViolations {
icon: \.user), ListRow(label: .default(title: L10n.commonPeople, icon: \.user),
details: .icon(CompoundIcon(\.infoSolid).foregroundStyle(.compound.iconCriticalPrimary)),
kind: .navigationLink {
context.send(viewAction: .processTapPeople)
})
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.people)
} else {
ListRow(label: .default(title: L10n.commonPeople, icon: \.user),
details: .title(String(context.viewState.joinedMembersCount)), details: .title(String(context.viewState.joinedMembersCount)),
kind: .navigationLink { kind: .navigationLink {
context.send(viewAction: .processTapPeople) context.send(viewAction: .processTapPeople)
}) })
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.people) .accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.people)
}
if context.viewState.canSeeKnockingRequests { if context.viewState.canSeeKnockingRequests {
ListRow(label: .default(title: L10n.screenRoomDetailsRequestsToJoinTitle, ListRow(label: .default(title: L10n.screenRoomDetailsRequestsToJoinTitle,
@@ -17,7 +17,7 @@ enum RoomMemberDetailsScreenViewModelAction {
struct RoomMemberDetailsScreenViewState: BindableState { struct RoomMemberDetailsScreenViewState: BindableState {
let userID: String let userID: String
var memberDetails: RoomMemberDetails? var memberDetails: RoomMemberDetails?
var isVerified: Bool? var verificationState: UserIdentityVerificationState?
var isOwnMemberDetails = false var isOwnMemberDetails = false
var isProcessingIgnoreRequest = false var isProcessingIgnoreRequest = false
var dmRoomID: String? var dmRoomID: String?
@@ -25,11 +25,15 @@ struct RoomMemberDetailsScreenViewState: BindableState {
var bindings: RoomMemberDetailsScreenViewStateBindings var bindings: RoomMemberDetailsScreenViewStateBindings
var showVerifiedBadge: Bool { var showVerifiedBadge: Bool {
isVerified == true // We purposely show the badge on your own account for consistency with Web. verificationState == .verified // We purposely show the badge on your own account for consistency with Web.
} }
var showVerificationSection: Bool { var showVerifyIdentitySection: Bool {
isVerified == false && !isOwnMemberDetails verificationState == .notVerified && !isOwnMemberDetails
}
var showWithdrawVerificationSection: Bool {
verificationState == .verificationViolation && !isOwnMemberDetails
} }
} }
@@ -90,6 +94,7 @@ enum RoomMemberDetailsScreenViewAction {
case createDirectChat case createDirectChat
case startCall(roomID: String) case startCall(roomID: String)
case verifyUser case verifyUser
case withdrawVerification
} }
enum RoomMemberDetailsScreenAlertType: Hashable { enum RoomMemberDetailsScreenAlertType: Hashable {
@@ -42,10 +42,20 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro
super.init(initialViewState: initialViewState, mediaProvider: mediaProvider) super.init(initialViewState: initialViewState, mediaProvider: mediaProvider)
showMemberLoadingIndicator() showMemberLoadingIndicator()
Task { Task {
await loadMember() await loadMember()
hideMemberLoadingIndicator() hideMemberLoadingIndicator()
} }
roomProxy.identityStatusChangesPublisher
.receive(on: DispatchQueue.main)
.sink { changes in
if changes.map(\.userId).contains(userID) {
Task { await self.loadMember() }
}
}
.store(in: &cancellables)
} }
// MARK: - Public // MARK: - Public
@@ -77,16 +87,15 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro
actionsSubject.send(.startCall(roomID: roomID)) actionsSubject.send(.startCall(roomID: roomID))
case .verifyUser: case .verifyUser:
actionsSubject.send(.verifyUser(userID: state.userID)) actionsSubject.send(.verifyUser(userID: state.userID))
case .withdrawVerification:
Task { await clientProxy.withdrawUserIdentityVerification(state.userID) }
} }
} }
// MARK: - Private // MARK: - Private
private func loadMember() async { private func loadMember() async {
async let memberResult = roomProxy.getMember(userID: state.userID) switch await roomProxy.getMember(userID: state.userID) {
async let identityResult = clientProxy.userIdentity(for: state.userID)
switch await memberResult {
case .success(let member): case .success(let member):
roomMemberProxy = member roomMemberProxy = member
state.memberDetails = RoomMemberDetails(withProxy: member) state.memberDetails = RoomMemberDetails(withProxy: member)
@@ -105,8 +114,8 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro
actionsSubject.send(.openUserProfile) actionsSubject.send(.openUserProfile)
} }
if case let .success(.some(identity)) = await identityResult { if case let .success(.some(identity)) = await clientProxy.userIdentity(for: state.userID) {
state.isVerified = identity.isVerified() state.verificationState = identity.verificationState
} else { } else {
MXLog.error("Failed to find the member's identity.") MXLog.error("Failed to find the member's identity.")
} }
@@ -15,7 +15,9 @@ struct RoomMemberDetailsScreen: View {
Form { Form {
headerSection headerSection
if context.viewState.showVerifyIdentitySection {
verificationSection verificationSection
}
if context.viewState.memberDetails != nil, !context.viewState.isOwnMemberDetails { if context.viewState.memberDetails != nil, !context.viewState.isOwnMemberDetails {
blockUserSection blockUserSection
@@ -46,8 +48,15 @@ struct RoomMemberDetailsScreen: View {
mediaProvider: context.mediaProvider) { url in mediaProvider: context.mediaProvider) { url in
context.send(viewAction: .displayAvatar(url)) context.send(viewAction: .displayAvatar(url))
} footer: { } footer: {
VStack(spacing: 24) {
if context.viewState.showWithdrawVerificationSection {
withdrawVerificationSection
}
otherUserFooter otherUserFooter
} }
.padding(.top, 24)
}
} else { } else {
AvatarHeaderView(user: UserProfileProxy(userID: context.viewState.userID), AvatarHeaderView(user: UserProfileProxy(userID: context.viewState.userID),
isVerified: context.viewState.showVerifiedBadge, isVerified: context.viewState.showVerifiedBadge,
@@ -56,6 +65,26 @@ struct RoomMemberDetailsScreen: View {
} }
} }
private var withdrawVerificationSection: some View {
VStack(spacing: 16) {
if let memberDetails = context.viewState.memberDetails {
Text(L10n.cryptoIdentityChangeProfilePinViolation(memberDetails.name ?? memberDetails.id))
.foregroundStyle(.compound.textCriticalPrimary)
.font(.compound.bodyMDSemibold)
} else {
Text(L10n.cryptoIdentityChangeProfilePinViolation(context.viewState.userID))
.foregroundStyle(.compound.textCriticalPrimary)
.font(.compound.bodyMDSemibold)
}
Button(L10n.cryptoIdentityChangeWithdrawVerificationAction) {
context.send(viewAction: .withdrawVerification)
}
.buttonStyle(.compound(.secondary, size: .medium))
}
.padding(.horizontal, 16)
}
private var otherUserFooter: some View { private var otherUserFooter: some View {
HStack(spacing: 8) { HStack(spacing: 8) {
if context.viewState.memberDetails != nil, !context.viewState.isOwnMemberDetails { if context.viewState.memberDetails != nil, !context.viewState.isOwnMemberDetails {
@@ -84,20 +113,15 @@ struct RoomMemberDetailsScreen: View {
.buttonStyle(FormActionButtonStyle(title: L10n.actionShare)) .buttonStyle(FormActionButtonStyle(title: L10n.actionShare))
} }
} }
.padding(.top, 32)
} }
@ViewBuilder
var verificationSection: some View { var verificationSection: some View {
if context.viewState.showVerificationSection {
Section { Section {
ListRow(label: .default(title: L10n.commonVerifyUser, icon: \.lock), ListRow(label: .default(title: L10n.commonVerifyUser, icon: \.lock), kind: .button {
kind: .button {
context.send(viewAction: .verifyUser) context.send(viewAction: .verifyUser)
}) })
} }
} }
}
@ViewBuilder @ViewBuilder
private var blockUserSection: some View { private var blockUserSection: some View {
@@ -138,6 +162,7 @@ struct RoomMemberDetailsScreen: View {
struct RoomMemberDetailsScreen_Previews: PreviewProvider, TestablePreview { struct RoomMemberDetailsScreen_Previews: PreviewProvider, TestablePreview {
static let verifiedUserViewModel = makeViewModel(member: .mockDan) static let verifiedUserViewModel = makeViewModel(member: .mockDan)
static let verificationViolationUserViewModel = makeViewModel(member: .mockBob)
static let otherUserViewModel = makeViewModel(member: .mockAlice) static let otherUserViewModel = makeViewModel(member: .mockAlice)
static let accountOwnerViewModel = makeViewModel(member: .mockMe) static let accountOwnerViewModel = makeViewModel(member: .mockMe)
static let ignoredUserViewModel = makeViewModel(member: .mockIgnored) static let ignoredUserViewModel = makeViewModel(member: .mockIgnored)
@@ -145,10 +170,16 @@ struct RoomMemberDetailsScreen_Previews: PreviewProvider, TestablePreview {
static var previews: some View { static var previews: some View {
RoomMemberDetailsScreen(context: verifiedUserViewModel.context) RoomMemberDetailsScreen(context: verifiedUserViewModel.context)
.snapshotPreferences(expect: verifiedUserViewModel.context.$viewState.map { state in .snapshotPreferences(expect: verifiedUserViewModel.context.$viewState.map { state in
state.isVerified == true state.verificationState == .verified
}) })
.previewDisplayName("Verified User") .previewDisplayName("Verified User")
RoomMemberDetailsScreen(context: verificationViolationUserViewModel.context)
.snapshotPreferences(expect: verificationViolationUserViewModel.context.$viewState.map { state in
state.verificationState == .verificationViolation
})
.previewDisplayName("Verification Violation User")
RoomMemberDetailsScreen(context: otherUserViewModel.context) RoomMemberDetailsScreen(context: otherUserViewModel.context)
.snapshotPreferences(expect: otherUserViewModel.context.$viewState.map { state in .snapshotPreferences(expect: otherUserViewModel.context.$viewState.map { state in
state.memberDetails?.role == .user && state.dmRoomID != nil state.memberDetails?.role == .user && state.dmRoomID != nil
@@ -171,12 +202,22 @@ struct RoomMemberDetailsScreen_Previews: PreviewProvider, TestablePreview {
static func makeViewModel(member: RoomMemberProxyMock) -> RoomMemberDetailsScreenViewModel { static func makeViewModel(member: RoomMemberProxyMock) -> RoomMemberDetailsScreenViewModel {
let roomProxyMock = JoinedRoomProxyMock(.init(name: "")) let roomProxyMock = JoinedRoomProxyMock(.init(name: ""))
roomProxyMock.getMemberUserIDReturnValue = .success(member) roomProxyMock.getMemberUserIDReturnValue = .success(member)
let clientProxyMock = ClientProxyMock(.init()) let clientProxyMock = ClientProxyMock(.init())
clientProxyMock.userIdentityForClosure = { userID in clientProxyMock.userIdentityForClosure = { userID in
let isVerified = userID == RoomMemberProxyMock.mockDan.userID let identity = switch userID {
return .success(UserIdentitySDKMock(configuration: .init(isVerified: isVerified))) case RoomMemberProxyMock.mockDan.userID:
UserIdentityProxyMock(configuration: .init(verificationState: .verified))
case RoomMemberProxyMock.mockBob.userID:
UserIdentityProxyMock(configuration: .init(verificationState: .verificationViolation))
default:
UserIdentityProxyMock(configuration: .init())
} }
return .success(identity)
}
// to avoid mock the call state for the account owner test case // to avoid mock the call state for the account owner test case
if member.userID != RoomMemberProxyMock.mockMe.userID { if member.userID != RoomMemberProxyMock.mockMe.userID {
clientProxyMock.directRoomForUserIDReturnValue = .success("roomID") clientProxyMock.directRoomForUserIDReturnValue = .success("roomID")
@@ -9,8 +9,9 @@ import Combine
import SwiftUI import SwiftUI
struct RoomMembersListScreenCoordinatorParameters { struct RoomMembersListScreenCoordinatorParameters {
let mediaProvider: MediaProviderProtocol let clientProxy: ClientProxyProtocol
let roomProxy: JoinedRoomProxyProtocol let roomProxy: JoinedRoomProxyProtocol
let mediaProvider: MediaProviderProtocol
let userIndicatorController: UserIndicatorControllerProtocol let userIndicatorController: UserIndicatorControllerProtocol
let analytics: AnalyticsService let analytics: AnalyticsService
} }
@@ -31,7 +32,8 @@ final class RoomMembersListScreenCoordinator: CoordinatorProtocol {
} }
init(parameters: RoomMembersListScreenCoordinatorParameters) { init(parameters: RoomMembersListScreenCoordinatorParameters) {
viewModel = RoomMembersListScreenViewModel(roomProxy: parameters.roomProxy, viewModel = RoomMembersListScreenViewModel(clientProxy: parameters.clientProxy,
roomProxy: parameters.roomProxy,
mediaProvider: parameters.mediaProvider, mediaProvider: parameters.mediaProvider,
userIndicatorController: parameters.userIndicatorController, userIndicatorController: parameters.userIndicatorController,
analytics: parameters.analytics) analytics: parameters.analytics)
@@ -29,10 +29,15 @@ enum RoomMembersListScreenMode {
case banned case banned
} }
struct RoomMemberListScreenEntry: Equatable {
let member: RoomMemberDetails
let verificationState: UserIdentityVerificationState
}
struct RoomMembersListScreenViewState: BindableState { struct RoomMembersListScreenViewState: BindableState {
private var joinedMembers: [RoomMemberDetails] private var joinedMembers: [RoomMemberListScreenEntry]
private var invitedMembers: [RoomMemberDetails] private var invitedMembers: [RoomMemberListScreenEntry]
private var bannedMembers: [RoomMemberDetails] private var bannedMembers: [RoomMemberListScreenEntry]
let joinedMembersCount: Int let joinedMembersCount: Int
var bannedMembersCount: Int { bannedMembers.count } var bannedMembersCount: Int { bannedMembers.count }
@@ -44,9 +49,9 @@ struct RoomMembersListScreenViewState: BindableState {
var bindings: RoomMembersListScreenViewStateBindings var bindings: RoomMembersListScreenViewStateBindings
init(joinedMembersCount: Int, init(joinedMembersCount: Int,
joinedMembers: [RoomMemberDetails] = [], joinedMembers: [RoomMemberListScreenEntry] = [],
invitedMembers: [RoomMemberDetails] = [], invitedMembers: [RoomMemberListScreenEntry] = [],
bannedMembers: [RoomMemberDetails] = [], bannedMembers: [RoomMemberListScreenEntry] = [],
bindings: RoomMembersListScreenViewStateBindings) { bindings: RoomMembersListScreenViewStateBindings) {
self.joinedMembersCount = joinedMembersCount self.joinedMembersCount = joinedMembersCount
self.joinedMembers = joinedMembers self.joinedMembers = joinedMembers
@@ -55,19 +60,19 @@ struct RoomMembersListScreenViewState: BindableState {
self.bindings = bindings self.bindings = bindings
} }
var visibleJoinedMembers: [RoomMemberDetails] { var visibleJoinedMembers: [RoomMemberListScreenEntry] {
joinedMembers joinedMembers
.filter { $0.matches(searchQuery: bindings.searchQuery) } .filter { $0.member.matches(searchQuery: bindings.searchQuery) }
} }
var visibleInvitedMembers: [RoomMemberDetails] { var visibleInvitedMembers: [RoomMemberListScreenEntry] {
invitedMembers invitedMembers
.filter { $0.matches(searchQuery: bindings.searchQuery) } .filter { $0.member.matches(searchQuery: bindings.searchQuery) }
} }
var visibleBannedMembers: [RoomMemberDetails] { var visibleBannedMembers: [RoomMemberListScreenEntry] {
bannedMembers bannedMembers
.filter { $0.matches(searchQuery: bindings.searchQuery) } .filter { $0.member.matches(searchQuery: bindings.searchQuery) }
} }
} }
@@ -11,6 +11,7 @@ import SwiftUI
typealias RoomMembersListScreenViewModelType = StateStoreViewModel<RoomMembersListScreenViewState, RoomMembersListScreenViewAction> typealias RoomMembersListScreenViewModelType = StateStoreViewModel<RoomMembersListScreenViewState, RoomMembersListScreenViewAction>
class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMembersListScreenViewModelProtocol { class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMembersListScreenViewModelProtocol {
private let clientProxy: ClientProxyProtocol
private let roomProxy: JoinedRoomProxyProtocol private let roomProxy: JoinedRoomProxyProtocol
private let userIndicatorController: UserIndicatorControllerProtocol private let userIndicatorController: UserIndicatorControllerProtocol
private let analytics: AnalyticsService private let analytics: AnalyticsService
@@ -24,10 +25,12 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
} }
init(initialMode: RoomMembersListScreenMode = .members, init(initialMode: RoomMembersListScreenMode = .members,
clientProxy: ClientProxyProtocol,
roomProxy: JoinedRoomProxyProtocol, roomProxy: JoinedRoomProxyProtocol,
mediaProvider: MediaProviderProtocol, mediaProvider: MediaProviderProtocol,
userIndicatorController: UserIndicatorControllerProtocol, userIndicatorController: UserIndicatorControllerProtocol,
analytics: AnalyticsService) { analytics: AnalyticsService) {
self.clientProxy = clientProxy
self.roomProxy = roomProxy self.roomProxy = roomProxy
self.userIndicatorController = userIndicatorController self.userIndicatorController = userIndicatorController
self.analytics = analytics self.analytics = analytics
@@ -59,22 +62,23 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
} }
func stop() { func stop() {
hideLoader() hideLoadingIndicator(Self.setupMembersLoadingIndicatorIdentifier)
hideLoadingIndicator(Self.updateStateLoadingIndicatorIdentifier)
} }
// MARK: - Members // MARK: - Members
private func setupMembers() { private func setupMembers() {
Task { Task {
showLoader() showLoadingIndicator(Self.setupMembersLoadingIndicatorIdentifier)
await roomProxy.updateMembers() await roomProxy.updateMembers()
hideLoader() hideLoadingIndicator(Self.setupMembersLoadingIndicatorIdentifier)
} }
roomProxy.membersPublisher roomProxy.membersPublisher.combineLatest(roomProxy.identityStatusChangesPublisher)
.filter { !$0.isEmpty } .filter { !$0.0.isEmpty }
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] members in .sink { [weak self] members, _ in
self?.updateState(members: members) self?.updateState(members: members)
} }
.store(in: &cancellables) .store(in: &cancellables)
@@ -87,11 +91,12 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
private func updateState(members: [RoomMemberProxyProtocol]) { private func updateState(members: [RoomMemberProxyProtocol]) {
Task { Task {
showLoader() showLoadingIndicator(Self.updateStateLoadingIndicatorIdentifier)
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.state = .init(joinedMembersCount: roomProxy.infoPublisher.value.joinedMembersCount, self.state = .init(joinedMembersCount: roomProxy.infoPublisher.value.joinedMembersCount,
joinedMembers: roomMembersDetails.joinedMembers, joinedMembers: roomMembersDetails.joinedMembers,
invitedMembers: roomMembersDetails.invitedMembers, invitedMembers: roomMembersDetails.invitedMembers,
@@ -102,25 +107,32 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
self.state.canKickUsers = await (try? roomProxy.canUserKick(userID: roomProxy.ownUserID).get()) == true self.state.canKickUsers = await (try? roomProxy.canUserKick(userID: roomProxy.ownUserID).get()) == true
self.state.canBanUsers = await (try? roomProxy.canUserBan(userID: roomProxy.ownUserID).get()) == true self.state.canBanUsers = await (try? roomProxy.canUserBan(userID: roomProxy.ownUserID).get()) == true
hideLoader() hideLoadingIndicator(Self.updateStateLoadingIndicatorIdentifier)
} }
} }
private func buildMembersDetails(members: [RoomMemberProxyProtocol]) async -> RoomMembersDetails { private func buildMembersDetails(members: [RoomMemberProxyProtocol]) async -> RoomMembersDetails {
await Task.detached { await Task.detached { [clientProxy, roomProxy] in
// accessing RoomMember's properties is very slow. We need to do it in a background thread. // accessing RoomMember's properties is very slow. We need to do it in a background thread.
var invitedMembers: [RoomMemberDetails] = .init() var invitedMembers: [RoomMemberListScreenEntry] = .init()
var joinedMembers: [RoomMemberDetails] = .init() var joinedMembers: [RoomMemberListScreenEntry] = .init()
var bannedMembers: [RoomMemberDetails] = .init() var bannedMembers: [RoomMemberListScreenEntry] = .init()
for member in members { for member in members {
var verificationState: UserIdentityVerificationState = .notVerified
if roomProxy.isEncrypted, // We don't care about identity statuses on non-encrypted rooms
case let .success(userIdentity) = await clientProxy.userIdentity(for: member.userID),
let userIdentity {
verificationState = userIdentity.verificationState
}
switch member.membership { switch member.membership {
case .invite: case .invite:
invitedMembers.append(.init(withProxy: member)) invitedMembers.append(.init(member: .init(withProxy: member), verificationState: verificationState))
case .join: case .join:
joinedMembers.append(.init(withProxy: member)) joinedMembers.append(.init(member: .init(withProxy: member), verificationState: verificationState))
case .ban: case .ban:
bannedMembers.append(.init(withProxy: member)) bannedMembers.append(.init(member: .init(withProxy: member), verificationState: verificationState))
default: default:
continue continue
} }
@@ -128,7 +140,7 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
return .init(invitedMembers: invitedMembers, return .init(invitedMembers: invitedMembers,
joinedMembers: joinedMembers, joinedMembers: joinedMembers,
bannedMembers: bannedMembers.sorted { $0.id.localizedStandardCompare($1.id) == .orderedAscending }) // Re-sort ignoring display name. bannedMembers: bannedMembers.sorted { $0.member.id.localizedStandardCompare($1.member.id) == .orderedAscending }) // Re-sort ignoring display name.
} }
.value .value
} }
@@ -215,18 +227,19 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
// MARK: - Indicators // MARK: - Indicators
private let userIndicatorID = UUID().uuidString private static let setupMembersLoadingIndicatorIdentifier = "\(RoomMembersListScreenViewModel.self)-SetupMembers"
private static let updateStateLoadingIndicatorIdentifier = "\(RoomMembersListScreenViewModel.self)-UpdateState"
private func showLoader() { private func showLoadingIndicator(_ identifier: String) {
userIndicatorController.submitIndicator(UserIndicator(id: userIndicatorID, userIndicatorController.submitIndicator(UserIndicator(id: identifier,
type: .modal(progress: .indeterminate, interactiveDismissDisabled: false, allowsInteraction: true), type: .modal(progress: .indeterminate, interactiveDismissDisabled: false, allowsInteraction: true),
title: L10n.commonLoading, title: L10n.commonLoading,
persistent: true), persistent: true),
delay: .milliseconds(200)) delay: .milliseconds(200))
} }
private func hideLoader() { private func hideLoadingIndicator(_ identifier: String) {
userIndicatorController.retractIndicatorWithId(userIndicatorID) userIndicatorController.retractIndicatorWithId(identifier)
} }
private func showManageMemberIndicator(title: String) { private func showManageMemberIndicator(title: String) {
@@ -247,7 +260,7 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
} }
private struct RoomMembersDetails { private struct RoomMembersDetails {
var invitedMembers: [RoomMemberDetails] var invitedMembers: [RoomMemberListScreenEntry]
var joinedMembers: [RoomMemberDetails] var joinedMembers: [RoomMemberListScreenEntry]
var bannedMembers: [RoomMemberDetails] var bannedMembers: [RoomMemberListScreenEntry]
} }
@@ -97,6 +97,7 @@ struct RoomMembersListManageMemberSheetLive_Previews: PreviewProvider {
private extension RoomMembersListScreenViewModel { private extension RoomMembersListScreenViewModel {
static var mock: RoomMembersListScreenViewModel { static var mock: RoomMembersListScreenViewModel {
RoomMembersListScreenViewModel(initialMode: .members, RoomMembersListScreenViewModel(initialMode: .members,
clientProxy: ClientProxyMock(.init()),
roomProxy: JoinedRoomProxyMock(.init(members: .allMembersAsAdmin)), roomProxy: JoinedRoomProxyMock(.init(members: .allMembersAsAdmin)),
mediaProvider: MediaProviderMock(configuration: .init()), mediaProvider: MediaProviderMock(configuration: .init()),
userIndicatorController: ServiceLocator.shared.userIndicatorController, userIndicatorController: ServiceLocator.shared.userIndicatorController,
@@ -59,23 +59,23 @@ struct RoomMembersListScreen: View {
var roomMembers: some View { var roomMembers: some View {
LazyVStack(alignment: .leading, spacing: 12) { LazyVStack(alignment: .leading, spacing: 12) {
membersSection(data: context.viewState.visibleInvitedMembers, sectionTitle: L10n.screenRoomMemberListPendingHeaderTitle) membersSection(entries: context.viewState.visibleInvitedMembers, sectionTitle: L10n.screenRoomMemberListPendingHeaderTitle)
membersSection(data: context.viewState.visibleJoinedMembers, sectionTitle: L10n.screenRoomMemberListHeaderTitle(Int(context.viewState.joinedMembersCount))) membersSection(entries: context.viewState.visibleJoinedMembers, sectionTitle: L10n.screenRoomMemberListHeaderTitle(Int(context.viewState.joinedMembersCount)))
} }
} }
var bannedUsers: some View { var bannedUsers: some View {
LazyVStack(alignment: .leading, spacing: 12) { LazyVStack(alignment: .leading, spacing: 12) {
membersSection(data: context.viewState.visibleBannedMembers) membersSection(entries: context.viewState.visibleBannedMembers)
} }
} }
@ViewBuilder @ViewBuilder
private func membersSection(data: [RoomMemberDetails], sectionTitle: String? = nil) -> some View { private func membersSection(entries: [RoomMemberListScreenEntry], sectionTitle: String? = nil) -> some View {
if !data.isEmpty { if !entries.isEmpty {
Section { Section {
ForEach(data, id: \.id) { member in ForEach(entries, id: \.member.id) { entry in
RoomMembersListScreenMemberCell(member: member, context: context) RoomMembersListScreenMemberCell(listEntry: entry, context: context)
} }
} header: { } header: {
if let sectionTitle { if let sectionTitle {
@@ -179,7 +179,22 @@ struct RoomMembersListScreen_Previews: PreviewProvider, TestablePreview {
members.append(.mockInvited) members.append(.mockInvited)
} }
let clientProxyMock = ClientProxyMock(.init())
clientProxyMock.userIdentityForClosure = { userID in
let identity = switch userID {
case RoomMemberProxyMock.mockAlice.userID:
UserIdentityProxyMock(configuration: .init(verificationState: .verified))
case RoomMemberProxyMock.mockBob.userID:
UserIdentityProxyMock(configuration: .init(verificationState: .verificationViolation))
default:
UserIdentityProxyMock(configuration: .init())
}
return .success(identity)
}
return RoomMembersListScreenViewModel(initialMode: initialMode, return RoomMembersListScreenViewModel(initialMode: initialMode,
clientProxy: clientProxyMock,
roomProxy: JoinedRoomProxyMock(.init(name: "Some room", roomProxy: JoinedRoomProxyMock(.init(name: "Some room",
members: members, members: members,
ownUserID: ownUserID, ownUserID: ownUserID,
@@ -8,22 +8,22 @@
import SwiftUI import SwiftUI
struct RoomMembersListScreenMemberCell: View { struct RoomMembersListScreenMemberCell: View {
let member: RoomMemberDetails let listEntry: RoomMemberListScreenEntry
let context: RoomMembersListScreenViewModel.Context let context: RoomMembersListScreenViewModel.Context
var body: some View { var body: some View {
Button { Button {
context.send(viewAction: .selectMember(member)) context.send(viewAction: .selectMember(listEntry.member))
} label: { } label: {
HStack(spacing: 8) { HStack(spacing: 8) {
LoadableAvatarImage(url: avatarURL, LoadableAvatarImage(url: avatarURL,
name: avatarName, name: avatarName,
contentID: member.id, contentID: listEntry.member.id,
avatarSize: .user(on: .roomDetails), avatarSize: .user(on: .roomDetails),
mediaProvider: context.mediaProvider) mediaProvider: context.mediaProvider)
.accessibilityHidden(true) .accessibilityHidden(true)
HStack(alignment: .firstTextBaseline, spacing: 4) { HStack(alignment: .center, spacing: 4) {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
Text(title) Text(title)
.font(.compound.bodyMDSemibold) .font(.compound.bodyMDSemibold)
@@ -39,6 +39,8 @@ struct RoomMembersListScreenMemberCell: View {
} }
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
VerificationBadge(verificationState: listEntry.verificationState)
if let role { if let role {
Text(role) Text(role)
.font(.compound.bodyXS) .font(.compound.bodyXS)
@@ -52,7 +54,7 @@ struct RoomMembersListScreenMemberCell: View {
} }
var role: String? { var role: String? {
switch member.role { switch listEntry.member.role {
case .administrator: case .administrator:
L10n.screenRoomMemberListRoleAdministrator L10n.screenRoomMemberListRoleAdministrator
case .moderator: case .moderator:
@@ -65,55 +67,72 @@ struct RoomMembersListScreenMemberCell: View {
// Computed properties to hide the user's profile when banned. // Computed properties to hide the user's profile when banned.
var title: String { var title: String {
guard !member.isBanned else { return member.id } guard !listEntry.member.isBanned else { return listEntry.member.id }
return member.name ?? member.id return listEntry.member.name ?? listEntry.member.id
} }
var subtitle: String? { var subtitle: String? {
member.isBanned ? nil : member.id listEntry.member.isBanned ? nil : listEntry.member.id
} }
var avatarName: String? { var avatarName: String? {
member.isBanned ? nil : member.name listEntry.member.isBanned ? nil : listEntry.member.name
} }
var avatarURL: URL? { var avatarURL: URL? {
member.isBanned ? nil : member.avatarURL listEntry.member.isBanned ? nil : listEntry.member.avatarURL
} }
} }
struct RoomMembersListMemberCell_Previews: PreviewProvider, TestablePreview { struct RoomMembersListMemberCell_Previews: PreviewProvider, TestablePreview {
static let members: [RoomMemberProxyMock] = [ static let members: [RoomMemberListScreenEntry] = [
.mockAlice, .init(member: .init(withProxy: RoomMemberProxyMock.mockAlice),
.mockAdmin, verificationState: .notVerified),
.mockModerator, .init(member: .init(withProxy: RoomMemberProxyMock.mockAdmin),
.init(with: .init(userID: "@nodisplayname:matrix.org", membership: .join)), verificationState: .verified),
.init(with: .init(userID: "@avatar:matrix.org", displayName: "Avatar", avatarURL: .mockMXCUserAvatar, membership: .join)) .init(member: .init(withProxy: RoomMemberProxyMock.mockModerator),
verificationState: .verificationViolation),
.init(member: .init(withProxy: RoomMemberProxyMock(with: .init(userID: "@nodisplayname:matrix.org",
membership: .join))),
verificationState: .notVerified),
.init(member: .init(withProxy: RoomMemberProxyMock(with: .init(userID: "@avatar:matrix.org",
displayName: "Avatar",
avatarURL: .mockMXCUserAvatar,
membership: .join))),
verificationState: .notVerified)
] ]
static let bannedMembers: [RoomMemberProxyMock] = [ static let bannedMembers: [RoomMemberListScreenEntry] = [
.init(with: .init(userID: "@nodisplayname:matrix.org", membership: .ban)), .init(member: .init(withProxy: RoomMemberProxyMock(with: .init(userID: "@nodisplayname:matrix.org",
.init(with: .init(userID: "@fake:matrix.org", displayName: "President", membership: .ban)), membership: .ban))),
.init(with: .init(userID: "@badavatar:matrix.org", avatarURL: .mockMXCUserAvatar, membership: .ban)) verificationState: .notVerified),
.init(member: .init(withProxy: RoomMemberProxyMock(with: .init(userID: "@fake:matrix.org",
displayName: "President",
membership: .ban))),
verificationState: .verified),
.init(member: .init(withProxy: RoomMemberProxyMock(with: .init(userID: "@badavatar:matrix.org",
avatarURL: .mockMXCUserAvatar,
membership: .ban))),
verificationState: .verificationViolation)
] ]
static let viewModel = RoomMembersListScreenViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Some room", static let viewModel = RoomMembersListScreenViewModel(clientProxy: ClientProxyMock(.init()),
members: members)), roomProxy: JoinedRoomProxyMock(.init(name: "Some room", members: [])),
mediaProvider: MediaProviderMock(configuration: .init()), mediaProvider: MediaProviderMock(configuration: .init()),
userIndicatorController: ServiceLocator.shared.userIndicatorController, userIndicatorController: ServiceLocator.shared.userIndicatorController,
analytics: ServiceLocator.shared.analytics) analytics: ServiceLocator.shared.analytics)
static var previews: some View { static var previews: some View {
VStack(spacing: 12) { VStack(spacing: 12) {
Section("Invited/Joined") { Section("Invited/Joined") {
ForEach(members, id: \.userID) { member in ForEach(members, id: \.member.id) { entry in
RoomMembersListScreenMemberCell(member: .init(withProxy: member), context: viewModel.context) RoomMembersListScreenMemberCell(listEntry: entry, context: viewModel.context)
} }
} }
// Banned members should have their profiles hidden and the avatar should use the first letter from their user ID. // Banned members should have their profiles hidden and the avatar should use the first letter from their user ID.
Section("Banned") { Section("Banned") {
ForEach(bannedMembers, id: \.userID) { member in ForEach(bannedMembers, id: \.member.id) { entry in
RoomMembersListScreenMemberCell(member: .init(withProxy: member), context: viewModel.context) RoomMembersListScreenMemberCell(listEntry: entry, context: viewModel.context)
} }
} }
} }
@@ -31,6 +31,7 @@ enum RoomScreenViewAction {
struct RoomScreenViewState: BindableState { struct RoomScreenViewState: BindableState {
var roomTitle = "" var roomTitle = ""
var roomAvatar: RoomAvatar var roomAvatar: RoomAvatar
var dmRecipientVerificationState: UserIdentityVerificationState?
var lastScrollDirection: ScrollDirection? var lastScrollDirection: ScrollDirection?
// This is used to control the banner // This is used to control the banner
@@ -77,6 +77,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
Task { Task {
await handleRoomInfoUpdate(roomProxy.infoPublisher.value) await handleRoomInfoUpdate(roomProxy.infoPublisher.value)
await updateVerificationBadge()
} }
setupSubscriptions(ongoingCallRoomIDPublisher: ongoingCallRoomIDPublisher) setupSubscriptions(ongoingCallRoomIDPublisher: ongoingCallRoomIDPublisher)
@@ -182,6 +184,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
} }
await self?.processIdentityStatusChanges(changes) await self?.processIdentityStatusChanges(changes)
await self?.updateVerificationBadge()
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
@@ -262,6 +265,23 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
} }
} }
private func updateVerificationBadge() async {
guard roomProxy.isDirectOneToOneRoom,
let dmRecipient = roomProxy.membersPublisher.value.first(where: { $0.userID != roomProxy.ownUserID }),
case let .success(userIdentity) = await clientProxy.userIdentity(for: dmRecipient.userID) else {
state.dmRecipientVerificationState = .notVerified
return
}
guard let userIdentity else {
MXLog.failure("User identity should be known at this point")
state.dmRecipientVerificationState = .notVerified
return
}
state.dmRecipientVerificationState = userIdentity.verificationState
}
private func resolveIdentityPinningViolation(_ userID: String) async { private func resolveIdentityPinningViolation(_ userID: String) async {
defer { defer {
hideLoadingIndicator() hideLoadingIndicator()
@@ -204,6 +204,7 @@ struct RoomScreen: View {
ToolbarItem(placement: .principal) { ToolbarItem(placement: .principal) {
RoomHeaderView(roomName: roomContext.viewState.roomTitle, RoomHeaderView(roomName: roomContext.viewState.roomTitle,
roomAvatar: roomContext.viewState.roomAvatar, roomAvatar: roomContext.viewState.roomAvatar,
dmRecipientVerificationState: roomContext.viewState.dmRecipientVerificationState,
mediaProvider: roomContext.mediaProvider) mediaProvider: roomContext.mediaProvider)
// Using a button stops it from getting truncated in the navigation bar // Using a button stops it from getting truncated in the navigation bar
.contentShape(.rect) .contentShape(.rect)
@@ -20,7 +20,6 @@ struct UserProfileScreenCoordinatorParameters {
enum UserProfileScreenCoordinatorAction { enum UserProfileScreenCoordinatorAction {
case openDirectChat(roomID: String) case openDirectChat(roomID: String)
case startCall(roomID: String) case startCall(roomID: String)
case verifyUser(userID: String)
case dismiss case dismiss
} }
@@ -52,8 +51,6 @@ final class UserProfileScreenCoordinator: CoordinatorProtocol {
actionsSubject.send(.openDirectChat(roomID: roomID)) actionsSubject.send(.openDirectChat(roomID: roomID))
case .startCall(let roomID): case .startCall(let roomID):
actionsSubject.send(.startCall(roomID: roomID)) actionsSubject.send(.startCall(roomID: roomID))
case .verifyUser(let userID):
actionsSubject.send(.verifyUser(userID: userID))
case .dismiss: case .dismiss:
actionsSubject.send(.dismiss) actionsSubject.send(.dismiss)
} }
@@ -10,7 +10,6 @@ import Foundation
enum UserProfileScreenViewModelAction { enum UserProfileScreenViewModelAction {
case openDirectChat(roomID: String) case openDirectChat(roomID: String)
case startCall(roomID: String) case startCall(roomID: String)
case verifyUser(userID: String)
case dismiss case dismiss
} }
@@ -29,10 +28,6 @@ struct UserProfileScreenViewState: BindableState {
var showVerifiedBadge: Bool { var showVerifiedBadge: Bool {
isVerified == true // We purposely show the badge on your own account for consistency with Web. isVerified == true // We purposely show the badge on your own account for consistency with Web.
} }
var showVerificationSection: Bool {
isVerified == false && !isOwnUser
}
} }
struct UserProfileScreenViewStateBindings { struct UserProfileScreenViewStateBindings {
@@ -48,7 +43,6 @@ enum UserProfileScreenViewAction {
case openDirectChat case openDirectChat
case createDirectChat case createDirectChat
case startCall(roomID: String) case startCall(roomID: String)
case verifyUser
case dismiss case dismiss
} }
@@ -66,8 +66,6 @@ class UserProfileScreenViewModel: UserProfileScreenViewModelType, UserProfileScr
Task { await createDirectChat() } Task { await createDirectChat() }
case .startCall(let roomID): case .startCall(let roomID):
actionsSubject.send(.startCall(roomID: roomID)) actionsSubject.send(.startCall(roomID: roomID))
case .verifyUser:
actionsSubject.send(.verifyUser(userID: state.userID))
case .dismiss: case .dismiss:
actionsSubject.send(.dismiss) actionsSubject.send(.dismiss)
} }
@@ -95,7 +93,7 @@ class UserProfileScreenViewModel: UserProfileScreenViewModelType, UserProfileScr
} }
if case let .success(.some(identity)) = await identityResult { if case let .success(.some(identity)) = await identityResult {
state.isVerified = identity.isVerified() state.isVerified = identity.verificationState == .verified
} else { } else {
MXLog.error("Failed to find the user's identity.") MXLog.error("Failed to find the user's identity.")
} }
@@ -14,8 +14,6 @@ struct UserProfileScreen: View {
var body: some View { var body: some View {
Form { Form {
headerSection headerSection
verificationSection
} }
.compoundList() .compoundList()
.navigationTitle(L10n.screenRoomMemberDetailsTitle) .navigationTitle(L10n.screenRoomMemberDetailsTitle)
@@ -84,18 +82,6 @@ struct UserProfileScreen: View {
.padding(.top, 32) .padding(.top, 32)
} }
@ViewBuilder
var verificationSection: some View {
if context.viewState.showVerificationSection {
Section {
ListRow(label: .default(title: L10n.commonVerifyUser, icon: \.lock),
kind: .button {
context.send(viewAction: .verifyUser)
})
}
}
}
@ToolbarContentBuilder @ToolbarContentBuilder
private var toolbar: some ToolbarContent { private var toolbar: some ToolbarContent {
if context.viewState.isPresentedModally { if context.viewState.isPresentedModally {
@@ -137,13 +123,22 @@ struct UserProfileScreen_Previews: PreviewProvider, TestablePreview {
static func makeViewModel(userID: String) -> UserProfileScreenViewModel { static func makeViewModel(userID: String) -> UserProfileScreenViewModel {
let clientProxyMock = ClientProxyMock(.init()) let clientProxyMock = ClientProxyMock(.init())
clientProxyMock.userIdentityForClosure = { userID in clientProxyMock.userIdentityForClosure = { userID in
let isVerified = userID == RoomMemberProxyMock.mockDan.userID let identity = switch userID {
return .success(UserIdentitySDKMock(configuration: .init(isVerified: isVerified))) case RoomMemberProxyMock.mockDan.userID:
UserIdentityProxyMock(configuration: .init(verificationState: .verified))
default:
UserIdentityProxyMock(configuration: .init())
} }
return .success(identity)
}
if userID != RoomMemberProxyMock.mockMe.userID { if userID != RoomMemberProxyMock.mockMe.userID {
clientProxyMock.directRoomForUserIDReturnValue = .success("roomID") clientProxyMock.directRoomForUserIDReturnValue = .success("roomID")
} }
return UserProfileScreenViewModel(userID: userID, return UserProfileScreenViewModel(userID: userID,
isPresentedModally: false, isPresentedModally: false,
clientProxy: clientProxyMock, clientProxy: clientProxyMock,
@@ -1018,9 +1018,9 @@ class ClientProxy: ClientProxyProtocol {
} }
} }
func userIdentity(for userID: String) async -> Result<UserIdentity?, ClientProxyError> { func userIdentity(for userID: String) async -> Result<UserIdentityProxyProtocol?, ClientProxyError> {
do { do {
return try await .success(client.encryption().userIdentity(userId: userID)) return try await .success(client.encryption().userIdentity(userId: userID).map(UserIdentityProxy.init))
} catch { } catch {
MXLog.error("Failed retrieving user identity: \(error)") MXLog.error("Failed retrieving user identity: \(error)")
return .failure(.sdkError(error)) return .failure(.sdkError(error))
@@ -191,5 +191,5 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
func withdrawUserIdentityVerification(_ userID: String) async -> Result<Void, ClientProxyError> func withdrawUserIdentityVerification(_ userID: String) async -> Result<Void, ClientProxyError>
func resetIdentity() async -> Result<IdentityResetHandle?, ClientProxyError> func resetIdentity() async -> Result<IdentityResetHandle?, ClientProxyError>
func userIdentity(for userID: String) async -> Result<UserIdentity?, ClientProxyError> func userIdentity(for userID: String) async -> Result<UserIdentityProxyProtocol?, ClientProxyError>
} }
@@ -0,0 +1,26 @@
//
// 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 MatrixRustSDK
class UserIdentityProxy: UserIdentityProxyProtocol {
private let userIdentity: UserIdentity
init(userIdentity: UserIdentity) {
self.userIdentity = userIdentity
}
var verificationState: UserIdentityVerificationState {
if userIdentity.hasVerificationViolation() {
return .verificationViolation
} else if userIdentity.isVerified() {
return .verified
}
return .notVerified
}
}
@@ -0,0 +1,17 @@
//
// 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 UserIdentityVerificationState {
case notVerified
case verified
case verificationViolation
}
// sourcery: AutoMockable
protocol UserIdentityProxyProtocol {
var verificationState: UserIdentityVerificationState { get }
}
@@ -571,8 +571,9 @@ class MockScreen: Identifiable {
case .roomMembersListScreenPendingInvites: case .roomMembersListScreenPendingInvites:
let navigationStackCoordinator = NavigationStackCoordinator() let navigationStackCoordinator = NavigationStackCoordinator()
let members: [RoomMemberProxyMock] = [.mockInvitedAlice, .mockBob, .mockCharlie] let members: [RoomMemberProxyMock] = [.mockInvitedAlice, .mockBob, .mockCharlie]
let coordinator = RoomMembersListScreenCoordinator(parameters: .init(mediaProvider: MediaProviderMock(configuration: .init()), let coordinator = RoomMembersListScreenCoordinator(parameters: .init(clientProxy: ClientProxyMock(.init()),
roomProxy: JoinedRoomProxyMock(.init(name: "test", members: members)), roomProxy: JoinedRoomProxyMock(.init(name: "test", members: members)),
mediaProvider: MediaProviderMock(configuration: .init()),
userIndicatorController: ServiceLocator.shared.userIndicatorController, userIndicatorController: ServiceLocator.shared.userIndicatorController,
analytics: ServiceLocator.shared.analytics)) analytics: ServiceLocator.shared.analytics))
navigationStackCoordinator.setRootCoordinator(coordinator) navigationStackCoordinator.setRootCoordinator(coordinator)
@@ -1037,6 +1037,12 @@ extension PreviewTests {
} }
} }
func test_verificationBadge() async throws {
for preview in VerificationBadge_Previews._allPreviews {
try await assertSnapshots(matching: preview)
}
}
func test_videoMediaEventsTimelineView() async throws { func test_videoMediaEventsTimelineView() async throws {
for preview in VideoMediaEventsTimelineView_Previews._allPreviews { for preview in VideoMediaEventsTimelineView_Previews._allPreviews {
try await assertSnapshots(matching: preview) try await assertSnapshots(matching: preview)
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:6ad1568981acd0a49c17795c4f9bf47c0270007639dd61effb68d708d49a8123 oid sha256:153b4927280abdaa674e93f843c84b6dd1a23070c37556d57294d1504759ff2d
size 111376 size 111371
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:c71906723f41774d71975cefc46b0ab8415e0fdebb63ec8768480a32935aeadb oid sha256:aa9c4ce4236f90ab89039f6353aa28091c2127b9d2acec0c808492f2b88750e3
size 123103 size 123098
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:19e3709844d8e70eebcd04eea5b56f2c32cb29c50626a3ae2834d839b8904efd oid sha256:8a9575ea8743524b675a174ae1e5b7bed6d85ac01f6b833daec9739997b0b2da
size 68156 size 68180
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:ae6d231128edddb04ba4fffa915c2fcf1169ea28196cce8431da8d3eaa43d4e3 oid sha256:48cc5116b97690c21daad083a15e9266068f9196a426b938020d3b57f8a4d8d2
size 82070 size 82093
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:b8d4f33061bf7a6298eff4e3ffb63194b7b0eee56f2677c55e0db5c4db4e8a05 oid sha256:5a3fcbc84d86ab21cfb58787fd83ab8e46e48e2fe35acaec61dff7f72fc0f938
size 19820 size 73712
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:94a649334de3e92c04c8c5fa24606ce4338b38867d3af71ba044ebc199bbf8d5
size 11439
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:b8d4f33061bf7a6298eff4e3ffb63194b7b0eee56f2677c55e0db5c4db4e8a05 oid sha256:5a3fcbc84d86ab21cfb58787fd83ab8e46e48e2fe35acaec61dff7f72fc0f938
size 19820 size 73712
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:94a649334de3e92c04c8c5fa24606ce4338b38867d3af71ba044ebc199bbf8d5
size 11439
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:0a28fd17b8b3cbaa0d27c70019904244d5d29ee2fc83266512e8ae3b386e32cf oid sha256:1dfa74df7fac6d93e37ee9332a59b442c97a912261bc40b73c786090d613f259
size 15862 size 56152
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:07945ef1e674a1744a26d59745956b94201a9a2b0fdb9a1c15933c32ce246e89
size 7886
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:0a28fd17b8b3cbaa0d27c70019904244d5d29ee2fc83266512e8ae3b386e32cf oid sha256:1dfa74df7fac6d93e37ee9332a59b442c97a912261bc40b73c786090d613f259
size 15862 size 56152
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:07945ef1e674a1744a26d59745956b94201a9a2b0fdb9a1c15933c32ce246e89
size 7886
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:bb9a139e108b9cca1fceafa4fb2d4b2c0ea7a8b76a916a72cab682f1895a2cbb oid sha256:bc9057e946a55019caf464ab7437197b5bd0ab5e83076aba8a64bf65c309773b
size 180293 size 180336
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:bc7992df6c2b7227c500f84531bdac9577db2cf067cb6aa9a81b81a8936771e4 oid sha256:75cd91c40c97f338248fb5d5e76a291010115220c4094e27058206b5d7f1307b
size 114445 size 114637
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:eb8a03e056c72838db67ac97b012792f1d98985952dcaeffe80455363b729a5a oid sha256:a36746c71d1e8fc4379b20b17adeed7af104bd2bf2bc0f59fb9c9cd4b77f6bba
size 114232 size 114418
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bcf2cefb0e51250c908006f9380d69d2ecf46a71c4a86aa54e20995498c0eb47
size 125040
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:e764eb1952e4820a15affa00461fb1e2f4a3de0b7ff498560aab738e042c2c0b oid sha256:d4cca2b5da6c0a8bd3946a9d2a7ffb5b23a448c3e07eb0a5dea7bdab025c76c9
size 199433 size 199464
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:f2f03c0e35ba417a0b55d2cade9bb8eeada26f0514497b411adc61675b25c03a oid sha256:48b686ff1f24cd842f4d8ae28c11c3a62113d2137e974c68f52e551bdb4514d8
size 180559 size 180604
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:26810d28eb8d22229c2fb6259d44182e8f459efc13d3da72af855c3bd217894c oid sha256:763fb73d2174b8c43a13eda6d833cfa59514069f3e5343d149f5b4db16db0ef2
size 116662 size 116882
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:4deab2abb40430b6c26169284463fcb45ca30f93b703906e83a7a80b0dd44085 oid sha256:add76379b3faa0915ddacb2f0fba8f0dafa629fcee802230b7b6fa04f84d87d1
size 116224 size 116404
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:622e29540af65ff83e1b0f4799431b3a44edba542d208f007f3027c612e3e043
size 133323
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:c1d159bceea6b6a367d6f6d1adc3c91f7c8ed5a89a12cc76d950108505e50834 oid sha256:72f3469670b4e242ff96cfd50116c4413b50dc59f229257eeaddea84fc802bdc
size 200973 size 201004
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:6bef0734ddc98e3d468375a8ed95de83880740c3cd9393506e49004f88a9c1b1 oid sha256:56b877e9188f4dfaab208f93d34fe1dbaceed62e5122a87bbf5b5708d59ef789
size 121431 size 121429
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:99d9738fe1e134a9570a510ee4e0a7622bccc0edda2316d7e083cb6800f47567 oid sha256:7cf3cda095d77c4336f766f2e8c42a8a6be21c460464080b33e4819fd955009e
size 66185 size 65826
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:2890c691ada40cd89e8659f9e4397e5f2e0958cc3a7540d7f42f0d525fda4f46 oid sha256:33df32f5444f672baadb0028384d11d2385c49eaf87734587efe686dfeb07ed4
size 65267 size 64908
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:03c2ebe4a22b3b8422e5f320f57a59ac2b8038c057327657878378458e5328d5
size 74321
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:f47c8be31c2dfb8109e021b872ddce7ce7d38a5015ada1a5332ee522b25f0382 oid sha256:5030171dd1afe4cbf988eaca778cf9ed0500f7284bba7ac51fb3243e1f632899
size 138264 size 138283
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:22eb5c47c43b1c9fb7bc0f2806a90750374ad3825ab0d017d57bf0fd61d696b0 oid sha256:6bdf3c45eb5a7ec4e44cf20b1a667f934b51c6316d05c85026749820544518df
size 122013 size 122011
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:4a076ef632d402d507a09abaa38822a81a2a1a094aaee6de62e2aed796478266 oid sha256:8a75782f44f8492435c9ebab3708644e955f14dca3ac35ab615a69984c7a71fa
size 74470 size 74437
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:c796a02083a8469da92b6d764092caf259a0f6ee4ff52baf7851e5fcfa2be76d oid sha256:703f8fdb10d5d1081eb1bfddef855de60c9bb21ee90ff08420813fab23ee3ba6
size 73660 size 73622
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5b02fe793ca33790c1c6da7b1eabea1b828324c6faeacf242857d26b0b94eeb2
size 93276
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:01b00054f275cb3ae4185449e7c5cbe375180260f0cde860ab290ed5dc6757b0 oid sha256:2cc7c0a660c1ddf91e1f7336525dd18ce8a6a3ff1e1746e070119fe74b2a539c
size 146295 size 146322
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:02f47841f18c62a867c4ff1a23bf16edf17c81f5e97f249c2cff30a9d6c869f6 oid sha256:150439a358f12324fd477216dffe3cbf9feccc704f019e00ad4f43e153bb97fa
size 169210 size 172233
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:2e5bb0918b63578e8aa80fe8eaa088f4cff4eea4d4ab886b945362186e85ae5c oid sha256:9f2e7d68d38ac78087a52dfd0ef401fccda75d9a7112bc357ae5b034cecb48db
size 172967 size 175918
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:a71ee5cc84935ecdfaa5fd164b46f01c1841ea4469282eaa424d729a65906148 oid sha256:818255f45004091e759445a5541c9b04aa6c58926c7479e5e48f2da9ca9c0222
size 113290 size 115529
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:3f67432fd158f5f91089243ea9919ade5e8b10a481625715a413d1e6ae580663 oid sha256:803e62688fba1c2c652a59f52784e39da86f6a80dee0e0862925e75d80bf83cd
size 117653 size 119715
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:b1157284b4d3bd61c1ed44c4f8c49cfd693cea4634f785d78a738e291d39d08e oid sha256:3e9df975598876636a317b5c6ab5a0f8d38c9afb7be4e25c4e6fad0c46bf5719
size 132965 size 134459
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:765e253eb1ab6eb5d9a77bc8b0a6c5d7e3ed064343ac055f9702b5fc810763aa oid sha256:feca48e2dcc6dc7f029b1b39716d3b1af94e1a12b6afc94cbf061fc68f2f3cac
size 136303 size 137742
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:02a3fb3f4dd20a12d2ff00ea73a6f88d4a30e96de1d6522b57a9c88ee1a7e05d oid sha256:c60d9ebc5781792b45e0237d8f69c524af59865f32e387c762f306744c411401
size 125648 size 127049
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:5399631ce7f310512b85452f95230e717a1505787e612f3078f65f9ebd604857 oid sha256:fb17504e82993df630ad2cb20854e3d2aa2f85baa3192f03800fb98419ce78b0
size 141900 size 143376
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d3be505a8d77dcef1b457e847e204efff0f7e76c81af095d3cfdd88f87a2e03c oid sha256:4d8d6fc490b3b7362bf326187c7526e00611bb585f3a2868bc0b1f98c6f9eaf4
size 144426 size 145856
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:bf7e90926372085ad952e03139a9d3c0815b804cc2045145d878e00de65e4671 oid sha256:ebef244d49b790601115f26146d52030fdca6632385a2c225bbd554d8e51d912
size 133587 size 135046
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:3ebd4283903f8901a4fa2bcaca20b77a866d037581413f5908cc3a8d7dcfea03 oid sha256:dc06908f037a0d2730d4a8b3b49742ddd24d72fc4ca5d0a14b1974385198ac78
size 83790 size 84815
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:4429d4306851f0d7e6e07c216dfc1b7abb3b8380ce1d1ed53da6a54ae8220362 oid sha256:9cb13eddbab510b9646fd2ee431e37208ef0d2f3b23204dce4fcf85032ba5891
size 86315 size 87183
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:6a5fa320eb70ec2e1181dd063e76cd26ba9c2594c64b02058e77e9f536dfa99c oid sha256:521a418127816a652a7e4d17146c3eeb1d4aa613922f11b53ef63115ef90ed1e
size 77966 size 78984
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:c598dafaa1584f2b8886183b1d9273c9b607322b493710725feeaec2f4d4f7e6 oid sha256:1d804804fc7e19802cf4d3156c34e26e2be96085fc5fe9987403940541a3e08c
size 93516 size 94418
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d6ea9ff5daaa20a19ec8b822cba67eae0d8796b20a0121f33304b26c503e085d oid sha256:47335b9d2db5c87e10bbbe1b441d99c9283974ac5bf0ecd716a5bf172e17af29
size 96436 size 97152
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:0951e99bb5bad9c5a5be23f56e7c302b38514c0760c47ef5a39d3ed982bb38f7 oid sha256:39e5aafbe8a8629ec28da856475bd1e20f83060fa4b19d543d43f58c35d9bbca
size 86447 size 87331
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d19fdc59c123919099ce464a0bb5478edb8df05e23609cd5dae1b430bcd469d1 oid sha256:02b4de01b6f3291309fdef1755bf2d1a4e989cecd777766394d512ef330fbfea
size 295900 size 295904
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:015366f6fe40da0753b014c0be10d6f46478d36e76fef8ac0d03608c44c8665d oid sha256:7be7c4f878b08ae3724587db110a3ea3ec1133f9731aca59924776ec59b54c66
size 304454 size 304445
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:e5fe32d96cc563369893c88d566260a465e5ae7f372b10f75f1cd6b9fa93877e oid sha256:5236d9950c7d6179f4351b486bedb519e2afb29f65603d1ee3fea9b862936ba7
size 181804 size 181809
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:6332e9688b6bc74fd053be710d9fc160016ff08fe08d755420871e5a79ad6c24 oid sha256:3a9027ee84e95fa1b65e7287b311a109f829302563216d57b0f4a72fbb0fcedf
size 180191 size 180206
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d19fdc59c123919099ce464a0bb5478edb8df05e23609cd5dae1b430bcd469d1 oid sha256:02b4de01b6f3291309fdef1755bf2d1a4e989cecd777766394d512ef330fbfea
size 295900 size 295904
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:015366f6fe40da0753b014c0be10d6f46478d36e76fef8ac0d03608c44c8665d oid sha256:7be7c4f878b08ae3724587db110a3ea3ec1133f9731aca59924776ec59b54c66
size 304454 size 304445
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:e5fe32d96cc563369893c88d566260a465e5ae7f372b10f75f1cd6b9fa93877e oid sha256:5236d9950c7d6179f4351b486bedb519e2afb29f65603d1ee3fea9b862936ba7
size 181804 size 181809
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:6332e9688b6bc74fd053be710d9fc160016ff08fe08d755420871e5a79ad6c24 oid sha256:3a9027ee84e95fa1b65e7287b311a109f829302563216d57b0f4a72fbb0fcedf
size 180191 size 180206
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:7c79cf23a403a1a8f7f870c5bc1b076d7db8fdc5306f8e2d570481b04f53243c oid sha256:7ec0cea60e7008080e384d60dbc8de8747cd55d8740b22292b286f04f9fe61db
size 108340 size 101556
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:12b13a72d49eed5e18f72dc9f9fe38e030e30cce68fa3e2ff0cf30f5fbe8967d oid sha256:f6883e72e65297302617486b88e8bca4e81e577aa40c888097f93099216f191f
size 110043 size 102538
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:cf92dab653565f100a445d2e8231e984761644436f2c9f565823f5c5730a0e28 oid sha256:94fe46228c52208114afa3c511f09398921697390b27e83b1e4930eb17db5757
size 59553 size 53177
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:516372c5b8db4c73878cbce0be4015381446623317529c1a44a30fb114efd277 oid sha256:62bf342ae32beb4efba3cff60e18da950cf406bbdcbd8b0e9bcca2e1906b1c06
size 66599 size 58597
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bc125492dfe1f025e9164856adc7c8c5c139c7b9f1e6ad6db977a2704e4649b3
size 6588
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bc125492dfe1f025e9164856adc7c8c5c139c7b9f1e6ad6db977a2704e4649b3
size 6588
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:95330c60b8d668574425379be7d0f41e8e17e11c76da95afaf5f89acfb68225e
size 3755
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:95330c60b8d668574425379be7d0f41e8e17e11c76da95afaf5f89acfb68225e
size 3755
@@ -40,8 +40,18 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
try await deferred.fulfill() try await deferred.fulfill()
let sortedMembers: [RoomMemberProxyMock] = [.mockAdmin, .mockModerator, .mockAlice, .mockDan] let sortedMembers: [RoomMemberListScreenEntry] = [
XCTAssertEqual(viewModel.state.visibleJoinedMembers, sortedMembers.map(RoomMemberDetails.init)) .init(member: .init(withProxy: RoomMemberProxyMock.mockAdmin),
verificationState: .notVerified),
.init(member: .init(withProxy: RoomMemberProxyMock.mockModerator),
verificationState: .notVerified),
.init(member: .init(withProxy: RoomMemberProxyMock.mockAlice),
verificationState: .notVerified),
.init(member: .init(withProxy: RoomMemberProxyMock.mockDan),
verificationState: .notVerified)
]
XCTAssertEqual(viewModel.state.visibleJoinedMembers, sortedMembers)
} }
func testSearch() async throws { func testSearch() async throws {
@@ -125,7 +135,7 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
// When tapping on another user in the list. // When tapping on another user in the list.
let memberDetailsAction = deferFulfillment(viewModel.actions) { $0.isSelectMember } let memberDetailsAction = deferFulfillment(viewModel.actions) { $0.isSelectMember }
guard let user = viewModel.state.visibleJoinedMembers.first(where: { $0.role == .user && $0.id != RoomMemberProxyMock.mockMe.userID }) 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
} }
@@ -145,7 +155,7 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
// 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.memberToManage != nil }
guard let user = viewModel.state.visibleJoinedMembers.first(where: { $0.role == .user && $0.id != RoomMemberProxyMock.mockMe.userID }) 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
} }
@@ -166,7 +176,7 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
// 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.memberToManage != nil }
guard let moderator = viewModel.state.visibleJoinedMembers.first(where: { $0.role == .moderator }) 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
} }
@@ -186,7 +196,7 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
// When tapping on another administrator in the list. // When tapping on another administrator in the list.
let memberDetailsAction = deferFulfillment(viewModel.actions) { $0.isSelectMember } let memberDetailsAction = deferFulfillment(viewModel.actions) { $0.isSelectMember }
guard let admin = viewModel.state.visibleJoinedMembers.first(where: { $0.role == .administrator && $0.id != RoomMemberProxyMock.mockMe.userID }) else { guard let admin = viewModel.state.visibleJoinedMembers.first(where: { $0.member.role == .administrator && $0.member.id != RoomMemberProxyMock.mockMe.userID })?.member else {
XCTFail("Expected to find another admin.") XCTFail("Expected to find another admin.")
return return
} }
@@ -205,7 +215,7 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
// When tapping on yourself in the list. // When tapping on yourself in the list.
let memberDetailsAction = deferFulfillment(viewModel.actions) { $0.isSelectMember } let memberDetailsAction = deferFulfillment(viewModel.actions) { $0.isSelectMember }
guard let ownMember = viewModel.state.visibleJoinedMembers.first(where: { $0.id == RoomMemberProxyMock.mockMe.userID }) else { guard let ownMember = viewModel.state.visibleJoinedMembers.first(where: { $0.member.id == RoomMemberProxyMock.mockMe.userID })?.member else {
XCTFail("Expected to find own user admin.") XCTFail("Expected to find own user admin.")
return return
} }
@@ -225,7 +235,7 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
// When tapping on a banned member in the list. // When tapping on a banned member in the list.
deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil } deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil }
guard let bannedMember = viewModel.state.visibleBannedMembers.first else { guard let bannedMember = viewModel.state.visibleBannedMembers.first?.member else {
XCTFail("Expected to find a banned user.") XCTFail("Expected to find a banned user.")
return return
} }
@@ -242,7 +252,7 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
let deferred = deferFulfillment(context.$viewState) { !$0.visibleJoinedMembers.isEmpty } let deferred = deferFulfillment(context.$viewState) { !$0.visibleJoinedMembers.isEmpty }
try await deferred.fulfill() try await deferred.fulfill()
context.send(viewAction: .kickMember(viewModel.state.visibleJoinedMembers[0])) context.send(viewAction: .kickMember(viewModel.state.visibleJoinedMembers[0].member))
// Calling the mock won't actually change any view state, so sleep instead. // Calling the mock won't actually change any view state, so sleep instead.
try await Task.sleep(for: .milliseconds(100)) try await Task.sleep(for: .milliseconds(100))
@@ -255,7 +265,7 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
let deferred = deferFulfillment(context.$viewState) { !$0.visibleJoinedMembers.isEmpty } let deferred = deferFulfillment(context.$viewState) { !$0.visibleJoinedMembers.isEmpty }
try await deferred.fulfill() try await deferred.fulfill()
context.send(viewAction: .banMember(viewModel.state.visibleJoinedMembers[0])) context.send(viewAction: .banMember(viewModel.state.visibleJoinedMembers[0].member))
// Calling the mock won't actually change any view state, so sleep instead. // Calling the mock won't actually change any view state, so sleep instead.
try await Task.sleep(for: .milliseconds(100)) try await Task.sleep(for: .milliseconds(100))
@@ -268,7 +278,7 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
let deferred = deferFulfillment(context.$viewState) { !$0.visibleJoinedMembers.isEmpty } let deferred = deferFulfillment(context.$viewState) { !$0.visibleJoinedMembers.isEmpty }
try await deferred.fulfill() try await deferred.fulfill()
context.send(viewAction: .unbanMember(viewModel.state.visibleJoinedMembers[0])) context.send(viewAction: .unbanMember(viewModel.state.visibleJoinedMembers[0].member))
// Calling the mock won't actually change any view state, so sleep instead. // Calling the mock won't actually change any view state, so sleep instead.
try await Task.sleep(for: .milliseconds(100)) try await Task.sleep(for: .milliseconds(100))
@@ -278,7 +288,8 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
private func setup(with members: [RoomMemberProxyMock]) { private func setup(with members: [RoomMemberProxyMock]) {
roomProxy = JoinedRoomProxyMock(.init(name: "test", members: members)) roomProxy = JoinedRoomProxyMock(.init(name: "test", members: members))
viewModel = .init(roomProxy: roomProxy, viewModel = .init(clientProxy: ClientProxyMock(.init()),
roomProxy: roomProxy,
mediaProvider: MediaProviderMock(configuration: .init()), mediaProvider: MediaProviderMock(configuration: .init()),
userIndicatorController: ServiceLocator.shared.userIndicatorController, userIndicatorController: ServiceLocator.shared.userIndicatorController,
analytics: ServiceLocator.shared.analytics) analytics: ServiceLocator.shared.analytics)

Some files were not shown because too many files have changed in this diff Show More