From 645542c581e8f7d23ec91841087bfce460b6958c Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Thu, 20 Apr 2023 16:28:30 +0200 Subject: [PATCH] Begin implementing verified link in UserView (IOS-140) --- .../View/Content/UserView+Configuration.swift | 13 ++++ .../Extension/NSAttributedString+Format.swift | 16 ++++ .../View/Content/UserView+ViewModel.swift | 42 +++++++++++ .../MastodonUI/View/Content/UserView.swift | 75 ++++++++++++++++++- 4 files changed, 142 insertions(+), 4 deletions(-) create mode 100644 MastodonSDK/Sources/MastodonUI/Extension/NSAttributedString+Format.swift diff --git a/Mastodon/Scene/Share/View/Content/UserView+Configuration.swift b/Mastodon/Scene/Share/View/Content/UserView+Configuration.swift index 2a2130406..2e5e9e925 100644 --- a/Mastodon/Scene/Share/View/Content/UserView+Configuration.swift +++ b/Mastodon/Scene/Share/View/Content/UserView+Configuration.swift @@ -46,5 +46,18 @@ extension UserView { .map { $0 as String? } .assign(to: \.authorUsername, on: viewModel) .store(in: &disposeBag) + + user.publisher(for: \.followersCount) + .map { Int($0) } + .assign(to: \.authorFollowers, on: viewModel) + .store(in: &disposeBag) + + user.publisher(for: \.fields) + .map { fields in + let firstVerified = fields.first(where: { $0.verifiedAt != nil }) + return firstVerified?.value + } + .assign(to: \.authorVerifiedLink, on: viewModel) + .store(in: &disposeBag) } } diff --git a/MastodonSDK/Sources/MastodonUI/Extension/NSAttributedString+Format.swift b/MastodonSDK/Sources/MastodonUI/Extension/NSAttributedString+Format.swift new file mode 100644 index 000000000..9eed1a595 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Extension/NSAttributedString+Format.swift @@ -0,0 +1,16 @@ +// Copyright © 2023 Mastodon gGmbH. All rights reserved. + +import Foundation + +public extension NSAttributedString { + convenience init(format: NSAttributedString, args: NSAttributedString...) { + let mutableNSAttributedString = NSMutableAttributedString(attributedString: format) + + args.forEach { attributedString in + let range = NSString(string: mutableNSAttributedString.string).range(of: "%@") + mutableNSAttributedString.replaceCharacters(in: range, with: attributedString) + } + + self.init(attributedString: mutableNSAttributedString) + } +} diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/UserView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/UserView+ViewModel.swift index a5fc97cbb..c70ca4588 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/UserView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/UserView+ViewModel.swift @@ -10,6 +10,8 @@ import UIKit import Combine import MetaTextKit import MastodonCore +import MastodonMeta +import MastodonAsset extension UserView { public final class ViewModel: ObservableObject { @@ -22,6 +24,8 @@ extension UserView { @Published public var authorAvatarImageURL: URL? @Published public var authorName: MetaContent? @Published public var authorUsername: String? + @Published public var authorFollowers: Int? + @Published public var authorVerifiedLink: String? } } @@ -74,5 +78,43 @@ extension UserView.ViewModel { } } .store(in: &disposeBag) + + $authorFollowers + .sink { count in + guard let count = count else { + userView.authorFollowersLabel.text = nil + return + } + userView.authorFollowersLabel.attributedText = NSAttributedString( + format: NSAttributedString(string: "%@ followers", attributes: [.font: Font.systemFont(ofSize: 15, weight: .regular)]), + args: NSAttributedString(string: count.formatted(), attributes: [.font: Font.systemFont(ofSize: 15, weight: .bold)]) + ) + } + .store(in: &disposeBag) + + $authorVerifiedLink + .sink { link in + userView.authorVerifiedImageView.image = link == nil ? UIImage(systemName: "questionmark.circle") : UIImage(systemName: "checkmark") + + switch link { + case let .some(link): + userView.authorVerifiedImageView.tintColor = Asset.Colors.brand.color + userView.authorVerifiedLabel.textColor = Asset.Colors.brand.color + do { + let mastodonContent = MastodonContent(content: link, emojis: [:]) + let content = try MastodonMetaContent.convert(document: mastodonContent) + userView.authorVerifiedLabel.configure(content: content) + } catch { + let content = PlaintextMetaContent(string: link) + userView.authorVerifiedLabel.configure(content: content) + } + case .none: + userView.authorVerifiedImageView.tintColor = .secondaryLabel + userView.authorVerifiedLabel.configure(content: PlaintextMetaContent(string: "No verified link")) + userView.authorVerifiedLabel.textColor = .secondaryLabel + } + + } + .store(in: &disposeBag) } } diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift index b033b1222..74ab6f03f 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift @@ -8,6 +8,8 @@ import UIKit import Combine import MetaTextKit +import MastodonAsset +import os public final class UserView: UIView { @@ -38,6 +40,44 @@ public final class UserView: UIView { // author username public let authorUsernameLabel = MetaLabel(style: .statusUsername) + public let authorFollowersLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = .secondaryLabel + return label + }() + + public let authorVerifiedLabel: MetaLabel = { + let label = MetaLabel(style: .profileFieldValue) + label.translatesAutoresizingMaskIntoConstraints = false + label.textAttributes = [ + .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular)), + .foregroundColor: UIColor.secondaryLabel + ] + label.linkAttributes = [ + .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold)), + .foregroundColor: Asset.Colors.brand.color + ] + label.isUserInteractionEnabled = false + return label + }() + + public let authorVerifiedImageView: UIImageView = { + let imageView = UIImageView() + imageView.setContentHuggingPriority(.required, for: .horizontal) + imageView.setContentCompressionResistancePriority(.defaultHigh, for: .vertical) + imageView.setContentHuggingPriority(.required, for: .vertical) + imageView.contentMode = .scaleAspectFit + return imageView + }() + + public let verifiedStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .horizontal + stackView.alignment = .center + return stackView + }() + public func prepareForReuse() { disposeBag.removeAll() @@ -82,11 +122,38 @@ extension UserView { labelStackView.axis = .vertical containerStackView.addArrangedSubview(labelStackView) - labelStackView.addArrangedSubview(authorNameLabel) - labelStackView.addArrangedSubview(authorUsernameLabel) - authorNameLabel.setContentCompressionResistancePriority(.required - 1, for: .vertical) - authorUsernameLabel.setContentCompressionResistancePriority(.required - 1, for: .vertical) + let nameStackView = UIStackView() + nameStackView.axis = .horizontal + let nameSpacer = UIView() + nameSpacer.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + + nameStackView.addArrangedSubview(authorNameLabel) + nameStackView.addArrangedSubview(authorUsernameLabel) + nameStackView.addArrangedSubview(nameSpacer) + + authorNameLabel.setContentCompressionResistancePriority(.required - 1, for: .vertical) + authorNameLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + + authorUsernameLabel.setContentCompressionResistancePriority(.required - 1, for: .vertical) + authorUsernameLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + + labelStackView.addArrangedSubview(nameStackView) + labelStackView.addArrangedSubview(authorFollowersLabel) + + let verifiedSpacerView = UIView() + + NSLayoutConstraint.activate([ + authorVerifiedImageView.widthAnchor.constraint(equalToConstant: 15), + verifiedSpacerView.widthAnchor.constraint(equalToConstant: 2) + ]) + + verifiedStackView.addArrangedSubview(authorVerifiedImageView) + verifiedStackView.addArrangedSubview(verifiedSpacerView) + verifiedStackView.addArrangedSubview(authorVerifiedLabel) + + labelStackView.addArrangedSubview(verifiedStackView) + avatarButton.isUserInteractionEnabled = false authorNameLabel.isUserInteractionEnabled = false authorUsernameLabel.isUserInteractionEnabled = false