mirror of
https://github.com/element-hq/element-x-ios.git
synced 2025-03-10 13:37:11 +00:00
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:
parent
b71c93dfaa
commit
f77faee981
@ -106,6 +106,7 @@
|
||||
126CBCF5B0145FA1377C1316 /* Tracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B574805B9812C111D6215D /* Tracing.swift */; };
|
||||
126EE01D8BEAEF26105D83C5 /* RoomDetailsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A5FEF17ED7E6176D922D4F /* RoomDetailsScreen.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 */; };
|
||||
12CCA59536EDD99A3272CF77 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC3F82523D6F48B926D6AF68 /* AppSettings.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 */; };
|
||||
241CDEFE23819867D9B39066 /* RoomChangePermissionsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE75941583A033A9EDC9FE0 /* RoomChangePermissionsScreenViewModel.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 */; };
|
||||
24A1BBADAC43DC3F3A7347DA /* AnalyticsPromptScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = E53BFB7E4F329621C844E8C3 /* AnalyticsPromptScreen.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 */; };
|
||||
4E8A2A2CFEB212F14E49E1A1 /* AppLockSetupSettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5484457C81B325660901B161 /* AppLockSetupSettingsScreen.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 */; };
|
||||
4EAC427267424192964B16B3 /* AppSettingsHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13BE9781699FB510E9263192 /* AppSettingsHook.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 */; };
|
||||
7A170A5A4A352954BB2A1B96 /* AuthenticationStartScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24E8C8817F59BEC7E358EB78 /* AuthenticationStartScreen.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 */; };
|
||||
7A71AEF419904209BB8C2833 /* UserAgentBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F2529D434C750ED78ADF1ED /* UserAgentBuilder.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 */; };
|
||||
C7774720A4B2E34693E3227C /* RoomNotificationSettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8896CDD20CA2D87EA3B848A1 /* RoomNotificationSettingsScreen.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 */; };
|
||||
C80E06ED97CE52704A46C148 /* ClientBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A1C33355FFB0F0953C35036 /* ClientBuilder.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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -1967,6 +1971,7 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -2047,6 +2052,7 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -2340,6 +2346,7 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -2444,7 +2451,6 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -2707,6 +2713,7 @@
|
||||
children = (
|
||||
693E16574C6F7F9FA1015A8C /* Search.swift */,
|
||||
832397B5C3D00A4BF52C5F0B /* ShouldScrollOnKeyboardDidShow.swift */,
|
||||
D1D97BAF04AA150C0EF03021 /* VerificationBadge.swift */,
|
||||
E2DA161C142B7AB8CC40F752 /* Animation */,
|
||||
CE2FBFD64A89F5DBE4EB30DB /* Layout */,
|
||||
E6E1D07163F8752D62DA4A93 /* Styles */,
|
||||
@ -3215,6 +3222,7 @@
|
||||
B0F5CC38803B8382D2C63222 /* TimelineControllerFactoryMock.swift */,
|
||||
62EACAFB3F3E017060F9F1C5 /* TimelineProviderMock.swift */,
|
||||
17A8AA0DFA06012A9DAB951E /* TimelineProxyMock.swift */,
|
||||
8536B40F578BA7FDB23D97B1 /* UserIdentityProxyMock.swift */,
|
||||
7893780A1FD6E3F38B3E9049 /* UserIndicatorControllerMock.swift */,
|
||||
AAD01F7FC2BBAC7351948595 /* UserProfile+Mock.swift */,
|
||||
F4469F6AE311BDC439B3A5EC /* UserSessionMock.swift */,
|
||||
@ -5652,6 +5660,8 @@
|
||||
D071F86CD47582B9196C9D16 /* UserDiscoverySection.swift */,
|
||||
65AAD845E53B0C8B5E0812C2 /* UserDiscoveryService.swift */,
|
||||
7F615A00DB223FF3280204D2 /* UserDiscoveryServiceProtocol.swift */,
|
||||
964093C7CA8823CAB7FFD88E /* UserIdentityProxy.swift */,
|
||||
0F793C422BDACE0C60C774F4 /* UserIdentityProxyProtocol.swift */,
|
||||
51C454AE59914B551A6D02C0 /* UserProfileProxy.swift */,
|
||||
);
|
||||
path = Users;
|
||||
@ -5717,7 +5727,6 @@
|
||||
children = (
|
||||
8EAF4A49F3ACD8BB8B0D2371 /* ClientSDKMock.swift */,
|
||||
5EFB1D29B0870AFB6A56E9B8 /* IdentityResetHandleSDKMock.swift */,
|
||||
E8DE9D0D480D087D0F676B52 /* UserIdentitySDKMock.swift */,
|
||||
);
|
||||
path = SDK;
|
||||
sourceTree = "<group>";
|
||||
@ -7624,7 +7633,9 @@
|
||||
828EA5009557C2B9DCD4CA0F /* UserDiscoverySection.swift in Sources */,
|
||||
044DD8F80231BC30570F7965 /* UserDiscoveryService.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 */,
|
||||
C4E0D03DF88242697545A9B7 /* UserIndicatorController.swift in Sources */,
|
||||
3467FEE8210D301FF1B77001 /* UserIndicatorControllerMock.swift in Sources */,
|
||||
@ -7651,6 +7662,7 @@
|
||||
7E91BAC17963ED41208F489B /* UserSessionStore.swift in Sources */,
|
||||
79D57E9AE03A2DC689D14EA2 /* UserSessionStoreMock.swift in Sources */,
|
||||
AC69B6DF15FC451AB2945036 /* UserSessionStoreProtocol.swift in Sources */,
|
||||
2447FADEF13225BB6227B977 /* VerificationBadge.swift in Sources */,
|
||||
5C33976A720B64094CBC56B1 /* VideoMediaEventsTimelineView.swift in Sources */,
|
||||
F07D88421A9BC4D03D4A5055 /* VideoRoomTimelineItem.swift in Sources */,
|
||||
1A83DD22F3E6F76B13B6E2F9 /* VideoRoomTimelineItemContent.swift in Sources */,
|
||||
@ -8509,7 +8521,7 @@
|
||||
repositoryURL = "https://github.com/element-hq/matrix-rust-components-swift";
|
||||
requirement = {
|
||||
kind = exactVersion;
|
||||
version = 25.02.07;
|
||||
version = 25.02.11;
|
||||
};
|
||||
};
|
||||
701C7BEF8F70F7A83E852DCC /* XCRemoteSwiftPackageReference "GZIP" */ = {
|
||||
|
@ -149,8 +149,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/element-hq/matrix-rust-components-swift",
|
||||
"state" : {
|
||||
"revision" : "bc819f09ac66bbe1adc2fde2afeb7ab023d1b909",
|
||||
"version" : "25.2.7"
|
||||
"revision" : "cc010fc6971370d1df2c0eb67cc5cfd577465b62",
|
||||
"version" : "25.2.11"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -889,8 +889,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
}
|
||||
|
||||
private func presentRoomMembersList() {
|
||||
let parameters = RoomMembersListScreenCoordinatorParameters(mediaProvider: userSession.mediaProvider,
|
||||
let parameters = RoomMembersListScreenCoordinatorParameters(clientProxy: userSession.clientProxy,
|
||||
roomProxy: roomProxy,
|
||||
mediaProvider: userSession.mediaProvider,
|
||||
userIndicatorController: userIndicatorController,
|
||||
analytics: analytics)
|
||||
let coordinator = RoomMembersListScreenCoordinator(parameters: parameters)
|
||||
@ -1275,8 +1276,6 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
stateMachine.tryEvent(.startChildFlow(roomID: roomID, via: [], entryPoint: .room))
|
||||
case .startCall(let roomID):
|
||||
Task { await self.presentCallScreen(roomID: roomID) }
|
||||
case .verifyUser(let userID):
|
||||
actionsSubject.send(.verifyUser(userID: userID))
|
||||
case .dismiss:
|
||||
break // Not supported when pushed.
|
||||
}
|
||||
|
@ -923,8 +923,6 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
stateMachine.processEvent(.selectRoom(roomID: roomID, via: [], entryPoint: .room))
|
||||
case .startCall(let roomID):
|
||||
Task { await self.presentCallScreen(roomID: roomID, notifyOtherParticipants: false) }
|
||||
case .verifyUser(let userID):
|
||||
presentSessionVerificationScreen(flow: .userIntiator(userID: userID))
|
||||
case .dismiss:
|
||||
navigationSplitCoordinator.setSheetCoordinator(nil)
|
||||
}
|
||||
|
@ -95,6 +95,6 @@ extension ClientProxyMock {
|
||||
return await .joined(JoinedRoomProxyMock(.init(id: room.id, name: room.name)))
|
||||
}
|
||||
|
||||
userIdentityForReturnValue = .success(UserIdentitySDKMock(configuration: .init()))
|
||||
userIdentityForReturnValue = .success(UserIdentityProxyMock(configuration: .init()))
|
||||
}
|
||||
}
|
||||
|
@ -4860,13 +4860,13 @@ class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable {
|
||||
var userIdentityForReceivedUserID: String?
|
||||
var userIdentityForReceivedInvocations: [String] = []
|
||||
|
||||
var userIdentityForUnderlyingReturnValue: Result<UserIdentity?, ClientProxyError>!
|
||||
var userIdentityForReturnValue: Result<UserIdentity?, ClientProxyError>! {
|
||||
var userIdentityForUnderlyingReturnValue: Result<UserIdentityProxyProtocol?, ClientProxyError>!
|
||||
var userIdentityForReturnValue: Result<UserIdentityProxyProtocol?, ClientProxyError>! {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return userIdentityForUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: Result<UserIdentity?, ClientProxyError>? = nil
|
||||
var returnValue: Result<UserIdentityProxyProtocol?, ClientProxyError>? = nil
|
||||
DispatchQueue.main.sync {
|
||||
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
|
||||
userIdentityForReceivedUserID = userID
|
||||
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 {
|
||||
var window: UIWindow?
|
||||
var alertInfo: AlertInfo<UUID>?
|
||||
|
@ -22876,6 +22876,71 @@ open class UserIdentitySDKMock: MatrixRustSDK.UserIdentity, @unchecked Sendable
|
||||
|
||||
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
|
||||
|
||||
var isVerifiedUnderlyingCallsCount = 0
|
||||
@ -23046,6 +23111,71 @@ open class UserIdentitySDKMock: MatrixRustSDK.UserIdentity, @unchecked Sendable
|
||||
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
|
||||
|
||||
open var withdrawVerificationThrowableError: Error?
|
||||
|
@ -1,21 +1,18 @@
|
||||
//
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// Copyright 2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
extension UserIdentitySDKMock {
|
||||
extension UserIdentityProxyMock {
|
||||
struct Configuration {
|
||||
var isVerified = false
|
||||
var verificationState: UserIdentityVerificationState = .notVerified
|
||||
}
|
||||
|
||||
convenience init(configuration: Configuration) {
|
||||
self.init()
|
||||
|
||||
isVerifiedReturnValue = configuration.isVerified
|
||||
underlyingVerificationState = configuration.verificationState
|
||||
}
|
||||
}
|
37
ElementX/Sources/Other/SwiftUI/VerificationBadge.swift
Normal file
37
ElementX/Sources/Other/SwiftUI/VerificationBadge.swift
Normal 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)
|
||||
}
|
||||
}
|
@ -12,17 +12,25 @@ import SwiftUI
|
||||
struct RoomHeaderView: View {
|
||||
let roomName: String
|
||||
let roomAvatar: RoomAvatar
|
||||
var dmRecipientVerificationState: UserIdentityVerificationState?
|
||||
|
||||
let mediaProvider: MediaProviderProtocol?
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 12) {
|
||||
HStack(spacing: 8) {
|
||||
avatarImage
|
||||
.accessibilityHidden(true)
|
||||
|
||||
HStack(spacing: 4) {
|
||||
Text(roomName)
|
||||
.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.
|
||||
.frame(idealWidth: .greatestFiniteMagnitude, maxWidth: .infinity, alignment: .leading)
|
||||
@ -38,20 +46,23 @@ struct RoomHeaderView: View {
|
||||
|
||||
struct RoomHeaderView_Previews: PreviewProvider, TestablePreview {
|
||||
static var previews: some View {
|
||||
RoomHeaderView(roomName: "Some Room name",
|
||||
roomAvatar: .room(id: "1",
|
||||
name: "Some Room Name",
|
||||
avatarURL: .mockMXCAvatar),
|
||||
mediaProvider: MediaProviderMock(configuration: .init()))
|
||||
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)
|
||||
.padding()
|
||||
}
|
||||
|
||||
static func makeHeader(avatarURL: URL?,
|
||||
verificationState: UserIdentityVerificationState) -> some View {
|
||||
RoomHeaderView(roomName: "Some Room name",
|
||||
roomAvatar: .room(id: "1",
|
||||
name: "Some Room Name",
|
||||
avatarURL: nil),
|
||||
avatarURL: avatarURL),
|
||||
dmRecipientVerificationState: verificationState,
|
||||
mediaProvider: MediaProviderMock(configuration: .init()))
|
||||
.previewLayout(.sizeThatFits)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
@ -39,7 +39,10 @@ struct RoomDetailsScreenViewState: BindableState {
|
||||
|
||||
var topic: AttributedString?
|
||||
var topicSummary: AttributedString?
|
||||
|
||||
var joinedMembersCount: Int
|
||||
var hasMemberIdentityVerificationStateViolations = false
|
||||
|
||||
var isProcessingIgnoreRequest = false
|
||||
var canInviteUsers = false
|
||||
var canEditRoomName = false
|
||||
|
@ -200,6 +200,12 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
|
||||
.throttle(for: .milliseconds(100), scheduler: DispatchQueue.main, latest: true)
|
||||
.weakAssign(to: \.state.knockRequestsCount, on: self)
|
||||
.store(in: &cancellables)
|
||||
|
||||
roomProxy.membersPublisher.combineLatest(roomProxy.identityStatusChangesPublisher)
|
||||
.sink { _ in
|
||||
Task { await self.updateMemberIdentityVerificationStates() }
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private func updateRoomInfo(_ roomInfo: RoomInfoProxy) {
|
||||
@ -239,6 +245,24 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
|
||||
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 {
|
||||
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
|
||||
|
@ -198,13 +198,22 @@ struct RoomDetailsScreen: View {
|
||||
|
||||
private var peopleSection: some View {
|
||||
Section {
|
||||
ListRow(label: .default(title: L10n.commonPeople,
|
||||
icon: \.user),
|
||||
if context.viewState.hasMemberIdentityVerificationStateViolations {
|
||||
ListRow(label: .default(title: L10n.commonPeople, icon: \.user),
|
||||
details: .icon(CompoundIcon(\.infoSolid).foregroundStyle(.compound.iconCriticalPrimary)),
|
||||
kind: .navigationLink {
|
||||
context.send(viewAction: .processTapPeople)
|
||||
})
|
||||
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.people)
|
||||
|
||||
} else {
|
||||
ListRow(label: .default(title: L10n.commonPeople, icon: \.user),
|
||||
details: .title(String(context.viewState.joinedMembersCount)),
|
||||
kind: .navigationLink {
|
||||
context.send(viewAction: .processTapPeople)
|
||||
})
|
||||
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.people)
|
||||
}
|
||||
|
||||
if context.viewState.canSeeKnockingRequests {
|
||||
ListRow(label: .default(title: L10n.screenRoomDetailsRequestsToJoinTitle,
|
||||
|
@ -17,7 +17,7 @@ enum RoomMemberDetailsScreenViewModelAction {
|
||||
struct RoomMemberDetailsScreenViewState: BindableState {
|
||||
let userID: String
|
||||
var memberDetails: RoomMemberDetails?
|
||||
var isVerified: Bool?
|
||||
var verificationState: UserIdentityVerificationState?
|
||||
var isOwnMemberDetails = false
|
||||
var isProcessingIgnoreRequest = false
|
||||
var dmRoomID: String?
|
||||
@ -25,11 +25,15 @@ struct RoomMemberDetailsScreenViewState: BindableState {
|
||||
var bindings: RoomMemberDetailsScreenViewStateBindings
|
||||
|
||||
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 {
|
||||
isVerified == false && !isOwnMemberDetails
|
||||
var showVerifyIdentitySection: Bool {
|
||||
verificationState == .notVerified && !isOwnMemberDetails
|
||||
}
|
||||
|
||||
var showWithdrawVerificationSection: Bool {
|
||||
verificationState == .verificationViolation && !isOwnMemberDetails
|
||||
}
|
||||
}
|
||||
|
||||
@ -90,6 +94,7 @@ enum RoomMemberDetailsScreenViewAction {
|
||||
case createDirectChat
|
||||
case startCall(roomID: String)
|
||||
case verifyUser
|
||||
case withdrawVerification
|
||||
}
|
||||
|
||||
enum RoomMemberDetailsScreenAlertType: Hashable {
|
||||
|
@ -42,10 +42,20 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro
|
||||
super.init(initialViewState: initialViewState, mediaProvider: mediaProvider)
|
||||
|
||||
showMemberLoadingIndicator()
|
||||
|
||||
Task {
|
||||
await loadMember()
|
||||
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
|
||||
@ -77,16 +87,15 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro
|
||||
actionsSubject.send(.startCall(roomID: roomID))
|
||||
case .verifyUser:
|
||||
actionsSubject.send(.verifyUser(userID: state.userID))
|
||||
case .withdrawVerification:
|
||||
Task { await clientProxy.withdrawUserIdentityVerification(state.userID) }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func loadMember() async {
|
||||
async let memberResult = roomProxy.getMember(userID: state.userID)
|
||||
async let identityResult = clientProxy.userIdentity(for: state.userID)
|
||||
|
||||
switch await memberResult {
|
||||
switch await roomProxy.getMember(userID: state.userID) {
|
||||
case .success(let member):
|
||||
roomMemberProxy = member
|
||||
state.memberDetails = RoomMemberDetails(withProxy: member)
|
||||
@ -105,8 +114,8 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro
|
||||
actionsSubject.send(.openUserProfile)
|
||||
}
|
||||
|
||||
if case let .success(.some(identity)) = await identityResult {
|
||||
state.isVerified = identity.isVerified()
|
||||
if case let .success(.some(identity)) = await clientProxy.userIdentity(for: state.userID) {
|
||||
state.verificationState = identity.verificationState
|
||||
} else {
|
||||
MXLog.error("Failed to find the member's identity.")
|
||||
}
|
||||
|
@ -15,7 +15,9 @@ struct RoomMemberDetailsScreen: View {
|
||||
Form {
|
||||
headerSection
|
||||
|
||||
if context.viewState.showVerifyIdentitySection {
|
||||
verificationSection
|
||||
}
|
||||
|
||||
if context.viewState.memberDetails != nil, !context.viewState.isOwnMemberDetails {
|
||||
blockUserSection
|
||||
@ -46,8 +48,15 @@ struct RoomMemberDetailsScreen: View {
|
||||
mediaProvider: context.mediaProvider) { url in
|
||||
context.send(viewAction: .displayAvatar(url))
|
||||
} footer: {
|
||||
VStack(spacing: 24) {
|
||||
if context.viewState.showWithdrawVerificationSection {
|
||||
withdrawVerificationSection
|
||||
}
|
||||
|
||||
otherUserFooter
|
||||
}
|
||||
.padding(.top, 24)
|
||||
}
|
||||
} else {
|
||||
AvatarHeaderView(user: UserProfileProxy(userID: context.viewState.userID),
|
||||
isVerified: context.viewState.showVerifiedBadge,
|
||||
@ -56,6 +65,26 @@ struct RoomMemberDetailsScreen: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var withdrawVerificationSection: some View {
|
||||
VStack(spacing: 16) {
|
||||
if let memberDetails = context.viewState.memberDetails {
|
||||
Text(L10n.cryptoIdentityChangeProfilePinViolation(memberDetails.name ?? memberDetails.id))
|
||||
.foregroundStyle(.compound.textCriticalPrimary)
|
||||
.font(.compound.bodyMDSemibold)
|
||||
} else {
|
||||
Text(L10n.cryptoIdentityChangeProfilePinViolation(context.viewState.userID))
|
||||
.foregroundStyle(.compound.textCriticalPrimary)
|
||||
.font(.compound.bodyMDSemibold)
|
||||
}
|
||||
|
||||
Button(L10n.cryptoIdentityChangeWithdrawVerificationAction) {
|
||||
context.send(viewAction: .withdrawVerification)
|
||||
}
|
||||
.buttonStyle(.compound(.secondary, size: .medium))
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
|
||||
private var otherUserFooter: some View {
|
||||
HStack(spacing: 8) {
|
||||
if context.viewState.memberDetails != nil, !context.viewState.isOwnMemberDetails {
|
||||
@ -84,20 +113,15 @@ struct RoomMemberDetailsScreen: View {
|
||||
.buttonStyle(FormActionButtonStyle(title: L10n.actionShare))
|
||||
}
|
||||
}
|
||||
.padding(.top, 32)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var verificationSection: some View {
|
||||
if context.viewState.showVerificationSection {
|
||||
Section {
|
||||
ListRow(label: .default(title: L10n.commonVerifyUser, icon: \.lock),
|
||||
kind: .button {
|
||||
ListRow(label: .default(title: L10n.commonVerifyUser, icon: \.lock), kind: .button {
|
||||
context.send(viewAction: .verifyUser)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var blockUserSection: some View {
|
||||
@ -138,6 +162,7 @@ struct RoomMemberDetailsScreen: View {
|
||||
|
||||
struct RoomMemberDetailsScreen_Previews: PreviewProvider, TestablePreview {
|
||||
static let verifiedUserViewModel = makeViewModel(member: .mockDan)
|
||||
static let verificationViolationUserViewModel = makeViewModel(member: .mockBob)
|
||||
static let otherUserViewModel = makeViewModel(member: .mockAlice)
|
||||
static let accountOwnerViewModel = makeViewModel(member: .mockMe)
|
||||
static let ignoredUserViewModel = makeViewModel(member: .mockIgnored)
|
||||
@ -145,10 +170,16 @@ struct RoomMemberDetailsScreen_Previews: PreviewProvider, TestablePreview {
|
||||
static var previews: some View {
|
||||
RoomMemberDetailsScreen(context: verifiedUserViewModel.context)
|
||||
.snapshotPreferences(expect: verifiedUserViewModel.context.$viewState.map { state in
|
||||
state.isVerified == true
|
||||
state.verificationState == .verified
|
||||
})
|
||||
.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)
|
||||
.snapshotPreferences(expect: otherUserViewModel.context.$viewState.map { state in
|
||||
state.memberDetails?.role == .user && state.dmRoomID != nil
|
||||
@ -171,12 +202,22 @@ struct RoomMemberDetailsScreen_Previews: PreviewProvider, TestablePreview {
|
||||
static func makeViewModel(member: RoomMemberProxyMock) -> RoomMemberDetailsScreenViewModel {
|
||||
let roomProxyMock = JoinedRoomProxyMock(.init(name: ""))
|
||||
roomProxyMock.getMemberUserIDReturnValue = .success(member)
|
||||
|
||||
let clientProxyMock = ClientProxyMock(.init())
|
||||
|
||||
clientProxyMock.userIdentityForClosure = { userID in
|
||||
let isVerified = userID == RoomMemberProxyMock.mockDan.userID
|
||||
return .success(UserIdentitySDKMock(configuration: .init(isVerified: isVerified)))
|
||||
let identity = switch userID {
|
||||
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
|
||||
if member.userID != RoomMemberProxyMock.mockMe.userID {
|
||||
clientProxyMock.directRoomForUserIDReturnValue = .success("roomID")
|
||||
|
@ -9,8 +9,9 @@ import Combine
|
||||
import SwiftUI
|
||||
|
||||
struct RoomMembersListScreenCoordinatorParameters {
|
||||
let mediaProvider: MediaProviderProtocol
|
||||
let clientProxy: ClientProxyProtocol
|
||||
let roomProxy: JoinedRoomProxyProtocol
|
||||
let mediaProvider: MediaProviderProtocol
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
let analytics: AnalyticsService
|
||||
}
|
||||
@ -31,7 +32,8 @@ final class RoomMembersListScreenCoordinator: CoordinatorProtocol {
|
||||
}
|
||||
|
||||
init(parameters: RoomMembersListScreenCoordinatorParameters) {
|
||||
viewModel = RoomMembersListScreenViewModel(roomProxy: parameters.roomProxy,
|
||||
viewModel = RoomMembersListScreenViewModel(clientProxy: parameters.clientProxy,
|
||||
roomProxy: parameters.roomProxy,
|
||||
mediaProvider: parameters.mediaProvider,
|
||||
userIndicatorController: parameters.userIndicatorController,
|
||||
analytics: parameters.analytics)
|
||||
|
@ -29,10 +29,15 @@ enum RoomMembersListScreenMode {
|
||||
case banned
|
||||
}
|
||||
|
||||
struct RoomMemberListScreenEntry: Equatable {
|
||||
let member: RoomMemberDetails
|
||||
let verificationState: UserIdentityVerificationState
|
||||
}
|
||||
|
||||
struct RoomMembersListScreenViewState: BindableState {
|
||||
private var joinedMembers: [RoomMemberDetails]
|
||||
private var invitedMembers: [RoomMemberDetails]
|
||||
private var bannedMembers: [RoomMemberDetails]
|
||||
private var joinedMembers: [RoomMemberListScreenEntry]
|
||||
private var invitedMembers: [RoomMemberListScreenEntry]
|
||||
private var bannedMembers: [RoomMemberListScreenEntry]
|
||||
|
||||
let joinedMembersCount: Int
|
||||
var bannedMembersCount: Int { bannedMembers.count }
|
||||
@ -44,9 +49,9 @@ struct RoomMembersListScreenViewState: BindableState {
|
||||
var bindings: RoomMembersListScreenViewStateBindings
|
||||
|
||||
init(joinedMembersCount: Int,
|
||||
joinedMembers: [RoomMemberDetails] = [],
|
||||
invitedMembers: [RoomMemberDetails] = [],
|
||||
bannedMembers: [RoomMemberDetails] = [],
|
||||
joinedMembers: [RoomMemberListScreenEntry] = [],
|
||||
invitedMembers: [RoomMemberListScreenEntry] = [],
|
||||
bannedMembers: [RoomMemberListScreenEntry] = [],
|
||||
bindings: RoomMembersListScreenViewStateBindings) {
|
||||
self.joinedMembersCount = joinedMembersCount
|
||||
self.joinedMembers = joinedMembers
|
||||
@ -55,19 +60,19 @@ struct RoomMembersListScreenViewState: BindableState {
|
||||
self.bindings = bindings
|
||||
}
|
||||
|
||||
var visibleJoinedMembers: [RoomMemberDetails] {
|
||||
var visibleJoinedMembers: [RoomMemberListScreenEntry] {
|
||||
joinedMembers
|
||||
.filter { $0.matches(searchQuery: bindings.searchQuery) }
|
||||
.filter { $0.member.matches(searchQuery: bindings.searchQuery) }
|
||||
}
|
||||
|
||||
var visibleInvitedMembers: [RoomMemberDetails] {
|
||||
var visibleInvitedMembers: [RoomMemberListScreenEntry] {
|
||||
invitedMembers
|
||||
.filter { $0.matches(searchQuery: bindings.searchQuery) }
|
||||
.filter { $0.member.matches(searchQuery: bindings.searchQuery) }
|
||||
}
|
||||
|
||||
var visibleBannedMembers: [RoomMemberDetails] {
|
||||
var visibleBannedMembers: [RoomMemberListScreenEntry] {
|
||||
bannedMembers
|
||||
.filter { $0.matches(searchQuery: bindings.searchQuery) }
|
||||
.filter { $0.member.matches(searchQuery: bindings.searchQuery) }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,7 @@ import SwiftUI
|
||||
typealias RoomMembersListScreenViewModelType = StateStoreViewModel<RoomMembersListScreenViewState, RoomMembersListScreenViewAction>
|
||||
|
||||
class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMembersListScreenViewModelProtocol {
|
||||
private let clientProxy: ClientProxyProtocol
|
||||
private let roomProxy: JoinedRoomProxyProtocol
|
||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||
private let analytics: AnalyticsService
|
||||
@ -24,10 +25,12 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
|
||||
}
|
||||
|
||||
init(initialMode: RoomMembersListScreenMode = .members,
|
||||
clientProxy: ClientProxyProtocol,
|
||||
roomProxy: JoinedRoomProxyProtocol,
|
||||
mediaProvider: MediaProviderProtocol,
|
||||
userIndicatorController: UserIndicatorControllerProtocol,
|
||||
analytics: AnalyticsService) {
|
||||
self.clientProxy = clientProxy
|
||||
self.roomProxy = roomProxy
|
||||
self.userIndicatorController = userIndicatorController
|
||||
self.analytics = analytics
|
||||
@ -59,22 +62,23 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
|
||||
}
|
||||
|
||||
func stop() {
|
||||
hideLoader()
|
||||
hideLoadingIndicator(Self.setupMembersLoadingIndicatorIdentifier)
|
||||
hideLoadingIndicator(Self.updateStateLoadingIndicatorIdentifier)
|
||||
}
|
||||
|
||||
// MARK: - Members
|
||||
|
||||
private func setupMembers() {
|
||||
Task {
|
||||
showLoader()
|
||||
showLoadingIndicator(Self.setupMembersLoadingIndicatorIdentifier)
|
||||
await roomProxy.updateMembers()
|
||||
hideLoader()
|
||||
hideLoadingIndicator(Self.setupMembersLoadingIndicatorIdentifier)
|
||||
}
|
||||
|
||||
roomProxy.membersPublisher
|
||||
.filter { !$0.isEmpty }
|
||||
roomProxy.membersPublisher.combineLatest(roomProxy.identityStatusChangesPublisher)
|
||||
.filter { !$0.0.isEmpty }
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] members in
|
||||
.sink { [weak self] members, _ in
|
||||
self?.updateState(members: members)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
@ -87,11 +91,12 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
|
||||
|
||||
private func updateState(members: [RoomMemberProxyProtocol]) {
|
||||
Task {
|
||||
showLoader()
|
||||
showLoadingIndicator(Self.updateStateLoadingIndicatorIdentifier)
|
||||
|
||||
let members = members.sorted()
|
||||
let roomMembersDetails = await buildMembersDetails(members: members)
|
||||
self.members = members
|
||||
|
||||
self.state = .init(joinedMembersCount: roomProxy.infoPublisher.value.joinedMembersCount,
|
||||
joinedMembers: roomMembersDetails.joinedMembers,
|
||||
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.canBanUsers = await (try? roomProxy.canUserBan(userID: roomProxy.ownUserID).get()) == true
|
||||
|
||||
hideLoader()
|
||||
hideLoadingIndicator(Self.updateStateLoadingIndicatorIdentifier)
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
var invitedMembers: [RoomMemberDetails] = .init()
|
||||
var joinedMembers: [RoomMemberDetails] = .init()
|
||||
var bannedMembers: [RoomMemberDetails] = .init()
|
||||
var invitedMembers: [RoomMemberListScreenEntry] = .init()
|
||||
var joinedMembers: [RoomMemberListScreenEntry] = .init()
|
||||
var bannedMembers: [RoomMemberListScreenEntry] = .init()
|
||||
|
||||
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 {
|
||||
case .invite:
|
||||
invitedMembers.append(.init(withProxy: member))
|
||||
invitedMembers.append(.init(member: .init(withProxy: member), verificationState: verificationState))
|
||||
case .join:
|
||||
joinedMembers.append(.init(withProxy: member))
|
||||
joinedMembers.append(.init(member: .init(withProxy: member), verificationState: verificationState))
|
||||
case .ban:
|
||||
bannedMembers.append(.init(withProxy: member))
|
||||
bannedMembers.append(.init(member: .init(withProxy: member), verificationState: verificationState))
|
||||
default:
|
||||
continue
|
||||
}
|
||||
@ -128,7 +140,7 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
|
||||
|
||||
return .init(invitedMembers: invitedMembers,
|
||||
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
|
||||
}
|
||||
@ -215,18 +227,19 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
|
||||
|
||||
// MARK: - Indicators
|
||||
|
||||
private let userIndicatorID = UUID().uuidString
|
||||
private static let setupMembersLoadingIndicatorIdentifier = "\(RoomMembersListScreenViewModel.self)-SetupMembers"
|
||||
private static let updateStateLoadingIndicatorIdentifier = "\(RoomMembersListScreenViewModel.self)-UpdateState"
|
||||
|
||||
private func showLoader() {
|
||||
userIndicatorController.submitIndicator(UserIndicator(id: userIndicatorID,
|
||||
private func showLoadingIndicator(_ identifier: String) {
|
||||
userIndicatorController.submitIndicator(UserIndicator(id: identifier,
|
||||
type: .modal(progress: .indeterminate, interactiveDismissDisabled: false, allowsInteraction: true),
|
||||
title: L10n.commonLoading,
|
||||
persistent: true),
|
||||
delay: .milliseconds(200))
|
||||
}
|
||||
|
||||
private func hideLoader() {
|
||||
userIndicatorController.retractIndicatorWithId(userIndicatorID)
|
||||
private func hideLoadingIndicator(_ identifier: String) {
|
||||
userIndicatorController.retractIndicatorWithId(identifier)
|
||||
}
|
||||
|
||||
private func showManageMemberIndicator(title: String) {
|
||||
@ -247,7 +260,7 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
|
||||
}
|
||||
|
||||
private struct RoomMembersDetails {
|
||||
var invitedMembers: [RoomMemberDetails]
|
||||
var joinedMembers: [RoomMemberDetails]
|
||||
var bannedMembers: [RoomMemberDetails]
|
||||
var invitedMembers: [RoomMemberListScreenEntry]
|
||||
var joinedMembers: [RoomMemberListScreenEntry]
|
||||
var bannedMembers: [RoomMemberListScreenEntry]
|
||||
}
|
||||
|
@ -97,6 +97,7 @@ struct RoomMembersListManageMemberSheetLive_Previews: PreviewProvider {
|
||||
private extension RoomMembersListScreenViewModel {
|
||||
static var mock: RoomMembersListScreenViewModel {
|
||||
RoomMembersListScreenViewModel(initialMode: .members,
|
||||
clientProxy: ClientProxyMock(.init()),
|
||||
roomProxy: JoinedRoomProxyMock(.init(members: .allMembersAsAdmin)),
|
||||
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
|
@ -59,23 +59,23 @@ struct RoomMembersListScreen: View {
|
||||
|
||||
var roomMembers: some View {
|
||||
LazyVStack(alignment: .leading, spacing: 12) {
|
||||
membersSection(data: context.viewState.visibleInvitedMembers, sectionTitle: L10n.screenRoomMemberListPendingHeaderTitle)
|
||||
membersSection(data: context.viewState.visibleJoinedMembers, sectionTitle: L10n.screenRoomMemberListHeaderTitle(Int(context.viewState.joinedMembersCount)))
|
||||
membersSection(entries: context.viewState.visibleInvitedMembers, sectionTitle: L10n.screenRoomMemberListPendingHeaderTitle)
|
||||
membersSection(entries: context.viewState.visibleJoinedMembers, sectionTitle: L10n.screenRoomMemberListHeaderTitle(Int(context.viewState.joinedMembersCount)))
|
||||
}
|
||||
}
|
||||
|
||||
var bannedUsers: some View {
|
||||
LazyVStack(alignment: .leading, spacing: 12) {
|
||||
membersSection(data: context.viewState.visibleBannedMembers)
|
||||
membersSection(entries: context.viewState.visibleBannedMembers)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func membersSection(data: [RoomMemberDetails], sectionTitle: String? = nil) -> some View {
|
||||
if !data.isEmpty {
|
||||
private func membersSection(entries: [RoomMemberListScreenEntry], sectionTitle: String? = nil) -> some View {
|
||||
if !entries.isEmpty {
|
||||
Section {
|
||||
ForEach(data, id: \.id) { member in
|
||||
RoomMembersListScreenMemberCell(member: member, context: context)
|
||||
ForEach(entries, id: \.member.id) { entry in
|
||||
RoomMembersListScreenMemberCell(listEntry: entry, context: context)
|
||||
}
|
||||
} header: {
|
||||
if let sectionTitle {
|
||||
@ -179,7 +179,22 @@ struct RoomMembersListScreen_Previews: PreviewProvider, TestablePreview {
|
||||
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,
|
||||
clientProxy: clientProxyMock,
|
||||
roomProxy: JoinedRoomProxyMock(.init(name: "Some room",
|
||||
members: members,
|
||||
ownUserID: ownUserID,
|
||||
|
@ -8,22 +8,22 @@
|
||||
import SwiftUI
|
||||
|
||||
struct RoomMembersListScreenMemberCell: View {
|
||||
let member: RoomMemberDetails
|
||||
let listEntry: RoomMemberListScreenEntry
|
||||
let context: RoomMembersListScreenViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
context.send(viewAction: .selectMember(member))
|
||||
context.send(viewAction: .selectMember(listEntry.member))
|
||||
} label: {
|
||||
HStack(spacing: 8) {
|
||||
LoadableAvatarImage(url: avatarURL,
|
||||
name: avatarName,
|
||||
contentID: member.id,
|
||||
contentID: listEntry.member.id,
|
||||
avatarSize: .user(on: .roomDetails),
|
||||
mediaProvider: context.mediaProvider)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
HStack(alignment: .firstTextBaseline, spacing: 4) {
|
||||
HStack(alignment: .center, spacing: 4) {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text(title)
|
||||
.font(.compound.bodyMDSemibold)
|
||||
@ -39,6 +39,8 @@ struct RoomMembersListScreenMemberCell: View {
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
VerificationBadge(verificationState: listEntry.verificationState)
|
||||
|
||||
if let role {
|
||||
Text(role)
|
||||
.font(.compound.bodyXS)
|
||||
@ -52,7 +54,7 @@ struct RoomMembersListScreenMemberCell: View {
|
||||
}
|
||||
|
||||
var role: String? {
|
||||
switch member.role {
|
||||
switch listEntry.member.role {
|
||||
case .administrator:
|
||||
L10n.screenRoomMemberListRoleAdministrator
|
||||
case .moderator:
|
||||
@ -65,55 +67,72 @@ struct RoomMembersListScreenMemberCell: View {
|
||||
// Computed properties to hide the user's profile when banned.
|
||||
|
||||
var title: String {
|
||||
guard !member.isBanned else { return member.id }
|
||||
return member.name ?? member.id
|
||||
guard !listEntry.member.isBanned else { return listEntry.member.id }
|
||||
return listEntry.member.name ?? listEntry.member.id
|
||||
}
|
||||
|
||||
var subtitle: String? {
|
||||
member.isBanned ? nil : member.id
|
||||
listEntry.member.isBanned ? nil : listEntry.member.id
|
||||
}
|
||||
|
||||
var avatarName: String? {
|
||||
member.isBanned ? nil : member.name
|
||||
listEntry.member.isBanned ? nil : listEntry.member.name
|
||||
}
|
||||
|
||||
var avatarURL: URL? {
|
||||
member.isBanned ? nil : member.avatarURL
|
||||
listEntry.member.isBanned ? nil : listEntry.member.avatarURL
|
||||
}
|
||||
}
|
||||
|
||||
struct RoomMembersListMemberCell_Previews: PreviewProvider, TestablePreview {
|
||||
static let members: [RoomMemberProxyMock] = [
|
||||
.mockAlice,
|
||||
.mockAdmin,
|
||||
.mockModerator,
|
||||
.init(with: .init(userID: "@nodisplayname:matrix.org", membership: .join)),
|
||||
.init(with: .init(userID: "@avatar:matrix.org", displayName: "Avatar", avatarURL: .mockMXCUserAvatar, membership: .join))
|
||||
static let members: [RoomMemberListScreenEntry] = [
|
||||
.init(member: .init(withProxy: RoomMemberProxyMock.mockAlice),
|
||||
verificationState: .notVerified),
|
||||
.init(member: .init(withProxy: RoomMemberProxyMock.mockAdmin),
|
||||
verificationState: .verified),
|
||||
.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] = [
|
||||
.init(with: .init(userID: "@nodisplayname:matrix.org", membership: .ban)),
|
||||
.init(with: .init(userID: "@fake:matrix.org", displayName: "President", membership: .ban)),
|
||||
.init(with: .init(userID: "@badavatar:matrix.org", avatarURL: .mockMXCUserAvatar, membership: .ban))
|
||||
static let bannedMembers: [RoomMemberListScreenEntry] = [
|
||||
.init(member: .init(withProxy: RoomMemberProxyMock(with: .init(userID: "@nodisplayname:matrix.org",
|
||||
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",
|
||||
members: members)),
|
||||
static let viewModel = RoomMembersListScreenViewModel(clientProxy: ClientProxyMock(.init()),
|
||||
roomProxy: JoinedRoomProxyMock(.init(name: "Some room", members: [])),
|
||||
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
analytics: ServiceLocator.shared.analytics)
|
||||
static var previews: some View {
|
||||
VStack(spacing: 12) {
|
||||
Section("Invited/Joined") {
|
||||
ForEach(members, id: \.userID) { member in
|
||||
RoomMembersListScreenMemberCell(member: .init(withProxy: member), context: viewModel.context)
|
||||
ForEach(members, id: \.member.id) { entry in
|
||||
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.
|
||||
Section("Banned") {
|
||||
ForEach(bannedMembers, id: \.userID) { member in
|
||||
RoomMembersListScreenMemberCell(member: .init(withProxy: member), context: viewModel.context)
|
||||
ForEach(bannedMembers, id: \.member.id) { entry in
|
||||
RoomMembersListScreenMemberCell(listEntry: entry, context: viewModel.context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ enum RoomScreenViewAction {
|
||||
struct RoomScreenViewState: BindableState {
|
||||
var roomTitle = ""
|
||||
var roomAvatar: RoomAvatar
|
||||
var dmRecipientVerificationState: UserIdentityVerificationState?
|
||||
|
||||
var lastScrollDirection: ScrollDirection?
|
||||
// This is used to control the banner
|
||||
|
@ -77,6 +77,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
|
||||
Task {
|
||||
await handleRoomInfoUpdate(roomProxy.infoPublisher.value)
|
||||
|
||||
await updateVerificationBadge()
|
||||
}
|
||||
|
||||
setupSubscriptions(ongoingCallRoomIDPublisher: ongoingCallRoomIDPublisher)
|
||||
@ -182,6 +184,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
}
|
||||
|
||||
await self?.processIdentityStatusChanges(changes)
|
||||
await self?.updateVerificationBadge()
|
||||
}
|
||||
}
|
||||
.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 {
|
||||
defer {
|
||||
hideLoadingIndicator()
|
||||
|
@ -204,6 +204,7 @@ struct RoomScreen: View {
|
||||
ToolbarItem(placement: .principal) {
|
||||
RoomHeaderView(roomName: roomContext.viewState.roomTitle,
|
||||
roomAvatar: roomContext.viewState.roomAvatar,
|
||||
dmRecipientVerificationState: roomContext.viewState.dmRecipientVerificationState,
|
||||
mediaProvider: roomContext.mediaProvider)
|
||||
// Using a button stops it from getting truncated in the navigation bar
|
||||
.contentShape(.rect)
|
||||
|
@ -20,7 +20,6 @@ struct UserProfileScreenCoordinatorParameters {
|
||||
enum UserProfileScreenCoordinatorAction {
|
||||
case openDirectChat(roomID: String)
|
||||
case startCall(roomID: String)
|
||||
case verifyUser(userID: String)
|
||||
case dismiss
|
||||
}
|
||||
|
||||
@ -52,8 +51,6 @@ final class UserProfileScreenCoordinator: CoordinatorProtocol {
|
||||
actionsSubject.send(.openDirectChat(roomID: roomID))
|
||||
case .startCall(let roomID):
|
||||
actionsSubject.send(.startCall(roomID: roomID))
|
||||
case .verifyUser(let userID):
|
||||
actionsSubject.send(.verifyUser(userID: userID))
|
||||
case .dismiss:
|
||||
actionsSubject.send(.dismiss)
|
||||
}
|
||||
|
@ -10,7 +10,6 @@ import Foundation
|
||||
enum UserProfileScreenViewModelAction {
|
||||
case openDirectChat(roomID: String)
|
||||
case startCall(roomID: String)
|
||||
case verifyUser(userID: String)
|
||||
case dismiss
|
||||
}
|
||||
|
||||
@ -29,10 +28,6 @@ struct UserProfileScreenViewState: BindableState {
|
||||
var showVerifiedBadge: Bool {
|
||||
isVerified == true // We purposely show the badge on your own account for consistency with Web.
|
||||
}
|
||||
|
||||
var showVerificationSection: Bool {
|
||||
isVerified == false && !isOwnUser
|
||||
}
|
||||
}
|
||||
|
||||
struct UserProfileScreenViewStateBindings {
|
||||
@ -48,7 +43,6 @@ enum UserProfileScreenViewAction {
|
||||
case openDirectChat
|
||||
case createDirectChat
|
||||
case startCall(roomID: String)
|
||||
case verifyUser
|
||||
case dismiss
|
||||
}
|
||||
|
||||
|
@ -66,8 +66,6 @@ class UserProfileScreenViewModel: UserProfileScreenViewModelType, UserProfileScr
|
||||
Task { await createDirectChat() }
|
||||
case .startCall(let roomID):
|
||||
actionsSubject.send(.startCall(roomID: roomID))
|
||||
case .verifyUser:
|
||||
actionsSubject.send(.verifyUser(userID: state.userID))
|
||||
case .dismiss:
|
||||
actionsSubject.send(.dismiss)
|
||||
}
|
||||
@ -95,7 +93,7 @@ class UserProfileScreenViewModel: UserProfileScreenViewModelType, UserProfileScr
|
||||
}
|
||||
|
||||
if case let .success(.some(identity)) = await identityResult {
|
||||
state.isVerified = identity.isVerified()
|
||||
state.isVerified = identity.verificationState == .verified
|
||||
} else {
|
||||
MXLog.error("Failed to find the user's identity.")
|
||||
}
|
||||
|
@ -14,8 +14,6 @@ struct UserProfileScreen: View {
|
||||
var body: some View {
|
||||
Form {
|
||||
headerSection
|
||||
|
||||
verificationSection
|
||||
}
|
||||
.compoundList()
|
||||
.navigationTitle(L10n.screenRoomMemberDetailsTitle)
|
||||
@ -84,18 +82,6 @@ struct UserProfileScreen: View {
|
||||
.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
|
||||
private var toolbar: some ToolbarContent {
|
||||
if context.viewState.isPresentedModally {
|
||||
@ -137,13 +123,22 @@ struct UserProfileScreen_Previews: PreviewProvider, TestablePreview {
|
||||
|
||||
static func makeViewModel(userID: String) -> UserProfileScreenViewModel {
|
||||
let clientProxyMock = ClientProxyMock(.init())
|
||||
|
||||
clientProxyMock.userIdentityForClosure = { userID in
|
||||
let isVerified = userID == RoomMemberProxyMock.mockDan.userID
|
||||
return .success(UserIdentitySDKMock(configuration: .init(isVerified: isVerified)))
|
||||
let identity = switch userID {
|
||||
case RoomMemberProxyMock.mockDan.userID:
|
||||
UserIdentityProxyMock(configuration: .init(verificationState: .verified))
|
||||
default:
|
||||
UserIdentityProxyMock(configuration: .init())
|
||||
}
|
||||
|
||||
return .success(identity)
|
||||
}
|
||||
|
||||
if userID != RoomMemberProxyMock.mockMe.userID {
|
||||
clientProxyMock.directRoomForUserIDReturnValue = .success("roomID")
|
||||
}
|
||||
|
||||
return UserProfileScreenViewModel(userID: userID,
|
||||
isPresentedModally: false,
|
||||
clientProxy: clientProxyMock,
|
||||
|
@ -1018,9 +1018,9 @@ class ClientProxy: ClientProxyProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
func userIdentity(for userID: String) async -> Result<UserIdentity?, ClientProxyError> {
|
||||
func userIdentity(for userID: String) async -> Result<UserIdentityProxyProtocol?, ClientProxyError> {
|
||||
do {
|
||||
return try await .success(client.encryption().userIdentity(userId: userID))
|
||||
return try await .success(client.encryption().userIdentity(userId: userID).map(UserIdentityProxy.init))
|
||||
} catch {
|
||||
MXLog.error("Failed retrieving user identity: \(error)")
|
||||
return .failure(.sdkError(error))
|
||||
|
@ -191,5 +191,5 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
|
||||
func withdrawUserIdentityVerification(_ userID: String) async -> Result<Void, 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>
|
||||
}
|
||||
|
26
ElementX/Sources/Services/Users/UserIdentityProxy.swift
Normal file
26
ElementX/Sources/Services/Users/UserIdentityProxy.swift
Normal 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
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
//
|
||||
// Copyright 2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
enum UserIdentityVerificationState {
|
||||
case notVerified
|
||||
case verified
|
||||
case verificationViolation
|
||||
}
|
||||
|
||||
// sourcery: AutoMockable
|
||||
protocol UserIdentityProxyProtocol {
|
||||
var verificationState: UserIdentityVerificationState { get }
|
||||
}
|
@ -571,8 +571,9 @@ class MockScreen: Identifiable {
|
||||
case .roomMembersListScreenPendingInvites:
|
||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||
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)),
|
||||
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
analytics: ServiceLocator.shared.analytics))
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
|
@ -1037,6 +1037,12 @@ extension PreviewTests {
|
||||
}
|
||||
}
|
||||
|
||||
func test_verificationBadge() async throws {
|
||||
for preview in VerificationBadge_Previews._allPreviews {
|
||||
try await assertSnapshots(matching: preview)
|
||||
}
|
||||
}
|
||||
|
||||
func test_videoMediaEventsTimelineView() async throws {
|
||||
for preview in VideoMediaEventsTimelineView_Previews._allPreviews {
|
||||
try await assertSnapshots(matching: preview)
|
||||
|
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_joinRoomScreen-iPad-en-GB.Knocked.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_joinRoomScreen-iPad-en-GB.Knocked.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_joinRoomScreen-iPad-pseudo.Knocked.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_joinRoomScreen-iPad-pseudo.Knocked.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_joinRoomScreen-iPhone-16-en-GB.Knocked.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_joinRoomScreen-iPhone-16-en-GB.Knocked.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_joinRoomScreen-iPhone-16-pseudo.Knocked.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_joinRoomScreen-iPhone-16-pseudo.Knocked.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomHeaderView-iPad-en-GB.1.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomHeaderView-iPad-en-GB.1.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomHeaderView-iPad-en-GB.2.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomHeaderView-iPad-en-GB.2.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomHeaderView-iPad-pseudo.1.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomHeaderView-iPad-pseudo.1.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomHeaderView-iPad-pseudo.2.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomHeaderView-iPad-pseudo.2.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomHeaderView-iPhone-16-en-GB.1.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomHeaderView-iPhone-16-en-GB.1.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomHeaderView-iPhone-16-en-GB.2.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomHeaderView-iPhone-16-en-GB.2.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomHeaderView-iPhone-16-pseudo.1.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomHeaderView-iPhone-16-pseudo.1.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomHeaderView-iPhone-16-pseudo.2.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomHeaderView-iPhone-16-pseudo.2.png
(Stored with Git LFS)
Binary file not shown.
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomMemberDetailsScreen-iPad-en-GB.Ignored-User.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomMemberDetailsScreen-iPad-en-GB.Ignored-User.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomMemberDetailsScreen-iPad-en-GB.Other-User.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomMemberDetailsScreen-iPad-en-GB.Other-User.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomMemberDetailsScreen-iPad-en-GB.Verification-Violation-User.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomMemberDetailsScreen-iPad-en-GB.Verification-Violation-User.png
(Stored with Git LFS)
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomMemberDetailsScreen-iPad-pseudo.Other-User.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomMemberDetailsScreen-iPad-pseudo.Other-User.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomMemberDetailsScreen-iPad-pseudo.Verification-Violation-User.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomMemberDetailsScreen-iPad-pseudo.Verification-Violation-User.png
(Stored with Git LFS)
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomMemberDetailsScreen-iPhone-16-en-GB.Verification-Violation-User.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomMemberDetailsScreen-iPhone-16-en-GB.Verification-Violation-User.png
(Stored with Git LFS)
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomMemberDetailsScreen-iPhone-16-pseudo.Verification-Violation-User.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomMemberDetailsScreen-iPhone-16-pseudo.Verification-Violation-User.png
(Stored with Git LFS)
Normal file
Binary file not shown.
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomMembersListMemberCell-iPad-en-GB.1.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomMembersListMemberCell-iPad-en-GB.1.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomMembersListMemberCell-iPad-pseudo.1.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomMembersListMemberCell-iPad-pseudo.1.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomMembersListMemberCell-iPhone-16-en-GB.1.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomMembersListMemberCell-iPhone-16-en-GB.1.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomMembersListMemberCell-iPhone-16-pseudo.1.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomMembersListMemberCell-iPhone-16-pseudo.1.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomMembersListScreen-iPad-en-GB.Admin-Members.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomMembersListScreen-iPad-en-GB.Admin-Members.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomMembersListScreen-iPad-en-GB.Invites.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomMembersListScreen-iPad-en-GB.Invites.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomMembersListScreen-iPad-en-GB.Member.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomMembersListScreen-iPad-en-GB.Member.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomMembersListScreen-iPad-pseudo.Admin-Members.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomMembersListScreen-iPad-pseudo.Admin-Members.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomMembersListScreen-iPad-pseudo.Invites.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomMembersListScreen-iPad-pseudo.Invites.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomMembersListScreen-iPad-pseudo.Member.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomMembersListScreen-iPad-pseudo.Member.png
(Stored with Git LFS)
Binary file not shown.
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomMembersListScreen-iPhone-16-en-GB.Invites.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomMembersListScreen-iPhone-16-en-GB.Invites.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomMembersListScreen-iPhone-16-en-GB.Member.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomMembersListScreen-iPhone-16-en-GB.Member.png
(Stored with Git LFS)
Binary file not shown.
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomMembersListScreen-iPhone-16-pseudo.Invites.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomMembersListScreen-iPhone-16-pseudo.Invites.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomMembersListScreen-iPhone-16-pseudo.Member.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomMembersListScreen-iPhone-16-pseudo.Member.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomScreen-iPad-en-GB.1.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomScreen-iPad-en-GB.1.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomScreen-iPad-pseudo.1.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomScreen-iPad-pseudo.1.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomScreen-iPhone-16-en-GB.1.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomScreen-iPhone-16-en-GB.1.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomScreen-iPhone-16-pseudo.1.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomScreen-iPhone-16-pseudo.1.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineView-iPad-en-GB.1.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineView-iPad-en-GB.1.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineView-iPad-pseudo.1.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineView-iPad-pseudo.1.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineView-iPhone-16-en-GB.1.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineView-iPhone-16-en-GB.1.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineView-iPhone-16-pseudo.1.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineView-iPhone-16-pseudo.1.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_userProfileScreen-iPad-en-GB.Other-User.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_userProfileScreen-iPad-en-GB.Other-User.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_userProfileScreen-iPad-pseudo.Other-User.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_userProfileScreen-iPad-pseudo.Other-User.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_userProfileScreen-iPhone-16-en-GB.Other-User.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_userProfileScreen-iPhone-16-en-GB.Other-User.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_userProfileScreen-iPhone-16-pseudo.Other-User.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_userProfileScreen-iPhone-16-pseudo.Other-User.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_verificationBadge-iPad-en-GB.1.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_verificationBadge-iPad-en-GB.1.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_verificationBadge-iPad-pseudo.1.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_verificationBadge-iPad-pseudo.1.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_verificationBadge-iPhone-16-en-GB.1.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_verificationBadge-iPhone-16-en-GB.1.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_verificationBadge-iPhone-16-pseudo.1.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_verificationBadge-iPhone-16-pseudo.1.png
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -40,8 +40,18 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
|
||||
|
||||
try await deferred.fulfill()
|
||||
|
||||
let sortedMembers: [RoomMemberProxyMock] = [.mockAdmin, .mockModerator, .mockAlice, .mockDan]
|
||||
XCTAssertEqual(viewModel.state.visibleJoinedMembers, sortedMembers.map(RoomMemberDetails.init))
|
||||
let sortedMembers: [RoomMemberListScreenEntry] = [
|
||||
.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 {
|
||||
@ -125,7 +135,7 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
|
||||
|
||||
// When tapping on another user in the list.
|
||||
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.")
|
||||
return
|
||||
}
|
||||
@ -145,7 +155,7 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
|
||||
|
||||
// When tapping on a user in the list.
|
||||
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.")
|
||||
return
|
||||
}
|
||||
@ -166,7 +176,7 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
|
||||
|
||||
// When tapping on a moderator in the list.
|
||||
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.")
|
||||
return
|
||||
}
|
||||
@ -186,7 +196,7 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
|
||||
|
||||
// When tapping on another administrator in the list.
|
||||
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.")
|
||||
return
|
||||
}
|
||||
@ -205,7 +215,7 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
|
||||
|
||||
// When tapping on yourself in the list.
|
||||
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.")
|
||||
return
|
||||
}
|
||||
@ -225,7 +235,7 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
|
||||
|
||||
// When tapping on a banned member in the list.
|
||||
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.")
|
||||
return
|
||||
}
|
||||
@ -242,7 +252,7 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
|
||||
let deferred = deferFulfillment(context.$viewState) { !$0.visibleJoinedMembers.isEmpty }
|
||||
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.
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
@ -255,7 +265,7 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
|
||||
let deferred = deferFulfillment(context.$viewState) { !$0.visibleJoinedMembers.isEmpty }
|
||||
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.
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
@ -268,7 +278,7 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
|
||||
let deferred = deferFulfillment(context.$viewState) { !$0.visibleJoinedMembers.isEmpty }
|
||||
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.
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
@ -278,7 +288,8 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
|
||||
|
||||
private func setup(with members: [RoomMemberProxyMock]) {
|
||||
roomProxy = JoinedRoomProxyMock(.init(name: "test", members: members))
|
||||
viewModel = .init(roomProxy: roomProxy,
|
||||
viewModel = .init(clientProxy: ClientProxyMock(.init()),
|
||||
roomProxy: roomProxy,
|
||||
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
analytics: ServiceLocator.shared.analytics)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user