Compare commits
No commits in common. "1b6f0c07fdeabb323b813d45d7053852cb7b29bc" and "afe47437e4917a1f352d38df06c0809981d0c49c" have entirely different histories.
1b6f0c07fd
...
afe47437e4
|
@ -59,7 +59,7 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate, Collect
|
||||||
|
|
||||||
resultsController = SearchResultsViewController(mastodonController: mastodonController)
|
resultsController = SearchResultsViewController(mastodonController: mastodonController)
|
||||||
resultsController.exploreNavigationController = self.navigationController!
|
resultsController.exploreNavigationController = self.navigationController!
|
||||||
searchController = MastodonSearchController(searchResultsController: resultsController, owner: self)
|
searchController = MastodonSearchController(searchResultsController: resultsController)
|
||||||
definesPresentationContext = true
|
definesPresentationContext = true
|
||||||
|
|
||||||
navigationItem.searchController = searchController
|
navigationItem.searchController = searchController
|
||||||
|
|
|
@ -34,7 +34,7 @@ class InlineTrendsViewController: UIViewController {
|
||||||
|
|
||||||
resultsController = SearchResultsViewController(mastodonController: mastodonController)
|
resultsController = SearchResultsViewController(mastodonController: mastodonController)
|
||||||
resultsController.exploreNavigationController = self.navigationController
|
resultsController.exploreNavigationController = self.navigationController
|
||||||
searchController = MastodonSearchController(searchResultsController: resultsController, owner: self)
|
searchController = MastodonSearchController(searchResultsController: resultsController)
|
||||||
searchController.obscuresBackgroundDuringPresentation = true
|
searchController.obscuresBackgroundDuringPresentation = true
|
||||||
searchController.hidesNavigationBarDuringPresentation = false
|
searchController.hidesNavigationBarDuringPresentation = false
|
||||||
definesPresentationContext = true
|
definesPresentationContext = true
|
||||||
|
|
|
@ -24,11 +24,7 @@ class MastodonSearchController: UISearchController {
|
||||||
super.searchResultsController as! SearchResultsViewController
|
super.searchResultsController as! SearchResultsViewController
|
||||||
}
|
}
|
||||||
|
|
||||||
private weak var owner: UIViewController?
|
init(searchResultsController: SearchResultsViewController) {
|
||||||
|
|
||||||
init(searchResultsController: SearchResultsViewController, owner: UIViewController) {
|
|
||||||
self.owner = owner
|
|
||||||
|
|
||||||
super.init(searchResultsController: searchResultsController)
|
super.init(searchResultsController: searchResultsController)
|
||||||
|
|
||||||
searchResultsController.tokenHandler = { [unowned self] token, op in
|
searchResultsController.tokenHandler = { [unowned self] token, op in
|
||||||
|
@ -156,12 +152,6 @@ extension MastodonSearchController: UISearchBarDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MastodonSearchController: MultiColumnNavigationCustomTargetProviding {
|
|
||||||
var multiColumnNavigationTargetViewController: UIViewController? {
|
|
||||||
owner
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension UISearchBar {
|
extension UISearchBar {
|
||||||
var searchQueryWithOperators: String {
|
var searchQueryWithOperators: String {
|
||||||
var parts = searchTextField.tokens.compactMap { $0.representedObject as? String }
|
var parts = searchTextField.tokens.compactMap { $0.representedObject as? String }
|
||||||
|
|
|
@ -30,8 +30,6 @@ class SearchTokenSuggestionCollectionViewCell: UICollectionViewCell {
|
||||||
label.topAnchor.constraint(equalTo: contentView.topAnchor),
|
label.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||||
label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
|
label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
|
||||||
])
|
])
|
||||||
|
|
||||||
addInteraction(UIPointerInteraction(delegate: self))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
|
@ -42,10 +40,3 @@ class SearchTokenSuggestionCollectionViewCell: UICollectionViewCell {
|
||||||
label.text = text
|
label.text = text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SearchTokenSuggestionCollectionViewCell: UIPointerInteractionDelegate {
|
|
||||||
func pointerInteraction(_ interaction: UIPointerInteraction, styleFor region: UIPointerRegion) -> UIPointerStyle? {
|
|
||||||
let preview = UITargetedPreview(view: self)
|
|
||||||
return UIPointerStyle(effect: .lift(preview))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -8,30 +8,19 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
/// View controllers, such as `UISearchController`, that live outside the normal VC hierarchy
|
|
||||||
/// can adopt this protocol to indicate to `MultiColumnNavigationController` the context for
|
|
||||||
/// navigation operations.
|
|
||||||
protocol MultiColumnNavigationCustomTargetProviding {
|
|
||||||
var multiColumnNavigationTargetViewController: UIViewController? { get }
|
|
||||||
}
|
|
||||||
|
|
||||||
class MultiColumnNavigationController: UIViewController {
|
class MultiColumnNavigationController: UIViewController {
|
||||||
|
|
||||||
private var isManuallyUpdating = false
|
private var isManuallyUpdating = false
|
||||||
private var _viewControllers: [UIViewController] = []
|
var viewControllers: [UIViewController] = [] {
|
||||||
var viewControllers: [UIViewController] {
|
didSet {
|
||||||
get {
|
guard isViewLoaded,
|
||||||
_viewControllers
|
!isManuallyUpdating else {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
set {
|
|
||||||
_viewControllers = newValue
|
|
||||||
if isViewLoaded,
|
|
||||||
!isManuallyUpdating {
|
|
||||||
updateViews()
|
updateViews()
|
||||||
scrollToEnd(animated: false)
|
scrollToEnd(animated: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private var scrollView = UIScrollView()
|
private var scrollView = UIScrollView()
|
||||||
private var stackView = UIStackView()
|
private var stackView = UIStackView()
|
||||||
|
@ -79,13 +68,13 @@ class MultiColumnNavigationController: UIViewController {
|
||||||
|
|
||||||
private func updateViews() {
|
private func updateViews() {
|
||||||
var i = 0
|
var i = 0
|
||||||
while i < _viewControllers.count {
|
while i < viewControllers.count {
|
||||||
let needsCloseButton = i > 0
|
let needsCloseButton = i > 0
|
||||||
if i <= stackView.arrangedSubviews.count - 1 {
|
if i <= stackView.arrangedSubviews.count - 1 {
|
||||||
let existing = stackView.arrangedSubviews[i] as! ColumnView
|
let existing = stackView.arrangedSubviews[i] as! ColumnView
|
||||||
existing.setContent(_viewControllers[i], needsCloseButton: needsCloseButton)
|
existing.setContent(viewControllers[i], needsCloseButton: needsCloseButton)
|
||||||
} else {
|
} else {
|
||||||
let new = ColumnView(owner: self, contentViewController: _viewControllers[i], needsCloseButton: needsCloseButton)
|
let new = ColumnView(owner: self, contentViewController: viewControllers[i], needsCloseButton: needsCloseButton)
|
||||||
stackView.addArrangedSubview(new)
|
stackView.addArrangedSubview(new)
|
||||||
}
|
}
|
||||||
i += 1
|
i += 1
|
||||||
|
@ -103,11 +92,9 @@ class MultiColumnNavigationController: UIViewController {
|
||||||
var index: Int? = nil
|
var index: Int? = nil
|
||||||
var current: UIViewController? = sender
|
var current: UIViewController? = sender
|
||||||
while let c = current {
|
while let c = current {
|
||||||
index = _viewControllers.firstIndex(of: c)
|
index = viewControllers.firstIndex(of: c)
|
||||||
if index != nil {
|
if index != nil {
|
||||||
break
|
break
|
||||||
} else if let targetProviding = c as? MultiColumnNavigationCustomTargetProviding {
|
|
||||||
current = targetProviding.multiColumnNavigationTargetViewController
|
|
||||||
} else {
|
} else {
|
||||||
current = c.parent
|
current = c.parent
|
||||||
}
|
}
|
||||||
|
@ -125,20 +112,19 @@ class MultiColumnNavigationController: UIViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
func replaceViewControllers(_ vcs: [UIViewController], after afterIndex: Int, animated: Bool) {
|
func replaceViewControllers(_ vcs: [UIViewController], after afterIndex: Int, animated: Bool) {
|
||||||
if afterIndex == _viewControllers.count - 1 && vcs.count == 1 {
|
if afterIndex == viewControllers.count - 1 && vcs.count == 1 {
|
||||||
pushViewController(vcs[0], animated: animated)
|
pushViewController(vcs[0], animated: animated)
|
||||||
} else {
|
} else {
|
||||||
_viewControllers = Array(_viewControllers[...afterIndex]) + vcs
|
viewControllers = Array(viewControllers[...afterIndex]) + vcs
|
||||||
updateViews()
|
|
||||||
scrollToEnd(animated: animated)
|
scrollToEnd(animated: animated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func scrollToEnd(animated: Bool) {
|
private func scrollToEnd(animated: Bool) {
|
||||||
if _viewControllers.isEmpty {
|
if viewControllers.isEmpty {
|
||||||
scrollView.setContentOffset(.init(x: -scrollView.adjustedLeadingContentInset, y: -scrollView.adjustedContentInset.top), animated: false)
|
scrollView.setContentOffset(.init(x: -scrollView.adjustedLeadingContentInset, y: -scrollView.adjustedContentInset.top), animated: false)
|
||||||
} else {
|
} else {
|
||||||
scrollColumnToEnd(columnIndex: _viewControllers.count - 1, animated: animated)
|
scrollColumnToEnd(columnIndex: viewControllers.count - 1, animated: animated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,12 +142,14 @@ class MultiColumnNavigationController: UIViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate func closeColumn(_ vc: UIViewController) {
|
fileprivate func closeColumn(_ vc: UIViewController) {
|
||||||
guard let index = _viewControllers.firstIndex(of: vc),
|
guard let index = viewControllers.firstIndex(of: vc),
|
||||||
index > 0 else {
|
index > 0 else {
|
||||||
// Can't close the last column
|
// Can't close the last column
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_viewControllers.removeSubrange(index...)
|
isManuallyUpdating = true
|
||||||
|
defer { isManuallyUpdating = false }
|
||||||
|
viewControllers.removeSubrange(index...)
|
||||||
animateChanges {
|
animateChanges {
|
||||||
for column in self.stackView.arrangedSubviews[index...] {
|
for column in self.stackView.arrangedSubviews[index...] {
|
||||||
column.layer.opacity = 0
|
column.layer.opacity = 0
|
||||||
|
@ -170,6 +158,7 @@ class MultiColumnNavigationController: UIViewController {
|
||||||
} completion: {
|
} completion: {
|
||||||
self.updateViews()
|
self.updateViews()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func animateChanges(_ animations: @escaping () -> Void, completion: (() -> Void)? = nil) {
|
private func animateChanges(_ animations: @escaping () -> Void, completion: (() -> Void)? = nil) {
|
||||||
|
@ -184,22 +173,19 @@ class MultiColumnNavigationController: UIViewController {
|
||||||
|
|
||||||
extension MultiColumnNavigationController: NavigationControllerProtocol {
|
extension MultiColumnNavigationController: NavigationControllerProtocol {
|
||||||
var topViewController: UIViewController? {
|
var topViewController: UIViewController? {
|
||||||
_viewControllers.last
|
viewControllers.last
|
||||||
}
|
}
|
||||||
|
|
||||||
func popToRootViewController(animated: Bool) -> [UIViewController]? {
|
func popToRootViewController(animated: Bool) -> [UIViewController]? {
|
||||||
guard !_viewControllers.isEmpty else {
|
let removed = Array(viewControllers.dropFirst())
|
||||||
return nil
|
viewControllers = [viewControllers.first!]
|
||||||
}
|
|
||||||
let removed = Array(_viewControllers.dropFirst())
|
|
||||||
_viewControllers = [_viewControllers.first!]
|
|
||||||
updateViews()
|
|
||||||
scrollToEnd(animated: animated)
|
|
||||||
return removed
|
return removed
|
||||||
}
|
}
|
||||||
|
|
||||||
func pushViewController(_ vc: UIViewController, animated: Bool) {
|
func pushViewController(_ vc: UIViewController, animated: Bool) {
|
||||||
_viewControllers.append(vc)
|
isManuallyUpdating = true
|
||||||
|
defer { isManuallyUpdating = false }
|
||||||
|
viewControllers.append(vc)
|
||||||
updateViews()
|
updateViews()
|
||||||
scrollToEnd(animated: animated)
|
scrollToEnd(animated: animated)
|
||||||
if animated {
|
if animated {
|
||||||
|
|
|
@ -36,8 +36,6 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
|
||||||
var emojiFont: UIFont = .preferredFont(forTextStyle: .body)
|
var emojiFont: UIFont = .preferredFont(forTextStyle: .body)
|
||||||
var emojiTextColor: UIColor = .label
|
var emojiTextColor: UIColor = .label
|
||||||
|
|
||||||
private let tapRecognizer = UITapGestureRecognizer()
|
|
||||||
|
|
||||||
// The link range currently being previewed
|
// The link range currently being previewed
|
||||||
private var currentPreviewedLinkRange: NSRange?
|
private var currentPreviewedLinkRange: NSRange?
|
||||||
// The preview created in the previewForHighlighting method, so that we can use the same one in previewForDismissing.
|
// The preview created in the previewForHighlighting method, so that we can use the same one in previewForDismissing.
|
||||||
|
@ -80,9 +78,8 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
|
||||||
updateLinkUnderlineStyle()
|
updateLinkUnderlineStyle()
|
||||||
|
|
||||||
// the text view's builtin link interaction code is tied to isSelectable, so we need to use our own tap recognizer
|
// the text view's builtin link interaction code is tied to isSelectable, so we need to use our own tap recognizer
|
||||||
tapRecognizer.addTarget(self, action: #selector(textTapped(_:)))
|
let recognizer = UITapGestureRecognizer(target: self, action: #selector(textTapped(_:)))
|
||||||
tapRecognizer.delegate = self
|
addGestureRecognizer(recognizer)
|
||||||
addGestureRecognizer(tapRecognizer)
|
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(_updateLinkUnderlineStyle), name: UIAccessibility.buttonShapesEnabledStatusDidChangeNotification, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(_updateLinkUnderlineStyle), name: UIAccessibility.buttonShapesEnabledStatusDidChangeNotification, object: nil)
|
||||||
underlineTextLinksCancellable =
|
underlineTextLinksCancellable =
|
||||||
|
@ -135,6 +132,12 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func textTapped(_ recognizer: UITapGestureRecognizer) {
|
@objc func textTapped(_ recognizer: UITapGestureRecognizer) {
|
||||||
|
// if there currently is a selection, deselct it on single-tap
|
||||||
|
if selectedRange.length > 0 {
|
||||||
|
// location doesn't matter since we are non-editable and the cursor isn't visible
|
||||||
|
selectedRange = NSRange(location: 0, length: 0)
|
||||||
|
}
|
||||||
|
|
||||||
let location = recognizer.location(in: self)
|
let location = recognizer.location(in: self)
|
||||||
if let (link, range) = getLinkAtPoint(location),
|
if let (link, range) = getLinkAtPoint(location),
|
||||||
link.scheme != dataDetectorsScheme {
|
link.scheme != dataDetectorsScheme {
|
||||||
|
@ -381,25 +384,3 @@ extension ContentTextView: UIContextMenuInteractionDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ContentTextView: UIGestureRecognizerDelegate {
|
|
||||||
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
||||||
// NB: This method is both a gesture recognizer delegate method and a UIView method.
|
|
||||||
// We only want to prevent our own tap gesture recognizer from beginning, but don't
|
|
||||||
// want to interfere with any other gestures that may begin over this view.
|
|
||||||
if gestureRecognizer === tapRecognizer {
|
|
||||||
let location = gestureRecognizer.location(in: self)
|
|
||||||
if let (link, _) = getLinkAtPoint(location) {
|
|
||||||
if link.scheme == dataDetectorsScheme {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -12,11 +12,7 @@ import SwiftUI
|
||||||
import SafariServices
|
import SafariServices
|
||||||
|
|
||||||
class ProfileFieldValueView: UIView {
|
class ProfileFieldValueView: UIView {
|
||||||
weak var navigationDelegate: TuskerNavigationDelegate? {
|
weak var navigationDelegate: TuskerNavigationDelegate?
|
||||||
didSet {
|
|
||||||
textView.navigationDelegate = navigationDelegate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static let converter = HTMLConverter(
|
private static let converter = HTMLConverter(
|
||||||
font: .preferredFont(forTextStyle: .body),
|
font: .preferredFont(forTextStyle: .body),
|
||||||
|
@ -27,8 +23,9 @@ class ProfileFieldValueView: UIView {
|
||||||
|
|
||||||
private let account: AccountMO
|
private let account: AccountMO
|
||||||
private let field: Account.Field
|
private let field: Account.Field
|
||||||
|
private var link: (String, URL)?
|
||||||
|
|
||||||
private let textView = ContentTextView()
|
private let label = EmojiLabel()
|
||||||
private var iconView: UIView?
|
private var iconView: UIView?
|
||||||
|
|
||||||
private var currentTargetedPreview: UITargetedPreview?
|
private var currentTargetedPreview: UITargetedPreview?
|
||||||
|
@ -41,28 +38,34 @@ class ProfileFieldValueView: UIView {
|
||||||
|
|
||||||
let converted = NSMutableAttributedString(attributedString: ProfileFieldValueView.converter.convert(field.value))
|
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)
|
#if os(visionOS)
|
||||||
textView.linkTextAttributes = [
|
converted.addAttribute(.foregroundColor, value: UIColor.link, range: range)
|
||||||
.foregroundColor: UIColor.link
|
|
||||||
]
|
|
||||||
#else
|
#else
|
||||||
textView.linkTextAttributes = [
|
converted.addAttribute(.foregroundColor, value: UIColor.tintColor, range: range)
|
||||||
.foregroundColor: UIColor.tintColor
|
|
||||||
]
|
|
||||||
#endif
|
#endif
|
||||||
textView.backgroundColor = nil
|
// the .link attribute in a UILabel always makes the color blue >.>
|
||||||
textView.isScrollEnabled = false
|
converted.removeAttribute(.link, range: range)
|
||||||
textView.isSelectable = false
|
}
|
||||||
textView.isEditable = false
|
}
|
||||||
textView.textContainerInset = .zero
|
|
||||||
textView.font = .preferredFont(forTextStyle: .body)
|
label.numberOfLines = 0
|
||||||
textView.adjustsFontForContentSizeCategory = true
|
label.font = .preferredFont(forTextStyle: .body)
|
||||||
textView.attributedText = converted
|
label.adjustsFontForContentSizeCategory = true
|
||||||
textView.setEmojis(account.emojis, identifier: account.id)
|
label.attributedText = converted
|
||||||
textView.isUserInteractionEnabled = true
|
label.setEmojis(account.emojis, identifier: account.id)
|
||||||
textView.setContentCompressionResistancePriority(.required, for: .vertical)
|
label.setContentCompressionResistancePriority(.required, for: .vertical)
|
||||||
textView.translatesAutoresizingMaskIntoConstraints = false
|
label.translatesAutoresizingMaskIntoConstraints = false
|
||||||
addSubview(textView)
|
addSubview(label)
|
||||||
|
|
||||||
let labelTrailingConstraint: NSLayoutConstraint
|
let labelTrailingConstraint: NSLayoutConstraint
|
||||||
|
|
||||||
|
@ -79,20 +82,20 @@ class ProfileFieldValueView: UIView {
|
||||||
icon.isPointerInteractionEnabled = true
|
icon.isPointerInteractionEnabled = true
|
||||||
icon.accessibilityLabel = "Verified link"
|
icon.accessibilityLabel = "Verified link"
|
||||||
addSubview(icon)
|
addSubview(icon)
|
||||||
labelTrailingConstraint = textView.trailingAnchor.constraint(equalTo: icon.leadingAnchor)
|
labelTrailingConstraint = label.trailingAnchor.constraint(equalTo: icon.leadingAnchor)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
icon.centerYAnchor.constraint(equalTo: textView.centerYAnchor),
|
icon.centerYAnchor.constraint(equalTo: label.centerYAnchor),
|
||||||
icon.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor),
|
icon.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor),
|
||||||
])
|
])
|
||||||
} else {
|
} else {
|
||||||
labelTrailingConstraint = textView.trailingAnchor.constraint(equalTo: trailingAnchor)
|
labelTrailingConstraint = label.trailingAnchor.constraint(equalTo: trailingAnchor)
|
||||||
}
|
}
|
||||||
|
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
textView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
label.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
labelTrailingConstraint,
|
labelTrailingConstraint,
|
||||||
textView.topAnchor.constraint(equalTo: topAnchor),
|
label.topAnchor.constraint(equalTo: topAnchor),
|
||||||
textView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
label.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,7 +104,7 @@ class ProfileFieldValueView: UIView {
|
||||||
}
|
}
|
||||||
|
|
||||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||||
var size = textView.sizeThatFits(size)
|
var size = label.sizeThatFits(size)
|
||||||
if let iconView {
|
if let iconView {
|
||||||
size.width += iconView.sizeThatFits(UIView.layoutFittingCompressedSize).width
|
size.width += iconView.sizeThatFits(UIView.layoutFittingCompressedSize).width
|
||||||
}
|
}
|
||||||
|
@ -109,7 +112,29 @@ class ProfileFieldValueView: UIView {
|
||||||
}
|
}
|
||||||
|
|
||||||
func setTextAlignment(_ alignment: NSTextAlignment) {
|
func setTextAlignment(_ alignment: NSTextAlignment) {
|
||||||
textView.textAlignment = alignment
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func verifiedIconTapped() {
|
@objc private func verifiedIconTapped() {
|
||||||
|
@ -119,7 +144,7 @@ class ProfileFieldValueView: UIView {
|
||||||
let view = ProfileFieldVerificationView(
|
let view = ProfileFieldVerificationView(
|
||||||
acct: account.acct,
|
acct: account.acct,
|
||||||
verifiedAt: field.verifiedAt!,
|
verifiedAt: field.verifiedAt!,
|
||||||
linkText: textView.text ?? "",
|
linkText: label.text ?? "",
|
||||||
navigationDelegate: navigationDelegate
|
navigationDelegate: navigationDelegate
|
||||||
)
|
)
|
||||||
let host = UIHostingController(rootView: view)
|
let host = UIHostingController(rootView: view)
|
||||||
|
@ -143,3 +168,49 @@ class ProfileFieldValueView: UIView {
|
||||||
navigationDelegate.present(toPresent, animated: true)
|
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!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue