Working pretty well

This commit is contained in:
Kyle Bashour 2022-11-23 21:51:39 -08:00
parent 595b46e96e
commit f8d1afc7e4
6 changed files with 119 additions and 93 deletions

View File

@ -9,17 +9,17 @@
</entity> </entity>
<entity name="Card" representedClassName="CoreDataStack.Card" syncable="YES"> <entity name="Card" representedClassName="CoreDataStack.Card" syncable="YES">
<attribute name="authorName" optional="YES" attributeType="String"/> <attribute name="authorName" optional="YES" attributeType="String"/>
<attribute name="authorURL" optional="YES" attributeType="String"/> <attribute name="authorURLRaw" optional="YES" attributeType="String"/>
<attribute name="blurhash" optional="YES" attributeType="String"/> <attribute name="blurhash" optional="YES" attributeType="String"/>
<attribute name="desc" attributeType="String"/> <attribute name="desc" attributeType="String"/>
<attribute name="embedURL" optional="YES" attributeType="String"/> <attribute name="embedURLRaw" optional="YES" attributeType="String"/>
<attribute name="height" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/> <attribute name="height" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="image" optional="YES" attributeType="String"/> <attribute name="image" optional="YES" attributeType="String"/>
<attribute name="providerName" optional="YES" attributeType="String"/> <attribute name="providerName" optional="YES" attributeType="String"/>
<attribute name="providerURL" optional="YES" attributeType="String"/> <attribute name="providerURLRaw" optional="YES" attributeType="String"/>
<attribute name="title" attributeType="String"/> <attribute name="title" attributeType="String"/>
<attribute name="typeRaw" attributeType="String"/> <attribute name="typeRaw" attributeType="String"/>
<attribute name="url" attributeType="String"/> <attribute name="urlRaw" attributeType="String"/>
<attribute name="width" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/> <attribute name="width" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="card" inverseEntity="Status"/> <relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="card" inverseEntity="Status"/>
</entity> </entity>

View File

