// // Copyright 2025 Element Creations Ltd. // Copyright 2024-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 RoomScreenFooterView: View { let details: RoomScreenFooterViewDetails? let mediaProvider: MediaProviderProtocol? let callback: (RoomScreenFooterViewAction) -> Void private var borderColor: Color { switch details { case .pinViolation: .compound.borderInfoSubtle case .verificationViolation: .compound.borderCriticalSubtle case .none: Color.compound.bgCanvasDefault } } private var gradient: Gradient { switch details { case .pinViolation: .compound.info case .verificationViolation: .compound.critical case .none: Gradient(colors: [.clear]) } } var body: some View { if let details { detailsView(details) .highlight(gradient: gradient, borderColor: borderColor) .padding(.top, 8) .fixedSize(horizontal: false, vertical: true) } } @ViewBuilder private func detailsView(_ details: RoomScreenFooterViewDetails) -> some View { switch details { case .pinViolation(let member, let learnMoreURL): pinViolation(member: member, learnMoreURL: learnMoreURL) case .verificationViolation(member: let member, learnMoreURL: let learnMoreURL): verificationViolation(member: member, learnMoreURL: learnMoreURL) } } private func pinViolation(member: RoomMemberProxyProtocol, learnMoreURL: URL) -> some View { VStack(spacing: 16) { HStack(spacing: 16) { LoadableAvatarImage(url: member.avatarURL, name: member.disambiguatedDisplayName, contentID: member.userID, avatarSize: .user(on: .timeline), mediaProvider: mediaProvider) Text(pinViolationDescriptionWithLearnMoreLink(displayName: member.displayName, userID: member.userID, url: learnMoreURL)) .font(.compound.bodyMD) .foregroundColor(.compound.textPrimary) } Button { callback(.resolvePinViolation(userID: member.userID)) } label: { Text(L10n.actionDismiss) .frame(maxWidth: .infinity) } .buttonStyle(.compound(.primary, size: .medium)) } .padding(.top, 16) .padding(.horizontal, 16) .padding(.bottom, 8) } private func verificationViolation(member: RoomMemberProxyProtocol, learnMoreURL: URL) -> some View { VStack(spacing: 16) { HStack(spacing: 16) { LoadableAvatarImage(url: member.avatarURL, name: member.disambiguatedDisplayName, contentID: member.userID, avatarSize: .user(on: .timeline), mediaProvider: mediaProvider) Text(verificationViolationDescriptionWithLearnMoreLink(displayName: member.displayName, userID: member.userID, url: learnMoreURL)) .font(.compound.bodyMD) .foregroundColor(.compound.textCriticalPrimary) } Button { callback(.resolveVerificationViolation(userID: member.userID)) } label: { Text(L10n.cryptoIdentityChangeWithdrawVerificationAction) .frame(maxWidth: .infinity) } .buttonStyle(.compound(.primary, size: .medium)) } .padding(.top, 16) .padding(.horizontal, 16) .padding(.bottom, 8) } private func pinViolationDescriptionWithLearnMoreLink(displayName: String?, userID: String, url: URL) -> AttributedString { let userIDPlaceholder = "{mxid}" let linkPlaceholder = "{link}" let displayName = displayName ?? fallbackDisplayName(userID) var description = AttributedString(L10n.cryptoIdentityChangePinViolationNew(displayName, userIDPlaceholder, linkPlaceholder)) var userIDString = AttributedString(L10n.cryptoIdentityChangePinViolationNewUserId(userID)) userIDString.bold() description.replace(userIDPlaceholder, with: userIDString) var linkString = AttributedString(L10n.actionLearnMore) linkString.link = url linkString.bold() description.replace(linkPlaceholder, with: linkString) return description } private func verificationViolationDescriptionWithLearnMoreLink(displayName: String?, userID: String, url: URL) -> AttributedString { let userIDPlaceholder = "{mxid}" let linkPlaceholder = "{link}" let displayName = displayName ?? fallbackDisplayName(userID) var description = AttributedString(L10n.cryptoIdentityChangeVerificationViolationNew(displayName, userIDPlaceholder, linkPlaceholder)) var userIDString = AttributedString(L10n.cryptoIdentityChangePinViolationNewUserId(userID)) userIDString.bold() description.replace(userIDPlaceholder, with: userIDString) var linkString = AttributedString(L10n.actionLearnMore) linkString.link = url linkString.bold() description.replace(linkPlaceholder, with: linkString) return description } private func fallbackDisplayName(_ userID: String) -> String { guard let localpart = userID.components(separatedBy: ":").first else { return userID } return String(localpart.trimmingPrefix("@")) } } struct RoomScreenFooterView_Previews: PreviewProvider, TestablePreview { static let bobDetails: RoomScreenFooterViewDetails = .pinViolation(member: RoomMemberProxyMock.mockBob, learnMoreURL: "https://element.io/") static let noNameDetails: RoomScreenFooterViewDetails = .pinViolation(member: RoomMemberProxyMock.mockNoName, learnMoreURL: "https://element.io/") static let verificationViolationDetails: RoomScreenFooterViewDetails = .verificationViolation(member: RoomMemberProxyMock.mockBob, learnMoreURL: "https://element.io/") static var previews: some View { RoomScreenFooterView(details: bobDetails, mediaProvider: MediaProviderMock(.init())) { _ in } .previewDisplayName("With displayname") RoomScreenFooterView(details: noNameDetails, mediaProvider: MediaProviderMock(.init())) { _ in } .previewDisplayName("Without displayname") RoomScreenFooterView(details: verificationViolationDetails, mediaProvider: MediaProviderMock(.init())) { _ in } .previewDisplayName("Verification Violation") } }