// // 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) } 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 isGrayscale = false 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)) 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)" updateImages(account: account) 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() } var fieldAccessibilityElements = [Any]() 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 fieldAccessibilityElements.append(nameLabel) fieldAccessibilityElements.append(valueTextView) } accessibilityElements = [ displayNameLabel!, usernameLabel!, noteTextView!, ] + fieldAccessibilityElements + [ moreButton!, pagesSegmentedControl!, ] } 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) if isGrayscale != Preferences.shared.grayscaleImages { updateImages(account: account) } } private func updateImages(account: AccountMO) { isGrayscale = Preferences.shared.grayscaleImages let accountID = account.id let avatarURL = account.avatar // always load original for avatars, because ImageCache.avatars stores them scaled-down in memory avatarRequest = ImageCache.avatars.get(avatarURL, loadOriginal: true) { [weak self] (_, image) in guard let self = self, let image = image, self.accountID == accountID, let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else { return } self.avatarRequest = nil DispatchQueue.main.async { self.avatarImageView.image = transformedImage } } if let header = account.header { headerRequest = ImageCache.headers.get(header) { [weak self] (_, image) in guard let self = self, let image = image, self.accountID == accountID, let transformedImage = ImageGrayscalifier.convertIfNecessary(url: header, image: image) else { return } self.headerRequest = nil DispatchQueue.main.async { self.headerImageView.image = transformedImage } } } } // MARK: Interaction @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) } } 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 } }