@ -10,7 +10,11 @@ import CoreData
public final class Card: NSManagedObject { public final class Card: NSManagedObject {
// sourcery: autoGenerateProperty // sourcery: autoGenerateProperty
@NSManaged public private(set) var url: String @NSManaged public private(set) var urlRaw: String
public var url: URL? {
URL(string: urlRaw)
}
// sourcery: autoGenerateProperty // sourcery: autoGenerateProperty
@NSManaged public private(set) var title: String @NSManaged public private(set) var title: String
// sourcery: autoGenerateProperty // sourcery: autoGenerateProperty
@ -26,19 +30,23 @@ public final class Card: NSManagedObject {
// sourcery: autoGenerateProperty // sourcery: autoGenerateProperty
@NSManaged public private(set) var authorName: String? @NSManaged public private(set) var authorName: String?
// sourcery: autoGenerateProperty // sourcery: autoGenerateProperty
@NSManaged public private(set) var authorURL: String? @NSManaged public private(set) var authorURLRaw: String?
// sourcery: autoGenerateProperty // sourcery: autoGenerateProperty
@NSManaged public private(set) var providerName: String? @NSManaged public private(set) var providerName: String?
// sourcery: autoGenerateProperty // sourcery: autoGenerateProperty
@NSManaged public private(set) var providerURL: String? @NSManaged public private(set) var providerURLRaw: String?
// sourcery: autoGenerateProperty // sourcery: autoGenerateProperty
@NSManaged public private(set) var width: Int64 @NSManaged public private(set) var width: Int64
// sourcery: autoGenerateProperty // sourcery: autoGenerateProperty
@NSManaged public private(set) var height: Int64 @NSManaged public private(set) var height: Int64
// sourcery: autoGenerateProperty // sourcery: autoGenerateProperty
@NSManaged public private(set) var image: String? @NSManaged public private(set) var image: String?
public var imageURL: URL? {
image.flatMap(URL.init)
}
// sourcery: autoGenerateProperty // sourcery: autoGenerateProperty
@NSManaged public private(set) var embedURL: String? @NSManaged public private(set) var embedURLRaw: String?
// sourcery: autoGenerateProperty // sourcery: autoGenerateProperty
@NSManaged public private(set) var blurhash: String? @NSManaged public private(set) var blurhash: String?
@ -75,64 +83,64 @@ extension Card: AutoGenerateProperty {
// Generated using Sourcery // Generated using Sourcery
// DO NOT EDIT // DO NOT EDIT
public struct Property { public struct Property {
public let url: String public let urlRaw: String
public let title: String public let title: String
public let desc: String public let desc: String
public let type: MastodonCardType public let type: MastodonCardType
public let authorName: String? public let authorName: String?
public let authorURL: String? public let authorURLRaw: String?
public let providerName: String? public let providerName: String?
public let providerURL: String? public let providerURLRaw: String?
public let width: Int64 public let width: Int64
public let height: Int64 public let height: Int64
public let image: String? public let image: String?
public let embedURL: String? public let embedURLRaw: String?
public let blurhash: String? public let blurhash: String?
public init( public init(
url: String, urlRaw: String,
title: String, title: String,
desc: String, desc: String,
type: MastodonCardType, type: MastodonCardType,
authorName: String?, authorName: String?,
authorURL: String?, authorURLRaw: String?,
providerName: String?, providerName: String?,
providerURL: String?, providerURLRaw: String?,
width: Int64, width: Int64,
height: Int64, height: Int64,
image: String?, image: String?,
embedURL: String?, embedURLRaw: String?,
blurhash: String? blurhash: String?
) { ) {
self.url = url self.urlRaw = urlRaw
self.title = title self.title = title
self.desc = desc self.desc = desc
self.type = type self.type = type
self.authorName = authorName self.authorName = authorName
self.authorURL = authorURL self.authorURLRaw = authorURLRaw
self.providerName = providerName self.providerName = providerName
self.providerURL = providerURL self.providerURLRaw = providerURLRaw
self.width = width self.width = width
self.height = height self.height = height
self.image = image self.image = image
self.embedURL = embedURL self.embedURLRaw = embedURLRaw
self.blurhash = blurhash self.blurhash = blurhash
} }
} }
public func configure(property: Property) { public func configure(property: Property) {
self.url = property.url self.urlRaw = property.urlRaw
self.title = property.title self.title = property.title
self.desc = property.desc self.desc = property.desc
self.type = property.type self.type = property.type
self.authorName = property.authorName self.authorName = property.authorName
self.authorURL = property.authorURL self.authorURLRaw = property.authorURLRaw
self.providerName = property.providerName self.providerName = property.providerName
self.providerURL = property.providerURL self.providerURLRaw = property.providerURLRaw
self.width = property.width self.width = property.width
self.height = property.height self.height = property.height
self.image = property.image self.image = property.image
self.embedURL = property.embedURL self.embedURLRaw = property.embedURLRaw
self.blurhash = property.blurhash self.blurhash = property.blurhash
} }

View File

@ -65,18 +65,18 @@ extension Persistence.Card {
} }
let property = Card.Property( let property = Card.Property(
url: context.entity.url, urlRaw: context.entity.url,
title: context.entity.title, title: context.entity.title,
desc: context.entity.description, desc: context.entity.description,
type: type, type: type,
authorName: context.entity.authorName, authorName: context.entity.authorName,
authorURL: context.entity.authorURL, authorURLRaw: context.entity.authorURL,
providerName: context.entity.providerName, providerName: context.entity.providerName,
providerURL: context.entity.providerURL, providerURLRaw: context.entity.providerURL,
width: Int64(context.entity.width ?? 0), width: Int64(context.entity.width ?? 0),
height: Int64(context.entity.height ?? 0), height: Int64(context.entity.height ?? 0),
image: context.entity.image, image: context.entity.image,
embedURL: context.entity.embedURL, embedURLRaw: context.entity.embedURL,
blurhash: context.entity.blurhash blurhash: context.entity.blurhash
) )

View File

@ -9,18 +9,26 @@ import AlamofireImage
import LinkPresentation import LinkPresentation
import MastodonAsset import MastodonAsset
import MastodonCore import MastodonCore
import CoreDataStack
import UIKit import UIKit
public final class LinkPreviewButton: UIControl { public final class LinkPreviewButton: UIControl {
private var linkPresentationTask: Task<Void, Error>?
private var url: URL?
private let containerStackView = UIStackView() private let containerStackView = UIStackView()
private let labelStackView = UIStackView() private let labelStackView = UIStackView()
private let imageView = UIImageView() private let imageView = UIImageView()
private let titleLabel = UILabel() private let titleLabel = UILabel()
private let subtitleLabel = UILabel() private let linkLabel = UILabel()
private lazy var compactImageConstraints = [
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor),
imageView.heightAnchor.constraint(equalTo: heightAnchor),
containerStackView.heightAnchor.constraint(equalToConstant: 85),
]
private lazy var largeImageConstraints = [
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 21 / 40),
]
public override init(frame: CGRect) { public override init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)
@ -29,25 +37,24 @@ public final class LinkPreviewButton: UIControl {
layer.cornerCurve = .continuous layer.cornerCurve = .continuous
layer.cornerRadius = 10 layer.cornerRadius = 10
layer.borderColor = ThemeService.shared.currentTheme.value.separator.cgColor layer.borderColor = ThemeService.shared.currentTheme.value.separator.cgColor
backgroundColor = ThemeService.shared.currentTheme.value.systemElevatedBackgroundColor
titleLabel.numberOfLines = 2 titleLabel.numberOfLines = 2
titleLabel.setContentCompressionResistancePriority(.defaultLow - 1, for: .horizontal) titleLabel.setContentCompressionResistancePriority(.defaultLow - 1, for: .horizontal)
titleLabel.text = "This is where I'd put a title... if I had one" titleLabel.text = "This is where I'd put a title... if I had one"
titleLabel.textColor = Asset.Colors.Label.primary.color titleLabel.textColor = Asset.Colors.Label.primary.color
subtitleLabel.text = "Subtitle" linkLabel.text = "Subtitle"
subtitleLabel.numberOfLines = 1 linkLabel.numberOfLines = 1
subtitleLabel.setContentCompressionResistancePriority(.defaultLow - 1, for: .horizontal) linkLabel.setContentCompressionResistancePriority(.defaultLow - 1, for: .horizontal)
subtitleLabel.textColor = Asset.Colors.Label.secondary.color linkLabel.textColor = Asset.Colors.Label.secondary.color
subtitleLabel.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 15, weight: .regular), maximumPointSize: 20)
imageView.backgroundColor = UIColor.black.withAlphaComponent(0.15) imageView.tintColor = Asset.Colors.Label.secondary.color
imageView.backgroundColor = ThemeService.shared.currentTheme.value.systemElevatedBackgroundColor
imageView.contentMode = .scaleAspectFill imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true imageView.clipsToBounds = true
labelStackView.addArrangedSubview(linkLabel)
labelStackView.addArrangedSubview(titleLabel) labelStackView.addArrangedSubview(titleLabel)
labelStackView.addArrangedSubview(subtitleLabel)
labelStackView.layoutMargins = .init(top: 8, left: 10, bottom: 8, right: 10) labelStackView.layoutMargins = .init(top: 8, left: 10, bottom: 8, right: 10)
labelStackView.isLayoutMarginsRelativeArrangement = true labelStackView.isLayoutMarginsRelativeArrangement = true
labelStackView.axis = .vertical labelStackView.axis = .vertical
@ -55,20 +62,16 @@ public final class LinkPreviewButton: UIControl {
containerStackView.addArrangedSubview(imageView) containerStackView.addArrangedSubview(imageView)
containerStackView.addArrangedSubview(labelStackView) containerStackView.addArrangedSubview(labelStackView)
containerStackView.distribution = .fill containerStackView.distribution = .fill
containerStackView.alignment = .center
addSubview(containerStackView) addSubview(containerStackView)
containerStackView.translatesAutoresizingMaskIntoConstraints = false containerStackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
containerStackView.heightAnchor.constraint(equalToConstant: 85),
containerStackView.topAnchor.constraint(equalTo: topAnchor), containerStackView.topAnchor.constraint(equalTo: topAnchor),
containerStackView.bottomAnchor.constraint(equalTo: bottomAnchor), containerStackView.bottomAnchor.constraint(equalTo: bottomAnchor),
containerStackView.leadingAnchor.constraint(equalTo: leadingAnchor), containerStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
containerStackView.trailingAnchor.constraint(equalTo: trailingAnchor), containerStackView.trailingAnchor.constraint(equalTo: trailingAnchor),
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor),
imageView.heightAnchor.constraint(equalTo: heightAnchor),
]) ])
} }
@ -76,36 +79,32 @@ public final class LinkPreviewButton: UIControl {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
public func configure(url: URL, trimmed: String) { public func configure(card: Card) {
guard url != self.url else { let isCompact = card.width == card.height
return
titleLabel.text = card.title
linkLabel.text = card.url?.host
imageView.contentMode = .center
imageView.sd_setImage(
with: card.imageURL,
placeholderImage: isCompact ? newsIcon : photoIcon
) { [weak imageView] image, _, _, _ in
if image != nil {
imageView?.contentMode = .scaleAspectFill
}
} }
reset() NSLayoutConstraint.deactivate(compactImageConstraints + largeImageConstraints)
subtitleLabel.text = trimmed
self.url = url
linkPresentationTask = Task { if isCompact {
do { containerStackView.alignment = .center
let metadata = try await LPMetadataProvider().startFetchingMetadata(for: url) containerStackView.axis = .horizontal
NSLayoutConstraint.activate(compactImageConstraints)
guard !Task.isCancelled else { } else {
return containerStackView.alignment = .fill
} containerStackView.axis = .vertical
NSLayoutConstraint.activate(largeImageConstraints)
self.titleLabel.text = metadata.title
if let result = try await metadata.imageProvider?.loadImageData() {
let image = UIImage(data: result.data)
guard !Task.isCancelled else {
return
}
self.imageView.image = image
}
} catch {
self.subtitleLabel.text = "Error loading link preview"
}
} }
} }
@ -117,11 +116,12 @@ public final class LinkPreviewButton: UIControl {
} }
} }
private func reset() { private var newsIcon: UIImage? {
linkPresentationTask?.cancel() UIImage(systemName: "newspaper.fill")
url = nil }
imageView.image = nil
titleLabel.text = nil private var photoIcon: UIImage? {
subtitleLabel.text = nil let configuration = UIImage.SymbolConfiguration(pointSize: 40)
return UIImage(systemName: "photo", withConfiguration: configuration)
} }
} }

