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