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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
101 changed files with 722 additions and 305 deletions

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" */ = {

View File

@ -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"
} }
}, },
{ {

View File

@ -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.
} }

View File

@ -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)
} }

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()))
} }
} }

View File

@ -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>?

View File

@ -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?

View File

@ -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
} }
} }

View File

@ -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)
}
}

View File

@ -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)
Text(roomName)
.lineLimit(1) HStack(spacing: 4) {
.font(.compound.bodyLGSemibold) Text(roomName)
.accessibilityIdentifier(A11yIdentifiers.roomScreen.name) .lineLimit(1)
.font(.compound.bodyLGSemibold)
.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 {
VStack(spacing: 8) {
makeHeader(avatarURL: nil, verificationState: .notVerified)
makeHeader(avatarURL: .mockMXCAvatar, verificationState: .notVerified)
makeHeader(avatarURL: .mockMXCAvatar, verificationState: .verified)
makeHeader(avatarURL: .mockMXCAvatar, verificationState: .verificationViolation)
}
.previewLayout(.sizeThatFits)
}
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: .mockMXCAvatar), avatarURL: avatarURL),
dmRecipientVerificationState: verificationState,
mediaProvider: MediaProviderMock(configuration: .init())) mediaProvider: MediaProviderMock(configuration: .init()))
.previewLayout(.sizeThatFits)
.padding()
RoomHeaderView(roomName: "Some Room name",
roomAvatar: .room(id: "1",
name: "Some Room Name",
avatarURL: nil),
mediaProvider: MediaProviderMock(configuration: .init()))
.previewLayout(.sizeThatFits)
.padding() .padding()
} }
} }

View File

@ -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

View File

@ -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

View File

@ -198,14 +198,23 @@ 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: .title(String(context.viewState.joinedMembersCount)), details: .icon(CompoundIcon(\.infoSolid).foregroundStyle(.compound.iconCriticalPrimary)),
kind: .navigationLink { kind: .navigationLink {
context.send(viewAction: .processTapPeople) context.send(viewAction: .processTapPeople)
}) })
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.people) .accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.people)
} else {
ListRow(label: .default(title: L10n.commonPeople, icon: \.user),
details: .title(String(context.viewState.joinedMembersCount)),
kind: .navigationLink {
context.send(viewAction: .processTapPeople)
})
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.people)
}
if context.viewState.canSeeKnockingRequests { if context.viewState.canSeeKnockingRequests {
ListRow(label: .default(title: L10n.screenRoomDetailsRequestsToJoinTitle, ListRow(label: .default(title: L10n.screenRoomDetailsRequestsToJoinTitle,
icon: \.askToJoin), icon: \.askToJoin),

View File

@ -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 {

View File

@ -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.")
} }

View File

@ -15,7 +15,9 @@ struct RoomMemberDetailsScreen: View {
Form { Form {
headerSection headerSection
verificationSection if context.viewState.showVerifyIdentitySection {
verificationSection
}
if context.viewState.memberDetails != nil, !context.viewState.isOwnMemberDetails { if context.viewState.memberDetails != nil, !context.viewState.isOwnMemberDetails {
blockUserSection blockUserSection
@ -46,7 +48,14 @@ 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: {
otherUserFooter VStack(spacing: 24) {
if context.viewState.showWithdrawVerificationSection {
withdrawVerificationSection
}
otherUserFooter
}
.padding(.top, 24)
} }
} else { } else {
AvatarHeaderView(user: UserProfileProxy(userID: context.viewState.userID), AvatarHeaderView(user: UserProfileProxy(userID: context.viewState.userID),
@ -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,18 +113,13 @@ 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), kind: .button {
ListRow(label: .default(title: L10n.commonVerifyUser, icon: \.lock), context.send(viewAction: .verifyUser)
kind: .button { })
context.send(viewAction: .verifyUser)
})
}
} }
} }
@ -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,9 +170,15 @@ 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
@ -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")

View File

@ -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)

View File

@ -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) }
} }
} }

View File

@ -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]
} }

View File

@ -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,

View File

@ -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,

View File

@ -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)
} }
} }
} }

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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)
} }

View File

@ -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
} }

View File

@ -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.")
} }

View File

@ -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)
@ -83,19 +81,7 @@ 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,

View File

@ -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))

View File

@ -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>
} }

View File

@ -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
}
}

View File

@ -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 }
}

View File

@ -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)

View File

@ -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)

Binary file not shown.

View File

@ -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