Tusker/Tusker/TuskerNavigationDelegate.swift

223 lines
8.9 KiB
Swift

//
// TuskerNavigationDelegate.swift
// Tusker
//
// Created by Shadowfacts on 9/30/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import UIKit
import SafariServices
import Pachyderm
import ComposeUI
import GalleryVC
@MainActor
protocol TuskerNavigationDelegate: UIViewController, ToastableViewController {
nonisolated var apiController: MastodonController! { get }
}
extension TuskerNavigationDelegate {
func show(_ vc: UIViewController) {
if vc is SFSafariViewController {
present(vc, animated: true)
} else {
show(vc, sender: self)
}
}
func selected(account accountID: String) {
// don't open if the account is the same as the current one
if let profileController = self as? ProfileStatusesViewController,
profileController.accountID == accountID {
return
}
show(ProfileViewController(accountID: accountID, mastodonController: apiController), sender: self)
}
func selected(mention: Mention) {
show(ProfileViewController(accountID: mention.id, mastodonController: apiController), sender: self)
}
func selected(tag: Hashtag) {
show(HashtagTimelineViewController(for: tag, mastodonController: apiController), sender: self)
}
func selected(url: URL, allowResolveStatuses: Bool = true, allowUniversalLinks: Bool = true) {
func openSafari() {
#if targetEnvironment(macCatalyst) || os(visionOS)
UIApplication.shared.open(url)
#else
if !ProcessInfo.processInfo.isiOSAppOnMac,
Preferences.shared.useInAppSafari,
url.scheme == "https" || url.scheme == "http" {
let config = SFSafariViewController.Configuration()
config.entersReaderIfAvailable = Preferences.shared.inAppSafariAutomaticReaderMode
let vc = SFSafariViewController(url: url, configuration: config)
vc.preferredControlTintColor = Preferences.shared.accentColor.color
present(vc, animated: true)
} else if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:])
} else {
var message = "The URL could not be opened."
if let scheme = url.scheme {
message += " This can happen if you do not have an app installed for '\(scheme)://' URLs."
}
let alert = UIAlertController(title: "Invalid URL", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
present(alert, animated: true)
}
#endif
}
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()
}
}
} else {
openSafari()
}
}
func selected(status statusID: String) {
self.selected(status: statusID, state: .unknown)
}
func selected(status statusID: String, state: CollapseState) {
show(ConversationViewController(for: statusID, state: state, mastodonController: apiController), sender: self)
}
func compose(editing draft: Draft?, animated: Bool = true, isDucked: Bool = false, completion: (() -> Void)? = nil) {
let draft = draft ?? apiController.createDraft()
let visionIdiom = UIUserInterfaceIdiom(rawValue: 6) // .vision is not available pre-iOS 17 :S
if [visionIdiom, .pad, .mac].contains(UIDevice.current.userInterfaceIdiom) {
let compose = UserActivityManager.editDraftActivity(id: draft.id, accountID: apiController.accountInfo!.id)
let options = UIWindowScene.ActivationRequestOptions()
#if os(visionOS)
options.placement = .prominent()
#else
options.preferredPresentationStyle = .prominent
#endif
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: compose, options: options, errorHandler: nil)
completion?()
} else {
let compose = ComposeHostingController(draft: draft, mastodonController: apiController)
#if os(visionOS)
fatalError("unreachable")
#else
if presentDuckable(compose, animated: animated, isDucked: isDucked, completion: completion) {
return
} else {
present(compose, animated: animated, completion: completion)
}
#endif
}
}
func compose(inReplyToID: String? = nil, mentioningAcct: String? = nil, animated: Bool = true) {
let draft = apiController.createDraft(inReplyToID: inReplyToID, mentioningAcct: mentioningAcct)
compose(editing: draft, animated: animated)
}
private func moreOptions(forURL url: URL) -> UIActivityViewController {
let customActivites: [UIActivity] = [
OpenInSafariActivity()
]
let activityController = UIActivityViewController(activityItems: [url], applicationActivities: customActivites)
activityController.completionWithItemsHandler = OpenInSafariActivity.completionHandler(navigator: self, url: url)
return activityController
}
private func moreOptions(forStatus statusID: String) -> UIActivityViewController {
guard let status = apiController.persistentContainer.status(for: statusID) else { fatalError("Missing cached status \(statusID)") }
guard let url = status.url else {
Logging.general.fault("Status missing URL: id=\(status.id, privacy: .public), reblog=\((status.reblog?.id).debugDescription, privacy: .public)")
fatalError("Cannot create UIActivityViewController for status without URL")
}
return UIActivityViewController(activityItems: [url, StatusActivityItemSource(status)], applicationActivities: nil)
}
private func moreOptions(forAccount accountID: String) -> UIActivityViewController {
guard let account = apiController.persistentContainer.account(for: accountID) else { fatalError("Missing cached account \(accountID)") }
return UIActivityViewController(activityItems: [account.url, AccountActivityItemSource(account)], applicationActivities: nil)
}
func showMoreOptions(forStatus statusID: String, source: PopoverSource) {
let vc = moreOptions(forStatus: statusID)
source.apply(to: vc)
present(vc, animated: true)
}
func showMoreOptions(forURL url: URL, source: PopoverSource) {
let vc = moreOptions(forURL: url)
source.apply(to: vc)
present(vc, animated: true)
}
func showMoreOptions(forAccount accountID: String, source: PopoverSource) {
let vc = moreOptions(forAccount: accountID)
source.apply(to: vc)
present(vc, animated: true)
}
}
enum PopoverSource {
case none
case view(WeakHolder<UIView>)
case barButtonItem(WeakHolder<UIBarButtonItem>)
@MainActor
func apply(to viewController: UIViewController) {
if let popoverPresentationController = viewController.popoverPresentationController {
switch self {
case .none:
break
case .view(let view):
popoverPresentationController.sourceView = view.object
case .barButtonItem(let item):
popoverPresentationController.barButtonItem = item.object
}
}
}
static func view(_ view: UIView?) -> Self {
.view(WeakHolder(view))
}
static func barButtonItem(_ item: UIBarButtonItem?) -> Self {
.barButtonItem(WeakHolder(item))
}
}
private let statusPathRegex = try! NSRegularExpression(
pattern:
"(^/@[a-z0-9_]+/\\d{18})" // mastodon
+ "|(^/@.+@.+/\\d{18})" // mastodon remote
+ "|(^/notice/[a-z0-9]{18})" // pleroma
+ "|(^/notes/[a-z0-9]{10})" // misskey
+ "|(^/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
}