Fix crash due to layout loop when laying out fields on certain profiles
Closes #378 Also make field layout more consistent, and tweak appearance
This commit is contained in:
parent
f775527d63
commit
d85f74f365
|
@ -118,6 +118,8 @@
|
||||||
D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C955213B365700269FB5 /* LargeImageExpandAnimationController.swift */; };
|
D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C955213B365700269FB5 /* LargeImageExpandAnimationController.swift */; };
|
||||||
D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C957213B367000269FB5 /* LargeImageShrinkAnimationController.swift */; };
|
D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C957213B367000269FB5 /* LargeImageShrinkAnimationController.swift */; };
|
||||||
D646C95A213B5D0500269FB5 /* LargeImageInteractionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C959213B5D0500269FB5 /* LargeImageInteractionController.swift */; };
|
D646C95A213B5D0500269FB5 /* LargeImageInteractionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C959213B5D0500269FB5 /* LargeImageInteractionController.swift */; };
|
||||||
|
D646DCAC2A06C8840059ECEB /* ProfileFieldValueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCAB2A06C8840059ECEB /* ProfileFieldValueView.swift */; };
|
||||||
|
D646DCAE2A06C8C90059ECEB /* ProfileFieldVerificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCAD2A06C8C90059ECEB /* ProfileFieldVerificationView.swift */; };
|
||||||
D647D92824257BEB0005044F /* AttachmentPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D647D92724257BEB0005044F /* AttachmentPreviewViewController.swift */; };
|
D647D92824257BEB0005044F /* AttachmentPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D647D92724257BEB0005044F /* AttachmentPreviewViewController.swift */; };
|
||||||
D64AAE9126C80DC600FC57FB /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9026C80DC600FC57FB /* ToastView.swift */; };
|
D64AAE9126C80DC600FC57FB /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9026C80DC600FC57FB /* ToastView.swift */; };
|
||||||
D64AAE9526C88C5000FC57FB /* ToastableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */; };
|
D64AAE9526C88C5000FC57FB /* ToastableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */; };
|
||||||
|
@ -511,6 +513,8 @@
|
||||||
D646C955213B365700269FB5 /* LargeImageExpandAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageExpandAnimationController.swift; sourceTree = "<group>"; };
|
D646C955213B365700269FB5 /* LargeImageExpandAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageExpandAnimationController.swift; sourceTree = "<group>"; };
|
||||||
D646C957213B367000269FB5 /* LargeImageShrinkAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageShrinkAnimationController.swift; sourceTree = "<group>"; };
|
D646C957213B367000269FB5 /* LargeImageShrinkAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageShrinkAnimationController.swift; sourceTree = "<group>"; };
|
||||||
D646C959213B5D0500269FB5 /* LargeImageInteractionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageInteractionController.swift; sourceTree = "<group>"; };
|
D646C959213B5D0500269FB5 /* LargeImageInteractionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageInteractionController.swift; sourceTree = "<group>"; };
|
||||||
|
D646DCAB2A06C8840059ECEB /* ProfileFieldValueView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldValueView.swift; sourceTree = "<group>"; };
|
||||||
|
D646DCAD2A06C8C90059ECEB /* ProfileFieldVerificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldVerificationView.swift; sourceTree = "<group>"; };
|
||||||
D647D92724257BEB0005044F /* AttachmentPreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentPreviewViewController.swift; sourceTree = "<group>"; };
|
D647D92724257BEB0005044F /* AttachmentPreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentPreviewViewController.swift; sourceTree = "<group>"; };
|
||||||
D64AAE9026C80DC600FC57FB /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = "<group>"; };
|
D64AAE9026C80DC600FC57FB /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = "<group>"; };
|
||||||
D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastableViewController.swift; sourceTree = "<group>"; };
|
D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastableViewController.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1136,6 +1140,8 @@
|
||||||
D6412B0A24B0D4C600F5412E /* ProfileHeaderView.xib */,
|
D6412B0A24B0D4C600F5412E /* ProfileHeaderView.xib */,
|
||||||
D6412B0C24B0D4CF00F5412E /* ProfileHeaderView.swift */,
|
D6412B0C24B0D4CF00F5412E /* ProfileHeaderView.swift */,
|
||||||
D651C5B32915B00400236EF6 /* ProfileFieldsView.swift */,
|
D651C5B32915B00400236EF6 /* ProfileFieldsView.swift */,
|
||||||
|
D646DCAB2A06C8840059ECEB /* ProfileFieldValueView.swift */,
|
||||||
|
D646DCAD2A06C8C90059ECEB /* ProfileFieldVerificationView.swift */,
|
||||||
D6A3A37F295515550036B6EF /* ProfileHeaderButton.swift */,
|
D6A3A37F295515550036B6EF /* ProfileHeaderButton.swift */,
|
||||||
D691771829A7B8820054D7EF /* ProfileHeaderMovedOverlayView.swift */,
|
D691771829A7B8820054D7EF /* ProfileHeaderMovedOverlayView.swift */,
|
||||||
);
|
);
|
||||||
|
@ -2032,6 +2038,7 @@
|
||||||
D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */,
|
D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */,
|
||||||
D68A76EC295369A8001DA1B3 /* AboutView.swift in Sources */,
|
D68A76EC295369A8001DA1B3 /* AboutView.swift in Sources */,
|
||||||
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */,
|
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */,
|
||||||
|
D646DCAE2A06C8C90059ECEB /* ProfileFieldVerificationView.swift in Sources */,
|
||||||
D61F75AD293AF39000C0B37F /* Filter+Helpers.swift in Sources */,
|
D61F75AD293AF39000C0B37F /* Filter+Helpers.swift in Sources */,
|
||||||
D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */,
|
D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */,
|
||||||
D6370B9C24421FF30092A7FF /* Tusker.xcdatamodeld in Sources */,
|
D6370B9C24421FF30092A7FF /* Tusker.xcdatamodeld in Sources */,
|
||||||
|
@ -2118,6 +2125,7 @@
|
||||||
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */,
|
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */,
|
||||||
D67895C0246870DE00D4CD9E /* LocalAccountAvatarView.swift in Sources */,
|
D67895C0246870DE00D4CD9E /* LocalAccountAvatarView.swift in Sources */,
|
||||||
D6ADB6E828E8C878009924AB /* PublicTimelineDescriptionCollectionViewCell.swift in Sources */,
|
D6ADB6E828E8C878009924AB /* PublicTimelineDescriptionCollectionViewCell.swift in Sources */,
|
||||||
|
D646DCAC2A06C8840059ECEB /* ProfileFieldValueView.swift in Sources */,
|
||||||
D6F4D79429ECB0AF00351B87 /* UIBackgroundConfiguration+AppColors.swift in Sources */,
|
D6F4D79429ECB0AF00351B87 /* UIBackgroundConfiguration+AppColors.swift in Sources */,
|
||||||
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */,
|
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */,
|
||||||
D681A29A249AD62D0085E54E /* LargeImageContentView.swift in Sources */,
|
D681A29A249AD62D0085E54E /* LargeImageContentView.swift in Sources */,
|
||||||
|
|
|
@ -206,10 +206,11 @@ class ProfileViewController: UIViewController, StateRestorableViewController {
|
||||||
headerView.layer.zPosition = 100
|
headerView.layer.zPosition = 100
|
||||||
view.addSubview(headerView)
|
view.addSubview(headerView)
|
||||||
let oldHeaderCellTop = oldHeaderCell.convert(CGPoint.zero, to: view).y
|
let oldHeaderCellTop = oldHeaderCell.convert(CGPoint.zero, to: view).y
|
||||||
// TODO: use safe area layout guide instead of manually adjusting this?
|
|
||||||
let headerTopOffset = oldHeaderCellTop - view.safeAreaInsets.top
|
let headerTopOffset = oldHeaderCellTop - view.safeAreaInsets.top
|
||||||
|
let headerBottomOffset = oldHeaderCell.convert(CGPoint(x: 0, y: oldHeaderCell.bounds.maxY), to: view).y// - view.safeAreaInsets.top
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
headerView.topAnchor.constraint(equalTo: view.topAnchor, constant: headerTopOffset),
|
headerView.topAnchor.constraint(equalTo: view.topAnchor, constant: headerTopOffset),
|
||||||
|
headerView.bottomAnchor.constraint(equalTo: view.topAnchor, constant: headerBottomOffset),
|
||||||
headerView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
headerView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
||||||
headerView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
headerView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
||||||
])
|
])
|
||||||
|
|
|
@ -21,7 +21,7 @@ class EmojiLabel: UILabel, BaseEmojiLabel {
|
||||||
func setEmojis(_ emojis: [Emoji], identifier: String) {
|
func setEmojis(_ emojis: [Emoji], identifier: String) {
|
||||||
guard emojis.count > 0, let attributedText = attributedText else { return }
|
guard emojis.count > 0, let attributedText = attributedText else { return }
|
||||||
|
|
||||||
replaceEmojis(in: attributedText.string, emojis: emojis, identifier: identifier) { [weak self] (newAttributedText, didReplaceEmojis) in
|
replaceEmojis(in: attributedText, emojis: emojis, identifier: identifier) { [weak self] (newAttributedText, didReplaceEmojis) in
|
||||||
guard let self = self, self.emojiIdentifier == identifier else { return }
|
guard let self = self, self.emojiIdentifier == identifier else { return }
|
||||||
self.hasEmojis = didReplaceEmojis
|
self.hasEmojis = didReplaceEmojis
|
||||||
self.attributedText = newAttributedText
|
self.attributedText = newAttributedText
|
||||||
|
|
|
@ -0,0 +1,212 @@
|
||||||
|
//
|
||||||
|
// ProfileFieldValueView.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 5/6/23.
|
||||||
|
// Copyright © 2023 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Pachyderm
|
||||||
|
import SwiftUI
|
||||||
|
import SafariServices
|
||||||
|
|
||||||
|
class ProfileFieldValueView: UIView {
|
||||||
|
weak var navigationDelegate: TuskerNavigationDelegate?
|
||||||
|
|
||||||
|
private static let converter: HTMLConverter = {
|
||||||
|
var converter = HTMLConverter()
|
||||||
|
converter.font = .preferredFont(forTextStyle: .body)
|
||||||
|
converter.monospaceFont = UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular))
|
||||||
|
return converter
|
||||||
|
}()
|
||||||
|
|
||||||
|
private let account: AccountMO
|
||||||
|
private let field: Account.Field
|
||||||
|
private var link: (String, URL)?
|
||||||
|
|
||||||
|
private let label = EmojiLabel()
|
||||||
|
private var iconView: UIView?
|
||||||
|
|
||||||
|
private var currentTargetedPreview: UITargetedPreview?
|
||||||
|
|
||||||
|
init(field: Account.Field, account: AccountMO) {
|
||||||
|
self.account = account
|
||||||
|
self.field = field
|
||||||
|
|
||||||
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
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 }
|
||||||
|
converted.addAttribute(.foregroundColor, value: UIColor.tintColor, range: range)
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
let labelTrailingConstraint: NSLayoutConstraint
|
||||||
|
|
||||||
|
if field.verifiedAt != nil {
|
||||||
|
var config = UIButton.Configuration.plain()
|
||||||
|
config.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration(scale: .medium)
|
||||||
|
config.image = UIImage(systemName: "checkmark")
|
||||||
|
config.baseForegroundColor = .systemGreen
|
||||||
|
let icon = UIButton(configuration: config)
|
||||||
|
self.iconView = icon
|
||||||
|
icon.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
icon.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
||||||
|
icon.addTarget(self, action: #selector(verifiedIconTapped), for: .touchUpInside)
|
||||||
|
icon.isPointerInteractionEnabled = true
|
||||||
|
icon.accessibilityLabel = "Verified link"
|
||||||
|
addSubview(icon)
|
||||||
|
labelTrailingConstraint = label.trailingAnchor.constraint(equalTo: icon.leadingAnchor)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
icon.centerYAnchor.constraint(equalTo: label.centerYAnchor),
|
||||||
|
icon.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor),
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
labelTrailingConstraint = label.trailingAnchor.constraint(equalTo: trailingAnchor)
|
||||||
|
}
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
label.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
labelTrailingConstraint,
|
||||||
|
label.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
label.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||||
|
var size = label.sizeThatFits(size)
|
||||||
|
if let iconView {
|
||||||
|
size.width += iconView.sizeThatFits(UIView.layoutFittingCompressedSize).width
|
||||||
|
}
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func verifiedIconTapped() {
|
||||||
|
guard let navigationDelegate else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let view = ProfileFieldVerificationView(
|
||||||
|
acct: account.acct,
|
||||||
|
verifiedAt: field.verifiedAt!,
|
||||||
|
linkText: label.text ?? "",
|
||||||
|
navigationDelegate: navigationDelegate
|
||||||
|
)
|
||||||
|
let host = UIHostingController(rootView: view)
|
||||||
|
let toPresent: UIViewController
|
||||||
|
if traitCollection.horizontalSizeClass == .compact || traitCollection.verticalSizeClass == .compact {
|
||||||
|
toPresent = UINavigationController(rootViewController: host)
|
||||||
|
toPresent.modalPresentationStyle = .pageSheet
|
||||||
|
let sheetPresentationController = toPresent.sheetPresentationController!
|
||||||
|
sheetPresentationController.detents = [
|
||||||
|
.medium()
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
host.modalPresentationStyle = .popover
|
||||||
|
let popoverPresentationController = host.popoverPresentationController!
|
||||||
|
popoverPresentationController.sourceView = iconView
|
||||||
|
host.preferredContentSize = host.sizeThatFits(in: CGSize(width: 400, height: CGFloat.infinity))
|
||||||
|
toPresent = host
|
||||||
|
}
|
||||||
|
navigationDelegate.present(toPresent, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ProfileFieldValueView: UIContextMenuInteractionDelegate, MenuActionProvider {
|
||||||
|
var toastableViewController: ToastableViewController? {
|
||||||
|
navigationDelegate
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
vc.preferredControlTintColor = Preferences.shared.accentColor.color
|
||||||
|
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!)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
//
|
||||||
|
// ProfileFieldVerificationView.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 5/6/23.
|
||||||
|
// Copyright © 2023 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
struct ProfileFieldVerificationView: View {
|
||||||
|
let acct: String
|
||||||
|
let verifiedAt: Date
|
||||||
|
let linkText: String
|
||||||
|
let navigationDelegate: TuskerNavigationDelegate
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
firstLine
|
||||||
|
secondLine
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.navigationTitle(Text("Verified Link"))
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .cancellationAction) {
|
||||||
|
Button("Done") {
|
||||||
|
navigationDelegate.dismiss(animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.environment(\.openURL, OpenURLAction(handler: { url in
|
||||||
|
// dismiss the sheet/popover first
|
||||||
|
navigationDelegate.dismiss(animated: true) {
|
||||||
|
navigationDelegate.selected(url: url)
|
||||||
|
}
|
||||||
|
return .handled
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
private var firstLine: Text {
|
||||||
|
var attrStr: AttributedString = "This link has been verified by your instance, "
|
||||||
|
var instance = AttributedString(navigationDelegate.apiController!.instanceURL.host!)
|
||||||
|
instance.font = .body.bold()
|
||||||
|
attrStr += instance
|
||||||
|
attrStr += "."
|
||||||
|
return Text(attrStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var secondLine: Text {
|
||||||
|
var attrStr: AttributedString = "The page at "
|
||||||
|
var linkStr: AttributedString
|
||||||
|
if linkText.count > 43 {
|
||||||
|
linkStr = AttributedString(linkText.prefix(40) + "…")
|
||||||
|
} else {
|
||||||
|
linkStr = AttributedString(linkText)
|
||||||
|
}
|
||||||
|
linkStr.link = URL(string: linkText)
|
||||||
|
attrStr += linkStr
|
||||||
|
attrStr += " was confirmed to link back to "
|
||||||
|
var acctStr = AttributedString("@\(acct)")
|
||||||
|
acctStr.font = .body.bold()
|
||||||
|
attrStr += acctStr
|
||||||
|
attrStr += AttributedString(" as of \(verifiedAt.formatted(date: .abbreviated, time: .shortened)).")
|
||||||
|
return Text(attrStr)
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,7 +8,6 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
class ProfileFieldsView: UIView {
|
class ProfileFieldsView: UIView {
|
||||||
|
|
||||||
|
@ -16,9 +15,19 @@ class ProfileFieldsView: UIView {
|
||||||
|
|
||||||
private var fields = [Account.Field]()
|
private var fields = [Account.Field]()
|
||||||
|
|
||||||
private let stack = UIStackView()
|
private var fieldViews: [(EmojiLabel, ProfileFieldValueView, UIView)] = []
|
||||||
private var fieldViews: [(EmojiLabel, ProfileFieldValueView)] = []
|
|
||||||
private var fieldConstraints: [NSLayoutConstraint] = []
|
private var fieldConstraints: [NSLayoutConstraint] = []
|
||||||
|
private lazy var dividerLayoutGuide: UILayoutGuide = {
|
||||||
|
let guide = UILayoutGuide()
|
||||||
|
addLayoutGuide(guide)
|
||||||
|
guide.widthAnchor.constraint(equalToConstant: 8).isActive = true
|
||||||
|
let centerDividerConstraint = guide.centerXAnchor.constraint(equalTo: centerXAnchor)
|
||||||
|
centerDividerConstraint.priority = .defaultHigh
|
||||||
|
centerDividerConstraint.isActive = true
|
||||||
|
return guide
|
||||||
|
}()
|
||||||
|
private var dividerXConstraint: NSLayoutConstraint?
|
||||||
|
private var boundsObservation: NSKeyValueObservation?
|
||||||
|
|
||||||
private var isUsingSingleColumn: Bool = false
|
private var isUsingSingleColumn: Bool = false
|
||||||
private var needsSingleColumn: Bool {
|
private var needsSingleColumn: Bool {
|
||||||
|
@ -43,16 +52,9 @@ class ProfileFieldsView: UIView {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func commonInit() {
|
private func commonInit() {
|
||||||
stack.axis = .vertical
|
boundsObservation = observe(\.bounds, changeHandler: { [unowned self] _, _ in
|
||||||
stack.alignment = .fill
|
self.setNeedsUpdateConstraints()
|
||||||
stack.translatesAutoresizingMaskIntoConstraints = false
|
})
|
||||||
addSubview(stack)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
stack.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
||||||
stack.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
||||||
stack.topAnchor.constraint(equalTo: topAnchor),
|
|
||||||
stack.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||||
|
@ -70,27 +72,53 @@ class ProfileFieldsView: UIView {
|
||||||
}
|
}
|
||||||
fields = account.fields
|
fields = account.fields
|
||||||
|
|
||||||
for (name, value) in fieldViews {
|
for (name, value, fieldContainer) in fieldViews {
|
||||||
name.removeFromSuperview()
|
name.removeFromSuperview()
|
||||||
value.removeFromSuperview()
|
value.removeFromSuperview()
|
||||||
|
fieldContainer.removeFromSuperview()
|
||||||
}
|
}
|
||||||
fieldViews = []
|
fieldViews = []
|
||||||
|
|
||||||
for field in account.fields {
|
for (index, field) in account.fields.enumerated() {
|
||||||
let nameLabel = EmojiLabel()
|
let nameLabel = EmojiLabel()
|
||||||
nameLabel.text = field.name
|
nameLabel.text = field.name
|
||||||
nameLabel.font = .preferredFont(forTextStyle: .body).withTraits(.traitBold)!
|
nameLabel.font = .preferredFont(forTextStyle: .body).withTraits(.traitBold)!
|
||||||
nameLabel.adjustsFontForContentSizeCategory = true
|
nameLabel.adjustsFontForContentSizeCategory = true
|
||||||
nameLabel.numberOfLines = 0
|
nameLabel.numberOfLines = 0
|
||||||
nameLabel.lineBreakMode = .byWordWrapping
|
nameLabel.lineBreakMode = .byWordWrapping
|
||||||
|
nameLabel.showsExpansionTextWhenTruncated = true
|
||||||
nameLabel.setEmojis(account.emojis, identifier: account.id)
|
nameLabel.setEmojis(account.emojis, identifier: account.id)
|
||||||
nameLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
nameLabel.setContentHuggingPriority(.defaultHigh, for: .vertical)
|
||||||
|
|
||||||
let valueView = ProfileFieldValueView(field: field, account: account)
|
let valueView = ProfileFieldValueView(field: field, account: account)
|
||||||
valueView.navigationDelegate = delegate
|
valueView.navigationDelegate = delegate
|
||||||
valueView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
valueView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||||
|
|
||||||
fieldViews.append((nameLabel, valueView))
|
let container = UIView()
|
||||||
|
container.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(container)
|
||||||
|
|
||||||
|
nameLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
container.addSubview(nameLabel)
|
||||||
|
valueView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
container.addSubview(valueView)
|
||||||
|
|
||||||
|
if index % 2 == 0 {
|
||||||
|
container.backgroundColor = .secondarySystemFill
|
||||||
|
} else {
|
||||||
|
container.backgroundColor = .quaternarySystemFill
|
||||||
|
}
|
||||||
|
if index == 0 || index == fields.count - 1 {
|
||||||
|
if fields.count > 1 && index == 0 {
|
||||||
|
container.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||||
|
} else if fields.count > 1 && index == fields.count - 1 {
|
||||||
|
container.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
||||||
|
}
|
||||||
|
container.layer.cornerRadius = 8
|
||||||
|
container.layer.cornerCurve = .continuous
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldViews.append((nameLabel, valueView, container))
|
||||||
}
|
}
|
||||||
|
|
||||||
configureFields()
|
configureFields()
|
||||||
|
@ -106,222 +134,83 @@ class ProfileFieldsView: UIView {
|
||||||
NSLayoutConstraint.deactivate(fieldConstraints)
|
NSLayoutConstraint.deactivate(fieldConstraints)
|
||||||
fieldConstraints = []
|
fieldConstraints = []
|
||||||
|
|
||||||
stack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
var prevContainer: UIView?
|
||||||
|
|
||||||
|
for (name, value, container) in fieldViews {
|
||||||
|
fieldConstraints.append(contentsOf: [
|
||||||
|
container.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
container.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
if needsSingleColumn {
|
if needsSingleColumn {
|
||||||
stack.spacing = 4
|
|
||||||
var isFirst = true
|
|
||||||
for (name, value) in fieldViews {
|
|
||||||
if isFirst {
|
|
||||||
isFirst = false
|
|
||||||
} else {
|
|
||||||
let spacer = UIView()
|
|
||||||
// don't need any height, since there's 4pts of padding on either side
|
|
||||||
spacer.heightAnchor.constraint(equalToConstant: 0).isActive = true
|
|
||||||
stack.addArrangedSubview(spacer)
|
|
||||||
}
|
|
||||||
name.textAlignment = .natural
|
name.textAlignment = .natural
|
||||||
stack.addArrangedSubview(name)
|
|
||||||
value.setTextAlignment(.natural)
|
value.setTextAlignment(.natural)
|
||||||
stack.addArrangedSubview(value)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
stack.spacing = 8
|
|
||||||
|
|
||||||
let dividerLayoutGuide = UILayoutGuide()
|
|
||||||
addLayoutGuide(dividerLayoutGuide)
|
|
||||||
fieldConstraints.append(contentsOf: [
|
fieldConstraints.append(contentsOf: [
|
||||||
dividerLayoutGuide.widthAnchor.constraint(equalToConstant: 8),
|
name.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 4),
|
||||||
|
name.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -4),
|
||||||
|
name.topAnchor.constraint(equalTo: container.topAnchor, constant: 4),
|
||||||
|
|
||||||
|
value.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 4),
|
||||||
|
value.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -4),
|
||||||
|
value.topAnchor.constraint(equalTo: name.bottomAnchor),
|
||||||
|
value.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -4),
|
||||||
])
|
])
|
||||||
|
} else {
|
||||||
for (name, value) in fieldViews {
|
|
||||||
name.textAlignment = .right
|
name.textAlignment = .right
|
||||||
name.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
|
|
||||||
value.setTextAlignment(.left)
|
value.setTextAlignment(.left)
|
||||||
value.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
|
|
||||||
let fieldContainer = UIView()
|
|
||||||
fieldContainer.addSubview(name)
|
|
||||||
fieldContainer.addSubview(value)
|
|
||||||
stack.addArrangedSubview(fieldContainer)
|
|
||||||
fieldConstraints.append(contentsOf: [
|
fieldConstraints.append(contentsOf: [
|
||||||
name.leadingAnchor.constraint(equalTo: fieldContainer.leadingAnchor),
|
container.heightAnchor.constraint(greaterThanOrEqualToConstant: 32),
|
||||||
|
|
||||||
|
name.leadingAnchor.constraint(greaterThanOrEqualTo: container.leadingAnchor, constant: 4),
|
||||||
name.trailingAnchor.constraint(equalTo: dividerLayoutGuide.leadingAnchor),
|
name.trailingAnchor.constraint(equalTo: dividerLayoutGuide.leadingAnchor),
|
||||||
name.topAnchor.constraint(equalTo: fieldContainer.topAnchor),
|
name.topAnchor.constraint(equalTo: container.topAnchor, constant: 4),
|
||||||
name.bottomAnchor.constraint(equalTo: fieldContainer.bottomAnchor),
|
name.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -4),
|
||||||
|
|
||||||
value.leadingAnchor.constraint(equalTo: dividerLayoutGuide.trailingAnchor),
|
value.leadingAnchor.constraint(equalTo: dividerLayoutGuide.trailingAnchor),
|
||||||
value.trailingAnchor.constraint(equalTo: fieldContainer.trailingAnchor),
|
value.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -4),
|
||||||
value.topAnchor.constraint(equalTo: fieldContainer.topAnchor),
|
value.topAnchor.constraint(equalTo: container.topAnchor, constant: 4),
|
||||||
value.bottomAnchor.constraint(equalTo: fieldContainer.bottomAnchor),
|
value.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -4),
|
||||||
|
|
||||||
name.widthAnchor.constraint(greaterThanOrEqualTo: value.widthAnchor, multiplier: 0.5),
|
|
||||||
name.widthAnchor.constraint(lessThanOrEqualTo: value.widthAnchor, multiplier: 2),
|
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let containerTopConstraint = container.topAnchor.constraint(equalTo: prevContainer?.bottomAnchor ?? topAnchor)
|
||||||
|
fieldConstraints.append(containerTopConstraint)
|
||||||
|
prevContainer = container
|
||||||
|
}
|
||||||
|
|
||||||
|
if let prevContainer {
|
||||||
|
let lastContainerBottomConstraint = prevContainer.bottomAnchor.constraint(equalTo: bottomAnchor)
|
||||||
|
fieldConstraints.append(lastContainerBottomConstraint)
|
||||||
}
|
}
|
||||||
|
|
||||||
NSLayoutConstraint.activate(fieldConstraints)
|
NSLayoutConstraint.activate(fieldConstraints)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
override func updateConstraints() {
|
||||||
|
if !needsSingleColumn,
|
||||||
|
!fieldViews.isEmpty {
|
||||||
|
let maxNameWidth = fieldViews.map {
|
||||||
|
$0.0.sizeThatFits(UIView.layoutFittingCompressedSize).width
|
||||||
|
}.max()!
|
||||||
|
let maxValueWidth = fieldViews.map {
|
||||||
|
$0.1.sizeThatFits(UIView.layoutFittingCompressedSize).width
|
||||||
|
}.max()!
|
||||||
|
|
||||||
private class ProfileFieldValueView: UIView {
|
let defaultWidth = (bounds.width - 8) / 2
|
||||||
weak var navigationDelegate: TuskerNavigationDelegate? {
|
|
||||||
didSet {
|
dividerXConstraint?.isActive = false
|
||||||
textView.navigationDelegate = navigationDelegate
|
if maxNameWidth > defaultWidth && maxValueWidth < defaultWidth {
|
||||||
|
dividerXConstraint = dividerLayoutGuide.leadingAnchor.constraint(equalTo: leadingAnchor, constant: min(maxNameWidth, bounds.width * 2 / 3))
|
||||||
|
dividerXConstraint!.isActive = true
|
||||||
|
} else if maxNameWidth < defaultWidth && maxValueWidth > defaultWidth {
|
||||||
|
dividerXConstraint = dividerLayoutGuide.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -min(maxValueWidth, bounds.width * 2 / 3))
|
||||||
|
dividerXConstraint!.isActive = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private let account: AccountMO
|
super.updateConstraints()
|
||||||
private let field: Account.Field
|
|
||||||
|
|
||||||
private let textView = ContentTextView()
|
|
||||||
private var iconView: UIView?
|
|
||||||
|
|
||||||
init(field: Account.Field, account: AccountMO) {
|
|
||||||
self.account = account
|
|
||||||
self.field = field
|
|
||||||
|
|
||||||
super.init(frame: .zero)
|
|
||||||
|
|
||||||
textView.isSelectable = false
|
|
||||||
textView.backgroundColor = .clear
|
|
||||||
textView.defaultFont = .preferredFont(forTextStyle: .body)
|
|
||||||
textView.monospaceFont = UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular))
|
|
||||||
textView.adjustsFontForContentSizeCategory = true
|
|
||||||
textView.setTextFromHtml(field.value)
|
|
||||||
textView.setEmojis(account.emojis, identifier: account.id)
|
|
||||||
textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
||||||
textView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
addSubview(textView)
|
|
||||||
|
|
||||||
let textViewTrailingConstraint: NSLayoutConstraint
|
|
||||||
|
|
||||||
if field.verifiedAt != nil {
|
|
||||||
var config = UIButton.Configuration.plain()
|
|
||||||
config.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration(scale: .medium)
|
|
||||||
config.image = UIImage(systemName: "checkmark")
|
|
||||||
config.baseForegroundColor = .systemGreen
|
|
||||||
let icon = UIButton(configuration: config)
|
|
||||||
self.iconView = icon
|
|
||||||
icon.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
icon.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
|
||||||
icon.addTarget(self, action: #selector(verifiedIconTapped), for: .touchUpInside)
|
|
||||||
icon.isPointerInteractionEnabled = true
|
|
||||||
icon.accessibilityLabel = "Verified link"
|
|
||||||
addSubview(icon)
|
|
||||||
textViewTrailingConstraint = textView.trailingAnchor.constraint(equalTo: icon.leadingAnchor)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
icon.lastBaselineAnchor.constraint(equalTo: textView.lastBaselineAnchor),
|
|
||||||
icon.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor),
|
|
||||||
])
|
|
||||||
} else {
|
|
||||||
textViewTrailingConstraint = textView.trailingAnchor.constraint(equalTo: trailingAnchor)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
textView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
||||||
textViewTrailingConstraint,
|
|
||||||
textView.topAnchor.constraint(equalTo: topAnchor),
|
|
||||||
textView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func setTextAlignment(_ alignment: NSTextAlignment) {
|
|
||||||
textView.textAlignment = alignment
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func verifiedIconTapped() {
|
|
||||||
guard let navigationDelegate else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let view = ProfileFieldVerificationView(
|
|
||||||
acct: account.acct,
|
|
||||||
verifiedAt: field.verifiedAt!,
|
|
||||||
linkText: textView.text,
|
|
||||||
navigationDelegate: navigationDelegate
|
|
||||||
)
|
|
||||||
let host = UIHostingController(rootView: view)
|
|
||||||
let toPresent: UIViewController
|
|
||||||
if traitCollection.horizontalSizeClass == .compact || traitCollection.verticalSizeClass == .compact {
|
|
||||||
toPresent = UINavigationController(rootViewController: host)
|
|
||||||
toPresent.modalPresentationStyle = .pageSheet
|
|
||||||
let sheetPresentationController = toPresent.sheetPresentationController!
|
|
||||||
sheetPresentationController.detents = [
|
|
||||||
.medium()
|
|
||||||
]
|
|
||||||
} else {
|
|
||||||
host.modalPresentationStyle = .popover
|
|
||||||
let popoverPresentationController = host.popoverPresentationController!
|
|
||||||
popoverPresentationController.sourceView = iconView
|
|
||||||
host.preferredContentSize = host.sizeThatFits(in: CGSize(width: 400, height: CGFloat.infinity))
|
|
||||||
toPresent = host
|
|
||||||
}
|
|
||||||
navigationDelegate.present(toPresent, animated: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
private struct ProfileFieldVerificationView: View {
|
|
||||||
let acct: String
|
|
||||||
let verifiedAt: Date
|
|
||||||
let linkText: String
|
|
||||||
let navigationDelegate: TuskerNavigationDelegate
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
firstLine
|
|
||||||
secondLine
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.navigationTitle(Text("Verified Link"))
|
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
|
||||||
.toolbar {
|
|
||||||
ToolbarItem(placement: .cancellationAction) {
|
|
||||||
Button("Done") {
|
|
||||||
navigationDelegate.dismiss(animated: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.environment(\.openURL, OpenURLAction(handler: { url in
|
|
||||||
// dismiss the sheet/popover first
|
|
||||||
navigationDelegate.dismiss(animated: true) {
|
|
||||||
navigationDelegate.selected(url: url)
|
|
||||||
}
|
|
||||||
return .handled
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
private var firstLine: Text {
|
|
||||||
var attrStr: AttributedString = "This link has been verified by your instance, "
|
|
||||||
var instance = AttributedString(navigationDelegate.apiController!.instanceURL.host!)
|
|
||||||
instance.font = .body.bold()
|
|
||||||
attrStr += instance
|
|
||||||
attrStr += "."
|
|
||||||
return Text(attrStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var secondLine: Text {
|
|
||||||
var attrStr: AttributedString = "The page at "
|
|
||||||
var linkStr: AttributedString
|
|
||||||
if linkText.count > 43 {
|
|
||||||
linkStr = AttributedString(linkText.prefix(40) + "…")
|
|
||||||
} else {
|
|
||||||
linkStr = AttributedString(linkText)
|
|
||||||
}
|
|
||||||
linkStr.link = URL(string: linkText)
|
|
||||||
attrStr += linkStr
|
|
||||||
attrStr += " was confirmed to link back to "
|
|
||||||
var acctStr = AttributedString("@\(acct)")
|
|
||||||
acctStr.font = .body.bold()
|
|
||||||
attrStr += acctStr
|
|
||||||
attrStr += AttributedString(" as of \(verifiedAt.formatted(date: .abbreviated, time: .shortened)).")
|
|
||||||
return Text(attrStr)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<deployment identifier="iOS"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21678"/>
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
@ -62,7 +62,7 @@
|
||||||
</connections>
|
</connections>
|
||||||
</button>
|
</button>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="u4P-3i-gEq">
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="u4P-3i-gEq">
|
||||||
<rect key="frame" x="16" y="266" width="398" height="596"/>
|
<rect key="frame" x="16" y="266" width="382" height="596"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Follows you" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="UF8-nI-KVj">
|
<label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Follows you" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="UF8-nI-KVj">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="75.5" height="0.0"/>
|
<rect key="frame" x="0.0" y="0.0" width="75.5" height="0.0"/>
|
||||||
|
@ -78,7 +78,7 @@
|
||||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||||
</textView>
|
</textView>
|
||||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="vKC-m1-Sbs" customClass="ProfileFieldsView" customModule="Tusker" customModuleProvider="target">
|
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="vKC-m1-Sbs" customClass="ProfileFieldsView" customModule="Tusker" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="263.5" width="398" height="128"/>
|
<rect key="frame" x="0.0" y="263.5" width="382" height="128"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="height" constant="128" placeholder="YES" id="xbR-M6-H0I"/>
|
<constraint firstAttribute="height" constant="128" placeholder="YES" id="xbR-M6-H0I"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
|
@ -104,7 +104,7 @@
|
||||||
</subviews>
|
</subviews>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstItem="vKC-m1-Sbs" firstAttribute="width" secondItem="u4P-3i-gEq" secondAttribute="width" id="0dI-ax-7eI"/>
|
<constraint firstItem="vKC-m1-Sbs" firstAttribute="width" secondItem="u4P-3i-gEq" secondAttribute="width" id="0dI-ax-7eI"/>
|
||||||
<constraint firstItem="1O8-2P-Gbf" firstAttribute="width" secondItem="u4P-3i-gEq" secondAttribute="width" constant="-16" id="hnA-3G-B9B"/>
|
<constraint firstItem="1O8-2P-Gbf" firstAttribute="width" secondItem="u4P-3i-gEq" secondAttribute="width" id="hnA-3G-B9B"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
</stackView>
|
</stackView>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="jwU-EH-hmC">
|
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="jwU-EH-hmC">
|
||||||
|
@ -155,7 +155,7 @@
|
||||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="dgG-dR-lSv" secondAttribute="trailing" id="j0d-hY-815"/>
|
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="dgG-dR-lSv" secondAttribute="trailing" id="j0d-hY-815"/>
|
||||||
<constraint firstItem="5ja-fK-Fqz" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="16" id="jPG-WM-9km"/>
|
<constraint firstItem="5ja-fK-Fqz" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="16" id="jPG-WM-9km"/>
|
||||||
<constraint firstItem="jwU-EH-hmC" firstAttribute="leading" secondItem="wT9-2J-uSY" secondAttribute="trailing" constant="8" id="oGR-6M-gdd"/>
|
<constraint firstItem="jwU-EH-hmC" firstAttribute="leading" secondItem="wT9-2J-uSY" secondAttribute="trailing" constant="8" id="oGR-6M-gdd"/>
|
||||||
<constraint firstAttribute="trailing" secondItem="u4P-3i-gEq" secondAttribute="trailing" id="ph6-NT-A02"/>
|
<constraint firstAttribute="trailing" secondItem="u4P-3i-gEq" secondAttribute="trailing" constant="16" id="ph6-NT-A02"/>
|
||||||
<constraint firstItem="u4P-3i-gEq" firstAttribute="top" secondItem="wT9-2J-uSY" secondAttribute="bottom" priority="999" constant="8" id="tKQ-6d-Z55"/>
|
<constraint firstItem="u4P-3i-gEq" firstAttribute="top" secondItem="wT9-2J-uSY" secondAttribute="bottom" priority="999" constant="8" id="tKQ-6d-Z55"/>
|
||||||
<constraint firstItem="u4P-3i-gEq" firstAttribute="top" secondItem="jwU-EH-hmC" secondAttribute="bottom" priority="999" constant="8" id="xDD-rx-gC0"/>
|
<constraint firstItem="u4P-3i-gEq" firstAttribute="top" secondItem="jwU-EH-hmC" secondAttribute="bottom" priority="999" constant="8" id="xDD-rx-gC0"/>
|
||||||
<constraint firstItem="cr8-p9-xkc" firstAttribute="centerY" secondItem="vFa-g3-xIP" secondAttribute="centerY" id="xjr-Hn-Tuk"/>
|
<constraint firstItem="cr8-p9-xkc" firstAttribute="centerY" secondItem="vFa-g3-xIP" secondAttribute="centerY" id="xjr-Hn-Tuk"/>
|
||||||
|
|
Loading…
Reference in New Issue