Show author in Card (IOS-284) (#1321)
This PR updates the Preview Card. Apart from a minor background color change, it introduces the new `StatusCardAuthorControl`, which is visible if the author of an article has a Mastodon-account. If the user taps on the account, the given profile will open. This feature will be available in Mastodon >4.3 (See [this PR](https://github.com/mastodon/mastodon/pull/30398), for example). HMI, if you need more information on how to test this etc. Please enjoy these screenshots with some fake data: ![1321](https://github.com/mastodon/mastodon-ios/assets/2580019/cc580c84-6700-4f15-b1b0-b845c21a445e)
This commit is contained in:
commit
4fdb1dd4a3
|
@ -232,6 +232,10 @@
|
|||
"edit_history": {
|
||||
"title": "Edit History",
|
||||
"original_post": "Original Post · %s"
|
||||
},
|
||||
"card": {
|
||||
"by": "By",
|
||||
"by_author": "By %s"
|
||||
}
|
||||
},
|
||||
"friendship": {
|
||||
|
@ -751,6 +755,7 @@
|
|||
"title": "Settings",
|
||||
"general": "General",
|
||||
"notifications": "Notifications",
|
||||
"privacy_safety": "Privacy & Safety",
|
||||
"support_mastodon": "Support Mastodon",
|
||||
"about_mastodon": "About Mastodon",
|
||||
"server_details": "Server Details",
|
||||
|
@ -821,6 +826,25 @@
|
|||
"go_to_settings": "Go to Notification Settings"
|
||||
}
|
||||
|
||||
},
|
||||
"privacy_safety": {
|
||||
"title": "Privacy & Safety",
|
||||
"preset": {
|
||||
"title": "Preset",
|
||||
"open_and_public": "Open & Public",
|
||||
"private_and_restricted": "Private & Restricted",
|
||||
"custom": "Custom"
|
||||
},
|
||||
"default_post_visibility": {
|
||||
"title": "Default Post Visibility",
|
||||
"public": "Public",
|
||||
"followers_only": "Followers Only",
|
||||
"only_people_mentioned": "Only People Mentioned"
|
||||
},
|
||||
"manually_approve_follow_requests": "Manually Approve Follow Requests",
|
||||
"show_followers_and_following": "Show Followers & Following",
|
||||
"suggest_my_account_to_others": "Suggest My Account to Others",
|
||||
"appear_in_search_engines": "Appear in Search Engines"
|
||||
}
|
||||
},
|
||||
"report": {
|
||||
|
|
|
@ -232,6 +232,10 @@
|
|||
"edit_history": {
|
||||
"title": "Edit History",
|
||||
"original_post": "Original Post · %s"
|
||||
},
|
||||
"card": {
|
||||
"by": "By",
|
||||
"by_author": "By %s"
|
||||
}
|
||||
},
|
||||
"friendship": {
|
||||
|
|
|
@ -1600,7 +1600,6 @@
|
|||
children = (
|
||||
5D03938E2612D200007FE196 /* Webview */,
|
||||
DB68A04F25E9028800CFDF14 /* NavigationController */,
|
||||
DB9D6C2025E502C60051B173 /* ViewModel */,
|
||||
2D7631A525C1532D00929FB9 /* View */,
|
||||
DBA5E7A6263BD298004598BB /* ContextMenu */,
|
||||
);
|
||||
|
@ -2697,13 +2696,6 @@
|
|||
path = Profile;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB9D6C2025E502C60051B173 /* ViewModel */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
path = ViewModel;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB9F58ED26EF435800E7BBE9 /* Account */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
|
@ -144,6 +144,17 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
|
|||
}
|
||||
}
|
||||
|
||||
func tableViewCell(
|
||||
_ cell: UITableViewCell,
|
||||
statusView: StatusView,
|
||||
cardControl: StatusCardControl,
|
||||
didTapProfile account: Mastodon.Entity.Account
|
||||
) {
|
||||
Task {
|
||||
await DataSourceFacade.coordinateToProfileScene(provider:self, account: account)
|
||||
}
|
||||
}
|
||||
|
||||
func tableViewCell(
|
||||
_ cell: UITableViewCell,
|
||||
statusView: StatusView,
|
||||
|
|
|
@ -491,8 +491,9 @@ extension NotificationView {
|
|||
|
||||
// MARK: - StatusViewDelegate
|
||||
extension NotificationView: StatusViewDelegate {
|
||||
public func statusView(_ statusView: StatusView, didTapCardWithURL url: URL) {
|
||||
assertionFailure()
|
||||
public func statusView(_ statusView: StatusView, didTapCardWithURL url: URL) { assertionFailure() }
|
||||
public func statusView(_ statusView: StatusView, cardControl: StatusCardControl, didTapProfile account: Mastodon.Entity.Account) {
|
||||
// no op
|
||||
}
|
||||
|
||||
public func statusView(_ statusView: StatusView, headerDidPressed header: UIView) {
|
||||
|
|
|
@ -40,6 +40,7 @@ protocol StatusTableViewCellDelegate: AnyObject, AutoGenerateProtocolDelegate {
|
|||
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, statusMetricView: StatusMetricView, favoriteButtonDidPressed button: UIButton)
|
||||
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, statusMetricView: StatusMetricView, showEditHistory button: UIButton)
|
||||
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, cardControl: StatusCardControl, didTapURL url: URL)
|
||||
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, cardControl: StatusCardControl, didTapProfile account: Mastodon.Entity.Account)
|
||||
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, cardControlMenu: StatusCardControl) -> [LabeledAction]?
|
||||
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, accessibilityActivate: Void)
|
||||
// sourcery:end
|
||||
|
@ -114,6 +115,10 @@ extension StatusViewDelegate where Self: StatusViewContainerTableViewCell {
|
|||
delegate?.tableViewCell(self, statusView: statusView, cardControl: cardControl, didTapURL: url)
|
||||
}
|
||||
|
||||
func statusView(_ statusView: StatusView, cardControl: StatusCardControl, didTapProfile account: Mastodon.Entity.Account) {
|
||||
delegate?.tableViewCell(self, statusView: statusView, cardControl: cardControl, didTapProfile: account)
|
||||
}
|
||||
|
||||
func statusView(_ statusView: StatusView, cardControlMenu: StatusCardControl) -> [LabeledAction]? {
|
||||
return delegate?.tableViewCell(self, statusView: statusView, cardControlMenu: cardControlMenu)
|
||||
}
|
||||
|
|
|
@ -411,6 +411,14 @@ public enum L10n {
|
|||
/// Boosts
|
||||
public static let reblogsTitle = L10n.tr("Localizable", "Common.Controls.Status.Buttons.ReblogsTitle", fallback: "Boosts")
|
||||
}
|
||||
public enum Card {
|
||||
/// By
|
||||
public static let by = L10n.tr("Localizable", "Common.Controls.Status.Card.By", fallback: "By")
|
||||
/// By %@
|
||||
public static func byAuthor(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Common.Controls.Status.Card.ByAuthor", String(describing: p1), fallback: "By %@")
|
||||
}
|
||||
}
|
||||
public enum EditHistory {
|
||||
/// Original Post · %@
|
||||
public static func originalPost(_ p1: Any) -> String {
|
||||
|
|
|
@ -136,6 +136,8 @@ Please check your internet connection.";
|
|||
"Common.Controls.Status.Buttons.EditHistoryTitle" = "Edit History";
|
||||
"Common.Controls.Status.Buttons.FavoritesTitle" = "Favorites";
|
||||
"Common.Controls.Status.Buttons.ReblogsTitle" = "Boosts";
|
||||
"Common.Controls.Status.Card.By" = "By";
|
||||
"Common.Controls.Status.Card.ByAuthor" = "By %@";
|
||||
"Common.Controls.Status.ContentWarning" = "Content Warning";
|
||||
"Common.Controls.Status.EditHistory.OriginalPost" = "Original Post · %@";
|
||||
"Common.Controls.Status.EditHistory.Title" = "Edit History";
|
||||
|
@ -562,19 +564,19 @@ If you disagree with the policy for **%@**, you can go back and pick a different
|
|||
"Scene.Settings.Overview.ServerDetails" = "Server Details";
|
||||
"Scene.Settings.Overview.SupportMastodon" = "Support Mastodon";
|
||||
"Scene.Settings.Overview.Title" = "Settings";
|
||||
"Scene.Settings.PrivacySafety.Title" = "Privacy & Safety";
|
||||
"Scene.Settings.PrivacySafety.Preset.Title" = "Preset";
|
||||
"Scene.Settings.PrivacySafety.Preset.OpenAndPublic" = "Open & Public";
|
||||
"Scene.Settings.PrivacySafety.Preset.PrivateAndRestricted" = "Private & Restricted";
|
||||
"Scene.Settings.PrivacySafety.Preset.Custom" = "Custom";
|
||||
"Scene.Settings.PrivacySafety.DefaultPostVisibility.Title" = "Default Post Visibility";
|
||||
"Scene.Settings.PrivacySafety.DefaultPostVisibility.Public" = "Public";
|
||||
"Scene.Settings.PrivacySafety.AppearInSearchEngines" = "Appear in Search Engines";
|
||||
"Scene.Settings.PrivacySafety.DefaultPostVisibility.FollowersOnly" = "Followers Only";
|
||||
"Scene.Settings.PrivacySafety.DefaultPostVisibility.OnlyPeopleMentioned" = "Only People Mentioned";
|
||||
"Scene.Settings.PrivacySafety.DefaultPostVisibility.Public" = "Public";
|
||||
"Scene.Settings.PrivacySafety.DefaultPostVisibility.Title" = "Default Post Visibility";
|
||||
"Scene.Settings.PrivacySafety.ManuallyApproveFollowRequests" = "Manually Approve Follow Requests";
|
||||
"Scene.Settings.PrivacySafety.Preset.Custom" = "Custom";
|
||||
"Scene.Settings.PrivacySafety.Preset.OpenAndPublic" = "Open & Public";
|
||||
"Scene.Settings.PrivacySafety.Preset.PrivateAndRestricted" = "Private & Restricted";
|
||||
"Scene.Settings.PrivacySafety.Preset.Title" = "Preset";
|
||||
"Scene.Settings.PrivacySafety.ShowFollowersAndFollowing" = "Show Followers & Following";
|
||||
"Scene.Settings.PrivacySafety.SuggestMyAccountToOthers" = "Suggest My Account to Others";
|
||||
"Scene.Settings.PrivacySafety.AppearInSearchEngines" = "Appear in Search Engines";
|
||||
"Scene.Settings.PrivacySafety.Title" = "Privacy & Safety";
|
||||
"Scene.Settings.ServerDetails.About" = "About";
|
||||
"Scene.Settings.ServerDetails.AboutInstance.LegalNotice" = "A legal notice";
|
||||
"Scene.Settings.ServerDetails.AboutInstance.MessageAdmin" = "Message Admin";
|
||||
|
@ -615,4 +617,4 @@ If you disagree with the policy for **%@**, you can go back and pick a different
|
|||
"Widget.MultipleFollowers.ConfigurationDescription" = "Show number of followers for multiple accounts.";
|
||||
"Widget.MultipleFollowers.ConfigurationDisplayName" = "Multiple followers";
|
||||
"Widget.MultipleFollowers.MockUser.AccountName" = "another@follower.social";
|
||||
"Widget.MultipleFollowers.MockUser.DisplayName" = "Another follower";
|
||||
"Widget.MultipleFollowers.MockUser.DisplayName" = "Another follower";
|
|
@ -22,8 +22,10 @@ extension Mastodon.Entity {
|
|||
public let title: String
|
||||
public let description: String
|
||||
public let type: Type
|
||||
|
||||
|
||||
@available(*, deprecated, message: "Use authors-array. Kept for compatibility")
|
||||
public let authorName: String?
|
||||
@available(*, deprecated, message: "Use authors-array. Kept for compatibility")
|
||||
public let authorURL: String?
|
||||
public let providerName: String?
|
||||
public let providerURL: String?
|
||||
|
@ -33,7 +35,9 @@ extension Mastodon.Entity {
|
|||
public let image: String?
|
||||
public let embedURL: String?
|
||||
public let blurhash: String?
|
||||
|
||||
public let authors: [Mastodon.Entity.Card.Author]?
|
||||
public let publishedAt: Date?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case url
|
||||
case title
|
||||
|
@ -49,10 +53,20 @@ extension Mastodon.Entity {
|
|||
case image
|
||||
case embedURL = "embed_url"
|
||||
case blurhash
|
||||
case authors
|
||||
case publishedAt = "published_at"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Mastodon.Entity.Card {
|
||||
public struct Author: Codable, Sendable {
|
||||
public let name: String?
|
||||
public let url: String?
|
||||
public let account: Mastodon.Entity.Account?
|
||||
}
|
||||
}
|
||||
|
||||
extension Mastodon.Entity.Card {
|
||||
public enum `Type`: RawRepresentable, Codable, Sendable {
|
||||
case link
|
||||
|
|
|
@ -27,7 +27,11 @@ extension Date {
|
|||
formatter.timeStyle = .none // none
|
||||
return formatter
|
||||
}()
|
||||
|
||||
|
||||
public var abbreviatedDate: String {
|
||||
return Date.abbreviatedDateFormatter.string(from: self)
|
||||
}
|
||||
|
||||
public var localizedAbbreviatedSlowedTimeAgoSinceNow: String {
|
||||
return Date.relativeTimestampFormatter.localizedString(for: self, relativeTo: Date())
|
||||
}
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
import UIKit
|
||||
import MastodonAsset
|
||||
import MastodonSDK
|
||||
|
||||
class StatusCardAuthorControl: UIControl {
|
||||
let authorLabel: UILabel
|
||||
let avatarImage: AvatarImageView
|
||||
private let contentStackView: UIStackView
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
authorLabel = UILabel()
|
||||
authorLabel.textAlignment = .center
|
||||
authorLabel.font = UIFontMetrics(forTextStyle: .footnote).scaledFont(for: .systemFont(ofSize: 16, weight: .bold))
|
||||
authorLabel.isUserInteractionEnabled = false
|
||||
|
||||
avatarImage = AvatarImageView()
|
||||
avatarImage.translatesAutoresizingMaskIntoConstraints = false
|
||||
avatarImage.configure(cornerConfiguration: AvatarImageView.CornerConfiguration(corner: .fixed(radius: 4)))
|
||||
avatarImage.isUserInteractionEnabled = false
|
||||
|
||||
contentStackView = UIStackView(arrangedSubviews: [avatarImage, authorLabel])
|
||||
contentStackView.alignment = .center
|
||||
contentStackView.spacing = 6
|
||||
contentStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentStackView.layoutMargins = UIEdgeInsets(horizontal: 6, vertical: 8)
|
||||
contentStackView.isUserInteractionEnabled = false
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
addSubview(contentStackView)
|
||||
setupConstraints()
|
||||
backgroundColor = Asset.Colors.Button.userFollowing.color
|
||||
layer.cornerRadius = 10
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
private func setupConstraints() {
|
||||
let constraints = [
|
||||
contentStackView.topAnchor.constraint(equalTo: topAnchor, constant: 6),
|
||||
contentStackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 6),
|
||||
trailingAnchor.constraint(equalTo: contentStackView.trailingAnchor, constant: 6),
|
||||
bottomAnchor.constraint(equalTo: contentStackView.bottomAnchor, constant: 6),
|
||||
|
||||
avatarImage.widthAnchor.constraint(equalToConstant: 16),
|
||||
avatarImage.widthAnchor.constraint(equalTo: avatarImage.heightAnchor),
|
||||
]
|
||||
|
||||
NSLayoutConstraint.activate(constraints)
|
||||
}
|
||||
|
||||
public func configure(with account: Mastodon.Entity.Account) {
|
||||
authorLabel.text = account.displayNameWithFallback
|
||||
avatarImage.configure(with: account.avatarImageURL())
|
||||
}
|
||||
}
|
|
@ -17,6 +17,7 @@ import MastodonSDK
|
|||
|
||||
public protocol StatusCardControlDelegate: AnyObject {
|
||||
func statusCardControl(_ statusCardControl: StatusCardControl, didTapURL url: URL)
|
||||
func statusCardControl(_ statusCardControl: StatusCardControl, didTapAuthor author: Mastodon.Entity.Account)
|
||||
func statusCardControlMenu(_ statusCardControl: StatusCardControl) -> [LabeledAction]?
|
||||
}
|
||||
|
||||
|
@ -26,13 +27,20 @@ public final class StatusCardControl: UIControl {
|
|||
private var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
private let containerStackView = UIStackView()
|
||||
private let headerContentStackView = UIStackView()
|
||||
private let labelStackView = UIStackView()
|
||||
|
||||
private let highlightView = UIView()
|
||||
private let dividerView = UIView()
|
||||
private let imageView = UIImageView()
|
||||
|
||||
private let publisherDateStackView: UIStackView
|
||||
private let publisherLabel = UILabel()
|
||||
private let publisherDateSeparaturLabel = UILabel()
|
||||
private let dateLabel = UILabel()
|
||||
|
||||
private let titleLabel = UILabel()
|
||||
private let linkLabel = UILabel()
|
||||
private let descriptionLabel = UILabel()
|
||||
private lazy var showEmbedButton: UIButton = {
|
||||
var configuration = UIButton.Configuration.gray()
|
||||
configuration.background.visualEffect = UIBlurEffect(style: .systemUltraThinMaterial)
|
||||
|
@ -48,12 +56,24 @@ public final class StatusCardControl: UIControl {
|
|||
}()
|
||||
private var html = ""
|
||||
|
||||
private let authorDivider: UIView
|
||||
|
||||
private let mastodonLogoImageView: UIImageView
|
||||
private let byLabel: UILabel
|
||||
private let authorLabel: UILabel
|
||||
private let authorAccountButton: StatusCardAuthorControl
|
||||
private let authorStackView: UIStackView
|
||||
|
||||
private static let cardContentPool = WKProcessPool()
|
||||
private var webView: WKWebView?
|
||||
|
||||
private var layout: Layout?
|
||||
private var layoutConstraints: [NSLayoutConstraint] = []
|
||||
private var dividerConstraint: NSLayoutConstraint?
|
||||
private var authorDividerConstraint: NSLayoutConstraint?
|
||||
|
||||
private var author: Mastodon.Entity.Account?
|
||||
private var url: URL?
|
||||
|
||||
public override var isHighlighted: Bool {
|
||||
didSet {
|
||||
|
@ -68,6 +88,54 @@ public final class StatusCardControl: UIControl {
|
|||
}
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
|
||||
let mastodonLogo = Asset.Scene.Sidebar.logo.image.withRenderingMode(.alwaysTemplate)
|
||||
mastodonLogoImageView = UIImageView(image: mastodonLogo)
|
||||
mastodonLogoImageView.tintColor = .gray
|
||||
mastodonLogoImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
byLabel = UILabel()
|
||||
byLabel.text = L10n.Common.Controls.Status.Card.by
|
||||
byLabel.numberOfLines = 1
|
||||
byLabel.textColor = .secondaryLabel
|
||||
byLabel.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 15, weight: .regular))
|
||||
|
||||
authorLabel = UILabel()
|
||||
authorLabel.numberOfLines = 1
|
||||
authorLabel.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 15, weight: .regular))
|
||||
authorLabel.textColor = .secondaryLabel
|
||||
|
||||
publisherLabel.numberOfLines = 1
|
||||
publisherLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
publisherLabel.font = UIFontMetrics(forTextStyle: .footnote).scaledFont(for: .systemFont(ofSize: 13, weight: .regular))
|
||||
publisherLabel.textColor = .secondaryLabel
|
||||
|
||||
publisherDateSeparaturLabel.numberOfLines = 1
|
||||
publisherDateSeparaturLabel.font = UIFontMetrics(forTextStyle: .footnote).scaledFont(for: .systemFont(ofSize: 13, weight: .regular))
|
||||
publisherDateSeparaturLabel.textColor = .secondaryLabel
|
||||
publisherDateSeparaturLabel.text = "·"
|
||||
|
||||
dateLabel.numberOfLines = 1
|
||||
dateLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
dateLabel.font = UIFontMetrics(forTextStyle: .footnote).scaledFont(for: .systemFont(ofSize: 13, weight: .regular))
|
||||
dateLabel.textColor = .secondaryLabel
|
||||
|
||||
publisherDateStackView = UIStackView(arrangedSubviews: [publisherLabel, publisherDateSeparaturLabel, dateLabel, UIView()])
|
||||
publisherDateStackView.axis = .horizontal
|
||||
publisherDateStackView.alignment = .firstBaseline
|
||||
publisherDateStackView.spacing = 3
|
||||
|
||||
authorAccountButton = StatusCardAuthorControl()
|
||||
|
||||
authorStackView = UIStackView(arrangedSubviews: [mastodonLogoImageView, byLabel, authorLabel, authorAccountButton, UIView()])
|
||||
authorStackView.alignment = .center
|
||||
authorStackView.layoutMargins = .init(top: 10, left: 16, bottom: 10, right: 16)
|
||||
authorStackView.isLayoutMarginsRelativeArrangement = true
|
||||
authorStackView.spacing = 8
|
||||
authorStackView.isUserInteractionEnabled = true
|
||||
|
||||
authorDivider = UIView.separatorLine
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
applyBranding()
|
||||
|
@ -82,11 +150,11 @@ public final class StatusCardControl: UIControl {
|
|||
|
||||
titleLabel.numberOfLines = 2
|
||||
titleLabel.textColor = Asset.Colors.Label.primary.color
|
||||
titleLabel.font = .preferredFont(forTextStyle: .body)
|
||||
titleLabel.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .bold))
|
||||
|
||||
linkLabel.numberOfLines = 1
|
||||
linkLabel.textColor = Asset.Colors.Label.secondary.color
|
||||
linkLabel.font = .preferredFont(forTextStyle: .subheadline)
|
||||
descriptionLabel.numberOfLines = 2
|
||||
descriptionLabel.textColor = Asset.Colors.Label.secondary.color
|
||||
descriptionLabel.font = .preferredFont(forTextStyle: .subheadline)
|
||||
|
||||
imageView.tintColor = Asset.Colors.Label.secondary.color
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
|
@ -96,17 +164,26 @@ public final class StatusCardControl: UIControl {
|
|||
imageView.setContentCompressionResistancePriority(.zero, for: .horizontal)
|
||||
imageView.setContentCompressionResistancePriority(.zero, for: .vertical)
|
||||
|
||||
labelStackView.addArrangedSubview(publisherDateStackView)
|
||||
labelStackView.addArrangedSubview(titleLabel)
|
||||
labelStackView.addArrangedSubview(linkLabel)
|
||||
labelStackView.layoutMargins = .init(top: 10, left: 10, bottom: 10, right: 10)
|
||||
labelStackView.addArrangedSubview(descriptionLabel)
|
||||
labelStackView.layoutMargins = .init(top: 16, left: 16, bottom: 16, right: 16)
|
||||
labelStackView.isLayoutMarginsRelativeArrangement = true
|
||||
labelStackView.isUserInteractionEnabled = false
|
||||
labelStackView.axis = .vertical
|
||||
labelStackView.spacing = 2
|
||||
|
||||
containerStackView.addArrangedSubview(imageView)
|
||||
containerStackView.addArrangedSubview(dividerView)
|
||||
containerStackView.addArrangedSubview(labelStackView)
|
||||
containerStackView.isUserInteractionEnabled = false
|
||||
headerContentStackView.addArrangedSubview(imageView)
|
||||
headerContentStackView.addArrangedSubview(dividerView)
|
||||
headerContentStackView.addArrangedSubview(labelStackView)
|
||||
headerContentStackView.isUserInteractionEnabled = true
|
||||
headerContentStackView.axis = .vertical
|
||||
headerContentStackView.spacing = 2
|
||||
headerContentStackView.setCustomSpacing(0, after: imageView)
|
||||
|
||||
containerStackView.addArrangedSubview(headerContentStackView)
|
||||
containerStackView.addArrangedSubview(authorDivider)
|
||||
containerStackView.addArrangedSubview(authorStackView)
|
||||
containerStackView.distribution = .fill
|
||||
|
||||
addSubview(containerStackView)
|
||||
|
@ -128,6 +205,7 @@ public final class StatusCardControl: UIControl {
|
|||
addInteraction(UIContextMenuInteraction(delegate: self))
|
||||
isAccessibilityElement = true
|
||||
accessibilityTraits.insert(.link)
|
||||
backgroundColor = .tertiarySystemFill
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
|
@ -137,14 +215,60 @@ public final class StatusCardControl: UIControl {
|
|||
public func configure(card: Mastodon.Entity.Card) {
|
||||
let title = card.title.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let url = URL(string: card.url)
|
||||
self.url = url
|
||||
if let host = url?.host {
|
||||
accessibilityLabel = "\(title) \(host)"
|
||||
} else {
|
||||
accessibilityLabel = title
|
||||
}
|
||||
|
||||
if let providerName = card.providerName {
|
||||
if let formattedPublishedDate = card.publishedAt?.abbreviatedDate {
|
||||
dateLabel.text = formattedPublishedDate
|
||||
publisherDateSeparaturLabel.isHidden = false
|
||||
} else {
|
||||
dateLabel.isHidden = true
|
||||
publisherDateSeparaturLabel.isHidden = true
|
||||
}
|
||||
|
||||
publisherLabel.text = providerName
|
||||
publisherDateStackView.isHidden = false
|
||||
} else {
|
||||
publisherDateStackView.isHidden = true
|
||||
}
|
||||
|
||||
if let author = card.authors?.first, let account = author.account {
|
||||
authorAccountButton.configure(with: account)
|
||||
authorAccountButton.isHidden = false
|
||||
authorLabel.isHidden = true
|
||||
byLabel.isHidden = false
|
||||
mastodonLogoImageView.isHidden = false
|
||||
self.author = account
|
||||
|
||||
authorAccountButton.addTarget(self, action: #selector(StatusCardControl.profileTapped(_:)), for: .touchUpInside)
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(StatusCardControl.contentTapped(_:)))
|
||||
headerContentStackView.addGestureRecognizer(tapGestureRecognizer)
|
||||
} else {
|
||||
if let author = card.authors?.first, let authorName = author.name, authorName.isEmpty == false {
|
||||
authorLabel.text = L10n.Common.Controls.Status.Card.byAuthor(authorName)
|
||||
} else if let authorName = card.authorName, authorName.isEmpty == false {
|
||||
authorLabel.text = L10n.Common.Controls.Status.Card.byAuthor(authorName)
|
||||
} else {
|
||||
authorLabel.text = url?.host
|
||||
}
|
||||
|
||||
author = nil
|
||||
authorLabel.isHidden = false
|
||||
byLabel.isHidden = true
|
||||
mastodonLogoImageView.isHidden = true
|
||||
authorAccountButton.isHidden = true
|
||||
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(StatusCardControl.contentTapped(_:)))
|
||||
addGestureRecognizer(tapGestureRecognizer)
|
||||
}
|
||||
|
||||
titleLabel.text = title
|
||||
linkLabel.text = url?.host
|
||||
descriptionLabel.text = card.description
|
||||
imageView.contentMode = .center
|
||||
|
||||
imageView.sd_setImage(
|
||||
|
@ -178,9 +302,9 @@ public final class StatusCardControl: UIControl {
|
|||
public override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
|
||||
if let window = window {
|
||||
layer.borderWidth = window.screen.pixelSize
|
||||
if let window {
|
||||
dividerConstraint?.constant = window.screen.pixelSize
|
||||
authorDividerConstraint?.constant = window.screen.pixelSize
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -190,6 +314,7 @@ public final class StatusCardControl: UIControl {
|
|||
|
||||
NSLayoutConstraint.deactivate(layoutConstraints)
|
||||
dividerConstraint?.deactivate()
|
||||
authorDividerConstraint?.deactivate()
|
||||
|
||||
let pixelSize = (window?.screen.pixelSize ?? 1)
|
||||
switch layout {
|
||||
|
@ -209,6 +334,7 @@ public final class StatusCardControl: UIControl {
|
|||
.constraint(lessThanOrEqualToConstant: 400),
|
||||
]
|
||||
dividerConstraint = dividerView.heightAnchor.constraint(equalToConstant: pixelSize).activate()
|
||||
authorDividerConstraint = authorDivider.heightAnchor.constraint(equalToConstant: pixelSize).activate()
|
||||
case .compact:
|
||||
containerStackView.alignment = .center
|
||||
containerStackView.axis = .horizontal
|
||||
|
@ -218,10 +344,17 @@ public final class StatusCardControl: UIControl {
|
|||
heightAnchor.constraint(equalToConstant: 85).priority(.defaultLow - 1),
|
||||
heightAnchor.constraint(greaterThanOrEqualToConstant: 85),
|
||||
dividerView.heightAnchor.constraint(equalTo: containerStackView.heightAnchor),
|
||||
authorDivider.heightAnchor.constraint(equalTo: containerStackView.heightAnchor),
|
||||
]
|
||||
dividerConstraint = dividerView.widthAnchor.constraint(equalToConstant: pixelSize).activate()
|
||||
authorDividerConstraint = authorDivider.widthAnchor.constraint(equalToConstant: pixelSize).activate()
|
||||
}
|
||||
|
||||
layoutConstraints.append(contentsOf: [
|
||||
mastodonLogoImageView.widthAnchor.constraint(equalToConstant: 20),
|
||||
mastodonLogoImageView.heightAnchor.constraint(equalTo: mastodonLogoImageView.widthAnchor),
|
||||
])
|
||||
|
||||
NSLayoutConstraint.activate(layoutConstraints)
|
||||
}
|
||||
|
||||
|
@ -236,7 +369,6 @@ public final class StatusCardControl: UIControl {
|
|||
}
|
||||
|
||||
private func applyBranding() {
|
||||
layer.borderColor = SystemTheme.separator.cgColor
|
||||
dividerView.backgroundColor = SystemTheme.separator
|
||||
imageView.backgroundColor = UIColor.tertiarySystemFill
|
||||
}
|
||||
|
@ -247,6 +379,18 @@ public final class StatusCardControl: UIControl {
|
|||
}
|
||||
set {}
|
||||
}
|
||||
|
||||
@objc private func profileTapped(_ sender: UIButton) {
|
||||
guard let author else { return }
|
||||
|
||||
delegate?.statusCardControl(self, didTapAuthor: author)
|
||||
}
|
||||
|
||||
@objc private func contentTapped(_ sender: Any) {
|
||||
guard let url else { return }
|
||||
|
||||
delegate?.statusCardControl(self, didTapURL: url)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: WKWebView delegates
|
||||
|
|
|
@ -35,6 +35,7 @@ public protocol StatusViewDelegate: AnyObject {
|
|||
func statusView(_ statusView: StatusView, statusMetricView: StatusMetricView, favoriteButtonDidPressed button: UIButton)
|
||||
func statusView(_ statusView: StatusView, statusMetricView: StatusMetricView, showEditHistory button: UIButton)
|
||||
func statusView(_ statusView: StatusView, cardControl: StatusCardControl, didTapURL url: URL)
|
||||
func statusView(_ statusView: StatusView, cardControl: StatusCardControl, didTapProfile account: Mastodon.Entity.Account)
|
||||
func statusView(_ statusView: StatusView, cardControlMenu: StatusCardControl) -> [LabeledAction]?
|
||||
|
||||
// a11y
|
||||
|
@ -373,7 +374,6 @@ extension StatusView {
|
|||
contentMetaText.textView.linkDelegate = self
|
||||
|
||||
// card
|
||||
statusCardControl.addTarget(self, action: #selector(statusCardControlPressed), for: .touchUpInside)
|
||||
statusCardControl.delegate = self
|
||||
|
||||
// media
|
||||
|
@ -410,12 +410,6 @@ extension StatusView {
|
|||
@objc private func spoilerOverlayViewTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) {
|
||||
delegate?.statusView(self, spoilerOverlayViewDidPressed: spoilerOverlayView)
|
||||
}
|
||||
|
||||
@objc private func statusCardControlPressed(_ sender: StatusCardControl) {
|
||||
guard let urlString = viewModel.card?.url, let url = URL(string: urlString) else { return }
|
||||
delegate?.statusView(self, didTapCardWithURL: url)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension StatusView {
|
||||
|
@ -798,6 +792,10 @@ extension StatusView: MastodonMenuDelegate {
|
|||
|
||||
// MARK: StatusCardControlDelegate
|
||||
extension StatusView: StatusCardControlDelegate {
|
||||
public func statusCardControl(_ statusCardControl: StatusCardControl, didTapAuthor author: Mastodon.Entity.Account) {
|
||||
delegate?.statusView(self, cardControl: statusCardControl, didTapProfile: author)
|
||||
}
|
||||
|
||||
public func statusCardControl(_ statusCardControl: StatusCardControl, didTapURL url: URL) {
|
||||
delegate?.statusView(self, cardControl: statusCardControl, didTapURL: url)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue