mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +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:
+55
-14
@@ -15,7 +15,9 @@ struct RoomMemberDetailsScreen: View {
|
||||
Form {
|
||||
headerSection
|
||||
|
||||
verificationSection
|
||||
if context.viewState.showVerifyIdentitySection {
|
||||
verificationSection
|
||||
}
|
||||
|
||||
if context.viewState.memberDetails != nil, !context.viewState.isOwnMemberDetails {
|
||||
blockUserSection
|
||||
@@ -46,7 +48,14 @@ struct RoomMemberDetailsScreen: View {
|
||||
mediaProvider: context.mediaProvider) { url in
|
||||
context.send(viewAction: .displayAvatar(url))
|
||||
} footer: {
|
||||
otherUserFooter
|
||||
VStack(spacing: 24) {
|
||||
if context.viewState.showWithdrawVerificationSection {
|
||||
withdrawVerificationSection
|
||||
}
|
||||
|
||||
otherUserFooter
|
||||
}
|
||||
.padding(.top, 24)
|
||||
}
|
||||
} else {
|
||||
AvatarHeaderView(user: UserProfileProxy(userID: context.viewState.userID),
|
||||
@@ -56,6 +65,26 @@ struct RoomMemberDetailsScreen: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var withdrawVerificationSection: some View {
|
||||
VStack(spacing: 16) {
|
||||
if let memberDetails = context.viewState.memberDetails {
|
||||
Text(L10n.cryptoIdentityChangeProfilePinViolation(memberDetails.name ?? memberDetails.id))
|
||||
.foregroundStyle(.compound.textCriticalPrimary)
|
||||
.font(.compound.bodyMDSemibold)
|
||||
} else {
|
||||
Text(L10n.cryptoIdentityChangeProfilePinViolation(context.viewState.userID))
|
||||
.foregroundStyle(.compound.textCriticalPrimary)
|
||||
.font(.compound.bodyMDSemibold)
|
||||
}
|
||||
|
||||
Button(L10n.cryptoIdentityChangeWithdrawVerificationAction) {
|
||||
context.send(viewAction: .withdrawVerification)
|
||||
}
|
||||
.buttonStyle(.compound(.secondary, size: .medium))
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
|
||||
private var otherUserFooter: some View {
|
||||
HStack(spacing: 8) {
|
||||
if context.viewState.memberDetails != nil, !context.viewState.isOwnMemberDetails {
|
||||
@@ -84,18 +113,13 @@ 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 {
|
||||
context.send(viewAction: .verifyUser)
|
||||
})
|
||||
}
|
||||
Section {
|
||||
ListRow(label: .default(title: L10n.commonVerifyUser, icon: \.lock), kind: .button {
|
||||
context.send(viewAction: .verifyUser)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,9 +170,15 @@ 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
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user