forked from shadowfacts/Tusker
273 lines
11 KiB
Swift
273 lines
11 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)
|
|
}
|
|
|
|
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
|
|
.receive(on: DispatchQueue.main)
|
|
.filter { [weak self] in $0 == self?.accountID }
|
|
.sink { [weak self] in self?.updateUI(for: $0) }
|
|
.store(in: &cancellables)
|
|
|
|
mastodonController.persistentContainer.relationshipSubject
|
|
.receive(on: DispatchQueue.main)
|
|
.filter { [weak self] in $0 == self?.accountID }
|
|
.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() {
|
|
// todo: mastodonController should never be nil, but ProfileHeaderViews are getting leaked
|
|
guard let mastodonController = mastodonController,
|
|
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 {
|
|
DispatchQueue.main.async {
|
|
self?.avatarRequest = nil
|
|
}
|
|
return
|
|
}
|
|
|
|
DispatchQueue.main.async {
|
|
self.avatarRequest = nil
|
|
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 {
|
|
DispatchQueue.main.async {
|
|
self?.headerRequest = nil
|
|
}
|
|
return
|
|
}
|
|
|
|
DispatchQueue.main.async {
|
|
self.headerRequest = nil
|
|
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)
|
|
UIImpactFeedbackGenerator(style: .light).impactOccurred()
|
|
}
|
|
|
|
}
|
|
|
|
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 }
|
|
}
|