From daeb2ef70ffa8d7756e60cff45259dbc32b56da4 Mon Sep 17 00:00:00 2001 From: Kyle Bashour Date: Fri, 11 Nov 2022 18:35:18 -0800 Subject: [PATCH 01/41] wip --- .../xcshareddata/swiftpm/Package.resolved | 9 ++ MastodonSDK/Package.swift | 2 + .../View/Content/OpenGraphView.swift | 82 +++++++++++++++++++ .../View/Content/StatusView+ViewModel.swift | 16 ++++ .../MastodonUI/View/Content/StatusView.swift | 11 ++- 5 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 MastodonSDK/Sources/MastodonUI/View/Content/OpenGraphView.swift diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index 64dc691bb..35c4b5ffc 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -108,6 +108,15 @@ "version" : "8.0.0" } }, + { + "identity" : "opengraph", + "kind" : "remoteSourceControl", + "location" : "https://github.com/satoshi-takano/OpenGraph", + "state" : { + "revision" : "3ef2b8b9b4972b57e9e78c91c26be770c0110057", + "version" : "1.5.0" + } + }, { "identity" : "pageboy", "kind" : "remoteSourceControl", diff --git a/MastodonSDK/Package.swift b/MastodonSDK/Package.swift index ca241038b..d44e0d122 100644 --- a/MastodonSDK/Package.swift +++ b/MastodonSDK/Package.swift @@ -49,6 +49,7 @@ let package = Package( .package(url: "https://github.com/SDWebImage/SDWebImage.git", from: "5.12.0"), .package(url: "https://github.com/eneko/Stripes.git", from: "0.2.0"), .package(url: "https://github.com/onevcat/Kingfisher.git", from: "7.4.1"), + .package(url: "https://github.com/satoshi-takano/OpenGraph", from: "1.0.0"), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. @@ -117,6 +118,7 @@ let package = Package( .product(name: "UIHostingConfigurationBackport", package: "UIHostingConfigurationBackport"), .product(name: "TabBarPager", package: "TabBarPager"), .product(name: "ThirdPartyMailer", package: "ThirdPartyMailer"), + .product(name: "OpenGraph", package: "OpenGraph"), .product(name: "OrderedCollections", package: "swift-collections"), .product(name: "Tabman", package: "Tabman"), .product(name: "MetaTextKit", package: "MetaTextKit"), diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/OpenGraphView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/OpenGraphView.swift new file mode 100644 index 000000000..f03fda847 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/View/Content/OpenGraphView.swift @@ -0,0 +1,82 @@ +// +// OpenGraphView.swift +// +// +// Created by Kyle Bashour on 11/11/22. +// + +import MastodonAsset +import MastodonCore +import OpenGraph +import UIKit + +public final class OpenGraphView: UIControl { + private let containerStackView = UIStackView() + private let labelStackView = UIStackView() + + private let imageView = UIImageView() + private let titleLabel = UILabel() + private let subtitleLabel = UILabel() + + public override init(frame: CGRect) { + super.init(frame: frame) + + clipsToBounds = true + layer.cornerCurve = .continuous + layer.cornerRadius = 10 + layer.borderColor = ThemeService.shared.currentTheme.value.separator.cgColor + backgroundColor = ThemeService.shared.currentTheme.value.systemElevatedBackgroundColor + + titleLabel.numberOfLines = 0 + 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.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) + + imageView.backgroundColor = UIColor.black.withAlphaComponent(0.15) + + labelStackView.addArrangedSubview(titleLabel) + labelStackView.addArrangedSubview(subtitleLabel) + labelStackView.layoutMargins = .init(top: 8, left: 10, bottom: 8, right: 10) + labelStackView.isLayoutMarginsRelativeArrangement = true + labelStackView.axis = .vertical + + containerStackView.addArrangedSubview(imageView) + containerStackView.addArrangedSubview(labelStackView) + containerStackView.distribution = .fill + containerStackView.alignment = .center + + addSubview(containerStackView) + + containerStackView.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + containerStackView.heightAnchor.constraint(equalToConstant: 80), + 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), + ]) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func configure(content: String) { + self.subtitleLabel.text = content + } + + public override func didMoveToWindow() { + super.didMoveToWindow() + + if let window = window { + layer.borderWidth = 1 / window.screen.scale + } + } +} diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift index 416226cbb..c17f41074 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift @@ -317,6 +317,22 @@ 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.linkPreview.configure(content: trimmed) + statusView.setLinkPreviewDisplay() + } } else { statusView.contentMetaText.reset() statusView.contentMetaText.textView.accessibilityLabel = "" diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift index 563bc7e3d..a3d953f0a 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift @@ -113,6 +113,8 @@ public final class StatusView: UIView { ] return metaText }() + + public let linkPreview = OpenGraphView() // content warning public let spoilerOverlayView = SpoilerOverlayView() @@ -217,6 +219,7 @@ public final class StatusView: UIView { setMediaDisplay(isDisplay: false) setPollDisplay(isDisplay: false) setFilterHintLabelDisplay(isDisplay: false) + setLinkPreviewDisplay(isDisplay: false) } public override init(frame: CGRect) { @@ -378,7 +381,7 @@ extension StatusView.Style { statusView.contentContainer.axis = .vertical statusView.contentContainer.spacing = 12 statusView.contentContainer.distribution = .fill - statusView.contentContainer.alignment = .top + statusView.contentContainer.alignment = .fill statusView.contentAdaptiveMarginContainerView.contentView = statusView.contentContainer statusView.contentAdaptiveMarginContainerView.margin = StatusView.containerLayoutMargin @@ -389,6 +392,8 @@ extension StatusView.Style { // status content statusView.contentContainer.addArrangedSubview(statusView.contentMetaText.textView) statusView.containerStackView.setCustomSpacing(16, after: statusView.contentMetaText.textView) + statusView.contentContainer.addArrangedSubview(statusView.linkPreview) + statusView.containerStackView.setCustomSpacing(16, after: statusView.linkPreview) statusView.spoilerOverlayView.translatesAutoresizingMaskIntoConstraints = false statusView.containerStackView.addSubview(statusView.spoilerOverlayView) @@ -539,6 +544,10 @@ extension StatusView { func setFilterHintLabelDisplay(isDisplay: Bool = true) { filterHintLabel.isHidden = !isDisplay } + + func setLinkPreviewDisplay(isDisplay: Bool = true) { + linkPreview.isHidden = !isDisplay + } // container width public var contentMaxLayoutWidth: CGFloat { From ae24f95e313a9f1c40c7a4278755cee4dfd9f60e Mon Sep 17 00:00:00 2001 From: Kyle Bashour Date: Mon, 14 Nov 2022 13:26:25 -0800 Subject: [PATCH 02/41] wip --- ...raphView.swift => LinkPreviewButton.swift} | 56 +++++++++++++++++-- .../View/Content/StatusView+ViewModel.swift | 4 +- .../MastodonUI/View/Content/StatusView.swift | 12 ++-- 3 files changed, 59 insertions(+), 13 deletions(-) rename MastodonSDK/Sources/MastodonUI/View/Content/{OpenGraphView.swift => LinkPreviewButton.swift} (64%) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/OpenGraphView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/LinkPreviewButton.swift similarity index 64% rename from MastodonSDK/Sources/MastodonUI/View/Content/OpenGraphView.swift rename to MastodonSDK/Sources/MastodonUI/View/Content/LinkPreviewButton.swift index f03fda847..344511217 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/OpenGraphView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/LinkPreviewButton.swift @@ -5,12 +5,17 @@ // Created by Kyle Bashour on 11/11/22. // +import AlamofireImage +import LinkPresentation import MastodonAsset import MastodonCore import OpenGraph import UIKit -public final class OpenGraphView: UIControl { +public final class LinkPreviewButton: UIControl { + private var linkPresentationTask: Task? + private var url: URL? + private let containerStackView = UIStackView() private let labelStackView = UIStackView() @@ -27,17 +32,20 @@ public final class OpenGraphView: UIControl { layer.borderColor = ThemeService.shared.currentTheme.value.separator.cgColor backgroundColor = ThemeService.shared.currentTheme.value.systemElevatedBackgroundColor - titleLabel.numberOfLines = 0 + 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) imageView.backgroundColor = UIColor.black.withAlphaComponent(0.15) + imageView.contentMode = .scaleAspectFill + imageView.clipsToBounds = true labelStackView.addArrangedSubview(titleLabel) labelStackView.addArrangedSubview(subtitleLabel) @@ -55,12 +63,13 @@ public final class OpenGraphView: UIControl { containerStackView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - containerStackView.heightAnchor.constraint(equalToConstant: 80), + 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), ]) } @@ -68,8 +77,37 @@ public final class OpenGraphView: UIControl { fatalError("init(coder:) has not been implemented") } - public func configure(content: String) { - self.subtitleLabel.text = content + public func configure(url: URL, trimmed: String) { + guard url != self.url else { + return + } + + reset() + subtitleLabel.text = trimmed + self.url = url + + 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" + } + } } public override func didMoveToWindow() { @@ -79,4 +117,12 @@ public final class OpenGraphView: UIControl { layer.borderWidth = 1 / window.screen.scale } } + + private func reset() { + linkPresentationTask?.cancel() + url = nil + imageView.image = nil + titleLabel.text = nil + subtitleLabel.text = nil + } } diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift index c17f41074..e18bfc953 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift @@ -330,8 +330,8 @@ extension StatusView.ViewModel { fatalError() } - statusView.linkPreview.configure(content: trimmed) - statusView.setLinkPreviewDisplay() + statusView.linkPreviewButton.configure(url: url, trimmed: trimmed) + statusView.setLinkPreviewButtonDisplay() } } else { statusView.contentMetaText.reset() diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift index a3d953f0a..eab862411 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift @@ -114,7 +114,7 @@ public final class StatusView: UIView { return metaText }() - public let linkPreview = OpenGraphView() + public let linkPreviewButton = LinkPreviewButton() // content warning public let spoilerOverlayView = SpoilerOverlayView() @@ -219,7 +219,7 @@ public final class StatusView: UIView { setMediaDisplay(isDisplay: false) setPollDisplay(isDisplay: false) setFilterHintLabelDisplay(isDisplay: false) - setLinkPreviewDisplay(isDisplay: false) + setLinkPreviewButtonDisplay(isDisplay: false) } public override init(frame: CGRect) { @@ -392,8 +392,8 @@ extension StatusView.Style { // status content statusView.contentContainer.addArrangedSubview(statusView.contentMetaText.textView) statusView.containerStackView.setCustomSpacing(16, after: statusView.contentMetaText.textView) - statusView.contentContainer.addArrangedSubview(statusView.linkPreview) - statusView.containerStackView.setCustomSpacing(16, after: statusView.linkPreview) + statusView.contentContainer.addArrangedSubview(statusView.linkPreviewButton) + statusView.containerStackView.setCustomSpacing(16, after: statusView.linkPreviewButton) statusView.spoilerOverlayView.translatesAutoresizingMaskIntoConstraints = false statusView.containerStackView.addSubview(statusView.spoilerOverlayView) @@ -545,8 +545,8 @@ extension StatusView { filterHintLabel.isHidden = !isDisplay } - func setLinkPreviewDisplay(isDisplay: Bool = true) { - linkPreview.isHidden = !isDisplay + func setLinkPreviewButtonDisplay(isDisplay: Bool = true) { + linkPreviewButton.isHidden = !isDisplay } // container width From a4cab15d86cc2f4bfafc85843e3a1fdf77afc8cc Mon Sep 17 00:00:00 2001 From: Kyle Bashour Date: Wed, 23 Nov 2022 19:03:54 -0800 Subject: [PATCH 03/41] Make it compile --- .../xcshareddata/swiftpm/Package.resolved | 33 +++++++------------ .../View/Content/LinkPreviewButton.swift | 1 - 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index 0a62cc7f5..26b58956b 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Alamofire/Alamofire.git", "state" : { - "revision" : "354dda32d89fc8cd4f5c46487f64957d355f53d8", - "version" : "5.6.1" + "revision" : "78424be314842833c04bc3bef5b72e85fff99204", + "version" : "5.6.4" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Flipboard/FLAnimatedImage.git", "state" : { - "revision" : "e7f9fd4681ae41bf6f3056db08af4f401d61da52", - "version" : "1.0.16" + "revision" : "d4f07b6f164d53c1212c3e54d6460738b1981e9f", + "version" : "1.0.17" } }, { @@ -104,8 +104,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/kean/Nuke.git", "state" : { - "revision" : "0ea7545b5c918285aacc044dc75048625c8257cc", - "version" : "10.8.0" + "revision" : "a002b7fd786f2df2ed4333fe73a9727499fd9d97", + "version" : "10.11.2" } }, { @@ -117,22 +117,13 @@ "version" : "8.0.0" } }, - { - "identity" : "opengraph", - "kind" : "remoteSourceControl", - "location" : "https://github.com/satoshi-takano/OpenGraph", - "state" : { - "revision" : "3ef2b8b9b4972b57e9e78c91c26be770c0110057", - "version" : "1.5.0" - } - }, { "identity" : "pageboy", "kind" : "remoteSourceControl", "location" : "https://github.com/uias/Pageboy", "state" : { - "revision" : "34ecb6e7c4e0e07494960ab2f7cc9a02293915a6", - "version" : "3.6.2" + "revision" : "af8fa81788b893205e1ff42ddd88c5b0b315d7c5", + "version" : "3.7.0" } }, { @@ -149,8 +140,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/SDWebImage/SDWebImage.git", "state" : { - "revision" : "2e63d0061da449ad0ed130768d05dceb1496de44", - "version" : "5.12.5" + "revision" : "3312bf5e67b52fbce7c3caf431b0cda721a9f7bb", + "version" : "5.14.2" } }, { @@ -194,8 +185,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/scinfu/SwiftSoup.git", "state" : { - "revision" : "41e7c263fb8c277e980ebcb9b0b5f6031d3d4886", - "version" : "2.4.2" + "revision" : "6778575285177365cbad3e5b8a72f2a20583cfec", + "version" : "2.4.3" } }, { diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/LinkPreviewButton.swift b/MastodonSDK/Sources/MastodonUI/View/Content/LinkPreviewButton.swift index 344511217..94de42ddc 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/LinkPreviewButton.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/LinkPreviewButton.swift @@ -9,7 +9,6 @@ import AlamofireImage import LinkPresentation import MastodonAsset import MastodonCore -import OpenGraph import UIKit public final class LinkPreviewButton: UIControl { From 595b46e96e10f8d16656949629ee6c2947fd3b36 Mon Sep 17 00:00:00 2001 From: Kyle Bashour Date: Wed, 23 Nov 2022 20:03:45 -0800 Subject: [PATCH 04/41] Add card persistence --- .../CoreData.xcdatamodeld/.xccurrentversion | 2 +- .../CoreData 5.xcdatamodel/contents | 271 ++++++++++++++++++ .../CoreDataStack/Entity/Mastodon/Card.swift | 162 +++++++++++ .../Entity/Mastodon/Status.swift | 10 +- .../Entity/Transient/MastodonCardType.swift | 34 +++ .../Persistence/Persistence+Card.swift | 94 ++++++ .../Persistence/Persistence+Status.swift | 18 +- .../Persistence/Persistence.swift | 1 + 8 files changed, 587 insertions(+), 5 deletions(-) create mode 100644 MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 5.xcdatamodel/contents create mode 100644 MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Card.swift create mode 100644 MastodonSDK/Sources/CoreDataStack/Entity/Transient/MastodonCardType.swift create mode 100644 MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Card.swift diff --git a/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/.xccurrentversion b/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/.xccurrentversion index 1d5ea989f..2145ac780 100644 --- a/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/.xccurrentversion +++ b/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - CoreData 4.xcdatamodel + CoreData 5.xcdatamodel diff --git a/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 5.xcdatamodel/contents b/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 5.xcdatamodel/contents new file mode 100644 index 000000000..5a0ef6a6a --- /dev/null +++ b/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 5.xcdatamodel/contents @@ -0,0 +1,271 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Card.swift b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Card.swift new file mode 100644 index 000000000..12f6d636e --- /dev/null +++ b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Card.swift @@ -0,0 +1,162 @@ +// +// Card.swift +// CoreDataStack +// +// Created by Kyle Bashour on 11/23/22. +// + +import Foundation +import CoreData + +public final class Card: NSManagedObject { + // sourcery: autoGenerateProperty + @NSManaged public private(set) var url: String + // sourcery: autoGenerateProperty + @NSManaged public private(set) var title: String + // sourcery: autoGenerateProperty + @NSManaged public private(set) var desc: String + + @NSManaged public private(set) var typeRaw: String + // sourcery: autoGenerateProperty + public var type: MastodonCardType { + get { MastodonCardType(rawValue: typeRaw) } + set { typeRaw = newValue.rawValue } + } + + // sourcery: autoGenerateProperty + @NSManaged public private(set) var authorName: String? + // sourcery: autoGenerateProperty + @NSManaged public private(set) var authorURL: String? + // sourcery: autoGenerateProperty + @NSManaged public private(set) var providerName: String? + // sourcery: autoGenerateProperty + @NSManaged public private(set) var providerURL: 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? + // sourcery: autoGenerateProperty + @NSManaged public private(set) var embedURL: String? + // sourcery: autoGenerateProperty + @NSManaged public private(set) var blurhash: String? + + // one-to-one relationship + @NSManaged public private(set) var status: Status +} + +extension Card { + + @discardableResult + public static func insert( + into context: NSManagedObjectContext, + property: Property + ) -> Card { + let object: Card = context.insertObject() + + object.configure(property: property) + + return object + } + +} + +extension Card: Managed { + public static var defaultSortDescriptors: [NSSortDescriptor] { + return [] + } +} + +// MARK: - AutoGenerateProperty +extension Card: AutoGenerateProperty { + // sourcery:inline:Card.AutoGenerateProperty + + // Generated using Sourcery + // DO NOT EDIT + public struct Property { + public let url: String + public let title: String + public let desc: String + public let type: MastodonCardType + public let authorName: String? + public let authorURL: String? + public let providerName: String? + public let providerURL: String? + public let width: Int64 + public let height: Int64 + public let image: String? + public let embedURL: String? + public let blurhash: String? + + public init( + url: String, + title: String, + desc: String, + type: MastodonCardType, + authorName: String?, + authorURL: String?, + providerName: String?, + providerURL: String?, + width: Int64, + height: Int64, + image: String?, + embedURL: String?, + blurhash: String? + ) { + self.url = url + self.title = title + self.desc = desc + self.type = type + self.authorName = authorName + self.authorURL = authorURL + self.providerName = providerName + self.providerURL = providerURL + self.width = width + self.height = height + self.image = image + self.embedURL = embedURL + self.blurhash = blurhash + } + } + + public func configure(property: Property) { + self.url = property.url + self.title = property.title + self.desc = property.desc + self.type = property.type + self.authorName = property.authorName + self.authorURL = property.authorURL + self.providerName = property.providerName + self.providerURL = property.providerURL + self.width = property.width + self.height = property.height + self.image = property.image + self.embedURL = property.embedURL + self.blurhash = property.blurhash + } + + public func update(property: Property) { + } + + // sourcery:end +} + +// MARK: - AutoGenerateRelationship +extension Card: AutoGenerateRelationship { + // sourcery:inline:Card.AutoGenerateRelationship + + // Generated using Sourcery + // DO NOT EDIT + public struct Relationship { + + public init( + ) { + } + } + + public func configure(relationship: Relationship) { + } + + // sourcery:end +} diff --git a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Status.swift b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Status.swift index 0c7291913..1bc641f5f 100644 --- a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Status.swift +++ b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Status.swift @@ -84,7 +84,9 @@ public final class Status: NSManagedObject { @NSManaged public private(set) var pinnedBy: MastodonUser? // sourcery: autoGenerateRelationship @NSManaged public private(set) var poll: Poll? - + // sourcery: autoGenerateRelationship + @NSManaged public private(set) var card: Card? + // one-to-many relationship @NSManaged public private(set) var feeds: Set @@ -379,15 +381,18 @@ extension Status: AutoGenerateRelationship { public let author: MastodonUser public let reblog: Status? public let poll: Poll? + public let card: Card? public init( author: MastodonUser, reblog: Status?, - poll: Poll? + poll: Poll?, + card: Card? ) { self.author = author self.reblog = reblog self.poll = poll + self.card = card } } @@ -395,6 +400,7 @@ extension Status: AutoGenerateRelationship { self.author = relationship.author self.reblog = relationship.reblog self.poll = relationship.poll + self.card = relationship.card } // sourcery:end } diff --git a/MastodonSDK/Sources/CoreDataStack/Entity/Transient/MastodonCardType.swift b/MastodonSDK/Sources/CoreDataStack/Entity/Transient/MastodonCardType.swift new file mode 100644 index 000000000..59401cf4a --- /dev/null +++ b/MastodonSDK/Sources/CoreDataStack/Entity/Transient/MastodonCardType.swift @@ -0,0 +1,34 @@ +// +// MastodonCardType.swift +// CoreDataStack +// +// Created by Kyle Bashour on 11/23/22. +// + +import Foundation + +public enum MastodonCardType: RawRepresentable, Equatable { + case link + case photo + case video + + case _other(String) + + public init(rawValue: String) { + switch rawValue { + case "link": self = .link + case "photo": self = .photo + case "video": self = .video + default: self = ._other(rawValue) + } + } + + public var rawValue: String { + switch self { + case .link: return "link" + case .photo: return "photo" + case .video: return "video" + case ._other(let value): return value + } + } +} diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Card.swift b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Card.swift new file mode 100644 index 000000000..8c3b4c312 --- /dev/null +++ b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Card.swift @@ -0,0 +1,94 @@ +// +// Persistence+Card.swift +// +// +// Created by MainasuK on 2021-12-9. +// + +import CoreData +import CoreDataStack +import Foundation +import MastodonSDK +import os.log + +extension Persistence.Card { + + public struct PersistContext { + public let domain: String + public let entity: Mastodon.Entity.Card + public let me: MastodonUser? + public let log = Logger(subsystem: "Card", category: "Persistence") + public init( + domain: String, + entity: Mastodon.Entity.Card, + me: MastodonUser? + ) { + self.domain = domain + self.entity = entity + self.me = me + } + } + + public struct PersistResult { + public let card: Card + public let isNewInsertion: Bool + + public init( + card: Card, + isNewInsertion: Bool + ) { + self.card = card + self.isNewInsertion = isNewInsertion + } + + #if DEBUG + public let logger = Logger(subsystem: "Persistence.MastodonCard.PersistResult", category: "Persist") + public func log() { + let pollInsertionFlag = isNewInsertion ? "+" : "-" + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(pollInsertionFlag)](\(card.title)):") + } + #endif + } + + public static func create( + in managedObjectContext: NSManagedObjectContext, + context: PersistContext + ) -> PersistResult { + var type: MastodonCardType { + switch context.entity.type { + case .link: return .link + case .photo: return .photo + case .video: return .video + case .rich: return ._other(context.entity.type.rawValue) + case ._other(let rawValue): return ._other(rawValue) + } + } + + let property = Card.Property( + url: context.entity.url, + title: context.entity.title, + desc: context.entity.description, + type: type, + authorName: context.entity.authorName, + authorURL: context.entity.authorURL, + providerName: context.entity.providerName, + providerURL: context.entity.providerURL, + width: Int64(context.entity.width ?? 0), + height: Int64(context.entity.height ?? 0), + image: context.entity.image, + embedURL: context.entity.embedURL, + blurhash: context.entity.blurhash + ) + + let card = Card.insert( + into: managedObjectContext, + property: property + ) + + return PersistResult( + card: card, + isNewInsertion: true + ) + } + +} diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Status.swift b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Status.swift index a15e974e4..97ab32e30 100644 --- a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Status.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Status.swift @@ -107,7 +107,20 @@ extension Persistence.Status { ) return result.poll }() - + + let card: Card? = { + guard let entity = context.entity.card else { return nil } + let result = Persistence.Card.create( + in: managedObjectContext, + context: Persistence.Card.PersistContext( + domain: context.domain, + entity: entity, + me: context.me + ) + ) + return result.card + }() + let authorResult = Persistence.MastodonUser.createOrMerge( in: managedObjectContext, context: Persistence.MastodonUser.PersistContext( @@ -122,7 +135,8 @@ extension Persistence.Status { let relationship = Status.Relationship( author: author, reblog: reblog, - poll: poll + poll: poll, + card: card ) let status = create( in: managedObjectContext, diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift index 350b603cc..3a36dec41 100644 --- a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift @@ -15,6 +15,7 @@ extension Persistence { public enum MastodonUser { } public enum Status { } public enum Poll { } + public enum Card { } public enum PollOption { } public enum Tag { } public enum SearchHistory { } From f8d1afc7e47255cbd1adc420eb25465a1f80fead Mon Sep 17 00:00:00 2001 From: Kyle Bashour Date: Wed, 23 Nov 2022 21:51:39 -0800 Subject: [PATCH 05/41] Working pretty well --- .../CoreData 5.xcdatamodel/contents | 8 +- .../CoreDataStack/Entity/Mastodon/Card.swift | 48 +++++---- .../Persistence/Persistence+Card.swift | 8 +- .../View/Content/LinkPreviewButton.swift | 98 +++++++++---------- .../Content/StatusView+Configuration.swift | 20 ++++ .../View/Content/StatusView+ViewModel.swift | 30 +++--- 6 files changed, 119 insertions(+), 93 deletions(-) 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 From ba7955bdb5cf49cdfde49cc108544991c5c74535 Mon Sep 17 00:00:00 2001 From: Kyle Bashour Date: Thu, 24 Nov 2022 07:48:07 -0800 Subject: [PATCH 06/41] Handle taps --- .../Provider/DataSourceFacade+Meta.swift | 18 ++---- .../Provider/DataSourceFacade+URL.swift | 32 +++++++++++ ...er+NotificationTableViewCellDelegate.swift | 55 +++++++++++++++++++ ...Provider+StatusTableViewCellDelegate.swift | 31 ++++++++++- .../NotificationTableViewCellDelegate.swift | 10 ++++ .../StatusTableViewCellDelegate.swift | 5 ++ .../View/Content/LinkPreviewButton.swift | 20 ++++++- .../View/Content/NotificationView.swift | 14 ++++- .../MastodonUI/View/Content/StatusView.swift | 15 ++++- 9 files changed, 184 insertions(+), 16 deletions(-) create mode 100644 Mastodon/Protocol/Provider/DataSourceFacade+URL.swift diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Meta.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Meta.swift index dd3c4903a..ca3bbd474 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Meta.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Meta.swift @@ -48,18 +48,12 @@ extension DataSourceFacade { assertionFailure() return } - let domain = provider.authContext.mastodonAuthenticationBox.domain - if url.host == domain, - url.pathComponents.count >= 4, - url.pathComponents[0] == "/", - url.pathComponents[1] == "web", - url.pathComponents[2] == "statuses" { - let statusID = url.pathComponents[3] - let threadViewModel = RemoteThreadViewModel(context: provider.context, authContext: provider.authContext, statusID: statusID) - _ = await provider.coordinator.present(scene: .thread(viewModel: threadViewModel), from: nil, transition: .show) - } else { - _ = await provider.coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil)) - } + + await responseToURLAction( + provider: provider, + status: status, + url: url + ) case .hashtag(_, let hashtag, _): let hashtagTimelineViewModel = HashtagTimelineViewModel(context: provider.context, authContext: provider.authContext, hashtag: hashtag) _ = await provider.coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: provider, transition: .show) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+URL.swift b/Mastodon/Protocol/Provider/DataSourceFacade+URL.swift new file mode 100644 index 000000000..a65de9537 --- /dev/null +++ b/Mastodon/Protocol/Provider/DataSourceFacade+URL.swift @@ -0,0 +1,32 @@ +// +// DataSourceFacade+URL.swift +// Mastodon +// +// Created by Kyle Bashour on 11/24/22. +// + +import Foundation +import CoreDataStack +import MetaTextKit +import MastodonCore + +extension DataSourceFacade { + static func responseToURLAction( + provider: DataSourceProvider & AuthContextProvider, + status: ManagedObjectRecord, + url: URL + ) async { + let domain = provider.authContext.mastodonAuthenticationBox.domain + if url.host == domain, + url.pathComponents.count >= 4, + url.pathComponents[0] == "/", + url.pathComponents[1] == "web", + url.pathComponents[2] == "statuses" { + let statusID = url.pathComponents[3] + let threadViewModel = RemoteThreadViewModel(context: provider.context, authContext: provider.authContext, statusID: statusID) + _ = await provider.coordinator.present(scene: .thread(viewModel: threadViewModel), from: nil, transition: .show) + } else { + _ = await provider.coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil)) + } + } +} diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift index e868f418f..279cb562e 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift @@ -516,6 +516,61 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut } +// MARK: - card +extension NotificationTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { + + func tableViewCell( + _ cell: UITableViewCell, + notificationView: NotificationView, + statusView: StatusView, + didTapCardWithURL url: URL + ) { + Task { + let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) + guard let item = await item(from: source) else { + assertionFailure() + return + } + guard case let .status(status) = item else { + assertionFailure("only works for status data provider") + return + } + + await DataSourceFacade.responseToURLAction( + provider: self, + status: status, + url: url + ) + } + } + + func tableViewCell( + _ cell: UITableViewCell, + notificationView: NotificationView, + quoteStatusView: StatusView, + didTapCardWithURL url: URL + ) { + Task { + let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) + guard let item = await item(from: source) else { + assertionFailure() + return + } + guard case let .status(status) = item else { + assertionFailure("only works for status data provider") + return + } + + await DataSourceFacade.responseToURLAction( + provider: self, + status: status, + url: url + ) + } + } + +} + // MARK: a11y extension NotificationTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, accessibilityActivate: Void) { diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift index c157b7086..721263f30 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift @@ -120,7 +120,36 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte ) } } - + +} + +// MARK: - card +extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { + + func tableViewCell( + _ cell: UITableViewCell, + statusView: StatusView, + didTapCardWithURL url: URL + ) { + Task { + let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) + guard let item = await item(from: source) else { + assertionFailure() + return + } + guard case let .status(status) = item else { + assertionFailure("only works for status data provider") + return + } + + await DataSourceFacade.responseToURLAction( + provider: self, + status: status, + url: url + ) + } + } + } // MARK: - media diff --git a/Mastodon/Scene/Notification/Cell/NotificationTableViewCellDelegate.swift b/Mastodon/Scene/Notification/Cell/NotificationTableViewCellDelegate.swift index 7a603d5f0..45ad59334 100644 --- a/Mastodon/Scene/Notification/Cell/NotificationTableViewCellDelegate.swift +++ b/Mastodon/Scene/Notification/Cell/NotificationTableViewCellDelegate.swift @@ -28,11 +28,13 @@ protocol NotificationTableViewCellDelegate: AnyObject, AutoGenerateProtocolDeleg func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, acceptFollowRequestButtonDidPressed button: UIButton) func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, rejectFollowRequestButtonDidPressed button: UIButton) func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) + func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, statusView: StatusView, didTapCardWithURL url: URL) func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, statusView: StatusView, spoilerOverlayViewDidPressed overlayView: SpoilerOverlayView) func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaView: MediaView, didSelectMediaViewAt index: Int) func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, statusView: StatusView, actionToolbarContainer: ActionToolbarContainer, buttonDidPressed button: UIButton, action: ActionToolbarContainer.Action) func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, quoteStatusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton) func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, quoteStatusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) + func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, quoteStatusView: StatusView, didTapCardWithURL url: URL) func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, quoteStatusView: StatusView, spoilerOverlayViewDidPressed overlayView: SpoilerOverlayView) func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, quoteStatusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaView: MediaView, didSelectMediaViewAt index: Int) func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, accessibilityActivate: Void) @@ -63,6 +65,10 @@ extension NotificationViewDelegate where Self: NotificationViewContainerTableVie delegate?.tableViewCell(self, notificationView: notificationView, statusView: statusView, metaText: metaText, didSelectMeta: meta) } + func notificationView(_ notificationView: NotificationView, statusView: StatusView, didTapCardWithURL url: URL) { + delegate?.tableViewCell(self, notificationView: notificationView, statusView: statusView, didTapCardWithURL: url) + } + func notificationView(_ notificationView: NotificationView, statusView: StatusView, spoilerOverlayViewDidPressed overlayView: SpoilerOverlayView) { delegate?.tableViewCell(self, notificationView: notificationView, statusView: statusView, spoilerOverlayViewDidPressed: overlayView) } @@ -83,6 +89,10 @@ extension NotificationViewDelegate where Self: NotificationViewContainerTableVie delegate?.tableViewCell(self, notificationView: notificationView, quoteStatusView: quoteStatusView, metaText: metaText, didSelectMeta: meta) } + func notificationView(_ notificationView: NotificationView, quoteStatusView: StatusView, didTapCardWithURL url: URL) { + delegate?.tableViewCell(self, notificationView: notificationView, quoteStatusView: quoteStatusView, didTapCardWithURL: url) + } + func notificationView(_ notificationView: NotificationView, quoteStatusView: StatusView, spoilerOverlayViewDidPressed overlayView: SpoilerOverlayView) { delegate?.tableViewCell(self, notificationView: notificationView, quoteStatusView: quoteStatusView, spoilerOverlayViewDidPressed: overlayView) } diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCellDelegate.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCellDelegate.swift index 034985c03..e76ba5006 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCellDelegate.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCellDelegate.swift @@ -27,6 +27,7 @@ protocol StatusTableViewCellDelegate: AnyObject, AutoGenerateProtocolDelegate { func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton) func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, contentSensitiveeToggleButtonDidPressed button: UIButton) func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) + func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, didTapCardWithURL url: URL) func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaView: MediaView, didSelectMediaViewAt index: Int) func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, pollTableView tableView: UITableView, didSelectRowAt indexPath: IndexPath) func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, pollVoteButtonPressed button: UIButton) @@ -61,6 +62,10 @@ extension StatusViewDelegate where Self: StatusViewContainerTableViewCell { delegate?.tableViewCell(self, statusView: statusView, metaText: metaText, didSelectMeta: meta) } + func statusView(_ statusView: StatusView, didTapCardWithURL url: URL) { + delegate?.tableViewCell(self, statusView: statusView, didTapCardWithURL: url) + } + func statusView(_ statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaView: MediaView, didSelectMediaViewAt index: Int) { delegate?.tableViewCell(self, statusView: statusView, mediaGridContainerView: mediaGridContainerView, mediaView: mediaView, didSelectMediaViewAt: index) } diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/LinkPreviewButton.swift b/MastodonSDK/Sources/MastodonUI/View/Content/LinkPreviewButton.swift index 4cb2dca6d..fc5d8b247 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/LinkPreviewButton.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/LinkPreviewButton.swift @@ -16,6 +16,7 @@ public final class LinkPreviewButton: UIControl { private let containerStackView = UIStackView() private let labelStackView = UIStackView() + private let highlightView = UIView() private let imageView = UIImageView() private let titleLabel = UILabel() private let linkLabel = UILabel() @@ -30,6 +31,12 @@ public final class LinkPreviewButton: UIControl { imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 21 / 40), ] + public override var isHighlighted: Bool { + didSet { + highlightView.isHidden = !isHighlighted + } + } + public override init(frame: CGRect) { super.init(frame: frame) @@ -38,6 +45,9 @@ public final class LinkPreviewButton: UIControl { layer.cornerRadius = 10 layer.borderColor = ThemeService.shared.currentTheme.value.separator.cgColor + highlightView.backgroundColor = UIColor.black.withAlphaComponent(0.1) + highlightView.isHidden = true + titleLabel.numberOfLines = 2 titleLabel.setContentCompressionResistancePriority(.defaultLow - 1, for: .horizontal) titleLabel.text = "This is where I'd put a title... if I had one" @@ -61,17 +71,23 @@ public final class LinkPreviewButton: UIControl { containerStackView.addArrangedSubview(imageView) containerStackView.addArrangedSubview(labelStackView) - containerStackView.distribution = .fill addSubview(containerStackView) + addSubview(highlightView) + containerStackView.isUserInteractionEnabled = false containerStackView.translatesAutoresizingMaskIntoConstraints = false + highlightView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ containerStackView.topAnchor.constraint(equalTo: topAnchor), containerStackView.bottomAnchor.constraint(equalTo: bottomAnchor), containerStackView.leadingAnchor.constraint(equalTo: leadingAnchor), containerStackView.trailingAnchor.constraint(equalTo: trailingAnchor), + highlightView.topAnchor.constraint(equalTo: topAnchor), + highlightView.bottomAnchor.constraint(equalTo: bottomAnchor), + highlightView.leadingAnchor.constraint(equalTo: leadingAnchor), + highlightView.trailingAnchor.constraint(equalTo: trailingAnchor), ]) } @@ -100,10 +116,12 @@ public final class LinkPreviewButton: UIControl { if isCompact { containerStackView.alignment = .center containerStackView.axis = .horizontal + containerStackView.distribution = .fill NSLayoutConstraint.activate(compactImageConstraints) } else { containerStackView.alignment = .fill containerStackView.axis = .vertical + containerStackView.distribution = .equalSpacing NSLayoutConstraint.activate(largeImageConstraints) } } diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift index ddc1add5c..eb077d6db 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift @@ -22,6 +22,7 @@ public protocol NotificationViewDelegate: AnyObject { func notificationView(_ notificationView: NotificationView, rejectFollowRequestButtonDidPressed button: UIButton) func notificationView(_ notificationView: NotificationView, statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) + func notificationView(_ notificationView: NotificationView, statusView: StatusView, didTapCardWithURL url: URL) func notificationView(_ notificationView: NotificationView, statusView: StatusView, spoilerOverlayViewDidPressed overlayView: SpoilerOverlayView) func notificationView(_ notificationView: NotificationView, statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaView: MediaView, didSelectMediaViewAt index: Int) @@ -29,6 +30,7 @@ public protocol NotificationViewDelegate: AnyObject { func notificationView(_ notificationView: NotificationView, quoteStatusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton) func notificationView(_ notificationView: NotificationView, quoteStatusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) + func notificationView(_ notificationView: NotificationView, quoteStatusView: StatusView, didTapCardWithURL url: URL) func notificationView(_ notificationView: NotificationView, quoteStatusView: StatusView, spoilerOverlayViewDidPressed overlayView: SpoilerOverlayView) func notificationView(_ notificationView: NotificationView, quoteStatusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaView: MediaView, didSelectMediaViewAt index: Int) @@ -496,7 +498,17 @@ extension NotificationView { // MARK: - StatusViewDelegate extension NotificationView: StatusViewDelegate { - + public func statusView(_ statusView: StatusView, didTapCardWithURL url: URL) { + switch statusView { + case self.statusView: + delegate?.notificationView(self, statusView: statusView, didTapCardWithURL: url) + case quoteStatusView: + delegate?.notificationView(self, quoteStatusView: statusView, didTapCardWithURL: url) + default: + assertionFailure() + } + } + public func statusView(_ statusView: StatusView, headerDidPressed header: UIView) { // do nothing } diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift index 3e90e03d4..a2621badd 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift @@ -23,6 +23,7 @@ public protocol StatusViewDelegate: AnyObject { func statusView(_ statusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton) func statusView(_ statusView: StatusView, contentSensitiveeToggleButtonDidPressed button: UIButton) func statusView(_ statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) + func statusView(_ statusView: StatusView, didTapCardWithURL url: URL) func statusView(_ statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaView: MediaView, didSelectMediaViewAt index: Int) func statusView(_ statusView: StatusView, pollTableView tableView: UITableView, didSelectRowAt indexPath: IndexPath) func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton) @@ -263,7 +264,13 @@ extension StatusView { // media mediaGridContainerView.delegate = self - + + linkPreviewButton.addTarget( + self, + action: #selector(linkPreviewButtonPressed), + for: .touchUpInside + ) + // poll pollTableView.translatesAutoresizingMaskIntoConstraints = false pollTableViewHeightLayoutConstraint = pollTableView.heightAnchor.constraint(equalToConstant: 44.0).priority(.required - 1) @@ -298,6 +305,12 @@ extension StatusView { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") delegate?.statusView(self, spoilerOverlayViewDidPressed: spoilerOverlayView) } + + @objc private func linkPreviewButtonPressed(_ sender: LinkPreviewButton) { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") + guard let url = viewModel.card?.url else { return } + delegate?.statusView(self, didTapCardWithURL: url) + } } From c8c05afac18cd0a7b7707989484de84fa2c23be3 Mon Sep 17 00:00:00 2001 From: Kyle Bashour Date: Thu, 24 Nov 2022 07:50:10 -0800 Subject: [PATCH 07/41] Revert package upgrades --- .../xcshareddata/swiftpm/Package.resolved | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index 26b58956b..409b8820d 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Alamofire/Alamofire.git", "state" : { - "revision" : "78424be314842833c04bc3bef5b72e85fff99204", - "version" : "5.6.4" + "revision" : "354dda32d89fc8cd4f5c46487f64957d355f53d8", + "version" : "5.6.1" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Flipboard/FLAnimatedImage.git", "state" : { - "revision" : "d4f07b6f164d53c1212c3e54d6460738b1981e9f", - "version" : "1.0.17" + "revision" : "e7f9fd4681ae41bf6f3056db08af4f401d61da52", + "version" : "1.0.16" } }, { @@ -104,8 +104,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/kean/Nuke.git", "state" : { - "revision" : "a002b7fd786f2df2ed4333fe73a9727499fd9d97", - "version" : "10.11.2" + "revision" : "0ea7545b5c918285aacc044dc75048625c8257cc", + "version" : "10.8.0" } }, { @@ -122,8 +122,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/uias/Pageboy", "state" : { - "revision" : "af8fa81788b893205e1ff42ddd88c5b0b315d7c5", - "version" : "3.7.0" + "revision" : "34ecb6e7c4e0e07494960ab2f7cc9a02293915a6", + "version" : "3.6.2" } }, { @@ -140,8 +140,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/SDWebImage/SDWebImage.git", "state" : { - "revision" : "3312bf5e67b52fbce7c3caf431b0cda721a9f7bb", - "version" : "5.14.2" + "revision" : "2e63d0061da449ad0ed130768d05dceb1496de44", + "version" : "5.12.5" } }, { @@ -185,8 +185,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/scinfu/SwiftSoup.git", "state" : { - "revision" : "6778575285177365cbad3e5b8a72f2a20583cfec", - "version" : "2.4.3" + "revision" : "41e7c263fb8c277e980ebcb9b0b5f6031d3d4886", + "version" : "2.4.2" } }, { From 5e36bea7d5bad0598a6d8c9125d24c7ebd58441c Mon Sep 17 00:00:00 2001 From: Kyle Bashour Date: Thu, 24 Nov 2022 08:56:49 -0800 Subject: [PATCH 08/41] Check in project changes --- Mastodon.xcodeproj/project.pbxproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 0c43b71df..453027074 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -23,6 +23,7 @@ 0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D33725E6401400AAD544 /* PickServerCell.swift */; }; 164F0EBC267D4FE400249499 /* BoopSound.caf in Resources */ = {isa = PBXBuildFile; fileRef = 164F0EBB267D4FE400249499 /* BoopSound.caf */; }; 18BC7629F65E6DB12CB8416D /* Pods_Mastodon_MastodonUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */; }; + 27D701F5292FC2D60031BCBB /* DataSourceFacade+URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27D701F4292FC2D60031BCBB /* DataSourceFacade+URL.swift */; }; 2A82294F29262EE000D2A1F7 /* AppContext+NextAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A82294E29262EE000D2A1F7 /* AppContext+NextAccount.swift */; }; 2AE244482927831100BDBF7C /* UIImage+SFSymbols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AE244472927831100BDBF7C /* UIImage+SFSymbols.swift */; }; 2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198642261BF09500F0B013 /* SearchResultItem.swift */; }; @@ -520,6 +521,7 @@ 0FB3D33725E6401400AAD544 /* PickServerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCell.swift; sourceTree = ""; }; 164F0EBB267D4FE400249499 /* BoopSound.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = BoopSound.caf; sourceTree = ""; }; 1D6D967E77A5357E2C6110D9 /* Pods-Mastodon.asdk - debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.asdk - debug.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.asdk - debug.xcconfig"; sourceTree = ""; }; + 27D701F4292FC2D60031BCBB /* DataSourceFacade+URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+URL.swift"; sourceTree = ""; }; 2A82294E29262EE000D2A1F7 /* AppContext+NextAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppContext+NextAccount.swift"; sourceTree = ""; }; 2AE244472927831100BDBF7C /* UIImage+SFSymbols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+SFSymbols.swift"; sourceTree = ""; }; 2D198642261BF09500F0B013 /* SearchResultItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultItem.swift; sourceTree = ""; }; @@ -2089,6 +2091,7 @@ DB63F778279ABF9C00455B82 /* DataSourceFacade+Reblog.swift */, DB63F77A279ACAE500455B82 /* DataSourceFacade+Favorite.swift */, DB0FCB67279507EF006C02E2 /* DataSourceFacade+Meta.swift */, + 27D701F4292FC2D60031BCBB /* DataSourceFacade+URL.swift */, DB0FCB79279576A2006C02E2 /* DataSourceFacade+Thread.swift */, DB63F74627990B0600455B82 /* DataSourceFacade+Hashtag.swift */, DB63F7532799491600455B82 /* DataSourceFacade+SearchHistory.swift */, @@ -3142,6 +3145,7 @@ DB03A793272A7E5700EE37C5 /* SidebarListHeaderView.swift in Sources */, DB4FFC2B269EC39600D62E92 /* SearchToSearchDetailViewControllerAnimatedTransitioning.swift in Sources */, DB5B7298273112C800081888 /* FollowingListViewModel.swift in Sources */, + 27D701F5292FC2D60031BCBB /* DataSourceFacade+URL.swift in Sources */, 0FB3D2F725E4C24D00AAD544 /* MastodonPickServerViewModel.swift in Sources */, DB5B54AE2833C15F00DEF8B2 /* UserListViewModel+Diffable.swift in Sources */, DB482A3F261331E8008AE74C /* UserTimelineViewModel+State.swift in Sources */, From 3a732b688c2bde83f0c1d4b09503e5c01153d905 Mon Sep 17 00:00:00 2001 From: Kyle Bashour Date: Fri, 25 Nov 2022 20:16:42 -0800 Subject: [PATCH 09/41] Better layout --- .../View/Content/LinkPreviewButton.swift | 87 ++++++++++++------- .../View/Content/StatusView+ViewModel.swift | 1 + .../MastodonUI/View/Content/StatusView.swift | 4 +- 3 files changed, 57 insertions(+), 35 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/LinkPreviewButton.swift b/MastodonSDK/Sources/MastodonUI/View/Content/LinkPreviewButton.swift index fc5d8b247..22c9dc858 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/LinkPreviewButton.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/LinkPreviewButton.swift @@ -6,29 +6,38 @@ // import AlamofireImage -import LinkPresentation +import Combine import MastodonAsset import MastodonCore import CoreDataStack import UIKit public final class LinkPreviewButton: UIControl { - private let containerStackView = UIStackView() - private let labelStackView = UIStackView() + private var disposeBag = Set() + private let labelContainer = UIView() private let highlightView = UIView() private let imageView = UIImageView() private let titleLabel = UILabel() private let linkLabel = UILabel() private lazy var compactImageConstraints = [ + labelContainer.topAnchor.constraint(greaterThanOrEqualTo: topAnchor), + labelContainer.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor), + labelContainer.centerYAnchor.constraint(equalTo: centerYAnchor), + labelContainer.leadingAnchor.constraint(equalTo: imageView.trailingAnchor), imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor), imageView.heightAnchor.constraint(equalTo: heightAnchor), - containerStackView.heightAnchor.constraint(equalToConstant: 85), + heightAnchor.constraint(equalToConstant: 85), ] private lazy var largeImageConstraints = [ + labelContainer.topAnchor.constraint(equalTo: imageView.bottomAnchor), + labelContainer.bottomAnchor.constraint(equalTo: bottomAnchor), + labelContainer.leadingAnchor.constraint(equalTo: leadingAnchor), imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 21 / 40), + imageView.trailingAnchor.constraint(equalTo: trailingAnchor), + imageView.widthAnchor.constraint(equalTo: widthAnchor), ] public override var isHighlighted: Bool { @@ -40,50 +49,62 @@ public final class LinkPreviewButton: UIControl { public override init(frame: CGRect) { super.init(frame: frame) + apply(theme: ThemeService.shared.currentTheme.value) + + ThemeService.shared.currentTheme.sink { [weak self] theme in + self?.apply(theme: theme) + }.store(in: &disposeBag) + clipsToBounds = true layer.cornerCurve = .continuous layer.cornerRadius = 10 - layer.borderColor = ThemeService.shared.currentTheme.value.separator.cgColor highlightView.backgroundColor = UIColor.black.withAlphaComponent(0.1) highlightView.isHidden = true 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 + titleLabel.font = .preferredFont(forTextStyle: .body) - linkLabel.text = "Subtitle" linkLabel.numberOfLines = 1 - linkLabel.setContentCompressionResistancePriority(.defaultLow - 1, for: .horizontal) linkLabel.textColor = Asset.Colors.Label.secondary.color + linkLabel.font = .preferredFont(forTextStyle: .subheadline) 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.layoutMargins = .init(top: 8, left: 10, bottom: 8, right: 10) - labelStackView.isLayoutMarginsRelativeArrangement = true - labelStackView.axis = .vertical + labelContainer.addSubview(titleLabel) + labelContainer.addSubview(linkLabel) + labelContainer.layoutMargins = .init(top: 10, left: 10, bottom: 10, right: 10) - containerStackView.addArrangedSubview(imageView) - containerStackView.addArrangedSubview(labelStackView) - - addSubview(containerStackView) + addSubview(imageView) + addSubview(labelContainer) addSubview(highlightView) - containerStackView.isUserInteractionEnabled = false - containerStackView.translatesAutoresizingMaskIntoConstraints = false + subviews.forEach { $0.isUserInteractionEnabled = false } + + labelContainer.translatesAutoresizingMaskIntoConstraints = false + titleLabel.translatesAutoresizingMaskIntoConstraints = false + linkLabel.translatesAutoresizingMaskIntoConstraints = false + imageView.translatesAutoresizingMaskIntoConstraints = false highlightView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - containerStackView.topAnchor.constraint(equalTo: topAnchor), - containerStackView.bottomAnchor.constraint(equalTo: bottomAnchor), - containerStackView.leadingAnchor.constraint(equalTo: leadingAnchor), - containerStackView.trailingAnchor.constraint(equalTo: trailingAnchor), + titleLabel.topAnchor.constraint(equalTo: labelContainer.layoutMarginsGuide.topAnchor), + titleLabel.leadingAnchor.constraint(equalTo: labelContainer.layoutMarginsGuide.leadingAnchor), + titleLabel.trailingAnchor.constraint(equalTo: labelContainer.layoutMarginsGuide.trailingAnchor), + + linkLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 2), + linkLabel.bottomAnchor.constraint(equalTo: labelContainer.layoutMarginsGuide.bottomAnchor), + linkLabel.leadingAnchor.constraint(equalTo: labelContainer.layoutMarginsGuide.leadingAnchor), + linkLabel.trailingAnchor.constraint(equalTo: labelContainer.layoutMarginsGuide.trailingAnchor), + + labelContainer.trailingAnchor.constraint(equalTo: trailingAnchor), + + imageView.topAnchor.constraint(equalTo: topAnchor), + imageView.leadingAnchor.constraint(equalTo: leadingAnchor), + highlightView.topAnchor.constraint(equalTo: topAnchor), highlightView.bottomAnchor.constraint(equalTo: bottomAnchor), highlightView.leadingAnchor.constraint(equalTo: leadingAnchor), @@ -114,16 +135,13 @@ public final class LinkPreviewButton: UIControl { NSLayoutConstraint.deactivate(compactImageConstraints + largeImageConstraints) if isCompact { - containerStackView.alignment = .center - containerStackView.axis = .horizontal - containerStackView.distribution = .fill NSLayoutConstraint.activate(compactImageConstraints) } else { - containerStackView.alignment = .fill - containerStackView.axis = .vertical - containerStackView.distribution = .equalSpacing NSLayoutConstraint.activate(largeImageConstraints) } + + setNeedsLayout() + layoutIfNeeded() } public override func didMoveToWindow() { @@ -139,7 +157,12 @@ public final class LinkPreviewButton: UIControl { } private var photoIcon: UIImage? { - let configuration = UIImage.SymbolConfiguration(pointSize: 40) + let configuration = UIImage.SymbolConfiguration(pointSize: 32) return UIImage(systemName: "photo", withConfiguration: configuration) } + + private func apply(theme: Theme) { + layer.borderColor = theme.separator.cgColor + imageView.backgroundColor = theme.systemElevatedBackgroundColor + } } diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift index e56d82aff..af6a1a680 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift @@ -316,6 +316,7 @@ extension StatusView.ViewModel { } statusView.contentMetaText.textView.alpha = isContentReveal ? 1 : 0 // keep the frame size and only display when revealing + statusView.linkPreviewButton.alpha = isContentReveal ? 1 : 0 statusView.setSpoilerOverlayViewHidden(isHidden: isContentReveal) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift index a2621badd..642ac3007 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift @@ -388,7 +388,7 @@ extension StatusView.Style { // content container: V - [ contentMetaText ] statusView.contentContainer.axis = .vertical - statusView.contentContainer.spacing = 12 + statusView.contentContainer.spacing = 10 statusView.contentContainer.distribution = .fill statusView.contentContainer.alignment = .fill @@ -400,9 +400,7 @@ extension StatusView.Style { // status content statusView.contentContainer.addArrangedSubview(statusView.contentMetaText.textView) - statusView.containerStackView.setCustomSpacing(16, after: statusView.contentMetaText.textView) statusView.contentContainer.addArrangedSubview(statusView.linkPreviewButton) - statusView.containerStackView.setCustomSpacing(16, after: statusView.linkPreviewButton) statusView.spoilerOverlayView.translatesAutoresizingMaskIntoConstraints = false statusView.containerStackView.addSubview(statusView.spoilerOverlayView) From 00af336298c63cbade770a38f412f1b1d56294ad Mon Sep 17 00:00:00 2001 From: Kyle Bashour Date: Fri, 25 Nov 2022 20:20:26 -0800 Subject: [PATCH 10/41] Remove some debugging --- .../View/Content/StatusView+Configuration.swift | 8 -------- 1 file changed, 8 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift index fb28d564d..96165cef6 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift @@ -40,14 +40,6 @@ 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) From 439217d0e1d4bc5962c578762579947f793d738e Mon Sep 17 00:00:00 2001 From: Kyle Bashour Date: Sat, 26 Nov 2022 19:21:47 -0800 Subject: [PATCH 11/41] Constraints work --- .../View/Content/LinkPreviewButton.swift | 83 +++++++++---------- 1 file changed, 37 insertions(+), 46 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/LinkPreviewButton.swift b/MastodonSDK/Sources/MastodonUI/View/Content/LinkPreviewButton.swift index 22c9dc858..db700cca1 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/LinkPreviewButton.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/LinkPreviewButton.swift @@ -15,29 +15,25 @@ import UIKit public final class LinkPreviewButton: UIControl { private var disposeBag = Set() - private let labelContainer = UIView() + private let containerStackView = UIStackView() + private let labelStackView = UIStackView() + private let highlightView = UIView() private let imageView = UIImageView() private let titleLabel = UILabel() private let linkLabel = UILabel() private lazy var compactImageConstraints = [ - labelContainer.topAnchor.constraint(greaterThanOrEqualTo: topAnchor), - labelContainer.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor), - labelContainer.centerYAnchor.constraint(equalTo: centerYAnchor), - labelContainer.leadingAnchor.constraint(equalTo: imageView.trailingAnchor), - imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor), imageView.heightAnchor.constraint(equalTo: heightAnchor), + imageView.widthAnchor.constraint(equalTo: heightAnchor), heightAnchor.constraint(equalToConstant: 85), ] private lazy var largeImageConstraints = [ - labelContainer.topAnchor.constraint(equalTo: imageView.bottomAnchor), - labelContainer.bottomAnchor.constraint(equalTo: bottomAnchor), - labelContainer.leadingAnchor.constraint(equalTo: leadingAnchor), - imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 21 / 40), - imageView.trailingAnchor.constraint(equalTo: trailingAnchor), - imageView.widthAnchor.constraint(equalTo: widthAnchor), + imageView.heightAnchor.constraint( + equalTo: imageView.widthAnchor, + multiplier: 21 / 40 + ).priority(.defaultLow - 1), ] public override var isHighlighted: Bool { @@ -73,43 +69,29 @@ public final class LinkPreviewButton: UIControl { imageView.tintColor = Asset.Colors.Label.secondary.color imageView.contentMode = .scaleAspectFill imageView.clipsToBounds = true + imageView.setContentHuggingPriority(.zero, for: .horizontal) + imageView.setContentHuggingPriority(.zero, for: .vertical) + imageView.setContentCompressionResistancePriority(.zero, for: .horizontal) + imageView.setContentCompressionResistancePriority(.zero, for: .vertical) - labelContainer.addSubview(titleLabel) - labelContainer.addSubview(linkLabel) - labelContainer.layoutMargins = .init(top: 10, left: 10, bottom: 10, right: 10) + labelStackView.addArrangedSubview(titleLabel) + labelStackView.addArrangedSubview(linkLabel) + labelStackView.layoutMargins = .init(top: 10, left: 10, bottom: 10, right: 10) + labelStackView.isLayoutMarginsRelativeArrangement = true + labelStackView.axis = .vertical - addSubview(imageView) - addSubview(labelContainer) + containerStackView.addArrangedSubview(imageView) + containerStackView.addArrangedSubview(labelStackView) + containerStackView.isUserInteractionEnabled = false + + addSubview(containerStackView) addSubview(highlightView) - subviews.forEach { $0.isUserInteractionEnabled = false } - - labelContainer.translatesAutoresizingMaskIntoConstraints = false - titleLabel.translatesAutoresizingMaskIntoConstraints = false - linkLabel.translatesAutoresizingMaskIntoConstraints = false - imageView.translatesAutoresizingMaskIntoConstraints = false + containerStackView.translatesAutoresizingMaskIntoConstraints = false highlightView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - titleLabel.topAnchor.constraint(equalTo: labelContainer.layoutMarginsGuide.topAnchor), - titleLabel.leadingAnchor.constraint(equalTo: labelContainer.layoutMarginsGuide.leadingAnchor), - titleLabel.trailingAnchor.constraint(equalTo: labelContainer.layoutMarginsGuide.trailingAnchor), - - linkLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 2), - linkLabel.bottomAnchor.constraint(equalTo: labelContainer.layoutMarginsGuide.bottomAnchor), - linkLabel.leadingAnchor.constraint(equalTo: labelContainer.layoutMarginsGuide.leadingAnchor), - linkLabel.trailingAnchor.constraint(equalTo: labelContainer.layoutMarginsGuide.trailingAnchor), - - labelContainer.trailingAnchor.constraint(equalTo: trailingAnchor), - - imageView.topAnchor.constraint(equalTo: topAnchor), - imageView.leadingAnchor.constraint(equalTo: leadingAnchor), - - highlightView.topAnchor.constraint(equalTo: topAnchor), - highlightView.bottomAnchor.constraint(equalTo: bottomAnchor), - highlightView.leadingAnchor.constraint(equalTo: leadingAnchor), - highlightView.trailingAnchor.constraint(equalTo: trailingAnchor), - ]) + containerStackView.pinToParent() + highlightView.pinToParent() } required init?(coder: NSCoder) { @@ -129,19 +111,24 @@ public final class LinkPreviewButton: UIControl { ) { [weak imageView] image, _, _, _ in if image != nil { imageView?.contentMode = .scaleAspectFill + self.containerStackView.setNeedsLayout() + self.containerStackView.layoutIfNeeded() } } NSLayoutConstraint.deactivate(compactImageConstraints + largeImageConstraints) if isCompact { + containerStackView.alignment = .center + containerStackView.axis = .horizontal + containerStackView.distribution = .fill NSLayoutConstraint.activate(compactImageConstraints) } else { + containerStackView.alignment = .fill + containerStackView.axis = .vertical + containerStackView.distribution = .equalSpacing NSLayoutConstraint.activate(largeImageConstraints) } - - setNeedsLayout() - layoutIfNeeded() } public override func didMoveToWindow() { @@ -166,3 +153,7 @@ public final class LinkPreviewButton: UIControl { imageView.backgroundColor = theme.systemElevatedBackgroundColor } } + +private extension UILayoutPriority { + static let zero = UILayoutPriority(rawValue: 0) +} From 4616d405190fc944c23c20628ca548e7a20d258c Mon Sep 17 00:00:00 2001 From: Kyle Bashour Date: Sat, 26 Nov 2022 19:22:05 -0800 Subject: [PATCH 12/41] More spacing --- MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift index 642ac3007..070bbc92e 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift @@ -388,7 +388,7 @@ extension StatusView.Style { // content container: V - [ contentMetaText ] statusView.contentContainer.axis = .vertical - statusView.contentContainer.spacing = 10 + statusView.contentContainer.spacing = 12 statusView.contentContainer.distribution = .fill statusView.contentContainer.alignment = .fill From 3a90b1c8652b40042ad619978ba1163d56757012 Mon Sep 17 00:00:00 2001 From: Kyle Bashour Date: Sat, 26 Nov 2022 21:47:49 -0800 Subject: [PATCH 13/41] Change name and improve a11y --- ...ewButton.swift => StatusCardControl.swift} | 26 ++++++++++++++----- .../View/Content/StatusView+ViewModel.swift | 6 ++--- .../MastodonUI/View/Content/StatusView.swift | 25 ++++++++---------- 3 files changed, 33 insertions(+), 24 deletions(-) rename MastodonSDK/Sources/MastodonUI/View/Content/{LinkPreviewButton.swift => StatusCardControl.swift} (87%) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/LinkPreviewButton.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift similarity index 87% rename from MastodonSDK/Sources/MastodonUI/View/Content/LinkPreviewButton.swift rename to MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift index db700cca1..3ff39be60 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/LinkPreviewButton.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift @@ -12,7 +12,7 @@ import MastodonCore import CoreDataStack import UIKit -public final class LinkPreviewButton: UIControl { +public final class StatusCardControl: UIControl { private var disposeBag = Set() private let containerStackView = UIStackView() @@ -25,21 +25,23 @@ public final class LinkPreviewButton: UIControl { private lazy var compactImageConstraints = [ imageView.heightAnchor.constraint(equalTo: heightAnchor), - imageView.widthAnchor.constraint(equalTo: heightAnchor), - heightAnchor.constraint(equalToConstant: 85), + imageView.widthAnchor.constraint(equalToConstant: 85), + heightAnchor.constraint(equalToConstant: 85).priority(.defaultLow - 1), + heightAnchor.constraint(greaterThanOrEqualToConstant: 85) ] private lazy var largeImageConstraints = [ imageView.heightAnchor.constraint( equalTo: imageView.widthAnchor, multiplier: 21 / 40 - ).priority(.defaultLow - 1), + ) + // This priority is important or constraints break; + // it still renders the card correctly. + .priority(.defaultLow - 1), ] public override var isHighlighted: Bool { - didSet { - highlightView.isHidden = !isHighlighted - } + didSet { highlightView.isHidden = !isHighlighted } } public override init(frame: CGRect) { @@ -55,6 +57,10 @@ public final class LinkPreviewButton: UIControl { layer.cornerCurve = .continuous layer.cornerRadius = 10 + if #available(iOS 15, *) { + maximumContentSizeCategory = .accessibilityLarge + } + highlightView.backgroundColor = UIColor.black.withAlphaComponent(0.1) highlightView.isHidden = true @@ -101,6 +107,12 @@ public final class LinkPreviewButton: UIControl { public func configure(card: Card) { let isCompact = card.width == card.height + if let host = card.url?.host { + accessibilityLabel = "\(card.title) \(host)" + } else { + accessibilityLabel = card.title + } + titleLabel.text = card.title linkLabel.text = card.url?.host imageView.contentMode = .center diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift index af6a1a680..77de106b1 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift @@ -316,7 +316,7 @@ extension StatusView.ViewModel { } statusView.contentMetaText.textView.alpha = isContentReveal ? 1 : 0 // keep the frame size and only display when revealing - statusView.linkPreviewButton.alpha = isContentReveal ? 1 : 0 + statusView.statusCardControl.alpha = isContentReveal ? 1 : 0 statusView.setSpoilerOverlayViewHidden(isHidden: isContentReveal) @@ -490,8 +490,8 @@ extension StatusView.ViewModel { private func bindCard(statusView: StatusView) { $card.sink { card in guard let card = card else { return } - statusView.linkPreviewButton.configure(card: card) - statusView.setLinkPreviewButtonDisplay() + statusView.statusCardControl.configure(card: card) + statusView.setStatusCardControlDisplay() } .store(in: &disposeBag) } diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift index 070bbc92e..249e2e1ec 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift @@ -115,7 +115,7 @@ public final class StatusView: UIView { return metaText }() - public let linkPreviewButton = LinkPreviewButton() + public let statusCardControl = StatusCardControl() // content warning public let spoilerOverlayView = SpoilerOverlayView() @@ -220,7 +220,7 @@ public final class StatusView: UIView { setMediaDisplay(isDisplay: false) setPollDisplay(isDisplay: false) setFilterHintLabelDisplay(isDisplay: false) - setLinkPreviewButtonDisplay(isDisplay: false) + setStatusCardControlDisplay(isDisplay: false) } public override init(frame: CGRect) { @@ -261,16 +261,13 @@ extension StatusView { // content contentMetaText.textView.delegate = self contentMetaText.textView.linkDelegate = self - + + // card + statusCardControl.addTarget(self, action: #selector(statusCardControlPressed), for: .touchUpInside) + // media mediaGridContainerView.delegate = self - linkPreviewButton.addTarget( - self, - action: #selector(linkPreviewButtonPressed), - for: .touchUpInside - ) - // poll pollTableView.translatesAutoresizingMaskIntoConstraints = false pollTableViewHeightLayoutConstraint = pollTableView.heightAnchor.constraint(equalToConstant: 44.0).priority(.required - 1) @@ -306,7 +303,7 @@ extension StatusView { delegate?.statusView(self, spoilerOverlayViewDidPressed: spoilerOverlayView) } - @objc private func linkPreviewButtonPressed(_ sender: LinkPreviewButton) { + @objc private func statusCardControlPressed(_ sender: StatusCardControl) { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") guard let url = viewModel.card?.url else { return } delegate?.statusView(self, didTapCardWithURL: url) @@ -386,7 +383,7 @@ extension StatusView.Style { statusView.authorAdaptiveMarginContainerView.margin = StatusView.containerLayoutMargin statusView.containerStackView.addArrangedSubview(statusView.authorAdaptiveMarginContainerView) - // content container: V - [ contentMetaText ] + // content container: V - [ contentMetaText statusCardControl ] statusView.contentContainer.axis = .vertical statusView.contentContainer.spacing = 12 statusView.contentContainer.distribution = .fill @@ -400,7 +397,7 @@ extension StatusView.Style { // status content statusView.contentContainer.addArrangedSubview(statusView.contentMetaText.textView) - statusView.contentContainer.addArrangedSubview(statusView.linkPreviewButton) + statusView.contentContainer.addArrangedSubview(statusView.statusCardControl) statusView.spoilerOverlayView.translatesAutoresizingMaskIntoConstraints = false statusView.containerStackView.addSubview(statusView.spoilerOverlayView) @@ -542,8 +539,8 @@ extension StatusView { filterHintLabel.isHidden = !isDisplay } - func setLinkPreviewButtonDisplay(isDisplay: Bool = true) { - linkPreviewButton.isHidden = !isDisplay + func setStatusCardControlDisplay(isDisplay: Bool = true) { + statusCardControl.isHidden = !isDisplay } // container width From 8a8ecb0b686a4707e1435e4129c8f345a414f62b Mon Sep 17 00:00:00 2001 From: Kyle Bashour Date: Sat, 26 Nov 2022 22:05:43 -0800 Subject: [PATCH 14/41] Improve layout --- .../Sources/MastodonUI/View/Content/StatusCardControl.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift index 3ff39be60..7dabe9bf6 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift @@ -89,6 +89,7 @@ public final class StatusCardControl: UIControl { containerStackView.addArrangedSubview(imageView) containerStackView.addArrangedSubview(labelStackView) containerStackView.isUserInteractionEnabled = false + containerStackView.distribution = .fill addSubview(containerStackView) addSubview(highlightView) @@ -133,12 +134,10 @@ public final class StatusCardControl: UIControl { if isCompact { containerStackView.alignment = .center containerStackView.axis = .horizontal - containerStackView.distribution = .fill NSLayoutConstraint.activate(compactImageConstraints) } else { containerStackView.alignment = .fill containerStackView.axis = .vertical - containerStackView.distribution = .equalSpacing NSLayoutConstraint.activate(largeImageConstraints) } } From 176067800c0239a3d4feb3d12d23e7eb71c21583 Mon Sep 17 00:00:00 2001 From: Kyle Bashour Date: Sat, 26 Nov 2022 23:42:02 -0800 Subject: [PATCH 15/41] Add card when merging --- .../CoreDataStack/Entity/Mastodon/Card.swift | 6 ++- .../Persistence/Persistence+Status.swift | 39 ++++++++++++------- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Card.swift b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Card.swift index 99f53f268..d656191f9 100644 --- a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Card.swift +++ b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Card.swift @@ -50,7 +50,7 @@ public final class Card: NSManagedObject { // sourcery: autoGenerateProperty @NSManaged public private(set) var blurhash: String? - // one-to-one relationship + // sourcery: autoGenerateRelationship @NSManaged public private(set) var status: Status } @@ -157,13 +157,17 @@ extension Card: AutoGenerateRelationship { // Generated using Sourcery // DO NOT EDIT public struct Relationship { + public let status: Status public init( + status: Status ) { + self.status = status } } public func configure(relationship: Relationship) { + self.status = relationship.status } // sourcery:end diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Status.swift b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Status.swift index 97ab32e30..aa5eb8546 100644 --- a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Status.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Status.swift @@ -87,7 +87,7 @@ extension Persistence.Status { } if let oldStatus = fetch(in: managedObjectContext, context: context) { - merge(mastodonStatus: oldStatus, context: context) + merge(in: managedObjectContext, mastodonStatus: oldStatus, context: context) return PersistResult( status: oldStatus, isNewInsertion: false, @@ -108,18 +108,7 @@ extension Persistence.Status { return result.poll }() - let card: Card? = { - guard let entity = context.entity.card else { return nil } - let result = Persistence.Card.create( - in: managedObjectContext, - context: Persistence.Card.PersistContext( - domain: context.domain, - entity: entity, - me: context.me - ) - ) - return result.card - }() + let card = createCard(in: managedObjectContext, context: context) let authorResult = Persistence.MastodonUser.createOrMerge( in: managedObjectContext, @@ -196,6 +185,7 @@ extension Persistence.Status { } public static func merge( + in managedObjectContext: NSManagedObjectContext, mastodonStatus status: Status, context: PersistContext ) { @@ -217,8 +207,31 @@ extension Persistence.Status { ) ) } + + if status.card == nil, context.entity.card != nil { + let card = createCard(in: managedObjectContext, context: context) + let relationship = Card.Relationship(status: status) + card?.configure(relationship: relationship) + } + update(status: status, context: context) } + + private static func createCard( + in managedObjectContext: NSManagedObjectContext, + context: PersistContext + ) -> Card? { + guard let entity = context.entity.card else { return nil } + let result = Persistence.Card.create( + in: managedObjectContext, + context: Persistence.Card.PersistContext( + domain: context.domain, + entity: entity, + me: context.me + ) + ) + return result.card + } private static func update( status: Status, From 459564ae6badc92ecf20a328e11f17849698d650 Mon Sep 17 00:00:00 2001 From: Kyle Bashour Date: Sat, 26 Nov 2022 23:49:51 -0800 Subject: [PATCH 16/41] Update table view --- .../StatusTableViewCell+ViewModel.swift | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell+ViewModel.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell+ViewModel.swift index 85184d406..2702d726b 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell+ViewModel.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell+ViewModel.swift @@ -67,6 +67,21 @@ extension StatusTableViewCell { } } .store(in: &disposeBag) + + statusView.viewModel.$card + .removeDuplicates() + .dropFirst() + .receive(on: DispatchQueue.main) + .sink { [weak tableView, weak self] _ in + guard let tableView = tableView else { return } + guard let _ = self else { return } + + UIView.performWithoutAnimation { + tableView.beginUpdates() + tableView.endUpdates() + } + } + .store(in: &disposeBag) } } From 61a07e9a5b308177dbf859ac843f7d9a0a99a24b Mon Sep 17 00:00:00 2001 From: Kyle Bashour Date: Sun, 27 Nov 2022 21:00:03 -0800 Subject: [PATCH 17/41] Layout improvements --- .../View/Content/StatusCardControl.swift | 103 +++++++++++------- 1 file changed, 64 insertions(+), 39 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift index 7dabe9bf6..6b3e00189 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift @@ -23,22 +23,8 @@ public final class StatusCardControl: UIControl { private let titleLabel = UILabel() private let linkLabel = UILabel() - private lazy var compactImageConstraints = [ - imageView.heightAnchor.constraint(equalTo: heightAnchor), - imageView.widthAnchor.constraint(equalToConstant: 85), - heightAnchor.constraint(equalToConstant: 85).priority(.defaultLow - 1), - heightAnchor.constraint(greaterThanOrEqualToConstant: 85) - ] - - private lazy var largeImageConstraints = [ - imageView.heightAnchor.constraint( - equalTo: imageView.widthAnchor, - multiplier: 21 / 40 - ) - // This priority is important or constraints break; - // it still renders the card correctly. - .priority(.defaultLow - 1), - ] + private var layout: Layout? + private var layoutConstraints: [NSLayoutConstraint] = [] public override var isHighlighted: Bool { didSet { highlightView.isHidden = !isHighlighted } @@ -85,6 +71,7 @@ public final class StatusCardControl: UIControl { labelStackView.layoutMargins = .init(top: 10, left: 10, bottom: 10, right: 10) labelStackView.isLayoutMarginsRelativeArrangement = true labelStackView.axis = .vertical + labelStackView.spacing = 2 containerStackView.addArrangedSubview(imageView) containerStackView.addArrangedSubview(labelStackView) @@ -106,8 +93,6 @@ public final class StatusCardControl: UIControl { } public func configure(card: Card) { - let isCompact = card.width == card.height - if let host = card.url?.host { accessibilityLabel = "\(card.title) \(host)" } else { @@ -120,26 +105,17 @@ public final class StatusCardControl: UIControl { imageView.sd_setImage( with: card.imageURL, - placeholderImage: isCompact ? newsIcon : photoIcon - ) { [weak imageView] image, _, _, _ in + placeholderImage: icon(for: card.layout) + ) { [weak self] image, _, _, _ in if image != nil { - imageView?.contentMode = .scaleAspectFill - self.containerStackView.setNeedsLayout() - self.containerStackView.layoutIfNeeded() + self?.imageView.contentMode = .scaleAspectFill } + + self?.containerStackView.setNeedsLayout() + self?.containerStackView.layoutIfNeeded() } - NSLayoutConstraint.deactivate(compactImageConstraints + largeImageConstraints) - - if isCompact { - containerStackView.alignment = .center - containerStackView.axis = .horizontal - NSLayoutConstraint.activate(compactImageConstraints) - } else { - containerStackView.alignment = .fill - containerStackView.axis = .vertical - NSLayoutConstraint.activate(largeImageConstraints) - } + updateConstraints(for: card.layout) } public override func didMoveToWindow() { @@ -150,13 +126,47 @@ public final class StatusCardControl: UIControl { } } - private var newsIcon: UIImage? { - UIImage(systemName: "newspaper.fill") + private func updateConstraints(for layout: Layout) { + guard layout != self.layout else { return } + self.layout = layout + + NSLayoutConstraint.deactivate(layoutConstraints) + + 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), + ] + 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) + ] + } + + NSLayoutConstraint.activate(layoutConstraints) } - private var photoIcon: UIImage? { - let configuration = UIImage.SymbolConfiguration(pointSize: 32) - return UIImage(systemName: "photo", withConfiguration: configuration) + 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) + } } private func apply(theme: Theme) { @@ -165,6 +175,21 @@ public final class StatusCardControl: UIControl { } } +private extension StatusCardControl { + enum Layout: Equatable { + case compact + case large(aspectRatio: CGFloat) + } +} + +private extension Card { + var layout: StatusCardControl.Layout { + return width == height || image == nil + ? .compact + : .large(aspectRatio: CGFloat(width) / CGFloat(height)) + } +} + private extension UILayoutPriority { static let zero = UILayoutPriority(rawValue: 0) } From 52f5213990f61a3c4d7f8445667db3218cfcba1f Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Fri, 2 Dec 2022 15:54:02 -0500 Subject: [PATCH 18/41] Allow a little bit of variance from square for compact layout --- .../Sources/MastodonUI/View/Content/StatusCardControl.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift index 6b3e00189..6b5a02da0 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift @@ -184,9 +184,10 @@ private extension StatusCardControl { private extension Card { var layout: StatusCardControl.Layout { - return width == height || image == nil + let aspectRatio = CGFloat(width) / CGFloat(height) + return abs(aspectRatio - 1) < 0.05 || image == nil ? .compact - : .large(aspectRatio: CGFloat(width) / CGFloat(height)) + : .large(aspectRatio: aspectRatio) } } From 16a814a27c4df96e6a1a11bcdc84df93dbd3528e Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Fri, 2 Dec 2022 16:02:05 -0500 Subject: [PATCH 19/41] Cap the height of the status card --- .../Sources/MastodonUI/View/Content/StatusCardControl.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift index 6b5a02da0..6bc8d6789 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift @@ -144,6 +144,9 @@ public final class StatusCardControl: UIControl { // This priority is important or constraints break; // it still renders the card correctly. .priority(.defaultLow - 1), + // set a reasonable max height for very tall images + imageView.heightAnchor + .constraint(lessThanOrEqualToConstant: 400) ] case .compact: containerStackView.alignment = .center From 1c5b66f7e715a31c167289d468981d25899160a8 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Fri, 2 Dec 2022 20:35:11 -0500 Subject: [PATCH 20/41] Embed a web view for viewing content inline --- .../CoreData 5.xcdatamodel/contents | 3 +- .../CoreDataStack/Entity/Mastodon/Card.swift | 8 ++- .../Persistence/Persistence+Card.swift | 3 +- .../Sources/MastodonExtension/UIView.swift | 18 ++++-- .../View/Content/StatusCardControl.swift | 64 ++++++++++++++++++- .../View/Content/StatusView+ViewModel.swift | 6 ++ 6 files changed, 91 insertions(+), 11 deletions(-) diff --git a/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 5.xcdatamodel/contents b/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 5.xcdatamodel/contents index c18d7492b..4d44775e1 100644 --- a/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 5.xcdatamodel/contents +++ b/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 5.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -14,6 +14,7 @@ + diff --git a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Card.swift b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Card.swift index d656191f9..64f5e2120 100644 --- a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Card.swift +++ b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Card.swift @@ -49,6 +49,8 @@ public final class Card: NSManagedObject { @NSManaged public private(set) var embedURLRaw: String? // sourcery: autoGenerateProperty @NSManaged public private(set) var blurhash: String? + // sourcery: autoGenerateProperty + @NSManaged public private(set) var html: String? // sourcery: autoGenerateRelationship @NSManaged public private(set) var status: Status @@ -96,6 +98,7 @@ extension Card: AutoGenerateProperty { public let image: String? public let embedURLRaw: String? public let blurhash: String? + public let html: String? public init( urlRaw: String, @@ -110,7 +113,8 @@ extension Card: AutoGenerateProperty { height: Int64, image: String?, embedURLRaw: String?, - blurhash: String? + blurhash: String?, + html: String? ) { self.urlRaw = urlRaw self.title = title @@ -125,6 +129,7 @@ extension Card: AutoGenerateProperty { self.image = image self.embedURLRaw = embedURLRaw self.blurhash = blurhash + self.html = html } } @@ -142,6 +147,7 @@ extension Card: AutoGenerateProperty { self.image = property.image self.embedURLRaw = property.embedURLRaw self.blurhash = property.blurhash + self.html = property.html } public func update(property: Property) { diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Card.swift b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Card.swift index 838d5e128..9ab8a817c 100644 --- a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Card.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Card.swift @@ -77,7 +77,8 @@ extension Persistence.Card { height: Int64(context.entity.height ?? 0), image: context.entity.image, embedURLRaw: context.entity.embedURL, - blurhash: context.entity.blurhash + blurhash: context.entity.blurhash, + html: context.entity.html.flatMap { $0.isEmpty ? nil : $0 } ) let card = Card.insert( diff --git a/MastodonSDK/Sources/MastodonExtension/UIView.swift b/MastodonSDK/Sources/MastodonExtension/UIView.swift index fa62be6c0..bfa253680 100644 --- a/MastodonSDK/Sources/MastodonExtension/UIView.swift +++ b/MastodonSDK/Sources/MastodonExtension/UIView.swift @@ -48,18 +48,22 @@ extension UIView { } public extension UIView { - - func pinToParent() { + + @discardableResult + func pinToParent() -> [NSLayoutConstraint] { pinTo(to: self.superview) } - - func pinTo(to view: UIView?) { - guard let pinToView = view else { return } - NSLayoutConstraint.activate([ + + @discardableResult + func pinTo(to view: UIView?) -> [NSLayoutConstraint] { + guard let pinToView = view else { return [] } + let constraints = [ topAnchor.constraint(equalTo: pinToView.topAnchor), leadingAnchor.constraint(equalTo: pinToView.leadingAnchor), trailingAnchor.constraint(equalTo: pinToView.trailingAnchor), bottomAnchor.constraint(equalTo: pinToView.bottomAnchor), - ]) + ] + NSLayoutConstraint.activate(constraints) + return constraints } } diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift index 6bc8d6789..7a63441e9 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift @@ -11,8 +11,11 @@ import MastodonAsset import MastodonCore import CoreDataStack import UIKit +import WebKit public final class StatusCardControl: UIControl { + public var urlToOpen = PassthroughSubject() + private var disposeBag = Set() private let containerStackView = UIStackView() @@ -23,6 +26,9 @@ public final class StatusCardControl: UIControl { private let titleLabel = UILabel() private let linkLabel = UILabel() + private static let cardContentPool = WKProcessPool() + private var webView: WKWebView? + private var layout: Layout? private var layoutConstraints: [NSLayoutConstraint] = [] @@ -115,6 +121,12 @@ public final class StatusCardControl: UIControl { self?.containerStackView.layoutIfNeeded() } + if let html = card.html, !html.isEmpty { + let webView = setupWebView() + webView.loadHTMLString("" + html, baseURL: nil) + addSubview(webView) + } + updateConstraints(for: card.layout) } @@ -123,6 +135,9 @@ public final class StatusCardControl: UIControl { if let window = window { layer.borderWidth = 1 / window.screen.scale + } else { + webView?.removeFromSuperview() + webView = nil } } @@ -159,6 +174,10 @@ public final class StatusCardControl: UIControl { ] } + if let webView { + layoutConstraints += webView.pinTo(to: imageView) + } + NSLayoutConstraint.activate(layoutConstraints) } @@ -178,6 +197,46 @@ public final class StatusCardControl: UIControl { } } +extension StatusCardControl: WKNavigationDelegate, WKUIDelegate { + fileprivate func setupWebView() -> WKWebView { + 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 + 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" { + urlToOpen.send(url) + return .cancel + } + return .allow + } + + public func webViewDidClose(_ webView: WKWebView) { + webView.removeFromSuperview() + self.webView = nil + } +} + private extension StatusCardControl { enum Layout: Equatable { case compact @@ -187,7 +246,10 @@ private extension StatusCardControl { private extension Card { var layout: StatusCardControl.Layout { - let aspectRatio = CGFloat(width) / CGFloat(height) + var aspectRatio = CGFloat(width) / CGFloat(height) + if !aspectRatio.isFinite { + aspectRatio = 1 + } return abs(aspectRatio - 1) < 0.05 || image == nil ? .compact : .large(aspectRatio: aspectRatio) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift index 77de106b1..634473a65 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift @@ -494,6 +494,12 @@ extension StatusView.ViewModel { statusView.setStatusCardControlDisplay() } .store(in: &disposeBag) + + statusView.statusCardControl.urlToOpen + .sink { url in + statusView.delegate?.statusView(statusView, didTapCardWithURL: url) + } + .store(in: &disposeBag) } private func bindToolbar(statusView: StatusView) { From a29e88b60be224d102aa48be6bf4c63a457139c9 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Fri, 2 Dec 2022 22:10:35 -0500 Subject: [PATCH 21/41] Fix web view reuse --- .../MastodonUI/View/Content/StatusCardControl.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift index 7a63441e9..6ed7f48c5 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift @@ -125,6 +125,9 @@ public final class StatusCardControl: UIControl { let webView = setupWebView() webView.loadHTMLString("" + html, baseURL: nil) addSubview(webView) + } else { + webView?.removeFromSuperview() + webView = nil } updateConstraints(for: card.layout) @@ -135,9 +138,6 @@ public final class StatusCardControl: UIControl { if let window = window { layer.borderWidth = 1 / window.screen.scale - } else { - webView?.removeFromSuperview() - webView = nil } } @@ -199,6 +199,8 @@ public final class StatusCardControl: UIControl { extension StatusCardControl: WKNavigationDelegate, WKUIDelegate { fileprivate func setupWebView() -> WKWebView { + if let webView { return webView } + let config = WKWebViewConfiguration() config.processPool = Self.cardContentPool config.websiteDataStore = .nonPersistent() // private/incognito mode From 946d47abdd9eed610fe547f2ff3ec006139234e3 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Fri, 2 Dec 2022 22:35:18 -0500 Subject: [PATCH 22/41] Fix highlight behavior --- .../MastodonUI/View/Content/StatusCardControl.swift | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift index 6ed7f48c5..bffa56f0f 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift @@ -33,7 +33,15 @@ public final class StatusCardControl: UIControl { private var layoutConstraints: [NSLayoutConstraint] = [] public override var isHighlighted: Bool { - didSet { highlightView.isHidden = !isHighlighted } + 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 + } + } } public override init(frame: CGRect) { @@ -53,7 +61,7 @@ public final class StatusCardControl: UIControl { maximumContentSizeCategory = .accessibilityLarge } - highlightView.backgroundColor = UIColor.black.withAlphaComponent(0.1) + highlightView.backgroundColor = UIColor.label.withAlphaComponent(0.1) highlightView.isHidden = true titleLabel.numberOfLines = 2 From 5932d00f2fba696a856cc1eb5d088477ced5fd4b Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Fri, 2 Dec 2022 22:55:16 -0500 Subject: [PATCH 23/41] add a divider between the image and the text in the card --- .../MastodonExtension/NSLayoutConstraint.swift | 12 ++++++++++++ .../MastodonUI/View/Content/StatusCardControl.swift | 13 +++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/MastodonSDK/Sources/MastodonExtension/NSLayoutConstraint.swift b/MastodonSDK/Sources/MastodonExtension/NSLayoutConstraint.swift index 057b17859..3251eb58b 100644 --- a/MastodonSDK/Sources/MastodonExtension/NSLayoutConstraint.swift +++ b/MastodonSDK/Sources/MastodonExtension/NSLayoutConstraint.swift @@ -17,4 +17,16 @@ extension NSLayoutConstraint { self.identifier = identifier return self } + + @discardableResult + public func activate() -> Self { + self.isActive = true + return self + } + + @discardableResult + public func deactivate() -> Self { + self.isActive = false + return self + } } diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift index bffa56f0f..dc8efc6c0 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift @@ -22,6 +22,7 @@ public final class StatusCardControl: UIControl { private let labelStackView = UIStackView() private let highlightView = UIView() + private let dividerView = UIView() private let imageView = UIImageView() private let titleLabel = UILabel() private let linkLabel = UILabel() @@ -31,6 +32,7 @@ public final class StatusCardControl: UIControl { private var layout: Layout? private var layoutConstraints: [NSLayoutConstraint] = [] + private var dividerConstraint: NSLayoutConstraint? public override var isHighlighted: Bool { didSet { @@ -88,6 +90,7 @@ public final class StatusCardControl: UIControl { labelStackView.spacing = 2 containerStackView.addArrangedSubview(imageView) + containerStackView.addArrangedSubview(dividerView) containerStackView.addArrangedSubview(labelStackView) containerStackView.isUserInteractionEnabled = false containerStackView.distribution = .fill @@ -146,6 +149,7 @@ public final class StatusCardControl: UIControl { if let window = window { layer.borderWidth = 1 / window.screen.scale + dividerConstraint?.constant = 1 / window.screen.scale } } @@ -154,7 +158,9 @@ public final class StatusCardControl: UIControl { self.layout = layout NSLayoutConstraint.deactivate(layoutConstraints) + dividerConstraint?.deactivate() + let pixelSize = 1 / (window?.screen.scale ?? 1) switch layout { case .large(let aspectRatio): containerStackView.alignment = .fill @@ -169,8 +175,9 @@ public final class StatusCardControl: UIControl { .priority(.defaultLow - 1), // set a reasonable max height for very tall images imageView.heightAnchor - .constraint(lessThanOrEqualToConstant: 400) + .constraint(lessThanOrEqualToConstant: 400), ] + dividerConstraint = dividerView.heightAnchor.constraint(equalToConstant: pixelSize).activate() case .compact: containerStackView.alignment = .center containerStackView.axis = .horizontal @@ -178,8 +185,9 @@ public final class StatusCardControl: UIControl { imageView.heightAnchor.constraint(equalTo: heightAnchor), imageView.widthAnchor.constraint(equalToConstant: 85), heightAnchor.constraint(equalToConstant: 85).priority(.defaultLow - 1), - heightAnchor.constraint(greaterThanOrEqualToConstant: 85) + heightAnchor.constraint(greaterThanOrEqualToConstant: 85), ] + dividerConstraint = dividerView.widthAnchor.constraint(equalToConstant: pixelSize).activate() } if let webView { @@ -201,6 +209,7 @@ public final class StatusCardControl: UIControl { private func apply(theme: Theme) { layer.borderColor = theme.separator.cgColor + dividerView.backgroundColor = theme.separator imageView.backgroundColor = theme.systemElevatedBackgroundColor } } From 7944ec6399894b546ddc6ca716a9a283cc274242 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Fri, 2 Dec 2022 23:29:14 -0500 Subject: [PATCH 24/41] Load embed web view only on tap (for privacy) --- .../View/Content/StatusCardControl.swift | 46 ++++++++++++++++--- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift index dc8efc6c0..402763be3 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift @@ -26,6 +26,23 @@ public final class StatusCardControl: UIControl { private let imageView = UIImageView() private let titleLabel = UILabel() 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 + 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 = "" private static let cardContentPool = WKProcessPool() private var webView: WKWebView? @@ -95,14 +112,23 @@ public final class StatusCardControl: UIControl { containerStackView.isUserInteractionEnabled = false containerStackView.distribution = .fill + showEmbedButton.setImage(UIImage(systemName: "play.fill"), for: .normal) + addSubview(containerStackView) addSubview(highlightView) + addSubview(showEmbedButton) containerStackView.translatesAutoresizingMaskIntoConstraints = false highlightView.translatesAutoresizingMaskIntoConstraints = false + showEmbedButton.translatesAutoresizingMaskIntoConstraints = false containerStackView.pinToParent() highlightView.pinToParent() + NSLayoutConstraint.activate([ + showEmbedButton.widthAnchor.constraint(equalTo: showEmbedButton.heightAnchor), + showEmbedButton.centerXAnchor.constraint(equalTo: imageView.centerXAnchor), + showEmbedButton.centerYAnchor.constraint(equalTo: imageView.centerYAnchor), + ]) } required init?(coder: NSCoder) { @@ -133,12 +159,13 @@ public final class StatusCardControl: UIControl { } if let html = card.html, !html.isEmpty { - let webView = setupWebView() - webView.loadHTMLString("" + html, baseURL: nil) - addSubview(webView) + showEmbedButton.isHidden = false + self.html = html } else { webView?.removeFromSuperview() webView = nil + showEmbedButton.isHidden = true + self.html = "" } updateConstraints(for: card.layout) @@ -190,10 +217,6 @@ public final class StatusCardControl: UIControl { dividerConstraint = dividerView.widthAnchor.constraint(equalToConstant: pixelSize).activate() } - if let webView { - layoutConstraints += webView.pinTo(to: imageView) - } - NSLayoutConstraint.activate(layoutConstraints) } @@ -214,6 +237,15 @@ public final class StatusCardControl: UIControl { } } +extension StatusCardControl { + fileprivate func showWebView() { + let webView = setupWebView() + webView.loadHTMLString("" + html, baseURL: nil) + addSubview(webView) + webView.pinTo(to: imageView) + } +} + extension StatusCardControl: WKNavigationDelegate, WKUIDelegate { fileprivate func setupWebView() -> WKWebView { if let webView { return webView } From c67e6ce45ec5f3a47576a5fc51deb1b4eec3622d Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sat, 3 Dec 2022 11:27:51 -0500 Subject: [PATCH 25/41] Fix white flash in dark mode --- .../MastodonUI/View/Content/StatusCardControl.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift index 402763be3..c3945f630 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift @@ -241,8 +241,10 @@ extension StatusCardControl { fileprivate func showWebView() { let webView = setupWebView() webView.loadHTMLString("" + html, baseURL: nil) - addSubview(webView) - webView.pinTo(to: imageView) + if webView.superview == nil { + addSubview(webView) + webView.pinTo(to: imageView) + } } } @@ -259,6 +261,8 @@ extension StatusCardControl: WKNavigationDelegate, WKUIDelegate { webView.uiDelegate = self webView.navigationDelegate = self webView.translatesAutoresizingMaskIntoConstraints = false + webView.isOpaque = false + webView.backgroundColor = .clear self.webView = webView return webView } From e46c25892dee5c58eb1805345d07998146e1ee7f Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sat, 3 Dec 2022 11:29:03 -0500 Subject: [PATCH 26/41] =?UTF-8?q?Add=20label=20to=20the=20=E2=80=9Cload=20?= =?UTF-8?q?embed=E2=80=9D=20button?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Localization/StringsConvertor/input/Base.lproj/app.json | 1 + Localization/app.json | 1 + .../Sources/MastodonLocalization/Generated/Strings.swift | 2 ++ .../Resources/Base.lproj/Localizable.strings | 1 + .../MastodonUI/View/Content/StatusCardControl.swift | 7 ++++--- 5 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Localization/StringsConvertor/input/Base.lproj/app.json b/Localization/StringsConvertor/input/Base.lproj/app.json index ea046bfbc..e09ab983c 100644 --- a/Localization/StringsConvertor/input/Base.lproj/app.json +++ b/Localization/StringsConvertor/input/Base.lproj/app.json @@ -132,6 +132,7 @@ "sensitive_content": "Sensitive Content", "media_content_warning": "Tap anywhere to reveal", "tap_to_reveal": "Tap to reveal", + "load_embed": "Load Embed", "poll": { "vote": "Vote", "closed": "Closed" diff --git a/Localization/app.json b/Localization/app.json index ea046bfbc..e09ab983c 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -132,6 +132,7 @@ "sensitive_content": "Sensitive Content", "media_content_warning": "Tap anywhere to reveal", "tap_to_reveal": "Tap to reveal", + "load_embed": "Load Embed", "poll": { "vote": "Vote", "closed": "Closed" diff --git a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift index 0392d2b05..335638030 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift +++ b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift @@ -272,6 +272,8 @@ public enum L10n { public enum Status { /// Content Warning public static let contentWarning = L10n.tr("Localizable", "Common.Controls.Status.ContentWarning", fallback: "Content Warning") + /// Load Embed + public static let loadEmbed = L10n.tr("Localizable", "Common.Controls.Status.LoadEmbed", fallback: "Load Embed") /// Tap anywhere to reveal public static let mediaContentWarning = L10n.tr("Localizable", "Common.Controls.Status.MediaContentWarning", fallback: "Tap anywhere to reveal") /// Sensitive Content diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings index adeaad07b..3c3e16ca3 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings @@ -107,6 +107,7 @@ Please check your internet connection."; "Common.Controls.Status.Actions.Unfavorite" = "Unfavorite"; "Common.Controls.Status.Actions.Unreblog" = "Undo reblog"; "Common.Controls.Status.ContentWarning" = "Content Warning"; +"Common.Controls.Status.LoadEmbed" = "Load Embed"; "Common.Controls.Status.MediaContentWarning" = "Tap anywhere to reveal"; "Common.Controls.Status.MetaEntity.Email" = "Email address: %@"; "Common.Controls.Status.MetaEntity.Hashtag" = "Hashtag: %@"; diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift index c3945f630..376f54eb4 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift @@ -9,6 +9,7 @@ import AlamofireImage import Combine import MastodonAsset import MastodonCore +import MastodonLocalization import CoreDataStack import UIKit import WebKit @@ -33,6 +34,9 @@ public final class StatusCardControl: UIControl { 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() }) @@ -112,8 +116,6 @@ public final class StatusCardControl: UIControl { containerStackView.isUserInteractionEnabled = false containerStackView.distribution = .fill - showEmbedButton.setImage(UIImage(systemName: "play.fill"), for: .normal) - addSubview(containerStackView) addSubview(highlightView) addSubview(showEmbedButton) @@ -125,7 +127,6 @@ public final class StatusCardControl: UIControl { containerStackView.pinToParent() highlightView.pinToParent() NSLayoutConstraint.activate([ - showEmbedButton.widthAnchor.constraint(equalTo: showEmbedButton.heightAnchor), showEmbedButton.centerXAnchor.constraint(equalTo: imageView.centerXAnchor), showEmbedButton.centerYAnchor.constraint(equalTo: imageView.centerYAnchor), ]) From 348e176f89fc4d3db2673cc86dfefd06c4d6479f Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sat, 3 Dec 2022 11:30:21 -0500 Subject: [PATCH 27/41] slight code reorg --- .../Sources/MastodonUI/View/Content/StatusCardControl.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift index 376f54eb4..51c125a87 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift @@ -238,7 +238,7 @@ public final class StatusCardControl: UIControl { } } -extension StatusCardControl { +extension StatusCardControl: WKNavigationDelegate, WKUIDelegate { fileprivate func showWebView() { let webView = setupWebView() webView.loadHTMLString("" + html, baseURL: nil) @@ -247,10 +247,8 @@ extension StatusCardControl { webView.pinTo(to: imageView) } } -} -extension StatusCardControl: WKNavigationDelegate, WKUIDelegate { - fileprivate func setupWebView() -> WKWebView { + private func setupWebView() -> WKWebView { if let webView { return webView } let config = WKWebViewConfiguration() From 3212e54bf52e482e232493efc6e706c13c22482f Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sat, 3 Dec 2022 13:07:55 -0500 Subject: [PATCH 28/41] Fix generating relay delegate methods with return values --- .../AutoGenerateProtocolDelegate.swifttemplate | 16 +++++++++++++++- ...toGenerateProtocolRelayDelegate.swifttemplate | 5 ++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/Mastodon/Template/AutoGenerateProtocolDelegate.swifttemplate b/Mastodon/Template/AutoGenerateProtocolDelegate.swifttemplate index 47eb4ce19..e8677d970 100644 --- a/Mastodon/Template/AutoGenerateProtocolDelegate.swifttemplate +++ b/Mastodon/Template/AutoGenerateProtocolDelegate.swifttemplate @@ -1,3 +1,17 @@ +<% +func methodDeclaration(_ method: SourceryRuntime.Method, newName: String) -> String { + var result = newName + if method.throws { + result = result + " throws" + } else if method.rethrows { + result = result + " rethrows" + } + if method.returnTypeName.isVoid { + return result + } + return result + " -> \(method.returnTypeName)" +} +-%> <% for type in types.implementing["AutoGenerateProtocolDelegate"] { guard let replaceOf = type.annotations["replaceOf"] as? String else { continue } guard let replaceWith = type.annotations["replaceWith"] as? String else { continue } @@ -5,7 +19,7 @@ guard let aProtocol = types.protocols.first(where: { $0.name == protocolToGenerate }) else { continue } -%> // sourcery:inline:<%= type.name %>.AutoGenerateProtocolDelegate <% for method in aProtocol.methods { -%> -<%= method.name.replacingOccurrences(of: replaceOf, with: replaceWith) %> +<%= methodDeclaration(method, newName: method.name.replacingOccurrences(of: replaceOf, with: replaceWith)) %> <% } -%> // sourcery:end <% } %> diff --git a/Mastodon/Template/AutoGenerateProtocolRelayDelegate.swifttemplate b/Mastodon/Template/AutoGenerateProtocolRelayDelegate.swifttemplate index b57f26038..d2b0dd58c 100644 --- a/Mastodon/Template/AutoGenerateProtocolRelayDelegate.swifttemplate +++ b/Mastodon/Template/AutoGenerateProtocolRelayDelegate.swifttemplate @@ -6,6 +6,9 @@ func methodDeclaration(_ method: SourceryRuntime.Method) -> String { } else if method.rethrows { result = result + " rethrows" } + if method.returnTypeName.isVoid { + return result + } return result + " -> \(method.returnTypeName)" } -%> @@ -42,7 +45,7 @@ func methodCall( guard let aProtocol = types.protocols.first(where: { $0.name == protocolToGenerate }) else { continue } -%> // sourcery:inline:<%= type.name %>.AutoGenerateProtocolRelayDelegate <% for method in aProtocol.methods { -%> -func <%= method.name -%> { +func <%= methodDeclaration(method) -%> { <%= methodCall(method, replaceOf: replaceOf, replaceWith: replaceWith) %> } From ebf383540391ff8c3fbc3dae62545474688baf63 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sat, 3 Dec 2022 13:09:04 -0500 Subject: [PATCH 29/41] extract out StatusActivityItem class --- Mastodon.xcodeproj/project.pbxproj | 8 +++ Mastodon/Helper/ImageProvider.swift | 39 ++++++++++++ .../Helper/URLActivityItemWithMetadata.swift | 33 ++++++++++ .../Provider/DataSourceFacade+Status.swift | 62 ++++--------------- .../Scene/ShareViewController.swift | 3 +- 5 files changed, 94 insertions(+), 51 deletions(-) create mode 100644 Mastodon/Helper/ImageProvider.swift create mode 100644 Mastodon/Helper/URLActivityItemWithMetadata.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 453027074..a970c5a61 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -89,6 +89,8 @@ 62FD27D12893707600B205C5 /* BookmarkViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62FD27D02893707600B205C5 /* BookmarkViewController.swift */; }; 62FD27D32893707B00B205C5 /* BookmarkViewController+DataSourceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62FD27D22893707B00B205C5 /* BookmarkViewController+DataSourceProvider.swift */; }; 62FD27D52893708A00B205C5 /* BookmarkViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62FD27D42893708A00B205C5 /* BookmarkViewModel+Diffable.swift */; }; + 85904C02293BC0EB0011C817 /* ImageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85904C01293BC0EB0011C817 /* ImageProvider.swift */; }; + 85904C04293BC1940011C817 /* URLActivityItemWithMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85904C03293BC1940011C817 /* URLActivityItemWithMetadata.swift */; }; 87FFDA5D898A5C42ADCB35E7 /* Pods_Mastodon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */; }; C24C97032922F30500BAE8CB /* RefreshControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = C24C97022922F30500BAE8CB /* RefreshControl.swift */; }; D87BFC8B291D5C6B00FEE264 /* MastodonLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87BFC8A291D5C6B00FEE264 /* MastodonLoginView.swift */; }; @@ -602,6 +604,8 @@ 7CB58D292DA7ACEF179A9050 /* Pods-Mastodon.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.profile.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.profile.xcconfig"; sourceTree = ""; }; 7CEFFAE9AF9284B13C0A758D /* Pods-MastodonTests.asdk - debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.asdk - debug.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.asdk - debug.xcconfig"; sourceTree = ""; }; 819CEC9DCAD8E8E7BD85A7BB /* Pods-Mastodon.asdk.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.asdk.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.asdk.xcconfig"; sourceTree = ""; }; + 85904C01293BC0EB0011C817 /* ImageProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageProvider.swift; sourceTree = ""; }; + 85904C03293BC1940011C817 /* URLActivityItemWithMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLActivityItemWithMetadata.swift; sourceTree = ""; }; 8850E70A1D5FF51432E43653 /* Pods-Mastodon-MastodonUITests.asdk - release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.asdk - release.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.asdk - release.xcconfig"; sourceTree = ""; }; 8E79CCBE51FBC3F7FE8CF49F /* Pods-MastodonTests.release snapshot.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.release snapshot.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.release snapshot.xcconfig"; sourceTree = ""; }; 8ED8C4B1F1BA2DCFF2926BB1 /* Pods-Mastodon-NotificationService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-NotificationService.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-NotificationService/Pods-Mastodon-NotificationService.debug.xcconfig"; sourceTree = ""; }; @@ -2520,6 +2524,8 @@ isa = PBXGroup; children = ( DBF3B7402733EB9400E21627 /* MastodonLocalCode.swift */, + 85904C01293BC0EB0011C817 /* ImageProvider.swift */, + 85904C03293BC1940011C817 /* URLActivityItemWithMetadata.swift */, ); path = Helper; sourceTree = ""; @@ -3172,6 +3178,7 @@ 62FD27D52893708A00B205C5 /* BookmarkViewModel+Diffable.swift in Sources */, DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */, 2D364F7225E66D7500204FDC /* MastodonResendEmailViewController.swift in Sources */, + 85904C04293BC1940011C817 /* URLActivityItemWithMetadata.swift in Sources */, DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */, DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */, DBEFCD80282A2AA900C0ABEA /* ReportServerRulesViewModel.swift in Sources */, @@ -3308,6 +3315,7 @@ DB1D843026566512000346B3 /* KeyboardPreference.swift in Sources */, DB852D1926FAEB6B00FC9D81 /* SidebarViewController.swift in Sources */, 2D206B9225F60EA700143C56 /* UIControl.swift in Sources */, + 85904C02293BC0EB0011C817 /* ImageProvider.swift in Sources */, DBDFF1932805554900557A48 /* DiscoveryPostsViewModel.swift in Sources */, DB3E6FE72806A7A200B035AE /* DiscoveryItem.swift in Sources */, DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */, diff --git a/Mastodon/Helper/ImageProvider.swift b/Mastodon/Helper/ImageProvider.swift new file mode 100644 index 000000000..11ea6d46e --- /dev/null +++ b/Mastodon/Helper/ImageProvider.swift @@ -0,0 +1,39 @@ +// +// ImageProvider.swift +// Mastodon +// +// Created by Jed Fox on 2022-12-03. +// + +import Foundation +import AlamofireImage +import UniformTypeIdentifiers +import UIKit + +class ImageProvider: NSObject, NSItemProviderWriting { + let url: URL + let filter: ImageFilter? + + init(url: URL, filter: ImageFilter? = nil) { + self.url = url + self.filter = filter + } + + var itemProvider: NSItemProvider { + NSItemProvider(object: self) + } + + static var writableTypeIdentifiersForItemProvider: [String] { + [UTType.png.identifier] + } + + func loadData(withTypeIdentifier typeIdentifier: String, forItemProviderCompletionHandler completionHandler: @escaping @Sendable (Data?, Error?) -> Void) -> Progress? { + let receipt = UIImageView.af.sharedImageDownloader.download(URLRequest(url: url), filter: filter, completion: { response in + switch response.result { + case .failure(let error): completionHandler(nil, error) + case .success(let image): completionHandler(image.pngData(), nil) + } + }) + return receipt?.request.downloadProgress + } +} diff --git a/Mastodon/Helper/URLActivityItemWithMetadata.swift b/Mastodon/Helper/URLActivityItemWithMetadata.swift new file mode 100644 index 000000000..82d1747fe --- /dev/null +++ b/Mastodon/Helper/URLActivityItemWithMetadata.swift @@ -0,0 +1,33 @@ +// +// URLActivityItemWithMetadata.swift +// Mastodon +// +// Created by Jed Fox on 2022-12-03. +// + +import UIKit +import LinkPresentation + +class URLActivityItemWithMetadata: NSObject, UIActivityItemSource { + init(url: URL, configureMetadata: (LPLinkMetadata) -> Void) { + self.url = url + self.metadata = LPLinkMetadata() + metadata.url = url + configureMetadata(metadata) + } + + let url: URL + let metadata: LPLinkMetadata + + func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any { + url + } + + func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? { + url + } + + func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? { + metadata + } +} diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift index ac9da6e81..9865029a1 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift @@ -59,8 +59,18 @@ extension DataSourceFacade { status: ManagedObjectRecord ) async throws -> UIActivityViewController { var activityItems: [Any] = try await dependency.context.managedObjectContext.perform { - guard let status = status.object(in: dependency.context.managedObjectContext) else { return [] } - return [StatusActivityItem(status: status)].compactMap { $0 } as [Any] + guard let status = status.object(in: dependency.context.managedObjectContext), + let url = URL(string: status.url ?? status.uri) + else { return [] } + return [ + URLActivityItemWithMetadata(url: url) { metadata in + metadata.title = "\(status.author.displayName) (@\(status.author.acctWithDomain))" + metadata.iconProvider = ImageProvider( + url: status.author.avatarImageURLWithFallback(domain: status.author.domain), + filter: ScaledToSizeFilter(size: CGSize.authorAvatarButtonSize) + ).itemProvider + } + ] as [Any] } var applicationActivities: [UIActivity] = [ SafariActivity(sceneCoordinator: dependency.coordinator), // open URL @@ -77,54 +87,6 @@ extension DataSourceFacade { ) return activityViewController } - - private class StatusActivityItem: NSObject, UIActivityItemSource { - init?(status: Status) { - guard let url = URL(string: status.url ?? status.uri) else { return nil } - self.url = url - self.metadata = LPLinkMetadata() - metadata.url = url - metadata.title = "\(status.author.displayName) (@\(status.author.acctWithDomain))" - metadata.iconProvider = NSItemProvider(object: IconProvider(url: status.author.avatarImageURLWithFallback(domain: status.author.domain))) - } - - let url: URL - let metadata: LPLinkMetadata - - func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any { - url - } - - func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? { - url - } - - func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? { - metadata - } - - private class IconProvider: NSObject, NSItemProviderWriting { - let url: URL - init(url: URL) { - self.url = url - } - - static var writableTypeIdentifiersForItemProvider: [String] { - [UTType.png.identifier] - } - - func loadData(withTypeIdentifier typeIdentifier: String, forItemProviderCompletionHandler completionHandler: @escaping @Sendable (Data?, Error?) -> Void) -> Progress? { - let filter = ScaledToSizeFilter(size: CGSize.authorAvatarButtonSize) - let receipt = UIImageView.af.sharedImageDownloader.download(URLRequest(url: url), filter: filter, completion: { response in - switch response.result { - case .failure(let error): completionHandler(nil, error) - case .success(let image): completionHandler(image.pngData(), nil) - } - }) - return receipt?.request.downloadProgress - } - } - } } // ActionToolBar diff --git a/ShareActionExtension/Scene/ShareViewController.swift b/ShareActionExtension/Scene/ShareViewController.swift index 4a093becd..cf4d997c1 100644 --- a/ShareActionExtension/Scene/ShareViewController.swift +++ b/ShareActionExtension/Scene/ShareViewController.swift @@ -95,7 +95,8 @@ extension ShareViewController { let composeContentViewModel = ComposeContentViewModel( context: context, authContext: authContext, - kind: .post + destination: .topLevel, + initialContent: "" ) let composeContentViewController = ComposeContentViewController() composeContentViewController.viewModel = composeContentViewModel From 3661b5ce90c0e210acd2ab201d693b6ac7eed19d Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sat, 3 Dec 2022 13:25:07 -0500 Subject: [PATCH 30/41] Refactor compose intialization - split ComposeContentViewModel.Kind into Destination (top level/reply) and an initial content string - replies get the mentions prepended to the initial content string --- .../Provider/DataSourceFacade+Status.swift | 2 +- ...tatusTableViewControllerNavigateable.swift | 2 +- .../Scene/Compose/ComposeViewController.swift | 3 +- Mastodon/Scene/Compose/ComposeViewModel.swift | 15 +++-- .../HashtagTimelineViewController.swift | 5 +- .../Scene/Profile/ProfileViewController.swift | 5 +- .../Root/MainTab/MainTabBarController.swift | 4 +- .../Root/Sidebar/SidebarViewController.swift | 2 +- .../Scene/Thread/ThreadViewController.swift | 2 +- Mastodon/Supporting Files/SceneDelegate.swift | 2 +- .../ComposeContentViewModel+DataSource.swift | 11 +--- .../ComposeContentViewModel.swift | 60 +++++++------------ 12 files changed, 49 insertions(+), 64 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift index 9865029a1..34cd4121e 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift @@ -117,7 +117,7 @@ extension DataSourceFacade { let composeViewModel = ComposeViewModel( context: provider.context, authContext: provider.authContext, - kind: .reply(status: status) + destination: .reply(parent: status) ) _ = provider.coordinator.present( scene: .compose(viewModel: composeViewModel), diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewControllerNavigateable.swift b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewControllerNavigateable.swift index e7b55f91c..0e0a24c9b 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewControllerNavigateable.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewControllerNavigateable.swift @@ -100,7 +100,7 @@ extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvid let composeViewModel = ComposeViewModel( context: self.context, authContext: authContext, - kind: .reply(status: status) + destination: .reply(parent: status) ) _ = self.coordinator.present( scene: .compose(viewModel: composeViewModel), diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index fbdbc7d12..9f287dbf8 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -34,7 +34,8 @@ final class ComposeViewController: UIViewController, NeedsDependency { return ComposeContentViewModel( context: context, authContext: viewModel.authContext, - kind: viewModel.kind + destination: viewModel.destination, + initialContent: viewModel.initialContent ) }() private(set) lazy var composeContentViewController: ComposeContentViewController = { diff --git a/Mastodon/Scene/Compose/ComposeViewModel.swift b/Mastodon/Scene/Compose/ComposeViewModel.swift index bf234b095..0dcdd9a2d 100644 --- a/Mastodon/Scene/Compose/ComposeViewModel.swift +++ b/Mastodon/Scene/Compose/ComposeViewModel.swift @@ -29,7 +29,8 @@ final class ComposeViewModel { // input let context: AppContext let authContext: AuthContext - let kind: ComposeContentViewModel.Kind + let destination: ComposeContentViewModel.Destination + let initialContent: String let traitCollectionDidChangePublisher = CurrentValueSubject(Void()) // use CurrentValueSubject to make initial event emit @@ -41,17 +42,19 @@ final class ComposeViewModel { init( context: AppContext, authContext: AuthContext, - kind: ComposeContentViewModel.Kind + destination: ComposeContentViewModel.Destination, + initialContent: String = "" ) { self.context = context self.authContext = authContext - self.kind = kind + self.destination = destination + self.initialContent = initialContent // end init self.title = { - switch kind { - case .post, .hashtag, .mention: return L10n.Scene.Compose.Title.newPost - case .reply: return L10n.Scene.Compose.Title.newReply + switch destination { + case .topLevel: return L10n.Scene.Compose.Title.newPost + case .reply: return L10n.Scene.Compose.Title.newReply } }() } diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift index 4a0be3816..1867d2c64 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift @@ -162,10 +162,13 @@ extension HashtagTimelineViewController { @objc private func composeBarButtonItemPressed(_ sender: UIBarButtonItem) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + let hashtag = "#" + viewModel.hashtag + UITextChecker.learnWord(hashtag) let composeViewModel = ComposeViewModel( context: context, authContext: viewModel.authContext, - kind: .hashtag(hashtag: viewModel.hashtag) + destination: .topLevel, + initialContent: hashtag ) _ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil)) } diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 1184fb3d7..3dbc03fe4 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -538,10 +538,13 @@ extension ProfileViewController { @objc private func replyBarButtonItemPressed(_ sender: UIBarButtonItem) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) guard let mastodonUser = viewModel.user else { return } + let mention = "@" + mastodonUser.acct + UITextChecker.learnWord(mention) let composeViewModel = ComposeViewModel( context: context, authContext: viewModel.authContext, - kind: .mention(user: mastodonUser.asRecord) + destination: .topLevel, + initialContent: mention ) _ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil)) } diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index 2e5d5ae58..1b23b490f 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -379,7 +379,7 @@ extension MainTabBarController { let composeViewModel = ComposeViewModel( context: context, authContext: authContext, - kind: .post + destination: .topLevel ) _ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: nil, transition: .modal(animated: true, completion: nil)) } @@ -804,7 +804,7 @@ extension MainTabBarController { let composeViewModel = ComposeViewModel( context: context, authContext: authContext, - kind: .post + destination: .topLevel ) _ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: nil, transition: .modal(animated: true, completion: nil)) } diff --git a/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift b/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift index 7c76585a6..0ffcd4c8b 100644 --- a/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift +++ b/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift @@ -227,7 +227,7 @@ extension SidebarViewController: UICollectionViewDelegate { let composeViewModel = ComposeViewModel( context: context, authContext: authContext, - kind: .post + destination: .topLevel ) _ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil)) default: diff --git a/Mastodon/Scene/Thread/ThreadViewController.swift b/Mastodon/Scene/Thread/ThreadViewController.swift index e8e6ce130..1b386aa6a 100644 --- a/Mastodon/Scene/Thread/ThreadViewController.swift +++ b/Mastodon/Scene/Thread/ThreadViewController.swift @@ -117,7 +117,7 @@ extension ThreadViewController { let composeViewModel = ComposeViewModel( context: context, authContext: viewModel.authContext, - kind: .reply(status: threadContext.status) + destination: .reply(parent: threadContext.status) ) _ = coordinator.present( scene: .compose(viewModel: composeViewModel), diff --git a/Mastodon/Supporting Files/SceneDelegate.swift b/Mastodon/Supporting Files/SceneDelegate.swift index 4476477fd..336a83f7f 100644 --- a/Mastodon/Supporting Files/SceneDelegate.swift +++ b/Mastodon/Supporting Files/SceneDelegate.swift @@ -185,7 +185,7 @@ extension SceneDelegate { let composeViewModel = ComposeViewModel( context: AppContext.shared, authContext: authContext, - kind: .post + destination: .topLevel ) _ = coordinator?.present(scene: .compose(viewModel: composeViewModel), from: nil, transition: .modal(animated: true, completion: nil)) logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): present compose scene") diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+DataSource.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+DataSource.swift index abbfe0e61..58ceeaa92 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+DataSource.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+DataSource.swift @@ -47,10 +47,7 @@ extension ComposeContentViewModel { } .store(in: &disposeBag) - switch kind { - case .post: - break - case .reply(let status): + if case .reply(let status) = destination { let cell = composeReplyToTableViewCell // bind frame publisher cell.$framePublisher @@ -66,10 +63,6 @@ extension ComposeContentViewModel { guard let replyTo = status.object(in: context.managedObjectContext) else { return } cell.statusView.configure(status: replyTo) } - case .hashtag: - break - case .mention: - break } } } @@ -83,7 +76,7 @@ extension ComposeContentViewModel: UITableViewDataSource { public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { switch Section.allCases[section] { case .replyTo: - switch kind { + switch destination { case .reply: return 1 default: return 0 } diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift index a1ddb6101..066a7ff78 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -32,7 +32,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { // input let context: AppContext - let kind: Kind + let destination: Destination weak var delegate: ComposeContentViewModelDelegate? @Published var viewLayoutFrame = ViewLayoutFrame() @@ -59,8 +59,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { customEmojiPickerInputViewModel.configure(textInput: textView) } } - // for hashtag: "# " - // for mention: "@ " + // allow dismissing the compose view without confirmation if content == intialContent @Published public var initialContent = "" @Published public var content = "" @Published public var contentWeightedLength = 0 @@ -138,11 +137,12 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { public init( context: AppContext, authContext: AuthContext, - kind: Kind + destination: Destination, + initialContent: String ) { self.context = context self.authContext = authContext - self.kind = kind + self.destination = destination self.visibility = { // default private when user locked var visibility: Mastodon.Entity.Status.Visibility = { @@ -152,8 +152,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { return author.locked ? .private : .public }() // set visibility for reply post - switch kind { - case .reply(let record): + if case .reply(let record) = destination { context.managedObjectContext.performAndWait { guard let status = record.object(in: context.managedObjectContext) else { assertionFailure() @@ -173,8 +172,6 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { break } } - default: - break } return visibility }() @@ -185,7 +182,8 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { // end init // setup initial value - switch kind { + let initialContentWithSpace = initialContent.isEmpty ? "" : initialContent + " " + switch destination { case .reply(let record): context.managedObjectContext.performAndWait { guard let status = record.object(in: context.managedObjectContext) else { @@ -214,29 +212,15 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { } let initialComposeContent = mentionAccts.joined(separator: " ") - let preInsertedContent: String? = initialComposeContent.isEmpty ? nil : initialComposeContent + " " - self.initialContent = preInsertedContent ?? "" - self.content = preInsertedContent ?? "" + let preInsertedContent = initialComposeContent.isEmpty ? "" : initialComposeContent + " " + self.initialContent = preInsertedContent + initialContentWithSpace + self.content = preInsertedContent + initialContentWithSpace } - case .hashtag(let hashtag): - let initialComposeContent = "#" + hashtag - UITextChecker.learnWord(initialComposeContent) - let preInsertedContent = initialComposeContent + " " - self.initialContent = preInsertedContent - self.content = preInsertedContent - case .mention(let record): - context.managedObjectContext.performAndWait { - guard let user = record.object(in: context.managedObjectContext) else { return } - let initialComposeContent = "@" + user.acct - UITextChecker.learnWord(initialComposeContent) - let preInsertedContent = initialComposeContent + " " - self.initialContent = preInsertedContent - self.content = preInsertedContent - } - case .post: - break + case .topLevel: + self.initialContent = initialContentWithSpace + self.content = initialContentWithSpace } - + // set limit let _configuration: Mastodon.Entity.Instance.Configuration? = { var configuration: Mastodon.Entity.Instance.Configuration? = nil @@ -443,11 +427,9 @@ extension ComposeContentViewModel { } extension ComposeContentViewModel { - public enum Kind { - case post - case hashtag(hashtag: String) - case mention(user: ManagedObjectRecord) - case reply(status: ManagedObjectRecord) + public enum Destination { + case topLevel + case reply(parent: ManagedObjectRecord) } public enum ScrollViewState { @@ -530,10 +512,10 @@ extension ComposeContentViewModel { return MastodonStatusPublisher( author: author, replyTo: { - switch self.kind { - case .reply(let status): return status - default: return nil + if case .reply(let status) = destination { + return status } + return nil }(), isContentWarningComposing: isContentWarningActive, contentWarning: contentWarning, From 17b39da316abe386de0ed32a98427430cb2cd422 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sat, 3 Dec 2022 13:38:32 -0500 Subject: [PATCH 31/41] =?UTF-8?q?Add=20=E2=80=9CCopy,=E2=80=9D=20=E2=80=9C?= =?UTF-8?q?Share,=E2=80=9D=20and=20=E2=80=9CShare=20Link=20in=20Post?= =?UTF-8?q?=E2=80=9D=20actions=20to=20cards?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../input/Base.lproj/app.json | 3 + Localization/app.json | 3 + ...Provider+StatusTableViewCellDelegate.swift | 96 +++++++++++++++++++ .../StatusTableViewCellDelegate.swift | 10 ++ .../Generated/Strings.swift | 8 ++ .../Resources/Base.lproj/Localizable.strings | 3 + .../View/Content/NotificationView.swift | 9 ++ .../View/Content/StatusCardControl.swift | 25 ++++- .../View/Content/StatusView+ViewModel.swift | 6 -- .../MastodonUI/View/Content/StatusView.swift | 14 +++ 10 files changed, 169 insertions(+), 8 deletions(-) diff --git a/Localization/StringsConvertor/input/Base.lproj/app.json b/Localization/StringsConvertor/input/Base.lproj/app.json index e09ab983c..4788a99d4 100644 --- a/Localization/StringsConvertor/input/Base.lproj/app.json +++ b/Localization/StringsConvertor/input/Base.lproj/app.json @@ -78,6 +78,7 @@ "sign_up": "Create account", "see_more": "See More", "preview": "Preview", + "copy": "Copy", "share": "Share", "share_user": "Share %s", "share_post": "Share Post", @@ -133,6 +134,7 @@ "media_content_warning": "Tap anywhere to reveal", "tap_to_reveal": "Tap to reveal", "load_embed": "Load Embed", + "link_via_user": "%s via %s", "poll": { "vote": "Vote", "closed": "Closed" @@ -154,6 +156,7 @@ "show_image": "Show image", "show_gif": "Show GIF", "show_video_player": "Show video player", + "share_link_in_post": "Share Link in Post", "tap_then_hold_to_show_menu": "Tap then hold to show menu" }, "tag": { diff --git a/Localization/app.json b/Localization/app.json index e09ab983c..4788a99d4 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -78,6 +78,7 @@ "sign_up": "Create account", "see_more": "See More", "preview": "Preview", + "copy": "Copy", "share": "Share", "share_user": "Share %s", "share_post": "Share Post", @@ -133,6 +134,7 @@ "media_content_warning": "Tap anywhere to reveal", "tap_to_reveal": "Tap to reveal", "load_embed": "Load Embed", + "link_via_user": "%s via %s", "poll": { "vote": "Vote", "closed": "Closed" @@ -154,6 +156,7 @@ "show_image": "Show image", "show_gif": "Show GIF", "show_video_player": "Show video player", + "share_link_in_post": "Share Link in Post", "tap_then_hold_to_show_menu": "Tap then hold to show menu" }, "tag": { diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift index 721263f30..0521a3486 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift @@ -10,6 +10,9 @@ import CoreDataStack import MetaTextKit import MastodonCore import MastodonUI +import MastodonLocalization +import MastodonAsset +import LinkPresentation // MARK: - header extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { @@ -150,6 +153,99 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte } } + func tableViewCell( + _ cell: UITableViewCell, + statusView: StatusView, + cardControl: StatusCardControl, + didTapURL url: URL + ) { + Task { + let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) + guard let item = await item(from: source) else { + assertionFailure() + return + } + guard case let .status(status) = item else { + assertionFailure("only works for status data provider") + return + } + + await DataSourceFacade.responseToURLAction( + provider: self, + status: status, + url: url + ) + } + } + + func tableViewCell( + _ cell: UITableViewCell, + statusView: StatusView, + cardControlMenu statusCardControl: StatusCardControl + ) -> UIMenu? { + guard let card = statusView.viewModel.card, + let url = card.url else { + return nil + } + + return UIMenu(children: [ + UIAction( + title: L10n.Common.Controls.Actions.copy, + image: UIImage(systemName: "doc.on.doc") + ) { _ in + UIPasteboard.general.url = url + }, + UIAction( + title: L10n.Common.Controls.Actions.share, + image: Asset.Arrow.squareAndArrowUp.image.withRenderingMode(.alwaysTemplate) + ) { _ in + Task { + await MainActor.run { + let activityViewController = UIActivityViewController( + activityItems: [ + URLActivityItemWithMetadata(url: url) { metadata in + metadata.title = card.title + + if let image = card.imageURL { + metadata.iconProvider = ImageProvider(url: image, filter: nil).itemProvider + } + } + ], + applicationActivities: [] + ) + self.coordinator.present( + scene: .activityViewController( + activityViewController: activityViewController, + sourceView: statusCardControl, barButtonItem: nil + ), + from: self, + transition: .activityViewControllerPresent(animated: true) + ) + } + } + }, + UIAction( + title: L10n.Common.Controls.Status.Actions.shareLinkInPost, + image: Asset.ObjectsAndTools.squareAndPencil.image.withRenderingMode(.alwaysTemplate) + ) { _ in + Task { + await MainActor.run { + self.coordinator.present( + scene: .compose(viewModel: ComposeViewModel( + context: self.context, + authContext: self.authContext, + destination: .topLevel, + initialContent: L10n.Common.Controls.Status.linkViaUser(url.absoluteString, "@" + (statusView.viewModel.authorUsername ?? "")) + )), + from: self, + transition: .modal(animated: true) + ) + } + } + } + ]) + } + } // MARK: - media diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCellDelegate.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCellDelegate.swift index e76ba5006..f21e0573c 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCellDelegate.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCellDelegate.swift @@ -37,6 +37,8 @@ protocol StatusTableViewCellDelegate: AnyObject, AutoGenerateProtocolDelegate { func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaSensitiveButtonDidPressed button: UIButton) func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, statusMetricView: StatusMetricView, reblogButtonDidPressed button: UIButton) func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, statusMetricView: StatusMetricView, favoriteButtonDidPressed button: UIButton) + func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, cardControl: StatusCardControl, didTapURL url: URL) + func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, cardControlMenu: StatusCardControl) -> UIMenu? func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, accessibilityActivate: Void) // sourcery:end } @@ -102,6 +104,14 @@ extension StatusViewDelegate where Self: StatusViewContainerTableViewCell { delegate?.tableViewCell(self, statusView: statusView, statusMetricView: statusMetricView, favoriteButtonDidPressed: button) } + func statusView(_ statusView: StatusView, cardControl: StatusCardControl, didTapURL url: URL) { + delegate?.tableViewCell(self, statusView: statusView, cardControl: cardControl, didTapURL: url) + } + + func statusView(_ statusView: StatusView, cardControlMenu: StatusCardControl) -> UIMenu? { + return delegate?.tableViewCell(self, statusView: statusView, cardControlMenu: cardControlMenu) + } + func statusView(_ statusView: StatusView, accessibilityActivate: Void) { delegate?.tableViewCell(self, statusView: statusView, accessibilityActivate: accessibilityActivate) } diff --git a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift index 335638030..a2d2faf73 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift +++ b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift @@ -112,6 +112,8 @@ public enum L10n { public static let confirm = L10n.tr("Localizable", "Common.Controls.Actions.Confirm", fallback: "Confirm") /// Continue public static let `continue` = L10n.tr("Localizable", "Common.Controls.Actions.Continue", fallback: "Continue") + /// Copy + public static let copy = L10n.tr("Localizable", "Common.Controls.Actions.Copy", fallback: "Copy") /// Copy Photo public static let copyPhoto = L10n.tr("Localizable", "Common.Controls.Actions.CopyPhoto", fallback: "Copy Photo") /// Delete @@ -272,6 +274,10 @@ public enum L10n { public enum Status { /// Content Warning public static let contentWarning = L10n.tr("Localizable", "Common.Controls.Status.ContentWarning", fallback: "Content Warning") + /// %@ via %@ + public static func linkViaUser(_ p1: Any, _ p2: Any) -> String { + return L10n.tr("Localizable", "Common.Controls.Status.LinkViaUser", String(describing: p1), String(describing: p2), fallback: "%@ via %@") + } /// Load Embed public static let loadEmbed = L10n.tr("Localizable", "Common.Controls.Status.LoadEmbed", fallback: "Load Embed") /// Tap anywhere to reveal @@ -303,6 +309,8 @@ public enum L10n { public static let reblog = L10n.tr("Localizable", "Common.Controls.Status.Actions.Reblog", fallback: "Reblog") /// Reply public static let reply = L10n.tr("Localizable", "Common.Controls.Status.Actions.Reply", fallback: "Reply") + /// Share Link in Post + public static let shareLinkInPost = L10n.tr("Localizable", "Common.Controls.Status.Actions.ShareLinkInPost", fallback: "Share Link in Post") /// Show GIF public static let showGif = L10n.tr("Localizable", "Common.Controls.Status.Actions.ShowGif", fallback: "Show GIF") /// Show image diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings index 3c3e16ca3..88a67ac1d 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings @@ -31,6 +31,7 @@ Please check your internet connection."; "Common.Controls.Actions.Compose" = "Compose"; "Common.Controls.Actions.Confirm" = "Confirm"; "Common.Controls.Actions.Continue" = "Continue"; +"Common.Controls.Actions.Copy" = "Copy"; "Common.Controls.Actions.CopyPhoto" = "Copy Photo"; "Common.Controls.Actions.Delete" = "Delete"; "Common.Controls.Actions.Discard" = "Discard"; @@ -100,6 +101,7 @@ Please check your internet connection."; "Common.Controls.Status.Actions.Menu" = "Menu"; "Common.Controls.Status.Actions.Reblog" = "Reblog"; "Common.Controls.Status.Actions.Reply" = "Reply"; +"Common.Controls.Status.Actions.ShareLinkInPost" = "Share Link in Post"; "Common.Controls.Status.Actions.ShowGif" = "Show GIF"; "Common.Controls.Status.Actions.ShowImage" = "Show image"; "Common.Controls.Status.Actions.ShowVideoPlayer" = "Show video player"; @@ -107,6 +109,7 @@ Please check your internet connection."; "Common.Controls.Status.Actions.Unfavorite" = "Unfavorite"; "Common.Controls.Status.Actions.Unreblog" = "Undo reblog"; "Common.Controls.Status.ContentWarning" = "Content Warning"; +"Common.Controls.Status.LinkViaUser" = "%@ via %@"; "Common.Controls.Status.LoadEmbed" = "Load Embed"; "Common.Controls.Status.MediaContentWarning" = "Tap anywhere to reveal"; "Common.Controls.Status.MetaEntity.Email" = "Email address: %@"; diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift index eb077d6db..c44a06b7d 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift @@ -611,6 +611,15 @@ extension NotificationView: StatusViewDelegate { assertionFailure() } + public func statusView(_ statusView: StatusView, cardControl: StatusCardControl, didTapURL url: URL) { + assertionFailure() + } + + public func statusView(_ statusView: StatusView, cardControlMenu: StatusCardControl) -> UIMenu? { + assertionFailure() + return nil + } + } // MARK: - MastodonMenuDelegate diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift index 51c125a87..433719a06 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift @@ -14,8 +14,13 @@ import CoreDataStack import UIKit import WebKit +public protocol StatusCardControlDelegate: AnyObject { + func statusCardControl(_ statusCardControl: StatusCardControl, didTapURL url: URL) + func statusCardControlMenu(_ statusCardControl: StatusCardControl) -> UIMenu? +} + public final class StatusCardControl: UIControl { - public var urlToOpen = PassthroughSubject() + public weak var delegate: StatusCardControlDelegate? private var disposeBag = Set() @@ -130,6 +135,8 @@ public final class StatusCardControl: UIControl { showEmbedButton.centerXAnchor.constraint(equalTo: imageView.centerXAnchor), showEmbedButton.centerYAnchor.constraint(equalTo: imageView.centerYAnchor), ]) + + addInteraction(UIContextMenuInteraction(delegate: self)) } required init?(coder: NSCoder) { @@ -238,6 +245,7 @@ public final class StatusCardControl: UIControl { } } +// MARK: WKWebView delegates extension StatusCardControl: WKNavigationDelegate, WKUIDelegate { fileprivate func showWebView() { let webView = setupWebView() @@ -279,7 +287,7 @@ extension StatusCardControl: WKNavigationDelegate, WKUIDelegate { navigationAction.navigationType == .linkActivated || navigationAction.navigationType == .other, let url = navigationAction.request.url, url.absoluteString != "about:blank" { - urlToOpen.send(url) + delegate?.statusCardControl(self, didTapURL: url) return .cancel } return .allow @@ -291,6 +299,19 @@ extension StatusCardControl: WKNavigationDelegate, WKUIDelegate { } } +// 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) + } +} + private extension StatusCardControl { enum Layout: Equatable { case compact diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift index 634473a65..77de106b1 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift @@ -494,12 +494,6 @@ extension StatusView.ViewModel { statusView.setStatusCardControlDisplay() } .store(in: &disposeBag) - - statusView.statusCardControl.urlToOpen - .sink { url in - statusView.delegate?.statusView(statusView, didTapCardWithURL: url) - } - .store(in: &disposeBag) } private func bindToolbar(statusView: StatusView) { diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift index 249e2e1ec..539276f41 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift @@ -33,6 +33,8 @@ public protocol StatusViewDelegate: AnyObject { func statusView(_ statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaSensitiveButtonDidPressed button: UIButton) func statusView(_ statusView: StatusView, statusMetricView: StatusMetricView, reblogButtonDidPressed button: UIButton) func statusView(_ statusView: StatusView, statusMetricView: StatusMetricView, favoriteButtonDidPressed button: UIButton) + func statusView(_ statusView: StatusView, cardControl: StatusCardControl, didTapURL url: URL) + func statusView(_ statusView: StatusView, cardControlMenu: StatusCardControl) -> UIMenu? // a11y func statusView(_ statusView: StatusView, accessibilityActivate: Void) @@ -264,6 +266,7 @@ extension StatusView { // card statusCardControl.addTarget(self, action: #selector(statusCardControlPressed), for: .touchUpInside) + statusCardControl.delegate = self // media mediaGridContainerView.delegate = self @@ -667,6 +670,17 @@ extension StatusView: MastodonMenuDelegate { } } +// MARK: StatusCardControlDelegate +extension StatusView: StatusCardControlDelegate { + public func statusCardControl(_ statusCardControl: StatusCardControl, didTapURL url: URL) { + delegate?.statusView(self, cardControl: statusCardControl, didTapURL: url) + } + + public func statusCardControlMenu(_ statusCardControl: StatusCardControl) -> UIMenu? { + delegate?.statusView(self, cardControlMenu: statusCardControl) + } +} + #if DEBUG import SwiftUI From 1379cdc448370c4ab05b360be3c7cbdb08fa287e Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sat, 3 Dec 2022 13:41:51 -0500 Subject: [PATCH 32/41] Disable cards in notifications MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit both to save space and because I’m too lazy to wire up the delegate methods for the menu --- ...er+NotificationTableViewCellDelegate.swift | 55 ------------------- .../NotificationTableViewCellDelegate.swift | 10 ---- .../View/Content/NotificationView.swift | 11 +--- .../MastodonUI/View/Content/StatusView.swift | 2 + 4 files changed, 3 insertions(+), 75 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift index 279cb562e..e868f418f 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift @@ -516,61 +516,6 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut } -// MARK: - card -extension NotificationTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { - - func tableViewCell( - _ cell: UITableViewCell, - notificationView: NotificationView, - statusView: StatusView, - didTapCardWithURL url: URL - ) { - Task { - let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) - guard let item = await item(from: source) else { - assertionFailure() - return - } - guard case let .status(status) = item else { - assertionFailure("only works for status data provider") - return - } - - await DataSourceFacade.responseToURLAction( - provider: self, - status: status, - url: url - ) - } - } - - func tableViewCell( - _ cell: UITableViewCell, - notificationView: NotificationView, - quoteStatusView: StatusView, - didTapCardWithURL url: URL - ) { - Task { - let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) - guard let item = await item(from: source) else { - assertionFailure() - return - } - guard case let .status(status) = item else { - assertionFailure("only works for status data provider") - return - } - - await DataSourceFacade.responseToURLAction( - provider: self, - status: status, - url: url - ) - } - } - -} - // MARK: a11y extension NotificationTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, accessibilityActivate: Void) { diff --git a/Mastodon/Scene/Notification/Cell/NotificationTableViewCellDelegate.swift b/Mastodon/Scene/Notification/Cell/NotificationTableViewCellDelegate.swift index 45ad59334..7a603d5f0 100644 --- a/Mastodon/Scene/Notification/Cell/NotificationTableViewCellDelegate.swift +++ b/Mastodon/Scene/Notification/Cell/NotificationTableViewCellDelegate.swift @@ -28,13 +28,11 @@ protocol NotificationTableViewCellDelegate: AnyObject, AutoGenerateProtocolDeleg func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, acceptFollowRequestButtonDidPressed button: UIButton) func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, rejectFollowRequestButtonDidPressed button: UIButton) func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) - func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, statusView: StatusView, didTapCardWithURL url: URL) func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, statusView: StatusView, spoilerOverlayViewDidPressed overlayView: SpoilerOverlayView) func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaView: MediaView, didSelectMediaViewAt index: Int) func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, statusView: StatusView, actionToolbarContainer: ActionToolbarContainer, buttonDidPressed button: UIButton, action: ActionToolbarContainer.Action) func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, quoteStatusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton) func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, quoteStatusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) - func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, quoteStatusView: StatusView, didTapCardWithURL url: URL) func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, quoteStatusView: StatusView, spoilerOverlayViewDidPressed overlayView: SpoilerOverlayView) func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, quoteStatusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaView: MediaView, didSelectMediaViewAt index: Int) func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, accessibilityActivate: Void) @@ -65,10 +63,6 @@ extension NotificationViewDelegate where Self: NotificationViewContainerTableVie delegate?.tableViewCell(self, notificationView: notificationView, statusView: statusView, metaText: metaText, didSelectMeta: meta) } - func notificationView(_ notificationView: NotificationView, statusView: StatusView, didTapCardWithURL url: URL) { - delegate?.tableViewCell(self, notificationView: notificationView, statusView: statusView, didTapCardWithURL: url) - } - func notificationView(_ notificationView: NotificationView, statusView: StatusView, spoilerOverlayViewDidPressed overlayView: SpoilerOverlayView) { delegate?.tableViewCell(self, notificationView: notificationView, statusView: statusView, spoilerOverlayViewDidPressed: overlayView) } @@ -89,10 +83,6 @@ extension NotificationViewDelegate where Self: NotificationViewContainerTableVie delegate?.tableViewCell(self, notificationView: notificationView, quoteStatusView: quoteStatusView, metaText: metaText, didSelectMeta: meta) } - func notificationView(_ notificationView: NotificationView, quoteStatusView: StatusView, didTapCardWithURL url: URL) { - delegate?.tableViewCell(self, notificationView: notificationView, quoteStatusView: quoteStatusView, didTapCardWithURL: url) - } - func notificationView(_ notificationView: NotificationView, quoteStatusView: StatusView, spoilerOverlayViewDidPressed overlayView: SpoilerOverlayView) { delegate?.tableViewCell(self, notificationView: notificationView, quoteStatusView: quoteStatusView, spoilerOverlayViewDidPressed: overlayView) } diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift index c44a06b7d..9196c340e 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift @@ -22,7 +22,6 @@ public protocol NotificationViewDelegate: AnyObject { func notificationView(_ notificationView: NotificationView, rejectFollowRequestButtonDidPressed button: UIButton) func notificationView(_ notificationView: NotificationView, statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) - func notificationView(_ notificationView: NotificationView, statusView: StatusView, didTapCardWithURL url: URL) func notificationView(_ notificationView: NotificationView, statusView: StatusView, spoilerOverlayViewDidPressed overlayView: SpoilerOverlayView) func notificationView(_ notificationView: NotificationView, statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaView: MediaView, didSelectMediaViewAt index: Int) @@ -30,7 +29,6 @@ public protocol NotificationViewDelegate: AnyObject { func notificationView(_ notificationView: NotificationView, quoteStatusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton) func notificationView(_ notificationView: NotificationView, quoteStatusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) - func notificationView(_ notificationView: NotificationView, quoteStatusView: StatusView, didTapCardWithURL url: URL) func notificationView(_ notificationView: NotificationView, quoteStatusView: StatusView, spoilerOverlayViewDidPressed overlayView: SpoilerOverlayView) func notificationView(_ notificationView: NotificationView, quoteStatusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaView: MediaView, didSelectMediaViewAt index: Int) @@ -499,14 +497,7 @@ extension NotificationView { // MARK: - StatusViewDelegate extension NotificationView: StatusViewDelegate { public func statusView(_ statusView: StatusView, didTapCardWithURL url: URL) { - switch statusView { - case self.statusView: - delegate?.notificationView(self, statusView: statusView, didTapCardWithURL: url) - case quoteStatusView: - delegate?.notificationView(self, quoteStatusView: statusView, didTapCardWithURL: url) - default: - assertionFailure() - } + assertionFailure() } public func statusView(_ statusView: StatusView, headerDidPressed header: UIView) { diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift index 539276f41..2b9e84e5e 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift @@ -488,6 +488,7 @@ extension StatusView.Style { statusView.headerAdaptiveMarginContainerView.removeFromSuperview() statusView.authorAdaptiveMarginContainerView.removeFromSuperview() + statusView.statusCardControl.removeFromSuperview() } func notificationQuote(statusView: StatusView) { @@ -496,6 +497,7 @@ extension StatusView.Style { statusView.contentAdaptiveMarginContainerView.bottomLayoutConstraint?.constant = 16 // fix bottom margin missing issue statusView.pollAdaptiveMarginContainerView.bottomLayoutConstraint?.constant = 16 // fix bottom margin missing issue statusView.actionToolbarAdaptiveMarginContainerView.removeFromSuperview() + statusView.statusCardControl.removeFromSuperview() } func composeStatusReplica(statusView: StatusView) { From 3c806393a385506d584c95c8bf1649341ef72521 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sat, 3 Dec 2022 14:00:01 -0500 Subject: [PATCH 33/41] Fix core data update --- .../CoreDataStack/CoreData.xcdatamodeld/.xccurrentversion | 2 +- .../CoreData.xcdatamodeld/CoreData 6.xcdatamodel/contents | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/.xccurrentversion b/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/.xccurrentversion index 2145ac780..e660b0a08 100644 --- a/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/.xccurrentversion +++ b/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - CoreData 5.xcdatamodel + CoreData 6.xcdatamodel diff --git a/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 6.xcdatamodel/contents b/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 6.xcdatamodel/contents index c9c274ddc..b146c2b97 100644 --- a/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 6.xcdatamodel/contents +++ b/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 6.xcdatamodel/contents @@ -224,6 +224,7 @@ + @@ -269,6 +270,7 @@ + From 285fbd4247815b9a79f380a510f681edfb6aeec3 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sat, 3 Dec 2022 14:02:22 -0500 Subject: [PATCH 34/41] Fix divider not visible in compact cards --- .../Sources/MastodonUI/View/Content/StatusCardControl.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift index 433719a06..3f297424c 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift @@ -221,6 +221,7 @@ public final class StatusCardControl: UIControl { imageView.widthAnchor.constraint(equalToConstant: 85), heightAnchor.constraint(equalToConstant: 85).priority(.defaultLow - 1), heightAnchor.constraint(greaterThanOrEqualToConstant: 85), + dividerView.heightAnchor.constraint(equalTo: containerStackView.heightAnchor), ] dividerConstraint = dividerView.widthAnchor.constraint(equalToConstant: pixelSize).activate() } From 1642839084f8f7cfd04d92ccdea486e35028355e Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sat, 3 Dec 2022 14:03:53 -0500 Subject: [PATCH 35/41] Force card into large mode if it has an embed --- .../Sources/MastodonUI/View/Content/StatusCardControl.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift index 3f297424c..6b6296cef 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift @@ -326,7 +326,7 @@ private extension Card { if !aspectRatio.isFinite { aspectRatio = 1 } - return abs(aspectRatio - 1) < 0.05 || image == nil + return (abs(aspectRatio - 1) < 0.05 || image == nil) && html == nil ? .compact : .large(aspectRatio: aspectRatio) } From 4f8ca8d481a42fd170c9859a1f23e9f42346239d Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sat, 3 Dec 2022 14:07:43 -0500 Subject: [PATCH 36/41] Use a non-opaque background color for the image view --- .../Sources/MastodonUI/View/Content/StatusCardControl.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift index 6b6296cef..aaca1cd6c 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift @@ -242,7 +242,7 @@ public final class StatusCardControl: UIControl { private func apply(theme: Theme) { layer.borderColor = theme.separator.cgColor dividerView.backgroundColor = theme.separator - imageView.backgroundColor = theme.systemElevatedBackgroundColor + imageView.backgroundColor = UIColor.tertiarySystemFill } } From cd6bdead013b94371e27699d67f519c8bb23b7b5 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Thu, 15 Dec 2022 07:39:05 -0500 Subject: [PATCH 37/41] DispatchQueue.main.async --- ...Provider+StatusTableViewCellDelegate.swift | 68 +++++++++---------- 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift index 0521a3486..3fa1a4256 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift @@ -195,52 +195,50 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte ) { _ in UIPasteboard.general.url = url }, + UIAction( title: L10n.Common.Controls.Actions.share, image: Asset.Arrow.squareAndArrowUp.image.withRenderingMode(.alwaysTemplate) ) { _ in - Task { - await MainActor.run { - let activityViewController = UIActivityViewController( - activityItems: [ - URLActivityItemWithMetadata(url: url) { metadata in - metadata.title = card.title - - if let image = card.imageURL { - metadata.iconProvider = ImageProvider(url: image, filter: nil).itemProvider - } + DispatchQueue.main.async { + let activityViewController = UIActivityViewController( + activityItems: [ + URLActivityItemWithMetadata(url: url) { metadata in + metadata.title = card.title + + if let image = card.imageURL { + metadata.iconProvider = ImageProvider(url: image, filter: nil).itemProvider } - ], - applicationActivities: [] - ) - self.coordinator.present( - scene: .activityViewController( - activityViewController: activityViewController, - sourceView: statusCardControl, barButtonItem: nil - ), - from: self, - transition: .activityViewControllerPresent(animated: true) - ) - } + } + ], + applicationActivities: [] + ) + self.coordinator.present( + scene: .activityViewController( + activityViewController: activityViewController, + sourceView: statusCardControl, barButtonItem: nil + ), + from: self, + transition: .activityViewControllerPresent(animated: true) + ) } }, + UIAction( title: L10n.Common.Controls.Status.Actions.shareLinkInPost, image: Asset.ObjectsAndTools.squareAndPencil.image.withRenderingMode(.alwaysTemplate) ) { _ in - Task { - await MainActor.run { - self.coordinator.present( - scene: .compose(viewModel: ComposeViewModel( - context: self.context, - authContext: self.authContext, - destination: .topLevel, - initialContent: L10n.Common.Controls.Status.linkViaUser(url.absoluteString, "@" + (statusView.viewModel.authorUsername ?? "")) - )), - from: self, - transition: .modal(animated: true) - ) - } + DispatchQueue.main.async { + self.coordinator.present( + scene: .compose(viewModel: ComposeViewModel( + context: self.context, + authContext: self.authContext, + destination: .topLevel, + initialContent: L10n.Common.Controls.Status.linkViaUser(url.absoluteString, "@" + (statusView.viewModel.authorUsername ?? "")) + )), + from: self, + transition: .modal(animated: true) + ) } } ]) From cc4df41fbb72fa5f650c118d1136acaebeff0724 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Thu, 15 Dec 2022 07:39:52 -0500 Subject: [PATCH 38/41] Disable divider autoresizing mask Co-Authored-By: Marcus Kida --- .../Sources/MastodonUI/View/Content/StatusCardControl.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift index aaca1cd6c..0a175b23b 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift @@ -128,6 +128,7 @@ public final class StatusCardControl: UIControl { containerStackView.translatesAutoresizingMaskIntoConstraints = false highlightView.translatesAutoresizingMaskIntoConstraints = false showEmbedButton.translatesAutoresizingMaskIntoConstraints = false + dividerView.translatesAutoresizingMaskIntoConstraints = false containerStackView.pinToParent() highlightView.pinToParent() From dccfb4e831393f3369088d6224bd69abe67ffab2 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Thu, 15 Dec 2022 07:42:10 -0500 Subject: [PATCH 39/41] Avoid division by 0 --- .../MastodonUI/Extension/UIScreen.swift | 18 ++++++++++++++++++ .../View/Content/StatusCardControl.swift | 6 +++--- 2 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 MastodonSDK/Sources/MastodonUI/Extension/UIScreen.swift diff --git a/MastodonSDK/Sources/MastodonUI/Extension/UIScreen.swift b/MastodonSDK/Sources/MastodonUI/Extension/UIScreen.swift new file mode 100644 index 000000000..831f2cb32 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Extension/UIScreen.swift @@ -0,0 +1,18 @@ +// +// UIScreen.swift +// +// +// Created by Jed Fox on 2022-12-15. +// + +import UIKit + +extension UIScreen { + public var pixelSize: CGFloat { + if scale > 0 { + return 1 / scale + } + // should never happen but just in case + return 1 + } +} diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift index 0a175b23b..bd2485cf4 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift @@ -184,8 +184,8 @@ public final class StatusCardControl: UIControl { super.didMoveToWindow() if let window = window { - layer.borderWidth = 1 / window.screen.scale - dividerConstraint?.constant = 1 / window.screen.scale + layer.borderWidth = window.screen.pixelSize + dividerConstraint?.constant = 1 / window.screen.pixelSize } } @@ -196,7 +196,7 @@ public final class StatusCardControl: UIControl { NSLayoutConstraint.deactivate(layoutConstraints) dividerConstraint?.deactivate() - let pixelSize = 1 / (window?.screen.scale ?? 1) + let pixelSize = 1 / (window?.screen.pixelSize ?? 1) switch layout { case .large(let aspectRatio): containerStackView.alignment = .fill From 1be9dcef667370796a42e66525340eb45aa40291 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Thu, 15 Dec 2022 07:43:25 -0500 Subject: [PATCH 40/41] Bump data model version --- .../CoreDataStack/CoreData.xcdatamodeld/.xccurrentversion | 2 +- .../{CoreData 6.xcdatamodel => CoreData 7.xcdatamodel}/contents | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/{CoreData 6.xcdatamodel => CoreData 7.xcdatamodel}/contents (100%) diff --git a/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/.xccurrentversion b/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/.xccurrentversion index e660b0a08..b2d4c7f6e 100644 --- a/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/.xccurrentversion +++ b/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - CoreData 6.xcdatamodel + CoreData 7.xcdatamodel diff --git a/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 6.xcdatamodel/contents b/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 7.xcdatamodel/contents similarity index 100% rename from MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 6.xcdatamodel/contents rename to MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 7.xcdatamodel/contents From f8556183a3e698fc135b2f143514cf70e6c45cfc Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Thu, 15 Dec 2022 08:11:51 -0500 Subject: [PATCH 41/41] Fix inverting pizelSize! --- .../Sources/MastodonUI/View/Content/StatusCardControl.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift index bd2485cf4..228159d1f 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift @@ -185,7 +185,7 @@ public final class StatusCardControl: UIControl { if let window = window { layer.borderWidth = window.screen.pixelSize - dividerConstraint?.constant = 1 / window.screen.pixelSize + dividerConstraint?.constant = window.screen.pixelSize } } @@ -196,7 +196,7 @@ public final class StatusCardControl: UIControl { NSLayoutConstraint.deactivate(layoutConstraints) dividerConstraint?.deactivate() - let pixelSize = 1 / (window?.screen.pixelSize ?? 1) + let pixelSize = (window?.screen.pixelSize ?? 1) switch layout { case .large(let aspectRatio): containerStackView.alignment = .fill