// // SuggestedProfileCardCollectionViewCell.swift // Tusker // // Created by Shadowfacts on 1/23/23. // Copyright © 2023 Shadowfacts. All rights reserved. // import UIKit import Pachyderm import SwiftUI class SuggestedProfileCardCollectionViewCell: UICollectionViewCell { weak var delegate: (any TuskerNavigationDelegate)? private var mastodonController: MastodonController! { delegate?.apiController } private var accountID: String! private var source: Suggestion.Source! @IBOutlet weak var headerImageView: CachedImageView! @IBOutlet weak var avatarContainerView: UIView! @IBOutlet weak var avatarImageView: CachedImageView! @IBOutlet weak var displayNameLabel: EmojiLabel! @IBOutlet weak var usernameLabel: UILabel! @IBOutlet weak var noteTextView: StatusContentTextView! @IBOutlet weak var suggestionSourceButton: UIButton! private var hoverGestureAnimator: UIViewPropertyAnimator? override func awakeFromNib() { super.awakeFromNib() layer.shadowOpacity = 0.2 layer.shadowRadius = 8 layer.shadowOffset = .zero layer.masksToBounds = false contentView.layer.cornerRadius = 12.5 updateLayerColors() headerImageView.cache = .headers avatarContainerView.layer.masksToBounds = true avatarImageView.cache = .avatars avatarImageView.layer.masksToBounds = true displayNameLabel.font = UIFontMetrics(forTextStyle: .title1).scaledFont(for: .systemFont(ofSize: 24, weight: .semibold)) displayNameLabel.adjustsFontForContentSizeCategory = true usernameLabel.font = UIFontMetrics.default.scaledFont(for: .systemFont(ofSize: 15, weight: .light)) usernameLabel.adjustsFontForContentSizeCategory = true noteTextView.defaultFont = .preferredFont(forTextStyle: .body) noteTextView.monospaceFont = UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular)) noteTextView.adjustsFontForContentSizeCategory = true noteTextView.textContainer.lineBreakMode = .byTruncatingTail addGestureRecognizer(UIHoverGestureRecognizer(target: self, action: #selector(hoverRecognized))) NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil) } @objc private func preferencesChanged() { guard let accountID, let mastodonController, let account = mastodonController.persistentContainer.account(for: accountID) else { return } updateUIForPreferences(account: account) } func updateUI(accountID: String, source: Suggestion.Source) { guard self.accountID != accountID, let account = mastodonController.persistentContainer.account(for: accountID) else { return } self.accountID = accountID self.source = source updateUIForPreferences(account: account) avatarImageView.update(for: account.avatar) headerImageView.update(for: account.header) usernameLabel.text = "@\(account.acct)" noteTextView.setTextFromHtml(account.note) var config = UIButton.Configuration.plain() config.image = source.image suggestionSourceButton.configuration = config suggestionSourceButton.setNeedsUpdateConfiguration() } private func updateUIForPreferences(account: AccountMO) { avatarContainerView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarContainerView) avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView) displayNameLabel.updateForAccountDisplayName(account: account) } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) updateLayerColors() } private func updateLayerColors() { if traitCollection.userInterfaceStyle == .dark { layer.shadowColor = UIColor.darkGray.cgColor } else { layer.shadowColor = UIColor.black.cgColor } } // MARK: Interaction @IBAction func suggestionSourceButtonPressed(_ sender: Any) { guard let delegate, let source else { return } let view = SuggestionSourceView(mastodonController: mastodonController, source: source) let host = UIHostingController(rootView: view) let toPresent: UIViewController if traitCollection.horizontalSizeClass == .compact || traitCollection.verticalSizeClass == .compact { toPresent = UINavigationController(rootViewController: host) toPresent.modalPresentationStyle = .pageSheet let sheetPresentationController = toPresent.sheetPresentationController! sheetPresentationController.detents = [ .medium() ] } else { host.modalPresentationStyle = .popover let popoverPresentationController = host.popoverPresentationController! popoverPresentationController.sourceView = suggestionSourceButton host.preferredContentSize = host.sizeThatFits(in: CGSize(width: 400, height: CGFloat.infinity)) toPresent = host } delegate.present(toPresent, animated: true) } @objc private func hoverRecognized(_ recognizer: UIHoverGestureRecognizer) { switch recognizer.state { case .began, .changed: hoverGestureAnimator = UIViewPropertyAnimator(duration: 0.2, curve: .easeInOut, animations: { self.transform = CGAffineTransform(scaleX: 1.05, y: 1.05) }) hoverGestureAnimator!.startAnimation() case .ended: hoverGestureAnimator?.stopAnimation(true) hoverGestureAnimator?.addAnimations { self.transform = .identity } hoverGestureAnimator?.startAnimation() default: break } } } private extension Suggestion.Source { var image: UIImage { switch self { case .global: return UIImage(systemName: "chart.line.uptrend.xyaxis")! case .pastInteractions: return UIImage(systemName: "clock.arrow.circlepath")! case .staff: return UIImage(systemName: "person.2")! } } var title: String { switch self { case .global: return "Popular Recently" case .pastInteractions: return "Past Interactions" case .staff: return "Staff Recommendation" } } } private struct SuggestionSourceView: View { let mastodonController: MastodonController let source: Suggestion.Source @Environment(\.dismiss) private var dismiss var body: some View { VStack(alignment: .leading) { HStack { Image(uiImage: source.image) Text(source.title) Spacer() } switch source { case .global: Text("This account is suggested for you because it has been highly active within the past 30 days.") case .pastInteractions: Text("This account is suggested for you because you have interacted with it before.") case .staff: Text("This account is recommended by the staff of \(mastodonController.accountInfo!.instanceURL.host!)") } } .padding() .navigationTitle("Suggestion Reason") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Done") { dismiss() } } } } }