Tusker/Tusker/Views/Profile Header/ProfileHeaderView.swift

229 lines
9.0 KiB
Swift

//
// 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?
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!
var avatarRequest: ImageCache.Request?
var headerRequest: ImageCache.Request?
private var accountUpdater: Cancellable?
override var frame: CGRect {
didSet {
print("set header frame for \(self)")
}
}
override var bounds: CGRect {
didSet {
print("set header bounds for \(self)")
}
}
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)
if #available(iOS 13.4, *) {
moreButton.addInteraction(UIPointerInteraction(delegate: self))
}
#if SDK_IOS_14
if #available(iOS 14.0, *) {
moreButton.showsMenuAsPrimaryAction = true
moreButton.isContextMenuInteractionEnabled = true
}
#endif
}
func updateUI(for accountID: String) {
self.accountID = accountID
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)
}
}
headerRequest = ImageCache.headers.get(account.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 {
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
}
DispatchQueue.main.async {
self.followsYouLabel.isHidden = !relationship.followedBy
}
}
}
fieldsStackView.isHidden = account.fields.isEmpty
fieldNamesStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
fieldValuesStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
for field in account.fields {
let nameLabel = UILabel()
nameLabel.text = field.name
nameLabel.font = .boldSystemFont(ofSize: 17)
nameLabel.textAlignment = .right
nameLabel.numberOfLines = 0
nameLabel.lineBreakMode = .byWordWrapping
nameLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
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
}
if accountUpdater == nil {
accountUpdater = mastodonController.persistentContainer.accountSubject
.filter { [weak self] in $0 == self?.accountID }
.receive(on: DispatchQueue.main)
.sink { [weak self] in self?.updateUI(for: $0) }
}
}
@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 {
fatalError("Missing cached account \(accountID!)")
}
delegate?.showLoadingLargeImage(url: account.avatar, cache: .avatars, description: nil, animatingFrom: avatarImageView)
}
@objc func headerPressed() {
guard let account = mastodonController.persistentContainer.account(for: accountID) else {
fatalError("Missing cached account \(accountID!)")
}
delegate?.showLoadingLargeImage(url: account.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 }
}