forked from shadowfacts/Tusker
322 lines
12 KiB
Swift
322 lines
12 KiB
Swift
//
|
|
// ProfileFieldsView.swift
|
|
// Tusker
|
|
//
|
|
// Created by Shadowfacts on 11/4/22.
|
|
// Copyright © 2022 Shadowfacts. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
import Pachyderm
|
|
import SwiftUI
|
|
|
|
class ProfileFieldsView: UIView {
|
|
|
|
weak var delegate: ProfileHeaderViewDelegate?
|
|
|
|
private let stack = UIStackView()
|
|
private var fieldViews: [(EmojiLabel, ProfileFieldValueView)] = []
|
|
private var fieldConstraints: [NSLayoutConstraint] = []
|
|
|
|
private var isUsingSingleColumn: Bool = false
|
|
private var needsSingleColumn: Bool {
|
|
traitCollection.horizontalSizeClass == .compact && traitCollection.preferredContentSizeCategory > .extraLarge
|
|
}
|
|
|
|
override var accessibilityElements: [Any]? {
|
|
get {
|
|
fieldViews.flatMap { [$0.0, $0.1] }
|
|
}
|
|
set {}
|
|
}
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
commonInit()
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
super.init(coder: coder)
|
|
commonInit()
|
|
}
|
|
|
|
private func commonInit() {
|
|
stack.axis = .vertical
|
|
stack.alignment = .fill
|
|
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?) {
|
|
super.traitCollectionDidChange(previousTraitCollection)
|
|
if isUsingSingleColumn != needsSingleColumn {
|
|
configureFields()
|
|
}
|
|
}
|
|
|
|
func updateUI(account: AccountMO) {
|
|
isHidden = account.fields.isEmpty
|
|
guard !account.fields.isEmpty else {
|
|
return
|
|
}
|
|
|
|
for (name, value) in fieldViews {
|
|
name.removeFromSuperview()
|
|
value.removeFromSuperview()
|
|
}
|
|
fieldViews = []
|
|
|
|
for field in account.fields {
|
|
let nameLabel = EmojiLabel()
|
|
nameLabel.text = field.name
|
|
nameLabel.font = .preferredFont(forTextStyle: .body).withTraits(.traitBold)!
|
|
nameLabel.adjustsFontForContentSizeCategory = true
|
|
nameLabel.numberOfLines = 0
|
|
nameLabel.lineBreakMode = .byWordWrapping
|
|
nameLabel.setEmojis(account.emojis, identifier: account.id)
|
|
nameLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
|
|
let valueView = ProfileFieldValueView(field: field, account: account)
|
|
valueView.navigationDelegate = delegate
|
|
valueView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
|
|
fieldViews.append((nameLabel, valueView))
|
|
}
|
|
|
|
configureFields()
|
|
}
|
|
|
|
@objc private func configureFields() {
|
|
guard !isHidden else {
|
|
return
|
|
}
|
|
|
|
isUsingSingleColumn = needsSingleColumn
|
|
|
|
NSLayoutConstraint.deactivate(fieldConstraints)
|
|
fieldConstraints = []
|
|
|
|
stack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
|
|
|
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
|
|
stack.addArrangedSubview(name)
|
|
value.setTextAlignment(.natural)
|
|
stack.addArrangedSubview(value)
|
|
}
|
|
} else {
|
|
stack.spacing = 8
|
|
|
|
let dividerLayoutGuide = UILayoutGuide()
|
|
addLayoutGuide(dividerLayoutGuide)
|
|
fieldConstraints.append(contentsOf: [
|
|
dividerLayoutGuide.widthAnchor.constraint(equalToConstant: 8),
|
|
])
|
|
|
|
for (name, value) in fieldViews {
|
|
name.textAlignment = .right
|
|
name.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
value.setTextAlignment(.left)
|
|
value.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
let fieldContainer = UIView()
|
|
fieldContainer.addSubview(name)
|
|
fieldContainer.addSubview(value)
|
|
stack.addArrangedSubview(fieldContainer)
|
|
fieldConstraints.append(contentsOf: [
|
|
name.leadingAnchor.constraint(equalTo: fieldContainer.leadingAnchor),
|
|
name.trailingAnchor.constraint(equalTo: dividerLayoutGuide.leadingAnchor),
|
|
name.topAnchor.constraint(equalTo: fieldContainer.topAnchor),
|
|
name.bottomAnchor.constraint(equalTo: fieldContainer.bottomAnchor),
|
|
|
|
value.leadingAnchor.constraint(equalTo: dividerLayoutGuide.trailingAnchor),
|
|
value.trailingAnchor.constraint(equalTo: fieldContainer.trailingAnchor),
|
|
value.topAnchor.constraint(equalTo: fieldContainer.topAnchor),
|
|
value.bottomAnchor.constraint(equalTo: fieldContainer.bottomAnchor),
|
|
|
|
name.widthAnchor.constraint(greaterThanOrEqualTo: value.widthAnchor, multiplier: 0.5),
|
|
name.widthAnchor.constraint(lessThanOrEqualTo: value.widthAnchor, multiplier: 2),
|
|
])
|
|
}
|
|
}
|
|
|
|
NSLayoutConstraint.activate(fieldConstraints)
|
|
}
|
|
|
|
}
|
|
|
|
private class ProfileFieldValueView: UIView {
|
|
weak var navigationDelegate: TuskerNavigationDelegate? {
|
|
didSet {
|
|
textView.navigationDelegate = navigationDelegate
|
|
}
|
|
}
|
|
|
|
private let account: AccountMO
|
|
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.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.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, constant: -4)
|
|
NSLayoutConstraint.activate([
|
|
icon.centerYAnchor.constraint(equalTo: centerYAnchor),
|
|
icon.trailingAnchor.constraint(equalTo: 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)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|