User verification state indicators (#3793)

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

* Show a DMs counterpart verification state in the room header

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

* Show verification badges in the room member list

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

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

* Update preview test snapshots
This commit is contained in:
Stefan Ceriu
2025-02-18 08:37:34 +02:00
committed by GitHub
parent b71c93dfaa
commit f77faee981
101 changed files with 722 additions and 305 deletions
@@ -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)