2020-10-25 20:05:28 +00:00
|
|
|
//
|
|
|
|
// StatusCardView.swift
|
|
|
|
// Tusker
|
|
|
|
//
|
|
|
|
// Created by Shadowfacts on 10/25/20.
|
|
|
|
// Copyright © 2020 Shadowfacts. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
import UIKit
|
|
|
|
import Pachyderm
|
|
|
|
import SafariServices
|
2022-02-04 04:11:29 +00:00
|
|
|
import WebURLFoundationExtras
|
2023-01-23 20:15:43 +00:00
|
|
|
import SwiftSoup
|
2020-10-25 20:05:28 +00:00
|
|
|
|
|
|
|
class StatusCardView: UIView {
|
|
|
|
|
|
|
|
weak var navigationDelegate: TuskerNavigationDelegate?
|
2022-05-02 03:04:56 +00:00
|
|
|
weak var actionProvider: MenuActionProvider?
|
2020-10-25 20:05:28 +00:00
|
|
|
|
2022-10-08 19:24:12 +00:00
|
|
|
private var statusID: String?
|
|
|
|
private(set) var card: Card?
|
2020-10-25 20:05:28 +00:00
|
|
|
|
|
|
|
private let activeBackgroundColor = UIColor.secondarySystemFill
|
|
|
|
private let inactiveBackgroundColor = UIColor.secondarySystemBackground
|
|
|
|
|
2020-11-01 18:59:58 +00:00
|
|
|
private var isGrayscale = false
|
2020-10-25 20:05:28 +00:00
|
|
|
|
2023-01-20 16:22:28 +00:00
|
|
|
private var hStack: UIStackView!
|
2023-02-25 20:23:13 +00:00
|
|
|
private var vStack: UIStackView!
|
2020-10-25 20:05:28 +00:00
|
|
|
private var titleLabel: UILabel!
|
|
|
|
private var descriptionLabel: UILabel!
|
2023-01-20 16:22:28 +00:00
|
|
|
private var domainLabel: UILabel!
|
2023-02-24 23:22:27 +00:00
|
|
|
private var imageView: CachedImageView!
|
2020-10-25 20:05:28 +00:00
|
|
|
private var placeholderImageView: UIImageView!
|
2023-02-24 23:22:27 +00:00
|
|
|
private var leadingSpacer: UIView!
|
|
|
|
private var trailingSpacer: UIView!
|
2020-10-25 20:05:28 +00:00
|
|
|
|
|
|
|
override init(frame: CGRect) {
|
|
|
|
super.init(frame: frame)
|
|
|
|
commonInit()
|
|
|
|
}
|
|
|
|
|
|
|
|
required init?(coder: NSCoder) {
|
|
|
|
super.init(coder: coder)
|
|
|
|
commonInit()
|
|
|
|
}
|
|
|
|
|
|
|
|
private func commonInit() {
|
2023-01-20 16:22:28 +00:00
|
|
|
self.layer.shadowColor = UIColor.black.cgColor
|
|
|
|
self.layer.shadowRadius = 5
|
|
|
|
self.layer.shadowOpacity = 0.2
|
|
|
|
self.layer.shadowOffset = .zero
|
2020-10-25 20:05:28 +00:00
|
|
|
|
|
|
|
self.addInteraction(UIContextMenuInteraction(delegate: self))
|
|
|
|
|
|
|
|
titleLabel = UILabel()
|
|
|
|
titleLabel.font = UIFont(descriptor: UIFontDescriptor.preferredFontDescriptor(withTextStyle: .subheadline).withSymbolicTraits(.traitBold)!, size: 0)
|
2022-11-04 02:15:54 +00:00
|
|
|
titleLabel.adjustsFontForContentSizeCategory = true
|
2020-10-25 20:05:28 +00:00
|
|
|
titleLabel.numberOfLines = 2
|
2023-01-20 18:58:40 +00:00
|
|
|
titleLabel.setContentHuggingPriority(.defaultHigh, for: .vertical)
|
2020-10-25 20:05:28 +00:00
|
|
|
|
|
|
|
descriptionLabel = UILabel()
|
|
|
|
descriptionLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .caption1), size: 0)
|
2022-11-04 02:15:54 +00:00
|
|
|
descriptionLabel.adjustsFontForContentSizeCategory = true
|
2023-01-20 18:58:40 +00:00
|
|
|
descriptionLabel.numberOfLines = 3
|
2020-10-25 20:05:28 +00:00
|
|
|
descriptionLabel.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
|
|
|
|
|
2023-01-20 16:22:28 +00:00
|
|
|
domainLabel = UILabel()
|
|
|
|
domainLabel.font = .preferredFont(forTextStyle: .caption2)
|
|
|
|
domainLabel.adjustsFontForContentSizeCategory = true
|
|
|
|
domainLabel.numberOfLines = 1
|
|
|
|
domainLabel.textColor = .tintColor
|
|
|
|
|
2023-02-25 20:23:13 +00:00
|
|
|
vStack = UIStackView(arrangedSubviews: [
|
2020-10-25 20:05:28 +00:00
|
|
|
titleLabel,
|
2023-01-20 16:22:28 +00:00
|
|
|
descriptionLabel,
|
|
|
|
domainLabel,
|
2020-10-25 20:05:28 +00:00
|
|
|
])
|
|
|
|
vStack.axis = .vertical
|
|
|
|
vStack.alignment = .leading
|
|
|
|
vStack.spacing = 0
|
|
|
|
|
2023-02-24 23:22:27 +00:00
|
|
|
imageView = CachedImageView(cache: .attachments)
|
2020-10-25 20:05:28 +00:00
|
|
|
imageView.contentMode = .scaleAspectFill
|
|
|
|
imageView.clipsToBounds = true
|
|
|
|
|
2023-02-24 23:22:27 +00:00
|
|
|
leadingSpacer = UIView()
|
|
|
|
leadingSpacer.backgroundColor = .clear
|
|
|
|
trailingSpacer = UIView()
|
|
|
|
trailingSpacer.backgroundColor = .clear
|
2023-01-20 16:22:28 +00:00
|
|
|
|
|
|
|
hStack = UIStackView(arrangedSubviews: [
|
2023-02-24 23:22:27 +00:00
|
|
|
leadingSpacer,
|
2020-10-25 20:05:28 +00:00
|
|
|
imageView,
|
2023-01-20 16:22:28 +00:00
|
|
|
vStack,
|
2023-02-24 23:22:27 +00:00
|
|
|
trailingSpacer,
|
2020-10-25 20:05:28 +00:00
|
|
|
])
|
|
|
|
hStack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
hStack.axis = .horizontal
|
|
|
|
hStack.alignment = .center
|
|
|
|
hStack.distribution = .fill
|
|
|
|
hStack.spacing = 4
|
2023-01-20 16:22:28 +00:00
|
|
|
hStack.clipsToBounds = true
|
|
|
|
hStack.layer.borderWidth = 0.5
|
2023-05-09 18:39:15 +00:00
|
|
|
hStack.layer.cornerCurve = .continuous
|
2023-01-20 16:22:28 +00:00
|
|
|
hStack.backgroundColor = inactiveBackgroundColor
|
2023-01-26 20:17:17 +00:00
|
|
|
updateBorderColor()
|
2020-10-25 20:05:28 +00:00
|
|
|
|
|
|
|
addSubview(hStack)
|
|
|
|
|
|
|
|
placeholderImageView = UIImageView(image: UIImage(systemName: "doc.text"))
|
|
|
|
placeholderImageView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
placeholderImageView.contentMode = .scaleAspectFit
|
|
|
|
placeholderImageView.tintColor = .gray
|
2023-02-24 23:22:27 +00:00
|
|
|
placeholderImageView.isHidden = true
|
2020-10-25 20:05:28 +00:00
|
|
|
|
|
|
|
addSubview(placeholderImageView)
|
|
|
|
|
|
|
|
NSLayoutConstraint.activate([
|
|
|
|
imageView.heightAnchor.constraint(equalTo: heightAnchor),
|
|
|
|
imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor),
|
|
|
|
|
2023-02-24 23:22:27 +00:00
|
|
|
vStack.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor, constant: -8),
|
2020-10-25 20:05:28 +00:00
|
|
|
|
2023-02-24 23:22:27 +00:00
|
|
|
leadingSpacer.widthAnchor.constraint(equalToConstant: 4),
|
|
|
|
trailingSpacer.widthAnchor.constraint(equalToConstant: 4),
|
2023-01-20 16:22:28 +00:00
|
|
|
|
2020-10-25 20:05:28 +00:00
|
|
|
hStack.leadingAnchor.constraint(equalTo: leadingAnchor),
|
2023-01-20 16:22:28 +00:00
|
|
|
hStack.trailingAnchor.constraint(equalTo: trailingAnchor),
|
2020-10-25 20:05:28 +00:00
|
|
|
hStack.topAnchor.constraint(equalTo: topAnchor),
|
|
|
|
hStack.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
|
|
|
|
|
|
placeholderImageView.widthAnchor.constraint(equalToConstant: 30),
|
|
|
|
placeholderImageView.heightAnchor.constraint(equalToConstant: 30),
|
|
|
|
placeholderImageView.centerXAnchor.constraint(equalTo: imageView.centerXAnchor),
|
|
|
|
placeholderImageView.centerYAnchor.constraint(equalTo: imageView.centerYAnchor),
|
|
|
|
])
|
|
|
|
}
|
|
|
|
|
2023-01-26 20:17:17 +00:00
|
|
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
|
|
|
super.traitCollectionDidChange(previousTraitCollection)
|
|
|
|
updateBorderColor()
|
|
|
|
}
|
|
|
|
|
2023-01-20 16:22:28 +00:00
|
|
|
override func layoutSubviews() {
|
|
|
|
super.layoutSubviews()
|
|
|
|
hStack.layer.cornerRadius = 0.1 * bounds.height
|
|
|
|
}
|
|
|
|
|
2023-01-26 20:17:17 +00:00
|
|
|
private func updateBorderColor() {
|
|
|
|
if traitCollection.userInterfaceStyle == .dark {
|
|
|
|
hStack.layer.borderColor = UIColor.darkGray.cgColor
|
|
|
|
} else {
|
|
|
|
hStack.layer.borderColor = UIColor.lightGray.cgColor
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-08 19:24:12 +00:00
|
|
|
func updateUI(status: StatusMO) {
|
|
|
|
guard status.id != statusID else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
self.card = status.card
|
|
|
|
self.statusID = status.id
|
|
|
|
|
|
|
|
guard let card = status.card else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-02-24 23:22:27 +00:00
|
|
|
if let image = card.image {
|
|
|
|
imageView.update(for: URL(image), blurhash: card.blurhash)
|
|
|
|
imageView.isHidden = false
|
|
|
|
leadingSpacer.isHidden = true
|
|
|
|
} else {
|
|
|
|
imageView.update(for: nil)
|
|
|
|
imageView.isHidden = true
|
|
|
|
leadingSpacer.isHidden = false
|
|
|
|
}
|
2020-11-01 18:59:58 +00:00
|
|
|
|
|
|
|
let title = card.title.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
|
titleLabel.text = title
|
|
|
|
titleLabel.isHidden = title.isEmpty
|
|
|
|
|
2023-01-23 20:15:43 +00:00
|
|
|
let description = try! SwiftSoup.parseBodyFragment(card.description).text()
|
2020-11-01 18:59:58 +00:00
|
|
|
descriptionLabel.text = description
|
|
|
|
descriptionLabel.isHidden = description.isEmpty
|
2023-01-20 16:22:28 +00:00
|
|
|
|
|
|
|
if let host = card.url.host {
|
|
|
|
domainLabel.text = host.serialized
|
|
|
|
domainLabel.isHidden = false
|
|
|
|
} else {
|
|
|
|
domainLabel.isHidden = true
|
|
|
|
}
|
2023-02-25 20:23:13 +00:00
|
|
|
|
|
|
|
let titleHeight = titleLabel.isHidden ? 0 : titleLabel.sizeThatFits(CGSize(width: titleLabel.bounds.width, height: UIView.layoutFittingCompressedSize.height)).height
|
|
|
|
let descriptionHeight = descriptionLabel.isHidden ? 0 : descriptionLabel.sizeThatFits(CGSize(width: descriptionLabel.bounds.width, height: UIView.layoutFittingCompressedSize.height)).height
|
|
|
|
let domainLabel = domainLabel.isHidden ? 0 : domainLabel.sizeThatFits(CGSize(width: domainLabel.bounds.width, height: UIView.layoutFittingCompressedSize.height)).height
|
|
|
|
if titleHeight + descriptionHeight + domainLabel > vStack.bounds.height {
|
|
|
|
descriptionLabel.isHidden = true
|
|
|
|
}
|
2020-11-01 18:59:58 +00:00
|
|
|
}
|
|
|
|
|
2020-10-25 20:05:28 +00:00
|
|
|
private func loadBlurHash() {
|
|
|
|
guard let card = card, let hash = card.blurhash else { return }
|
|
|
|
|
2021-11-24 20:02:35 +00:00
|
|
|
AttachmentView.queue.async { [weak self] in
|
2020-10-25 20:05:28 +00:00
|
|
|
guard let self = self else { return }
|
|
|
|
|
|
|
|
let size: CGSize
|
|
|
|
if let width = card.width, let height = card.height {
|
2022-10-29 15:47:53 +00:00
|
|
|
let aspectRatio = CGFloat(width) / CGFloat(height)
|
|
|
|
if aspectRatio > 1 {
|
|
|
|
size = CGSize(width: 32, height: 32 / aspectRatio)
|
|
|
|
} else {
|
|
|
|
size = CGSize(width: 32 * aspectRatio, height: 32)
|
|
|
|
}
|
2020-10-25 20:05:28 +00:00
|
|
|
} else {
|
2022-10-29 15:47:53 +00:00
|
|
|
size = CGSize(width: 32, height: 32)
|
2020-10-25 20:05:28 +00:00
|
|
|
}
|
2022-10-29 15:47:53 +00:00
|
|
|
|
2020-10-25 20:05:28 +00:00
|
|
|
guard let preview = UIImage(blurHash: hash, size: size) else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
DispatchQueue.main.async { [weak self] in
|
|
|
|
guard let self = self,
|
|
|
|
self.card?.url == card.url,
|
|
|
|
self.imageView.image == nil else { return }
|
|
|
|
|
|
|
|
self.imageView.image = preview
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
2023-01-20 16:22:28 +00:00
|
|
|
hStack.backgroundColor = activeBackgroundColor
|
2020-10-25 20:05:28 +00:00
|
|
|
setNeedsDisplay()
|
|
|
|
}
|
|
|
|
|
|
|
|
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
|
|
}
|
|
|
|
|
|
|
|
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
2023-01-20 16:22:28 +00:00
|
|
|
hStack.backgroundColor = inactiveBackgroundColor
|
2020-10-25 20:05:28 +00:00
|
|
|
setNeedsDisplay()
|
|
|
|
|
|
|
|
if let card = card, let delegate = navigationDelegate {
|
2022-02-04 04:11:29 +00:00
|
|
|
delegate.selected(url: URL(card.url)!)
|
2020-10-25 20:05:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
|
2023-01-20 16:22:28 +00:00
|
|
|
hStack.backgroundColor = inactiveBackgroundColor
|
2020-10-25 20:05:28 +00:00
|
|
|
setNeedsDisplay()
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
extension StatusCardView: UIContextMenuInteractionDelegate {
|
|
|
|
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
|
|
|
|
guard let card = card else { return nil }
|
|
|
|
|
|
|
|
return UIContextMenuConfiguration(identifier: nil) {
|
2023-01-16 16:24:42 +00:00
|
|
|
let vc = SFSafariViewController(url: URL(card.url)!)
|
|
|
|
vc.preferredControlTintColor = Preferences.shared.accentColor.color
|
|
|
|
return vc
|
2020-10-25 20:05:28 +00:00
|
|
|
} actionProvider: { (_) in
|
2022-11-30 03:41:36 +00:00
|
|
|
let actions = self.actionProvider?.actionsForURL(URL(card.url)!, source: .view(self)) ?? []
|
2020-10-25 20:05:28 +00:00
|
|
|
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: actions)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-20 16:22:28 +00:00
|
|
|
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, previewForHighlightingMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
|
|
|
|
let params = UIPreviewParameters()
|
|
|
|
params.visiblePath = UIBezierPath(roundedRect: hStack.bounds, cornerRadius: hStack.layer.cornerRadius)
|
|
|
|
return UITargetedPreview(view: hStack, parameters: params)
|
|
|
|
}
|
|
|
|
|
2020-10-25 20:05:28 +00:00
|
|
|
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
|
|
|
|
if let viewController = animator.previewViewController,
|
|
|
|
let delegate = navigationDelegate {
|
|
|
|
animator.preferredCommitStyle = .pop
|
|
|
|
animator.addCompletion {
|
|
|
|
if let customPresenting = viewController as? CustomPreviewPresenting {
|
|
|
|
customPresenting.presentFromPreview(presenter: delegate)
|
|
|
|
} else {
|
|
|
|
delegate.show(viewController)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|