View File

@ -40,6 +40,14 @@ extension StatusView {
extension StatusView { extension StatusView {
public func configure(status: Status) { public func configure(status: Status) {
if let card = status.card {
print("---- \(card.title)")
print("---- \(card.url)")
print("---- \(card.image)")
print("---- \(card.width)")
print("---- \(card.height)")
}
viewModel.objects.insert(status) viewModel.objects.insert(status)
if let reblog = status.reblog { if let reblog = status.reblog {
viewModel.objects.insert(reblog) viewModel.objects.insert(reblog)
@ -53,6 +61,7 @@ extension StatusView {
configureContent(status: status) configureContent(status: status)
configureMedia(status: status) configureMedia(status: status)
configurePoll(status: status) configurePoll(status: status)
configureCard(status: status)
configureToolbar(status: status) configureToolbar(status: status)
configureFilter(status: status) configureFilter(status: status)
} }
@ -349,6 +358,17 @@ extension StatusView {
.assign(to: \.isVoting, on: viewModel) .assign(to: \.isVoting, on: viewModel)
.store(in: &disposeBag) .store(in: &disposeBag)
} }
private func configureCard(status: Status) {
let status = status.reblog ?? status
if viewModel.mediaViewConfigurations.isEmpty {
status.publisher(for: \.card)
.assign(to: \.card, on: viewModel)
.store(in: &disposeBag)
} else {
viewModel.card = nil
}
}
private func configureToolbar(status: Status) { private func configureToolbar(status: Status) {
let status = status.reblog ?? status let status = status.reblog ?? status

View File

@ -69,7 +69,10 @@ extension StatusView {
@Published public var voteCount = 0 @Published public var voteCount = 0
@Published public var expireAt: Date? @Published public var expireAt: Date?
@Published public var expired: Bool = false @Published public var expired: Bool = false
// Card
@Published public var card: Card?
// Visibility // Visibility
@Published public var visibility: MastodonVisibility = .public @Published public var visibility: MastodonVisibility = .public
@ -185,6 +188,7 @@ extension StatusView.ViewModel {
bindContent(statusView: statusView) bindContent(statusView: statusView)
bindMedia(statusView: statusView) bindMedia(statusView: statusView)
bindPoll(statusView: statusView) bindPoll(statusView: statusView)
bindCard(statusView: statusView)
bindToolbar(statusView: statusView) bindToolbar(statusView: statusView)
bindMetric(statusView: statusView) bindMetric(statusView: statusView)
bindMenu(statusView: statusView) bindMenu(statusView: statusView)
@ -306,21 +310,6 @@ extension StatusView.ViewModel {
statusView.contentMetaText.textView.accessibilityTraits = [.staticText] statusView.contentMetaText.textView.accessibilityTraits = [.staticText]
statusView.contentMetaText.textView.accessibilityElementsHidden = false statusView.contentMetaText.textView.accessibilityElementsHidden = false
if let url = content.entities.first(where: {
switch $0.meta {
case .url:
return true
default:
return false
}
}) {
guard case .url(let text, let trimmed, let url, _) = url.meta, let url = URL(string: url) else {
fatalError()
}
statusView.linkPreviewButton.configure(url: url, trimmed: trimmed)
statusView.setLinkPreviewButtonDisplay()
}
} else { } else {
statusView.contentMetaText.reset() statusView.contentMetaText.reset()
statusView.contentMetaText.textView.accessibilityLabel = "" statusView.contentMetaText.textView.accessibilityLabel = ""
@ -496,6 +485,15 @@ extension StatusView.ViewModel {
.assign(to: \.isEnabled, on: statusView.pollVoteButton) .assign(to: \.isEnabled, on: statusView.pollVoteButton)
.store(in: &disposeBag) .store(in: &disposeBag)
} }
private func bindCard(statusView: StatusView) {
$card.sink { card in
guard let card = card else { return }
statusView.linkPreviewButton.configure(card: card)
statusView.setLinkPreviewButtonDisplay()
}
.store(in: &disposeBag)
}
private func bindToolbar(statusView: StatusView) { private func bindToolbar(statusView: StatusView) {
$replyCount $replyCount