From ad4b2388832e2757cdec88e3f74508ef89b9ec4e Mon Sep 17 00:00:00 2001 From: Justin Mazzocchi <2831158+jzzocc@users.noreply.github.com> Date: Tue, 10 Nov 2020 23:31:56 -0800 Subject: [PATCH] Account relationship and identity proofs --- DB/Sources/DB/Content/AccountRecord.swift | 2 + .../Content/ContentDatabase+Migration.swift | 27 +++++++++++ DB/Sources/DB/Content/ContentDatabase.swift | 47 +++++++++++++++++-- .../DB/Content/IdentityProofRecord.swift | 25 ++++++++++ DB/Sources/DB/Content/ProfileInfo.swift | 23 +++++++++ DB/Sources/DB/Entities/Profile.swift | 24 ++++++++++ .../Extensions/IdentityProof+Extensions.swift | 16 +++++++ .../Extensions/Relationship+Extensions.swift | 24 ++++++++++ .../Mastodon/Entities/IdentityProof.swift | 19 ++++++++ .../Mastodon/Entities/Relationship.swift | 18 +++++++ .../Endpoints/IdentityProofsEndpoint.swift | 31 ++++++++++++ .../Endpoints/RelationshipsEndpoint.swift | 31 ++++++++++++ .../Services/AccountService.swift | 10 +++- .../Services/ProfileService.swift | 25 ++++++++-- View Controllers/ProfileViewController.swift | 8 ++++ .../Sources/ViewModels/AccountViewModel.swift | 2 + .../Sources/ViewModels/ProfileViewModel.swift | 4 ++ Views/AccountFieldView.swift | 24 +++++----- Views/AccountHeaderView.swift | 20 +++++++- 19 files changed, 359 insertions(+), 21 deletions(-) create mode 100644 DB/Sources/DB/Content/IdentityProofRecord.swift create mode 100644 DB/Sources/DB/Content/ProfileInfo.swift create mode 100644 DB/Sources/DB/Entities/Profile.swift create mode 100644 DB/Sources/DB/Extensions/IdentityProof+Extensions.swift create mode 100644 DB/Sources/DB/Extensions/Relationship+Extensions.swift create mode 100644 Mastodon/Sources/Mastodon/Entities/IdentityProof.swift create mode 100644 Mastodon/Sources/Mastodon/Entities/Relationship.swift create mode 100644 MastodonAPI/Sources/MastodonAPI/Endpoints/IdentityProofsEndpoint.swift create mode 100644 MastodonAPI/Sources/MastodonAPI/Endpoints/RelationshipsEndpoint.swift diff --git a/DB/Sources/DB/Content/AccountRecord.swift b/DB/Sources/DB/Content/AccountRecord.swift index 5fd5332..3acba74 100644 --- a/DB/Sources/DB/Content/AccountRecord.swift +++ b/DB/Sources/DB/Content/AccountRecord.swift @@ -54,6 +54,8 @@ extension AccountRecord { extension AccountRecord { static let moved = belongsTo(AccountRecord.self) + static let relationship = hasOne(Relationship.self) + static let identityProofs = hasMany(IdentityProofRecord.self) static let pinnedStatusJoins = hasMany(AccountPinnedStatusJoin.self) .order(AccountPinnedStatusJoin.Columns.index) static let pinnedStatuses = hasMany( diff --git a/DB/Sources/DB/Content/ContentDatabase+Migration.swift b/DB/Sources/DB/Content/ContentDatabase+Migration.swift index 1d8d17c..e4338e8 100644 --- a/DB/Sources/DB/Content/ContentDatabase+Migration.swift +++ b/DB/Sources/DB/Content/ContentDatabase+Migration.swift @@ -30,6 +30,33 @@ extension ContentDatabase { t.column("movedId", .text).references("accountRecord") } + try db.create(table: "relationship") { t in + t.column("id", .text).primaryKey(onConflict: .replace) + .references("accountRecord", onDelete: .cascade) + t.column("following", .boolean).notNull() + t.column("requested", .boolean).notNull() + t.column("endorsed", .boolean).notNull() + t.column("followedBy", .boolean).notNull() + t.column("muting", .boolean).notNull() + t.column("mutingNotifications", .boolean).notNull() + t.column("showingReblogs", .boolean).notNull() + t.column("blocking", .boolean).notNull() + t.column("domainBlocking", .boolean).notNull() + t.column("blockedBy", .boolean).notNull() + t.column("note", .text).notNull() + } + + try db.create(table: "identityProofRecord") { t in + t.column("accountId", .text).notNull().references("accountRecord", onDelete: .cascade) + t.column("provider", .text).notNull() + t.column("providerUsername", .text).notNull() + t.column("profileUrl", .text).notNull() + t.column("proofUrl", .text).notNull() + t.column("updatedAt", .date).notNull() + + t.primaryKey(["accountId", "provider"], onConflict: .replace) + } + try db.create(table: "statusRecord") { t in t.column("id", .text).primaryKey(onConflict: .replace) t.column("uri", .text).notNull() diff --git a/DB/Sources/DB/Content/ContentDatabase.swift b/DB/Sources/DB/Content/ContentDatabase.swift index 00c4bd8..6eeb3d6 100644 --- a/DB/Sources/DB/Content/ContentDatabase.swift +++ b/DB/Sources/DB/Content/ContentDatabase.swift @@ -233,6 +233,39 @@ public extension ContentDatabase { .eraseToAnyPublisher() } + func insert(account: Account) -> AnyPublisher { + databaseWriter.writePublisher(updates: account.save) + .ignoreOutput() + .eraseToAnyPublisher() + } + + func insert(identityProofs: [IdentityProof], id: Account.Id) -> AnyPublisher { + databaseWriter.writePublisher { + for identityProof in identityProofs { + try IdentityProofRecord( + accountId: id, + provider: identityProof.provider, + providerUsername: identityProof.providerUsername, + profileUrl: identityProof.profileUrl, + proofUrl: identityProof.proofUrl, + updatedAt: identityProof.updatedAt) + .save($0) + } + } + .ignoreOutput() + .eraseToAnyPublisher() + } + + func insert(relationships: [Relationship]) -> AnyPublisher { + databaseWriter.writePublisher { + for relationship in relationships { + try relationship.save($0) + } + } + .ignoreOutput() + .eraseToAnyPublisher() + } + func setLists(_ lists: [List]) -> AnyPublisher { databaseWriter.writePublisher { for list in lists { @@ -347,12 +380,20 @@ public extension ContentDatabase { .eraseToAnyPublisher() } - func accountPublisher(id: Account.Id) -> AnyPublisher { - ValueObservation.tracking(AccountInfo.request(AccountRecord.filter(AccountRecord.Columns.id == id)).fetchOne) + func profilePublisher(id: Account.Id) -> AnyPublisher { + ValueObservation.tracking(ProfileInfo.request(AccountRecord.filter(AccountRecord.Columns.id == id)).fetchOne) + .removeDuplicates() + .publisher(in: databaseWriter) + .compactMap { $0 } + .map(Profile.init(info:)) + .eraseToAnyPublisher() + } + + func relationshipPublisher(id: Account.Id) -> AnyPublisher { + ValueObservation.tracking(Relationship.filter(Relationship.Columns.id == id).fetchOne) .removeDuplicates() .publisher(in: databaseWriter) .compactMap { $0 } - .map(Account.init(info:)) .eraseToAnyPublisher() } diff --git a/DB/Sources/DB/Content/IdentityProofRecord.swift b/DB/Sources/DB/Content/IdentityProofRecord.swift new file mode 100644 index 0000000..60bd55c --- /dev/null +++ b/DB/Sources/DB/Content/IdentityProofRecord.swift @@ -0,0 +1,25 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation +import GRDB +import Mastodon + +struct IdentityProofRecord: ContentDatabaseRecord, Hashable { + let accountId: Account.Id + let provider: String + let providerUsername: String + let profileUrl: URL + let proofUrl: URL + let updatedAt: Date +} + +extension IdentityProofRecord { + enum Columns { + static let accountId = Column(IdentityProofRecord.CodingKeys.accountId) + static let provider = Column(IdentityProofRecord.CodingKeys.provider) + static let providerUsername = Column(IdentityProofRecord.CodingKeys.providerUsername) + static let profileUrl = Column(IdentityProofRecord.CodingKeys.profileUrl) + static let proofUrl = Column(IdentityProofRecord.CodingKeys.proofUrl) + static let updatedAt = Column(IdentityProofRecord.CodingKeys.updatedAt) + } +} diff --git a/DB/Sources/DB/Content/ProfileInfo.swift b/DB/Sources/DB/Content/ProfileInfo.swift new file mode 100644 index 0000000..25e6515 --- /dev/null +++ b/DB/Sources/DB/Content/ProfileInfo.swift @@ -0,0 +1,23 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation +import GRDB +import Mastodon + +struct ProfileInfo: Codable, Hashable, FetchableRecord { + let accountInfo: AccountInfo + let relationship: Relationship? + let identityProofRecords: [IdentityProofRecord] +} + +extension ProfileInfo { + static func addingIncludes(_ request: T) -> T where T.RowDecoder == AccountRecord { + AccountInfo.addingIncludes(request) + .including(optional: AccountRecord.relationship.forKey(CodingKeys.relationship)) + .including(all: AccountRecord.identityProofs.forKey(CodingKeys.identityProofRecords)) + } + + static func request(_ request: QueryInterfaceRequest) -> QueryInterfaceRequest { + addingIncludes(request).asRequest(of: self) + } +} diff --git a/DB/Sources/DB/Entities/Profile.swift b/DB/Sources/DB/Entities/Profile.swift new file mode 100644 index 0000000..927f856 --- /dev/null +++ b/DB/Sources/DB/Entities/Profile.swift @@ -0,0 +1,24 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation +import Mastodon + +public struct Profile: Codable, Hashable { + public let account: Account + public let relationship: Relationship? + public let identityProofs: [IdentityProof] + + public init(account: Account) { + self.account = account + self.relationship = nil + self.identityProofs = [] + } +} + +extension Profile { + init(info: ProfileInfo) { + account = Account(info: info.accountInfo) + relationship = info.relationship + identityProofs = info.identityProofRecords.map(IdentityProof.init(record:)) + } +} diff --git a/DB/Sources/DB/Extensions/IdentityProof+Extensions.swift b/DB/Sources/DB/Extensions/IdentityProof+Extensions.swift new file mode 100644 index 0000000..c849682 --- /dev/null +++ b/DB/Sources/DB/Extensions/IdentityProof+Extensions.swift @@ -0,0 +1,16 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation +import GRDB +import Mastodon + +extension IdentityProof { + init(record: IdentityProofRecord) { + self.init( + provider: record.provider, + providerUsername: record.providerUsername, + profileUrl: record.profileUrl, + proofUrl: record.proofUrl, + updatedAt: record.updatedAt) + } +} diff --git a/DB/Sources/DB/Extensions/Relationship+Extensions.swift b/DB/Sources/DB/Extensions/Relationship+Extensions.swift new file mode 100644 index 0000000..5e8c0cd --- /dev/null +++ b/DB/Sources/DB/Extensions/Relationship+Extensions.swift @@ -0,0 +1,24 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation +import GRDB +import Mastodon + +extension Relationship: ContentDatabaseRecord {} + +extension Relationship { + enum Columns: String, ColumnExpression { + case id + case following + case requested + case endorsed + case followedBy + case muting + case mutingNotifications + case showingReblogs + case blocking + case domainBlocking + case blockedBy + case note + } +} diff --git a/Mastodon/Sources/Mastodon/Entities/IdentityProof.swift b/Mastodon/Sources/Mastodon/Entities/IdentityProof.swift new file mode 100644 index 0000000..d6c071b --- /dev/null +++ b/Mastodon/Sources/Mastodon/Entities/IdentityProof.swift @@ -0,0 +1,19 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation + +public struct IdentityProof: Codable, Hashable { + public let provider: String + public let providerUsername: String + public let profileUrl: URL + public let proofUrl: URL + public let updatedAt: Date + + public init(provider: String, providerUsername: String, profileUrl: URL, proofUrl: URL, updatedAt: Date) { + self.provider = provider + self.providerUsername = providerUsername + self.profileUrl = profileUrl + self.proofUrl = proofUrl + self.updatedAt = updatedAt + } +} diff --git a/Mastodon/Sources/Mastodon/Entities/Relationship.swift b/Mastodon/Sources/Mastodon/Entities/Relationship.swift new file mode 100644 index 0000000..b8747b0 --- /dev/null +++ b/Mastodon/Sources/Mastodon/Entities/Relationship.swift @@ -0,0 +1,18 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation + +public struct Relationship: Codable, Hashable { + public let id: Account.Id + public let following: Bool + public let requested: Bool + @DecodableDefault.False public private(set) var endorsed: Bool + public let followedBy: Bool + public let muting: Bool + @DecodableDefault.False public private(set) var mutingNotifications: Bool + @DecodableDefault.False public private(set) var showingReblogs: Bool + public let blocking: Bool + public let domainBlocking: Bool + @DecodableDefault.False public private(set) var blockedBy: Bool + @DecodableDefault.EmptyString public private(set) var note: String +} diff --git a/MastodonAPI/Sources/MastodonAPI/Endpoints/IdentityProofsEndpoint.swift b/MastodonAPI/Sources/MastodonAPI/Endpoints/IdentityProofsEndpoint.swift new file mode 100644 index 0000000..557e1e7 --- /dev/null +++ b/MastodonAPI/Sources/MastodonAPI/Endpoints/IdentityProofsEndpoint.swift @@ -0,0 +1,31 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation +import HTTP +import Mastodon + +public enum IdentityProofsEndpoint { + case identityProofs(id: Account.Id) +} + +extension IdentityProofsEndpoint: Endpoint { + public typealias ResultType = [IdentityProof] + + public var context: [String] { + defaultContext + ["accounts"] + } + + public var pathComponentsInContext: [String] { + switch self { + case let .identityProofs(id): + return [id, "identity_proofs"] + } + } + + public var method: HTTPMethod { + switch self { + case .identityProofs: + return .get + } + } +} diff --git a/MastodonAPI/Sources/MastodonAPI/Endpoints/RelationshipsEndpoint.swift b/MastodonAPI/Sources/MastodonAPI/Endpoints/RelationshipsEndpoint.swift new file mode 100644 index 0000000..1b2a9c1 --- /dev/null +++ b/MastodonAPI/Sources/MastodonAPI/Endpoints/RelationshipsEndpoint.swift @@ -0,0 +1,31 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation +import HTTP +import Mastodon + +public enum RelationshipsEndpoint { + case relationships(ids: [Account.Id]) +} + +extension RelationshipsEndpoint: Endpoint { + public typealias ResultType = [Relationship] + + public var pathComponentsInContext: [String] { + ["accounts", "relationships"] + } + + public var queryParameters: [URLQueryItem] { + switch self { + case let .relationships(ids): + return ids.map { URLQueryItem(name: "id[]", value: $0) } + } + } + + public var method: HTTPMethod { + switch self { + case .relationships: + return .get + } + } +} diff --git a/ServiceLayer/Sources/ServiceLayer/Services/AccountService.swift b/ServiceLayer/Sources/ServiceLayer/Services/AccountService.swift index 6792e0f..8e39f41 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/AccountService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/AccountService.swift @@ -8,13 +8,21 @@ import MastodonAPI public struct AccountService { public let account: Account + public let relationship: Relationship? + public let identityProofs: [IdentityProof] public let navigationService: NavigationService private let mastodonAPIClient: MastodonAPIClient private let contentDatabase: ContentDatabase - init(account: Account, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) { + init(account: Account, + relationship: Relationship? = nil, + identityProofs: [IdentityProof] = [], + mastodonAPIClient: MastodonAPIClient, + contentDatabase: ContentDatabase) { self.account = account + self.relationship = relationship + self.identityProofs = identityProofs navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) self.mastodonAPIClient = mastodonAPIClient self.contentDatabase = contentDatabase diff --git a/ServiceLayer/Sources/ServiceLayer/Services/ProfileService.swift b/ServiceLayer/Sources/ServiceLayer/Services/ProfileService.swift index 41ce6ff..d3a7da1 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/ProfileService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/ProfileService.swift @@ -34,17 +34,24 @@ public struct ProfileService { self.mastodonAPIClient = mastodonAPIClient self.contentDatabase = contentDatabase - var accountPublisher = contentDatabase.accountPublisher(id: id) + var accountPublisher = contentDatabase.profilePublisher(id: id) if let account = account { accountPublisher = accountPublisher - .merge(with: Just(account).setFailureType(to: Error.self)) + .merge(with: Just(Profile(account: account)).setFailureType(to: Error.self)) .removeDuplicates() .eraseToAnyPublisher() } accountServicePublisher = accountPublisher - .map { AccountService(account: $0, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) } + .map { + AccountService( + account: $0.account, + relationship: $0.relationship, + identityProofs: $0.identityProofs, + mastodonAPIClient: mastodonAPIClient, + contentDatabase: contentDatabase) + } .eraseToAnyPublisher() } } @@ -57,6 +64,18 @@ public extension ProfileService { contentDatabase: contentDatabase) } + func fetchProfile() -> AnyPublisher { + Publishers.Merge3( + mastodonAPIClient.request(AccountEndpoint.accounts(id: id)) + .flatMap { contentDatabase.insert(account: $0) }, + mastodonAPIClient.request(RelationshipsEndpoint.relationships(ids: [id])) + .flatMap { contentDatabase.insert(relationships: $0) }, + mastodonAPIClient.request(IdentityProofsEndpoint.identityProofs(id: id)) + .catch { _ in Empty() } + .flatMap { contentDatabase.insert(identityProofs: $0, id: id) }) + .eraseToAnyPublisher() + } + func fetchPinnedStatuses() -> AnyPublisher { mastodonAPIClient.request( StatusesEndpoint.accountsStatuses( diff --git a/View Controllers/ProfileViewController.swift b/View Controllers/ProfileViewController.swift index 88b4f25..a1f03a7 100644 --- a/View Controllers/ProfileViewController.swift +++ b/View Controllers/ProfileViewController.swift @@ -45,4 +45,12 @@ final class ProfileViewController: TableViewController { tableView.tableHeaderView = accountHeaderView } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + viewModel.fetchProfile() + .sink { _ in } + .store(in: &cancellables) + } } diff --git a/ViewModels/Sources/ViewModels/AccountViewModel.swift b/ViewModels/Sources/ViewModels/AccountViewModel.swift index 8c01244..12dc13c 100644 --- a/ViewModels/Sources/ViewModels/AccountViewModel.swift +++ b/ViewModels/Sources/ViewModels/AccountViewModel.swift @@ -36,6 +36,8 @@ public extension AccountViewModel { var isLocked: Bool { accountService.account.locked } + var identityProofs: [IdentityProof] { accountService.identityProofs } + var fields: [Account.Field] { accountService.account.fields } var note: NSAttributedString { accountService.account.note.attributed } diff --git a/ViewModels/Sources/ViewModels/ProfileViewModel.swift b/ViewModels/Sources/ViewModels/ProfileViewModel.swift index 86d1440..4081c78 100644 --- a/ViewModels/Sources/ViewModels/ProfileViewModel.swift +++ b/ViewModels/Sources/ViewModels/ProfileViewModel.swift @@ -55,6 +55,10 @@ public extension ProfileViewModel { imagePresentationsSubject.send(accountViewModel.avatarURL(profile: true)) } + + func fetchProfile() -> AnyPublisher { + profileService.fetchProfile().assignErrorsToAlertItem(to: \.alertItem, on: self) + } } extension ProfileViewModel: CollectionViewModel { diff --git a/Views/AccountFieldView.swift b/Views/AccountFieldView.swift index a4cb387..9578e0a 100644 --- a/Views/AccountFieldView.swift +++ b/Views/AccountFieldView.swift @@ -8,11 +8,9 @@ final class AccountFieldView: UIView { let valueTextView = TouchFallthroughTextView() // swiftlint:disable:next function_body_length - init(field: Account.Field, emoji: [Emoji]) { + init(name: String, value: NSAttributedString, verifiedAt: Date?, emoji: [Emoji]) { super.init(frame: .zero) - let verified = field.verifiedAt != nil - backgroundColor = .systemBackground let nameBackgroundView = UIView() @@ -25,9 +23,9 @@ final class AccountFieldView: UIView { addSubview(valueBackgroundView) valueBackgroundView.translatesAutoresizingMaskIntoConstraints = false - valueBackgroundView.backgroundColor = verified - ? UIColor.systemGreen.withAlphaComponent(0.25) - : .systemBackground + valueBackgroundView.backgroundColor = verifiedAt == nil + ? .systemBackground + : UIColor.systemGreen.withAlphaComponent(0.25) addSubview(nameLabel) nameLabel.translatesAutoresizingMaskIntoConstraints = false @@ -36,7 +34,7 @@ final class AccountFieldView: UIView { nameLabel.textAlignment = .center nameLabel.textColor = .secondaryLabel - let mutableName = NSMutableAttributedString(string: field.name) + let mutableName = NSMutableAttributedString(string: name) mutableName.insert(emoji: emoji, view: nameLabel) mutableName.resizeAttachments(toLineHeight: nameLabel.font.lineHeight) @@ -53,14 +51,14 @@ final class AccountFieldView: UIView { valueTextView.isScrollEnabled = false valueTextView.backgroundColor = .clear - if verified { + if verifiedAt != nil { valueTextView.linkTextAttributes = [ .foregroundColor: UIColor.systemGreen as Any, .underlineColor: UIColor.clear] } - let valueFont = UIFont.preferredFont(forTextStyle: verified ? .headline : .body) - let mutableValue = NSMutableAttributedString(attributedString: field.value.attributed) + let valueFont = UIFont.preferredFont(forTextStyle: verifiedAt == nil ? .body : .headline) + let mutableValue = NSMutableAttributedString(attributedString: value) let valueRange = NSRange(location: 0, length: mutableValue.length) mutableValue.removeAttribute(.font, range: valueRange) @@ -85,10 +83,10 @@ final class AccountFieldView: UIView { addSubview(checkButton) checkButton.translatesAutoresizingMaskIntoConstraints = false checkButton.tintColor = .systemGreen - checkButton.isHidden = !verified + checkButton.isHidden = verifiedAt == nil checkButton.showsMenuAsPrimaryAction = true - if let verifiedAt = field.verifiedAt { + if let verifiedAt = verifiedAt { checkButton.menu = UIMenu( title: String.localizedStringWithFormat( NSLocalizedString("account.field.verified", comment: ""), @@ -118,7 +116,7 @@ final class AccountFieldView: UIView { dividerView.widthAnchor.constraint(equalToConstant: .hairline), checkButton.leadingAnchor.constraint(equalTo: dividerView.trailingAnchor, constant: .defaultSpacing), valueTextView.leadingAnchor.constraint( - equalTo: verified ? checkButton.trailingAnchor : dividerView.trailingAnchor, + equalTo: verifiedAt == nil ? dividerView.trailingAnchor : checkButton.trailingAnchor, constant: .defaultSpacing), valueTextView.topAnchor.constraint(greaterThanOrEqualTo: topAnchor, constant: .defaultSpacing), valueTextView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -.defaultSpacing), diff --git a/Views/AccountHeaderView.swift b/Views/AccountHeaderView.swift index e8cf292..159a5ce 100644 --- a/Views/AccountHeaderView.swift +++ b/Views/AccountHeaderView.swift @@ -43,8 +43,26 @@ final class AccountHeaderView: UIView { view.removeFromSuperview() } + for identityProof in accountViewModel.identityProofs { + let fieldView = AccountFieldView( + name: identityProof.provider, + value: NSAttributedString( + string: identityProof.providerUsername, + attributes: [.link: identityProof.profileUrl]), + verifiedAt: identityProof.updatedAt, + emoji: []) + + fieldView.valueTextView.delegate = self + + fieldsStackView.addArrangedSubview(fieldView) + } + for field in accountViewModel.fields { - let fieldView = AccountFieldView(field: field, emoji: accountViewModel.emoji) + let fieldView = AccountFieldView( + name: field.name, + value: field.value.attributed, + verifiedAt: field.verifiedAt, + emoji: accountViewModel.emoji) fieldView.valueTextView.delegate = self