forked from shadowfacts/Tusker
Try to resolve statuses from links that match known patterns
This commit is contained in:
parent
63ed3b6e10
commit
2229b332e0
|
@ -8,20 +8,16 @@
|
|||
|
||||
import UIKit
|
||||
import Pachyderm
|
||||
import WebURL
|
||||
import WebURLFoundationExtras
|
||||
|
||||
class ConversationViewController: UIViewController {
|
||||
|
||||
weak var mastodonController: MastodonController!
|
||||
|
||||
let mainStatusID: String
|
||||
private(set) var mode: Mode
|
||||
let mainStatusState: CollapseState
|
||||
var statusIDToScrollToOnLoad: String {
|
||||
didSet {
|
||||
if case .displaying(let vc) = state {
|
||||
vc.statusIDToScrollToOnLoad = statusIDToScrollToOnLoad
|
||||
}
|
||||
}
|
||||
}
|
||||
var statusIDToScrollToOnLoad: String?
|
||||
var showStatusesAutomatically = false {
|
||||
didSet {
|
||||
if case .displaying(let vc) = state {
|
||||
|
@ -57,6 +53,8 @@ class ConversationViewController: UIViewController {
|
|||
embedChild(vc)
|
||||
case .notFound:
|
||||
showMainStatusNotFound()
|
||||
case .unableToResolve(let error):
|
||||
showUnableToResolve(error)
|
||||
}
|
||||
|
||||
updateVisibilityBarButtonItem()
|
||||
|
@ -64,9 +62,16 @@ class ConversationViewController: UIViewController {
|
|||
}
|
||||
|
||||
init(for mainStatusID: String, state mainStatusState: CollapseState, mastodonController: MastodonController) {
|
||||
self.mainStatusID = mainStatusID
|
||||
self.mode = .localID(mainStatusID)
|
||||
self.mainStatusState = mainStatusState
|
||||
self.statusIDToScrollToOnLoad = mainStatusID
|
||||
self.mastodonController = mastodonController
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
init(resolving url: URL, mastodonController: MastodonController) {
|
||||
self.mode = .resolve(url)
|
||||
self.mainStatusState = .unknown
|
||||
self.mastodonController = mastodonController
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
@ -121,7 +126,8 @@ class ConversationViewController: UIViewController {
|
|||
guard let userInfo = notification.userInfo,
|
||||
let accountID = mastodonController.accountInfo?.id,
|
||||
userInfo["accountID"] as? String == accountID,
|
||||
let statusIDs = userInfo["statusIDs"] as? [String] else {
|
||||
let statusIDs = userInfo["statusIDs"] as? [String],
|
||||
case .localID(let mainStatusID) = mode else {
|
||||
return
|
||||
}
|
||||
if statusIDs.contains(mainStatusID) {
|
||||
|
@ -137,6 +143,10 @@ class ConversationViewController: UIViewController {
|
|||
// MARK: Loading
|
||||
|
||||
private func loadMainStatus() async {
|
||||
guard let mainStatusID = await resolveStatusIfNecessary() else {
|
||||
return
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func doLoadMainStatus() async -> StatusMO? {
|
||||
switch await FetchStatusService(statusID: mainStatusID, mastodonController: mastodonController).run() {
|
||||
|
@ -169,10 +179,37 @@ class ConversationViewController: UIViewController {
|
|||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func resolveStatusIfNecessary() async -> String? {
|
||||
switch mode {
|
||||
case .localID(let id):
|
||||
return id
|
||||
case .resolve(let url):
|
||||
let indicator = UIActivityIndicatorView(style: .medium)
|
||||
indicator.startAnimating()
|
||||
state = .loading(indicator)
|
||||
|
||||
let url = WebURL(url)!
|
||||
let request = Client.search(query: url.serialized(), types: [.statuses], resolve: true)
|
||||
do {
|
||||
let (results, _) = try await mastodonController.run(request)
|
||||
guard let status = results.statuses.first(where: { $0.url == url }) else {
|
||||
throw UnableToResolveError()
|
||||
}
|
||||
_ = mastodonController.persistentContainer.addOrUpdateOnViewContext(status: status)
|
||||
mode = .localID(status.id)
|
||||
return status.id
|
||||
} catch {
|
||||
state = .unableToResolve(error)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func mainStatusLoaded(_ mainStatus: StatusMO) async {
|
||||
let vc = ConversationCollectionViewController(for: mainStatusID, state: mainStatusState, mastodonController: mastodonController)
|
||||
vc.statusIDToScrollToOnLoad = statusIDToScrollToOnLoad
|
||||
let vc = ConversationCollectionViewController(for: mainStatus.id, state: mainStatusState, mastodonController: mastodonController)
|
||||
vc.statusIDToScrollToOnLoad = statusIDToScrollToOnLoad ?? mainStatus.id
|
||||
vc.showStatusesAutomatically = showStatusesAutomatically
|
||||
vc.addMainStatus(mainStatus)
|
||||
state = .displaying(vc)
|
||||
|
@ -227,6 +264,66 @@ class ConversationViewController: UIViewController {
|
|||
self.showToast(configuration: config, animated: true)
|
||||
}
|
||||
|
||||
private func showUnableToResolve(_ error: Error) {
|
||||
let image = UIImageView(image: UIImage(systemName: "exclamationmark.triangle.fill")!)
|
||||
image.tintColor = .secondaryLabel
|
||||
image.contentMode = .scaleAspectFit
|
||||
|
||||
let title = UILabel()
|
||||
title.textColor = .secondaryLabel
|
||||
title.font = .preferredFont(forTextStyle: .title1).withTraits(.traitBold)!
|
||||
title.adjustsFontForContentSizeCategory = true
|
||||
title.text = "Couldn't Load Post"
|
||||
|
||||
let subtitle = UILabel()
|
||||
subtitle.textColor = .secondaryLabel
|
||||
subtitle.font = .preferredFont(forTextStyle: .body)
|
||||
subtitle.adjustsFontForContentSizeCategory = true
|
||||
subtitle.numberOfLines = 0
|
||||
subtitle.textAlignment = .center
|
||||
if let error = error as? UnableToResolveError {
|
||||
subtitle.text = error.localizedDescription
|
||||
} else if let error = error as? Client.Error {
|
||||
subtitle.text = error.localizedDescription
|
||||
} else {
|
||||
subtitle.text = error.localizedDescription
|
||||
}
|
||||
|
||||
var config = UIButton.Configuration.plain()
|
||||
config.title = "Open in Safari"
|
||||
config.image = UIImage(systemName: "safari")
|
||||
config.imagePadding = 4
|
||||
let button = UIButton(configuration: config, primaryAction: UIAction(handler: { [unowned self] _ in
|
||||
guard case .resolve(let url) = self.mode else {
|
||||
return
|
||||
}
|
||||
self.selected(url: url, allowResolveStatuses: false)
|
||||
}))
|
||||
|
||||
let stack = UIStackView(arrangedSubviews: [
|
||||
image,
|
||||
title,
|
||||
subtitle,
|
||||
button,
|
||||
])
|
||||
stack.axis = .vertical
|
||||
stack.alignment = .center
|
||||
stack.spacing = 8
|
||||
stack.isAccessibilityElement = true
|
||||
stack.accessibilityLabel = "\(title.text!). \(subtitle.text!)"
|
||||
|
||||
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(stack)
|
||||
NSLayoutConstraint.activate([
|
||||
image.widthAnchor.constraint(equalToConstant: 64),
|
||||
image.heightAnchor.constraint(equalToConstant: 64),
|
||||
|
||||
stack.leadingAnchor.constraint(equalToSystemSpacingAfter: view.safeAreaLayoutGuide.leadingAnchor, multiplier: 1),
|
||||
view.safeAreaLayoutGuide.trailingAnchor.constraint(equalToSystemSpacingAfter: stack.trailingAnchor, multiplier: 1),
|
||||
stack.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
|
||||
@objc func toggleCollapseButtonPressed() {
|
||||
|
@ -240,15 +337,35 @@ class ConversationViewController: UIViewController {
|
|||
|
||||
}
|
||||
|
||||
extension ConversationViewController {
|
||||
enum Mode {
|
||||
case localID(String)
|
||||
case resolve(URL)
|
||||
}
|
||||
}
|
||||
|
||||
extension ConversationViewController {
|
||||
struct UnableToResolveError: Error {
|
||||
var localizedDescription: String {
|
||||
"Unable to resolve status from URL"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ConversationViewController {
|
||||
enum State {
|
||||
case unloaded
|
||||
case loading(UIActivityIndicatorView)
|
||||
case displaying(ConversationCollectionViewController)
|
||||
case notFound
|
||||
case unableToResolve(Error)
|
||||
}
|
||||
}
|
||||
|
||||
extension ConversationViewController: TuskerNavigationDelegate {
|
||||
var apiController: MastodonController! { mastodonController }
|
||||
}
|
||||
|
||||
extension ConversationViewController: ToastableViewController {
|
||||
var toastScrollView: UIScrollView? {
|
||||
if case .displaying(let vc) = state {
|
||||
|
|
|
@ -44,7 +44,7 @@ extension TuskerNavigationDelegate {
|
|||
show(HashtagTimelineViewController(for: tag, mastodonController: apiController), sender: self)
|
||||
}
|
||||
|
||||
func selected(url: URL, allowUniversalLinks: Bool = true) {
|
||||
func selected(url: URL, allowResolveStatuses: Bool = true, allowUniversalLinks: Bool = true) {
|
||||
func openSafari() {
|
||||
if Preferences.shared.useInAppSafari,
|
||||
url.scheme == "https" || url.scheme == "http" {
|
||||
|
@ -66,8 +66,12 @@ extension TuskerNavigationDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
if allowUniversalLinks && Preferences.shared.openLinksInApps,
|
||||
url.scheme == "https" || url.scheme == "http" {
|
||||
if allowResolveStatuses,
|
||||
isLikelyResolvableAsStatus(url) {
|
||||
show(ConversationViewController(resolving: url, mastodonController: apiController))
|
||||
} else if allowUniversalLinks,
|
||||
Preferences.shared.openLinksInApps,
|
||||
url.scheme == "https" || url.scheme == "http" {
|
||||
UIApplication.shared.open(url, options: [.universalLinksOnly: true]) { (success) in
|
||||
if (!success) {
|
||||
openSafari()
|
||||
|
@ -217,3 +221,21 @@ enum PopoverSource {
|
|||
.barButtonItem(WeakHolder(item))
|
||||
}
|
||||
}
|
||||
|
||||
private let statusPathRegex = try! NSRegularExpression(
|
||||
pattern:
|
||||
"(^/@[a-z0-9_]+/\\d{18})" // mastodon
|
||||
+ "|(^/notice/[a-z0-9]{18})" // pleroma
|
||||
+ "|(^/p/[a-z0-9_]+/\\d{18})" // pixelfed
|
||||
+ "|(^/i/web/post/\\d{18})" // pixelfed web frontend
|
||||
+ "|(^/u/.+/h/[a-z0-9]{18})" // honk
|
||||
+ "|(^/@.+/statuses/[a-z0-9]{26})" // gotosocial
|
||||
,
|
||||
options: .caseInsensitive
|
||||
)
|
||||
|
||||
private func isLikelyResolvableAsStatus(_ url: URL) -> Bool {
|
||||
let path = url.path
|
||||
let range = NSRange(location: 0, length: path.utf16.count)
|
||||
return statusPathRegex.numberOfMatches(in: path, range: range) == 1
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue