From c88076eec0599d90b80d059a69f7703863d8fed9 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sat, 8 Jun 2024 10:23:24 -0700 Subject: [PATCH] Use text view for profile field value view Fixes #501 --- .../ProfileFieldValueView.swift | 145 +++++------------- 1 file changed, 37 insertions(+), 108 deletions(-) diff --git a/Tusker/Views/Profile Header/ProfileFieldValueView.swift b/Tusker/Views/Profile Header/ProfileFieldValueView.swift index e2f613dc..b1e64b55 100644 --- a/Tusker/Views/Profile Header/ProfileFieldValueView.swift +++ b/Tusker/Views/Profile Header/ProfileFieldValueView.swift @@ -12,7 +12,11 @@ import SwiftUI import SafariServices class ProfileFieldValueView: UIView { - weak var navigationDelegate: TuskerNavigationDelegate? + weak var navigationDelegate: TuskerNavigationDelegate? { + didSet { + textView.navigationDelegate = navigationDelegate + } + } private static let converter = HTMLConverter( font: .preferredFont(forTextStyle: .body), @@ -23,9 +27,8 @@ class ProfileFieldValueView: UIView { private let account: AccountMO private let field: Account.Field - private var link: (String, URL)? - private let label = EmojiLabel() + private let textView = ContentTextView() private var iconView: UIView? private var currentTargetedPreview: UITargetedPreview? @@ -38,34 +41,28 @@ class ProfileFieldValueView: UIView { let converted = NSMutableAttributedString(attributedString: ProfileFieldValueView.converter.convert(field.value)) - var range = NSRange(location: 0, length: 0) - if converted.length != 0, - let url = converted.attribute(.link, at: 0, longestEffectiveRange: &range, in: converted.fullRange) as? URL { - link = (converted.attributedSubstring(from: range).string, url) - label.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(linkTapped))) - label.addInteraction(UIContextMenuInteraction(delegate: self)) - label.isUserInteractionEnabled = true - - converted.enumerateAttribute(.link, in: converted.fullRange) { value, range, stop in - guard value != nil else { return } - #if os(visionOS) - converted.addAttribute(.foregroundColor, value: UIColor.link, range: range) - #else - converted.addAttribute(.foregroundColor, value: UIColor.tintColor, range: range) - #endif - // the .link attribute in a UILabel always makes the color blue >.> - converted.removeAttribute(.link, range: range) - } - } - - label.numberOfLines = 0 - label.font = .preferredFont(forTextStyle: .body) - label.adjustsFontForContentSizeCategory = true - label.attributedText = converted - label.setEmojis(account.emojis, identifier: account.id) - label.setContentCompressionResistancePriority(.required, for: .vertical) - label.translatesAutoresizingMaskIntoConstraints = false - addSubview(label) + #if os(visionOS) + textView.linkTextAttributes = [ + .foregroundColor: UIColor.link + ] + #else + textView.linkTextAttributes = [ + .foregroundColor: UIColor.tintColor + ] + #endif + textView.backgroundColor = nil + textView.isScrollEnabled = false + textView.isSelectable = false + textView.isEditable = false + textView.textContainerInset = .zero + textView.font = .preferredFont(forTextStyle: .body) + textView.adjustsFontForContentSizeCategory = true + textView.attributedText = converted + textView.setEmojis(account.emojis, identifier: account.id) + textView.isUserInteractionEnabled = true + textView.setContentCompressionResistancePriority(.required, for: .vertical) + textView.translatesAutoresizingMaskIntoConstraints = false + addSubview(textView) let labelTrailingConstraint: NSLayoutConstraint @@ -82,20 +79,20 @@ class ProfileFieldValueView: UIView { icon.isPointerInteractionEnabled = true icon.accessibilityLabel = "Verified link" addSubview(icon) - labelTrailingConstraint = label.trailingAnchor.constraint(equalTo: icon.leadingAnchor) + labelTrailingConstraint = textView.trailingAnchor.constraint(equalTo: icon.leadingAnchor) NSLayoutConstraint.activate([ - icon.centerYAnchor.constraint(equalTo: label.centerYAnchor), + icon.centerYAnchor.constraint(equalTo: textView.centerYAnchor), icon.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor), ]) } else { - labelTrailingConstraint = label.trailingAnchor.constraint(equalTo: trailingAnchor) + labelTrailingConstraint = textView.trailingAnchor.constraint(equalTo: trailingAnchor) } NSLayoutConstraint.activate([ - label.leadingAnchor.constraint(equalTo: leadingAnchor), + textView.leadingAnchor.constraint(equalTo: leadingAnchor), labelTrailingConstraint, - label.topAnchor.constraint(equalTo: topAnchor), - label.bottomAnchor.constraint(equalTo: bottomAnchor), + textView.topAnchor.constraint(equalTo: topAnchor), + textView.bottomAnchor.constraint(equalTo: bottomAnchor), ]) } @@ -104,7 +101,7 @@ class ProfileFieldValueView: UIView { } override func sizeThatFits(_ size: CGSize) -> CGSize { - var size = label.sizeThatFits(size) + var size = textView.sizeThatFits(size) if let iconView { size.width += iconView.sizeThatFits(UIView.layoutFittingCompressedSize).width } @@ -112,29 +109,7 @@ class ProfileFieldValueView: UIView { } func setTextAlignment(_ alignment: NSTextAlignment) { - label.textAlignment = alignment - } - - func getHashtagOrURL() -> (Hashtag?, URL)? { - guard let (text, url) = link else { - return nil - } - if text.starts(with: "#") { - return (Hashtag(name: String(text.dropFirst()), url: url), url) - } else { - return (nil, url) - } - } - - @objc private func linkTapped() { - guard let (hashtag, url) = getHashtagOrURL() else { - return - } - if let hashtag { - navigationDelegate?.selected(tag: hashtag) - } else { - navigationDelegate?.selected(url: url) - } + textView.textAlignment = alignment } @objc private func verifiedIconTapped() { @@ -144,7 +119,7 @@ class ProfileFieldValueView: UIView { let view = ProfileFieldVerificationView( acct: account.acct, verifiedAt: field.verifiedAt!, - linkText: label.text ?? "", + linkText: textView.text ?? "", navigationDelegate: navigationDelegate ) let host = UIHostingController(rootView: view) @@ -168,49 +143,3 @@ class ProfileFieldValueView: UIView { navigationDelegate.present(toPresent, animated: true) } } - -extension ProfileFieldValueView: UIContextMenuInteractionDelegate, MenuActionProvider { - func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? { - guard let (hashtag, url) = getHashtagOrURL(), - let navigationDelegate else { - return nil - } - if let hashtag { - return UIContextMenuConfiguration { - HashtagTimelineViewController(for: hashtag, mastodonController: navigationDelegate.apiController) - } actionProvider: { _ in - UIMenu(children: self.actionsForHashtag(hashtag, source: .view(self))) - } - } else { - return UIContextMenuConfiguration { - let vc = SFSafariViewController(url: url) - #if !os(visionOS) - vc.preferredControlTintColor = Preferences.shared.accentColor.color - #endif - return vc - } actionProvider: { _ in - UIMenu(children: self.actionsForURL(url, source: .view(self))) - } - } - } - - - func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configuration: UIContextMenuConfiguration, highlightPreviewForItemWithIdentifier identifier: NSCopying) -> UITargetedPreview? { - var rect = label.textRect(forBounds: label.bounds, limitedToNumberOfLines: 0) - // the rect should be vertically centered, but textRect doesn't seem to take the label's vertical alignment into account - rect.origin.x = 0 - rect.origin.y = (bounds.height - rect.height) / 2 - let parameters = UIPreviewParameters(textLineRects: [rect as NSValue]) - let preview = UITargetedPreview(view: label, parameters: parameters) - currentTargetedPreview = preview - return preview - } - - func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configuration: UIContextMenuConfiguration, dismissalPreviewForItemWithIdentifier identifier: NSCopying) -> UITargetedPreview? { - return currentTargetedPreview - } - - func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) { - MenuPreviewHelper.willPerformPreviewAction(animator: animator, presenter: navigationDelegate!) - } -}