// // SuggestedProfileCardCollectionViewCell.swift // Tusker // // Created by Shadowfacts on 1/23/23. // Copyright © 2023 Shadowfacts. All rights reserved. // #if !os(visionOS) 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: AccountDisplayNameLabel! @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 contentView.layer.cornerCurve = .continuous contentView.backgroundColor = .appGroupedCellBackground updateLayerColors() headerImageView.cache = .headers avatarContainerView.layer.masksToBounds = true avatarContainerView.layer.cornerCurve = .continuous avatarImageView.cache = .avatars avatarImageView.layer.masksToBounds = true avatarImageView.layer.cornerCurve = .continuous 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.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.setBodyTextFromHTML(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) } // Unneeded on visionOS since there is no light/dark mode #if !os(visionOS) override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) updateLayerColors() } #endif 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 #if !os(visionOS) let sheetPresentationController = toPresent.sheetPresentationController! sheetPresentationController.detents = [ .medium() ] #endif } 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.withRenderingMode(.alwaysTemplate)) 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() } } } } } #endif