// // ProfileHeaderView.swift // Tusker // // Created by Shadowfacts on 7/4/20. // Copyright © 2020 Shadowfacts. All rights reserved. // import UIKit import Pachyderm import Combine protocol ProfileHeaderViewDelegate: TuskerNavigationDelegate { func profileHeader(_ headerView: ProfileHeaderView, selectedPostsIndexChangedTo newIndex: Int) func profileHeader(_ headerView: ProfileHeaderView, showMoreOptionsFor accountID: String, sourceView: UIView) } class ProfileHeaderView: UIView { static func create() -> ProfileHeaderView { let nib = UINib(nibName: "ProfileHeaderView", bundle: .main) return nib.instantiate(withOwner: nil, options: nil).first as! ProfileHeaderView } weak var delegate: ProfileHeaderViewDelegate? { didSet { createObservers() } } var mastodonController: MastodonController! { delegate?.apiController } @IBOutlet weak var headerImageView: UIImageView! @IBOutlet weak var avatarContainerView: UIView! @IBOutlet weak var avatarImageView: UIImageView! @IBOutlet weak var moreButton: VisualEffectImageButton! @IBOutlet weak var displayNameLabel: EmojiLabel! @IBOutlet weak var usernameLabel: UILabel! @IBOutlet weak var followsYouLabel: UILabel! @IBOutlet weak var noteTextView: StatusContentTextView! @IBOutlet weak var fieldsStackView: UIStackView! @IBOutlet weak var fieldNamesStackView: UIStackView! @IBOutlet weak var fieldValuesStackView: UIStackView! @IBOutlet weak var pagesSegmentedControl: UISegmentedControl! var accountID: String! private var avatarRequest: ImageCache.Request? private var headerRequest: ImageCache.Request? private var cancellables = [AnyCancellable]() deinit { avatarRequest?.cancel() headerRequest?.cancel() } override func awakeFromNib() { super.awakeFromNib() avatarContainerView.layer.masksToBounds = true avatarImageView.layer.masksToBounds = true avatarImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(avatarPressed))) avatarImageView.isUserInteractionEnabled = true headerImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(headerPressed))) headerImageView.isUserInteractionEnabled = true moreButton.layer.cornerRadius = 16 moreButton.layer.masksToBounds = true NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil) moreButton.addInteraction(UIPointerInteraction(delegate: self)) if #available(iOS 14.0, *) { moreButton.showsMenuAsPrimaryAction = true moreButton.isContextMenuInteractionEnabled = true } } private func createObservers() { cancellables = [] mastodonController.persistentContainer.accountSubject .filter { [weak self] in $0 == self?.accountID } .receive(on: DispatchQueue.main) .sink { [weak self] in self?.updateUI(for: $0) } .store(in: &cancellables) mastodonController.persistentContainer.relationshipSubject .filter { [weak self] in $0 == self?.accountID } .receive(on: DispatchQueue.main) .sink { [weak self] (_) in self?.updateRelationship() } .store(in: &cancellables) } func updateUI(for accountID: String) { self.accountID = accountID guard let mastodonController = mastodonController else { return } guard let account = mastodonController.persistentContainer.account(for: accountID) else { fatalError("Missing cached account \(accountID)") } updateUIForPreferences() usernameLabel.text = "@\(account.acct)" avatarRequest = ImageCache.avatars.get(account.avatar) { [weak self] (data) in guard let self = self, let data = data, self.accountID == accountID else { return } self.avatarRequest = nil DispatchQueue.main.async { self.avatarImageView.image = UIImage(data: data) } } if let header = account.header { headerRequest = ImageCache.headers.get(header) { [weak self] (data) in guard let self = self, let data = data, self.accountID == accountID else { return } self.headerRequest = nil DispatchQueue.main.async { self.headerImageView.image = UIImage(data: data) } } } if #available(iOS 14.0, *) { moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: actionsForProfile(accountID: accountID, sourceView: moreButton)) } noteTextView.navigationDelegate = delegate noteTextView.setTextFromHtml(account.note) noteTextView.setEmojis(account.emojis) // don't show relationship label for the user's own account if accountID != mastodonController.account?.id { // while fetching the most up-to-date, show the current data (if any) updateRelationship() let request = Client.getRelationships(accounts: [accountID]) mastodonController.run(request) { [weak self] (response) in guard let self = self, case let .success(results, _) = response, let relationship = results.first else { return } self.mastodonController.persistentContainer.addOrUpdate(relationship: relationship) } } fieldsStackView.isHidden = account.fields.isEmpty fieldNamesStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } fieldValuesStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } for field in account.fields { let nameLabel = EmojiLabel() nameLabel.text = field.name nameLabel.font = .boldSystemFont(ofSize: 17) nameLabel.textAlignment = .right nameLabel.numberOfLines = 0 nameLabel.lineBreakMode = .byWordWrapping nameLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) nameLabel.setEmojis(account.emojis, identifier: "") fieldNamesStackView.addArrangedSubview(nameLabel) let valueTextView = ContentTextView() valueTextView.isSelectable = false valueTextView.font = .systemFont(ofSize: 17) valueTextView.setTextFromHtml(field.value) valueTextView.setEmojis(account.emojis) valueTextView.textAlignment = .left valueTextView.awakeFromNib() valueTextView.navigationDelegate = delegate valueTextView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) fieldValuesStackView.addArrangedSubview(valueTextView) nameLabel.heightAnchor.constraint(equalTo: valueTextView.heightAnchor).isActive = true } } private func updateRelationship() { guard let relationship = mastodonController.persistentContainer.relationship(forAccount: accountID) else { return } followsYouLabel.isHidden = !relationship.followedBy } @objc private func updateUIForPreferences() { guard let account = mastodonController.persistentContainer.account(for: accountID) else { fatalError("Missing cached account \(accountID!)") } avatarContainerView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarContainerView) avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView) displayNameLabel.updateForAccountDisplayName(account: account) } // MARK: Interaction @IBAction func morePressed(_ sender: Any) { guard #available(iOS 14.0, *) else { // can't use TuskerNavigationDelegate method, because it doesn't know about the (un)follow activity delegate?.profileHeader(self, showMoreOptionsFor: accountID, sourceView: moreButton) return } } @objc func avatarPressed() { guard let account = mastodonController.persistentContainer.account(for: accountID) else { return } delegate?.showLoadingLargeImage(url: account.avatar, cache: .avatars, description: nil, animatingFrom: avatarImageView) } @objc func headerPressed() { guard let account = mastodonController.persistentContainer.account(for: accountID), let header = account.header else { return } delegate?.showLoadingLargeImage(url: header, cache: .headers, description: nil, animatingFrom: headerImageView) } @IBAction func postsSegmentedControlChanged(_ sender: UISegmentedControl) { delegate?.profileHeader(self, selectedPostsIndexChangedTo: sender.selectedSegmentIndex) } } @available(iOS 13.4, *) extension ProfileHeaderView: UIPointerInteractionDelegate { func pointerInteraction(_ interaction: UIPointerInteraction, styleFor region: UIPointerRegion) -> UIPointerStyle? { let preview = UITargetedPreview(view: moreButton) return UIPointerStyle(effect: .lift(preview), shape: .none) } } extension ProfileHeaderView: MenuPreviewProvider { var navigationDelegate: TuskerNavigationDelegate? { delegate } }