diff --git a/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 5.xcdatamodel/contents b/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 5.xcdatamodel/contents
index 5a0ef6a6a..c18d7492b 100644
--- a/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 5.xcdatamodel/contents
+++ b/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 5.xcdatamodel/contents
@@ -9,17 +9,17 @@
-
+
-
+
-
+
-
+
diff --git a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Card.swift b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Card.swift
index 12f6d636e..99f53f268 100644
--- a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Card.swift
+++ b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Card.swift
@@ -10,7 +10,11 @@ import CoreData
public final class Card: NSManagedObject {
// 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
@NSManaged public private(set) var title: String
// sourcery: autoGenerateProperty
@@ -26,19 +30,23 @@ public final class Card: NSManagedObject {
// sourcery: autoGenerateProperty
@NSManaged public private(set) var authorName: String?
// sourcery: autoGenerateProperty
- @NSManaged public private(set) var authorURL: String?
+ @NSManaged public private(set) var authorURLRaw: String?
// sourcery: autoGenerateProperty
@NSManaged public private(set) var providerName: String?
// sourcery: autoGenerateProperty
- @NSManaged public private(set) var providerURL: String?
+ @NSManaged public private(set) var providerURLRaw: String?
// sourcery: autoGenerateProperty
@NSManaged public private(set) var width: Int64
// sourcery: autoGenerateProperty
@NSManaged public private(set) var height: Int64
// sourcery: autoGenerateProperty
@NSManaged public private(set) var image: String?
+ public var imageURL: URL? {
+ image.flatMap(URL.init)
+ }
+
// sourcery: autoGenerateProperty
- @NSManaged public private(set) var embedURL: String?
+ @NSManaged public private(set) var embedURLRaw: String?
// sourcery: autoGenerateProperty
@NSManaged public private(set) var blurhash: String?
@@ -75,64 +83,64 @@ extension Card: AutoGenerateProperty {
// Generated using Sourcery
// DO NOT EDIT
public struct Property {
- public let url: String
+ public let urlRaw: String
public let title: String
public let desc: String
public let type: MastodonCardType
public let authorName: String?
- public let authorURL: String?
+ public let authorURLRaw: String?
public let providerName: String?
- public let providerURL: String?
+ public let providerURLRaw: String?
public let width: Int64
public let height: Int64
public let image: String?
- public let embedURL: String?
+ public let embedURLRaw: String?
public let blurhash: String?
public init(
- url: String,
+ urlRaw: String,
title: String,
desc: String,
type: MastodonCardType,
authorName: String?,
- authorURL: String?,
+ authorURLRaw: String?,
providerName: String?,
- providerURL: String?,
+ providerURLRaw: String?,
width: Int64,
height: Int64,
image: String?,
- embedURL: String?,
+ embedURLRaw: String?,
blurhash: String?
) {
- self.url = url
+ self.urlRaw = urlRaw
self.title = title
self.desc = desc
self.type = type
self.authorName = authorName
- self.authorURL = authorURL
+ self.authorURLRaw = authorURLRaw
self.providerName = providerName
- self.providerURL = providerURL
+ self.providerURLRaw = providerURLRaw
self.width = width
self.height = height
self.image = image
- self.embedURL = embedURL
+ self.embedURLRaw = embedURLRaw
self.blurhash = blurhash
}
}
public func configure(property: Property) {
- self.url = property.url
+ self.urlRaw = property.urlRaw
self.title = property.title
self.desc = property.desc
self.type = property.type
self.authorName = property.authorName
- self.authorURL = property.authorURL
+ self.authorURLRaw = property.authorURLRaw
self.providerName = property.providerName
- self.providerURL = property.providerURL
+ self.providerURLRaw = property.providerURLRaw
self.width = property.width
self.height = property.height
self.image = property.image
- self.embedURL = property.embedURL
+ self.embedURLRaw = property.embedURLRaw
self.blurhash = property.blurhash
}
diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Card.swift b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Card.swift
index 8c3b4c312..838d5e128 100644
--- a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Card.swift
+++ b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Card.swift
@@ -65,18 +65,18 @@ extension Persistence.Card {
}
let property = Card.Property(
- url: context.entity.url,
+ urlRaw: context.entity.url,
title: context.entity.title,
desc: context.entity.description,
type: type,
authorName: context.entity.authorName,
- authorURL: context.entity.authorURL,
+ authorURLRaw: context.entity.authorURL,
providerName: context.entity.providerName,
- providerURL: context.entity.providerURL,
+ providerURLRaw: context.entity.providerURL,
width: Int64(context.entity.width ?? 0),
height: Int64(context.entity.height ?? 0),
image: context.entity.image,
- embedURL: context.entity.embedURL,
+ embedURLRaw: context.entity.embedURL,
blurhash: context.entity.blurhash
)
diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/LinkPreviewButton.swift b/MastodonSDK/Sources/MastodonUI/View/Content/LinkPreviewButton.swift
index 94de42ddc..4cb2dca6d 100644
--- a/MastodonSDK/Sources/MastodonUI/View/Content/LinkPreviewButton.swift
+++ b/MastodonSDK/Sources/MastodonUI/View/Content/LinkPreviewButton.swift
@@ -9,18 +9,26 @@ import AlamofireImage
import LinkPresentation
import MastodonAsset
import MastodonCore
+import CoreDataStack
import UIKit
public final class LinkPreviewButton: UIControl {
- private var linkPresentationTask: Task?
- private var url: URL?
-
private let containerStackView = UIStackView()
private let labelStackView = UIStackView()
private let imageView = UIImageView()
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) {
super.init(frame: frame)
@@ -29,25 +37,24 @@ public final class LinkPreviewButton: UIControl {
layer.cornerCurve = .continuous
layer.cornerRadius = 10
layer.borderColor = ThemeService.shared.currentTheme.value.separator.cgColor
- backgroundColor = ThemeService.shared.currentTheme.value.systemElevatedBackgroundColor
titleLabel.numberOfLines = 2
titleLabel.setContentCompressionResistancePriority(.defaultLow - 1, for: .horizontal)
titleLabel.text = "This is where I'd put a title... if I had one"
titleLabel.textColor = Asset.Colors.Label.primary.color
- subtitleLabel.text = "Subtitle"
- subtitleLabel.numberOfLines = 1
- subtitleLabel.setContentCompressionResistancePriority(.defaultLow - 1, for: .horizontal)
- subtitleLabel.textColor = Asset.Colors.Label.secondary.color
- subtitleLabel.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 15, weight: .regular), maximumPointSize: 20)
+ linkLabel.text = "Subtitle"
+ linkLabel.numberOfLines = 1
+ linkLabel.setContentCompressionResistancePriority(.defaultLow - 1, for: .horizontal)
+ linkLabel.textColor = Asset.Colors.Label.secondary.color
- 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.clipsToBounds = true
+ labelStackView.addArrangedSubview(linkLabel)
labelStackView.addArrangedSubview(titleLabel)
- labelStackView.addArrangedSubview(subtitleLabel)
labelStackView.layoutMargins = .init(top: 8, left: 10, bottom: 8, right: 10)
labelStackView.isLayoutMarginsRelativeArrangement = true
labelStackView.axis = .vertical
@@ -55,20 +62,16 @@ public final class LinkPreviewButton: UIControl {
containerStackView.addArrangedSubview(imageView)
containerStackView.addArrangedSubview(labelStackView)
containerStackView.distribution = .fill
- containerStackView.alignment = .center
addSubview(containerStackView)
containerStackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
- containerStackView.heightAnchor.constraint(equalToConstant: 85),
containerStackView.topAnchor.constraint(equalTo: topAnchor),
containerStackView.bottomAnchor.constraint(equalTo: bottomAnchor),
containerStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
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")
}
- public func configure(url: URL, trimmed: String) {
- guard url != self.url else {
- return
+ public func configure(card: Card) {
+ let isCompact = card.width == card.height
+
+ 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()
- subtitleLabel.text = trimmed
- self.url = url
+ NSLayoutConstraint.deactivate(compactImageConstraints + largeImageConstraints)
- linkPresentationTask = Task {
- do {
- let metadata = try await LPMetadataProvider().startFetchingMetadata(for: url)
-
- guard !Task.isCancelled else {
- return
- }
-
- 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"
- }
+ if isCompact {
+ containerStackView.alignment = .center
+ containerStackView.axis = .horizontal
+ NSLayoutConstraint.activate(compactImageConstraints)
+ } else {
+ containerStackView.alignment = .fill
+ containerStackView.axis = .vertical
+ NSLayoutConstraint.activate(largeImageConstraints)
}
}
@@ -117,11 +116,12 @@ public final class LinkPreviewButton: UIControl {
}
}
- private func reset() {
- linkPresentationTask?.cancel()
- url = nil
- imageView.image = nil
- titleLabel.text = nil
- subtitleLabel.text = nil
+ private var newsIcon: UIImage? {
+ UIImage(systemName: "newspaper.fill")
+ }
+
+ private var photoIcon: UIImage? {
+ let configuration = UIImage.SymbolConfiguration(pointSize: 40)
+ return UIImage(systemName: "photo", withConfiguration: configuration)
}
}
diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift
index 47e4f18ff..fb28d564d 100644
--- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift
+++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift
@@ -40,6 +40,14 @@ extension StatusView {
extension StatusView {
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)
if let reblog = status.reblog {
viewModel.objects.insert(reblog)
@@ -53,6 +61,7 @@ extension StatusView {
configureContent(status: status)
configureMedia(status: status)
configurePoll(status: status)
+ configureCard(status: status)
configureToolbar(status: status)
configureFilter(status: status)
}
@@ -349,6 +358,17 @@ extension StatusView {
.assign(to: \.isVoting, on: viewModel)
.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) {
let status = status.reblog ?? status
diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift
index dc3f840f8..e56d82aff 100644
--- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift
+++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift
@@ -69,7 +69,10 @@ extension StatusView {
@Published public var voteCount = 0
@Published public var expireAt: Date?
@Published public var expired: Bool = false
-
+
+ // Card
+ @Published public var card: Card?
+
// Visibility
@Published public var visibility: MastodonVisibility = .public
@@ -185,6 +188,7 @@ extension StatusView.ViewModel {
bindContent(statusView: statusView)
bindMedia(statusView: statusView)
bindPoll(statusView: statusView)
+ bindCard(statusView: statusView)
bindToolbar(statusView: statusView)
bindMetric(statusView: statusView)
bindMenu(statusView: statusView)
@@ -306,21 +310,6 @@ extension StatusView.ViewModel {
statusView.contentMetaText.textView.accessibilityTraits = [.staticText]
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 {
statusView.contentMetaText.reset()
statusView.contentMetaText.textView.accessibilityLabel = ""
@@ -496,6 +485,15 @@ extension StatusView.ViewModel {
.assign(to: \.isEnabled, on: statusView.pollVoteButton)
.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) {
$replyCount