1
0
mirror of https://github.com/mastodon/mastodon-ios.git synced 2024-12-14 18:05:50 +01:00
mastodon-app-ufficiale-ipho.../MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift

339 lines
12 KiB
Swift
Raw Normal View History

2022-11-12 03:35:18 +01:00
//
// OpenGraphView.swift
//
//
// Created by Kyle Bashour on 11/11/22.
//
2022-11-14 22:26:25 +01:00
import AlamofireImage
2022-11-26 05:16:42 +01:00
import Combine
2022-11-12 03:35:18 +01:00
import MastodonAsset
import MastodonCore
import MastodonLocalization
2022-11-24 06:51:39 +01:00
import CoreDataStack
2022-11-12 03:35:18 +01:00
import UIKit
import WebKit
2022-11-12 03:35:18 +01:00
public protocol StatusCardControlDelegate: AnyObject {
func statusCardControl(_ statusCardControl: StatusCardControl, didTapURL url: URL)
func statusCardControlMenu(_ statusCardControl: StatusCardControl) -> UIMenu?
}
2022-11-27 06:47:49 +01:00
public final class StatusCardControl: UIControl {
public weak var delegate: StatusCardControlDelegate?
2022-11-26 05:16:42 +01:00
private var disposeBag = Set<AnyCancellable>()
2022-11-12 03:35:18 +01:00
2022-11-27 04:21:47 +01:00
private let containerStackView = UIStackView()
private let labelStackView = UIStackView()
2022-11-24 16:48:07 +01:00
private let highlightView = UIView()
private let dividerView = UIView()
2022-11-12 03:35:18 +01:00
private let imageView = UIImageView()
private let titleLabel = UILabel()
2022-11-24 06:51:39 +01:00
private let linkLabel = UILabel()
private lazy var showEmbedButton: UIButton = {
if #available(iOS 15.0, *) {
var configuration = UIButton.Configuration.gray()
configuration.background.visualEffect = UIBlurEffect(style: .systemUltraThinMaterial)
configuration.baseBackgroundColor = .clear
configuration.cornerStyle = .capsule
configuration.buttonSize = .large
configuration.title = L10n.Common.Controls.Status.loadEmbed
configuration.image = UIImage(systemName: "play.fill")
configuration.imagePadding = 12
return UIButton(configuration: configuration, primaryAction: UIAction { [weak self] _ in
self?.showWebView()
})
}
return UIButton(type: .system, primaryAction: UIAction { [weak self] _ in
self?.showWebView()
})
}()
private var html = ""
2022-11-24 06:51:39 +01:00
private static let cardContentPool = WKProcessPool()
private var webView: WKWebView?
2022-11-28 06:00:03 +01:00
private var layout: Layout?
private var layoutConstraints: [NSLayoutConstraint] = []
private var dividerConstraint: NSLayoutConstraint?
2022-11-12 03:35:18 +01:00
2022-11-24 16:48:07 +01:00
public override var isHighlighted: Bool {
2022-12-03 04:35:18 +01:00
didSet {
// override UIKit behavior of highlighting subviews when cell is highlighted
if isHighlighted,
let cell = sequence(first: self, next: \.superview).first(where: { $0 is UITableViewCell }) as? UITableViewCell {
highlightView.isHidden = cell.isHighlighted
} else {
highlightView.isHidden = !isHighlighted
}
}
2022-11-24 16:48:07 +01:00
}
2022-11-12 03:35:18 +01:00
public override init(frame: CGRect) {
super.init(frame: frame)
2022-11-26 05:16:42 +01:00
apply(theme: ThemeService.shared.currentTheme.value)
ThemeService.shared.currentTheme.sink { [weak self] theme in
self?.apply(theme: theme)
}.store(in: &disposeBag)
2022-11-12 03:35:18 +01:00
clipsToBounds = true
layer.cornerCurve = .continuous
layer.cornerRadius = 10
2022-11-27 06:47:49 +01:00
if #available(iOS 15, *) {
maximumContentSizeCategory = .accessibilityLarge
}
2022-12-03 04:35:18 +01:00
highlightView.backgroundColor = UIColor.label.withAlphaComponent(0.1)
2022-11-24 16:48:07 +01:00
highlightView.isHidden = true
2022-11-14 22:26:25 +01:00
titleLabel.numberOfLines = 2
2022-11-12 03:35:18 +01:00
titleLabel.textColor = Asset.Colors.Label.primary.color
2022-11-26 05:16:42 +01:00
titleLabel.font = .preferredFont(forTextStyle: .body)
2022-11-12 03:35:18 +01:00
2022-11-24 06:51:39 +01:00
linkLabel.numberOfLines = 1
linkLabel.textColor = Asset.Colors.Label.secondary.color
2022-11-26 05:16:42 +01:00
linkLabel.font = .preferredFont(forTextStyle: .subheadline)
2022-11-12 03:35:18 +01:00
2022-11-24 06:51:39 +01:00
imageView.tintColor = Asset.Colors.Label.secondary.color
2022-11-14 22:26:25 +01:00
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
2022-11-27 04:21:47 +01:00
imageView.setContentHuggingPriority(.zero, for: .horizontal)
imageView.setContentHuggingPriority(.zero, for: .vertical)
imageView.setContentCompressionResistancePriority(.zero, for: .horizontal)
imageView.setContentCompressionResistancePriority(.zero, for: .vertical)
labelStackView.addArrangedSubview(titleLabel)
labelStackView.addArrangedSubview(linkLabel)
labelStackView.layoutMargins = .init(top: 10, left: 10, bottom: 10, right: 10)
labelStackView.isLayoutMarginsRelativeArrangement = true
labelStackView.axis = .vertical
2022-11-28 06:00:03 +01:00
labelStackView.spacing = 2
2022-11-27 04:21:47 +01:00
containerStackView.addArrangedSubview(imageView)
containerStackView.addArrangedSubview(dividerView)
2022-11-27 04:21:47 +01:00
containerStackView.addArrangedSubview(labelStackView)
containerStackView.isUserInteractionEnabled = false
2022-11-27 07:05:43 +01:00
containerStackView.distribution = .fill
2022-11-27 04:21:47 +01:00
addSubview(containerStackView)
2022-11-24 16:48:07 +01:00
addSubview(highlightView)
addSubview(showEmbedButton)
2022-11-12 03:35:18 +01:00
2022-11-27 04:21:47 +01:00
containerStackView.translatesAutoresizingMaskIntoConstraints = false
2022-11-24 16:48:07 +01:00
highlightView.translatesAutoresizingMaskIntoConstraints = false
showEmbedButton.translatesAutoresizingMaskIntoConstraints = false
dividerView.translatesAutoresizingMaskIntoConstraints = false
2022-11-12 03:35:18 +01:00
2022-11-27 04:21:47 +01:00
containerStackView.pinToParent()
highlightView.pinToParent()
NSLayoutConstraint.activate([
showEmbedButton.centerXAnchor.constraint(equalTo: imageView.centerXAnchor),
showEmbedButton.centerYAnchor.constraint(equalTo: imageView.centerYAnchor),
])
addInteraction(UIContextMenuInteraction(delegate: self))
2022-11-12 03:35:18 +01:00
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
2022-11-24 06:51:39 +01:00
public func configure(card: Card) {
2022-11-27 06:47:49 +01:00
if let host = card.url?.host {
accessibilityLabel = "\(card.title) \(host)"
} else {
accessibilityLabel = card.title
}
2022-11-24 06:51:39 +01:00
titleLabel.text = card.title
linkLabel.text = card.url?.host
imageView.contentMode = .center
2022-11-14 22:26:25 +01:00
2022-11-24 06:51:39 +01:00
imageView.sd_setImage(
with: card.imageURL,
2022-11-28 06:00:03 +01:00
placeholderImage: icon(for: card.layout)
) { [weak self] image, _, _, _ in
2022-11-24 06:51:39 +01:00
if image != nil {
2022-11-28 06:00:03 +01:00
self?.imageView.contentMode = .scaleAspectFill
2022-11-24 06:51:39 +01:00
}
2022-11-14 22:26:25 +01:00
2022-11-28 06:00:03 +01:00
self?.containerStackView.setNeedsLayout()
self?.containerStackView.layoutIfNeeded()
2022-11-14 22:26:25 +01:00
}
2022-11-28 06:00:03 +01:00
if let html = card.html, !html.isEmpty {
showEmbedButton.isHidden = false
self.html = html
2022-12-03 04:10:35 +01:00
} else {
webView?.removeFromSuperview()
webView = nil
showEmbedButton.isHidden = true
self.html = ""
}
2022-11-28 06:00:03 +01:00
updateConstraints(for: card.layout)
2022-11-12 03:35:18 +01:00
}
public override func didMoveToWindow() {
super.didMoveToWindow()
if let window = window {
layer.borderWidth = 1 / window.screen.scale
dividerConstraint?.constant = 1 / window.screen.scale
2022-11-12 03:35:18 +01:00
}
}
2022-11-14 22:26:25 +01:00
2022-11-28 06:00:03 +01:00
private func updateConstraints(for layout: Layout) {
guard layout != self.layout else { return }
self.layout = layout
NSLayoutConstraint.deactivate(layoutConstraints)
dividerConstraint?.deactivate()
2022-11-28 06:00:03 +01:00
let pixelSize = 1 / (window?.screen.scale ?? 1)
2022-11-28 06:00:03 +01:00
switch layout {
case .large(let aspectRatio):
containerStackView.alignment = .fill
containerStackView.axis = .vertical
layoutConstraints = [
imageView.widthAnchor.constraint(
equalTo: imageView.heightAnchor,
multiplier: aspectRatio
)
// This priority is important or constraints break;
// it still renders the card correctly.
.priority(.defaultLow - 1),
2022-12-02 22:02:05 +01:00
// set a reasonable max height for very tall images
imageView.heightAnchor
.constraint(lessThanOrEqualToConstant: 400),
2022-11-28 06:00:03 +01:00
]
dividerConstraint = dividerView.heightAnchor.constraint(equalToConstant: pixelSize).activate()
2022-11-28 06:00:03 +01:00
case .compact:
containerStackView.alignment = .center
containerStackView.axis = .horizontal
layoutConstraints = [
imageView.heightAnchor.constraint(equalTo: heightAnchor),
imageView.widthAnchor.constraint(equalToConstant: 85),
heightAnchor.constraint(equalToConstant: 85).priority(.defaultLow - 1),
heightAnchor.constraint(greaterThanOrEqualToConstant: 85),
dividerView.heightAnchor.constraint(equalTo: containerStackView.heightAnchor),
2022-11-28 06:00:03 +01:00
]
dividerConstraint = dividerView.widthAnchor.constraint(equalToConstant: pixelSize).activate()
2022-11-28 06:00:03 +01:00
}
NSLayoutConstraint.activate(layoutConstraints)
2022-11-24 06:51:39 +01:00
}
2022-11-28 06:00:03 +01:00
private func icon(for layout: Layout) -> UIImage? {
switch layout {
case .compact:
return UIImage(systemName: "newspaper.fill")
case .large:
let configuration = UIImage.SymbolConfiguration(pointSize: 32)
return UIImage(systemName: "photo", withConfiguration: configuration)
}
2022-11-14 22:26:25 +01:00
}
2022-11-26 05:16:42 +01:00
private func apply(theme: Theme) {
layer.borderColor = theme.separator.cgColor
dividerView.backgroundColor = theme.separator
imageView.backgroundColor = UIColor.tertiarySystemFill
2022-11-26 05:16:42 +01:00
}
2022-11-12 03:35:18 +01:00
}
2022-11-27 04:21:47 +01:00
// MARK: WKWebView delegates
2022-12-03 17:30:21 +01:00
extension StatusCardControl: WKNavigationDelegate, WKUIDelegate {
fileprivate func showWebView() {
let webView = setupWebView()
webView.loadHTMLString("<meta name='viewport' content='width=device-width,user-scalable=no'><style>body { margin: 0; color-scheme: light dark; } body > :only-child { width: 100vw !important; height: 100vh !important }</style>" + html, baseURL: nil)
2022-12-03 17:27:51 +01:00
if webView.superview == nil {
addSubview(webView)
webView.pinTo(to: imageView)
}
}
2022-12-03 17:30:21 +01:00
private func setupWebView() -> WKWebView {
2022-12-03 04:10:35 +01:00
if let webView { return webView }
let config = WKWebViewConfiguration()
config.processPool = Self.cardContentPool
config.websiteDataStore = .nonPersistent() // private/incognito mode
config.suppressesIncrementalRendering = true
config.allowsInlineMediaPlayback = true
let webView = WKWebView(frame: .zero, configuration: config)
webView.uiDelegate = self
webView.navigationDelegate = self
webView.translatesAutoresizingMaskIntoConstraints = false
2022-12-03 17:27:51 +01:00
webView.isOpaque = false
webView.backgroundColor = .clear
self.webView = webView
return webView
}
public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy {
let isTopLevelNavigation: Bool
if let frame = navigationAction.targetFrame {
isTopLevelNavigation = frame.isMainFrame
} else {
isTopLevelNavigation = true
}
if isTopLevelNavigation,
// ignore form submits and such
navigationAction.navigationType == .linkActivated || navigationAction.navigationType == .other,
let url = navigationAction.request.url,
url.absoluteString != "about:blank" {
delegate?.statusCardControl(self, didTapURL: url)
return .cancel
}
return .allow
}
public func webViewDidClose(_ webView: WKWebView) {
webView.removeFromSuperview()
self.webView = nil
}
}
// MARK: UIContextMenuInteractionDelegate
extension StatusCardControl {
public override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { elements in
self.delegate?.statusCardControlMenu(self)
}
}
public override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, previewForDismissingMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
UITargetedPreview(view: self)
}
}
2022-11-28 06:00:03 +01:00
private extension StatusCardControl {
enum Layout: Equatable {
case compact
case large(aspectRatio: CGFloat)
}
}
private extension Card {
var layout: StatusCardControl.Layout {
var aspectRatio = CGFloat(width) / CGFloat(height)
if !aspectRatio.isFinite {
aspectRatio = 1
}
return (abs(aspectRatio - 1) < 0.05 || image == nil) && html == nil
2022-11-28 06:00:03 +01:00
? .compact
: .large(aspectRatio: aspectRatio)
2022-11-28 06:00:03 +01:00
}
}
2022-11-27 04:21:47 +01:00
private extension UILayoutPriority {
static let zero = UILayoutPriority(rawValue: 0)
}