forked from shadowfacts/Tusker
223 lines
8.9 KiB
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
|
|
}
|