2018-09-30 23:29:52 +00:00
|
|
|
//
|
|
|
|
// TuskerNavigationDelegate.swift
|
|
|
|
// Tusker
|
|
|
|
//
|
|
|
|
// Created by Shadowfacts on 9/30/18.
|
|
|
|
// Copyright © 2018 Shadowfacts. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
import UIKit
|
|
|
|
import SafariServices
|
|
|
|
import Pachyderm
|
2023-04-16 17:23:13 +00:00
|
|
|
import ComposeUI
|
2024-03-19 18:58:51 +00:00
|
|
|
import GalleryVC
|
2018-09-30 23:29:52 +00:00
|
|
|
|
2023-02-19 20:23:25 +00:00
|
|
|
@MainActor
|
2022-10-08 16:15:12 +00:00
|
|
|
protocol TuskerNavigationDelegate: UIViewController, ToastableViewController {
|
2023-05-10 14:34:48 +00:00
|
|
|
nonisolated var apiController: MastodonController! { get }
|
2018-09-30 23:29:52 +00:00
|
|
|
}
|
|
|
|
|
2020-08-15 21:47:33 +00:00
|
|
|
extension TuskerNavigationDelegate {
|
2018-09-30 23:29:52 +00:00
|
|
|
|
2019-09-07 21:10:58 +00:00
|
|
|
func show(_ vc: UIViewController) {
|
2024-03-19 19:11:47 +00:00
|
|
|
if vc is SFSafariViewController {
|
2020-01-18 21:00:38 +00:00
|
|
|
present(vc, animated: true)
|
|
|
|
} else {
|
|
|
|
show(vc, sender: self)
|
|
|
|
}
|
2019-09-07 21:10:58 +00:00
|
|
|
}
|
|
|
|
|
2018-09-30 23:29:52 +00:00
|
|
|
func selected(account accountID: String) {
|
|
|
|
// don't open if the account is the same as the current one
|
2022-10-29 01:38:56 +00:00
|
|
|
if let profileController = self as? ProfileStatusesViewController,
|
2018-09-30 23:29:52 +00:00
|
|
|
profileController.accountID == accountID {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-10-29 01:38:56 +00:00
|
|
|
show(ProfileViewController(accountID: accountID, mastodonController: apiController), sender: self)
|
2018-10-12 01:20:58 +00:00
|
|
|
}
|
|
|
|
|
2018-09-30 23:29:52 +00:00
|
|
|
func selected(mention: Mention) {
|
2020-07-05 20:17:56 +00:00
|
|
|
show(ProfileViewController(accountID: mention.id, mastodonController: apiController), sender: self)
|
2018-10-12 01:20:58 +00:00
|
|
|
}
|
|
|
|
|
2018-09-30 23:29:52 +00:00
|
|
|
func selected(tag: Hashtag) {
|
2020-01-05 20:25:07 +00:00
|
|
|
show(HashtagTimelineViewController(for: tag, mastodonController: apiController), sender: self)
|
2018-10-12 01:20:58 +00:00
|
|
|
}
|
|
|
|
|
2023-01-21 18:56:39 +00:00
|
|
|
func selected(url: URL, allowResolveStatuses: Bool = true, allowUniversalLinks: Bool = true) {
|
2019-11-15 00:53:27 +00:00
|
|
|
func openSafari() {
|
2024-04-15 13:34:44 +00:00
|
|
|
#if targetEnvironment(macCatalyst) || os(visionOS)
|
2024-02-03 17:03:41 +00:00
|
|
|
UIApplication.shared.open(url)
|
|
|
|
#else
|
2024-04-15 13:34:44 +00:00
|
|
|
if !ProcessInfo.processInfo.isiOSAppOnMac,
|
|
|
|
Preferences.shared.useInAppSafari,
|
2021-04-25 16:58:51 +00:00
|
|
|
url.scheme == "https" || url.scheme == "http" {
|
2019-11-15 00:53:27 +00:00
|
|
|
let config = SFSafariViewController.Configuration()
|
|
|
|
config.entersReaderIfAvailable = Preferences.shared.inAppSafariAutomaticReaderMode
|
2023-01-16 16:24:42 +00:00
|
|
|
let vc = SFSafariViewController(url: url, configuration: config)
|
|
|
|
vc.preferredControlTintColor = Preferences.shared.accentColor.color
|
|
|
|
present(vc, animated: true)
|
2021-04-25 16:58:51 +00:00
|
|
|
} else if UIApplication.shared.canOpenURL(url) {
|
2019-11-15 00:53:27 +00:00
|
|
|
UIApplication.shared.open(url, options: [:])
|
2021-04-25 16:58:51 +00:00
|
|
|
} 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)
|
2022-12-02 23:06:15 +00:00
|
|
|
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
|
2021-04-25 16:58:51 +00:00
|
|
|
present(alert, animated: true)
|
2019-11-15 00:53:27 +00:00
|
|
|
}
|
2024-02-03 17:03:41 +00:00
|
|
|
#endif
|
2019-11-15 00:53:27 +00:00
|
|
|
}
|
|
|
|
|
2023-01-21 18:56:39 +00:00
|
|
|
if allowResolveStatuses,
|
|
|
|
isLikelyResolvableAsStatus(url) {
|
|
|
|
show(ConversationViewController(resolving: url, mastodonController: apiController))
|
|
|
|
} else if allowUniversalLinks,
|
|
|
|
Preferences.shared.openLinksInApps,
|
|
|
|
url.scheme == "https" || url.scheme == "http" {
|
2019-02-22 15:20:22 +00:00
|
|
|
UIApplication.shared.open(url, options: [.universalLinksOnly: true]) { (success) in
|
|
|
|
if (!success) {
|
2019-11-15 00:53:27 +00:00
|
|
|
openSafari()
|
2019-02-22 15:20:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
2019-11-15 00:53:27 +00:00
|
|
|
openSafari()
|
2019-02-22 15:20:22 +00:00
|
|
|
}
|
2018-10-12 01:20:58 +00:00
|
|
|
}
|
|
|
|
|
2018-09-30 23:29:52 +00:00
|
|
|
func selected(status statusID: String) {
|
2019-11-28 23:36:58 +00:00
|
|
|
self.selected(status: statusID, state: .unknown)
|
|
|
|
}
|
|
|
|
|
2022-12-03 23:21:49 +00:00
|
|
|
func selected(status statusID: String, state: CollapseState) {
|
2023-02-06 23:10:38 +00:00
|
|
|
show(ConversationViewController(for: statusID, state: state, mastodonController: apiController), sender: self)
|
2018-09-30 23:29:52 +00:00
|
|
|
}
|
|
|
|
|
2024-08-19 17:29:48 +00:00
|
|
|
func compose(editing draft: Draft?, animated: Bool = true, isDucked: Bool = false, completion: (() -> Void)? = nil) {
|
2023-02-25 18:55:46 +00:00
|
|
|
let draft = draft ?? apiController.createDraft()
|
2023-10-20 02:50:20 +00:00
|
|
|
let visionIdiom = UIUserInterfaceIdiom(rawValue: 6) // .vision is not available pre-iOS 17 :S
|
|
|
|
if [visionIdiom, .pad, .mac].contains(UIDevice.current.userInterfaceIdiom) {
|
2021-06-11 14:50:31 +00:00
|
|
|
let compose = UserActivityManager.editDraftActivity(id: draft.id, accountID: apiController.accountInfo!.id)
|
|
|
|
let options = UIWindowScene.ActivationRequestOptions()
|
2023-10-20 02:50:20 +00:00
|
|
|
#if os(visionOS)
|
|
|
|
options.placement = .prominent()
|
|
|
|
#else
|
2021-06-11 14:50:31 +00:00
|
|
|
options.preferredPresentationStyle = .prominent
|
2023-10-20 02:50:20 +00:00
|
|
|
#endif
|
2021-06-11 14:50:31 +00:00
|
|
|
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: compose, options: options, errorHandler: nil)
|
2024-08-19 17:29:48 +00:00
|
|
|
completion?()
|
2021-06-11 14:50:31 +00:00
|
|
|
} else {
|
2023-04-16 17:47:48 +00:00
|
|
|
let compose = ComposeHostingController(draft: draft, mastodonController: apiController)
|
2023-10-20 02:50:20 +00:00
|
|
|
#if os(visionOS)
|
|
|
|
fatalError("unreachable")
|
|
|
|
#else
|
2024-09-12 14:30:58 +00:00
|
|
|
if presentDuckable(compose, animated: animated, isDucked: isDucked, completion: completion) {
|
2022-11-09 03:14:40 +00:00
|
|
|
return
|
|
|
|
} else {
|
2024-08-19 17:29:48 +00:00
|
|
|
present(compose, animated: animated, completion: completion)
|
2022-11-09 03:14:40 +00:00
|
|
|
}
|
2023-10-20 02:50:20 +00:00
|
|
|
#endif
|
2021-06-11 14:50:31 +00:00
|
|
|
}
|
2018-10-21 02:07:04 +00:00
|
|
|
}
|
2019-01-19 19:31:31 +00:00
|
|
|
|
2022-11-28 20:21:05 +00:00
|
|
|
func compose(inReplyToID: String? = nil, mentioningAcct: String? = nil, animated: Bool = true) {
|
2020-09-01 01:39:36 +00:00
|
|
|
let draft = apiController.createDraft(inReplyToID: inReplyToID, mentioningAcct: mentioningAcct)
|
2022-11-28 20:21:05 +00:00
|
|
|
compose(editing: draft, animated: animated)
|
2020-09-01 01:39:36 +00:00
|
|
|
}
|
|
|
|
|
2020-01-18 02:29:53 +00:00
|
|
|
private func moreOptions(forURL url: URL) -> UIActivityViewController {
|
2019-09-05 17:40:10 +00:00
|
|
|
let customActivites: [UIActivity] = [
|
|
|
|
OpenInSafariActivity()
|
|
|
|
]
|
|
|
|
let activityController = UIActivityViewController(activityItems: [url], applicationActivities: customActivites)
|
2020-09-13 19:51:06 +00:00
|
|
|
activityController.completionWithItemsHandler = OpenInSafariActivity.completionHandler(navigator: self, url: url)
|
2019-09-05 17:40:10 +00:00
|
|
|
return activityController
|
|
|
|
}
|
|
|
|
|
2020-01-18 02:29:53 +00:00
|
|
|
private func moreOptions(forStatus statusID: String) -> UIActivityViewController {
|
2020-05-02 23:52:35 +00:00
|
|
|
guard let status = apiController.persistentContainer.status(for: statusID) else { fatalError("Missing cached status \(statusID)") }
|
2022-10-10 18:21:12 +00:00
|
|
|
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")
|
|
|
|
}
|
2020-05-15 02:00:58 +00:00
|
|
|
|
2021-02-06 18:47:45 +00:00
|
|
|
return UIActivityViewController(activityItems: [url, StatusActivityItemSource(status)], applicationActivities: nil)
|
2018-11-09 20:48:08 +00:00
|
|
|
}
|
|
|
|
|
2020-01-18 02:29:53 +00:00
|
|
|
private func moreOptions(forAccount accountID: String) -> UIActivityViewController {
|
2020-05-02 23:52:35 +00:00
|
|
|
guard let account = apiController.persistentContainer.account(for: accountID) else { fatalError("Missing cached account \(accountID)") }
|
2020-05-15 02:30:45 +00:00
|
|
|
|
2021-02-06 18:47:45 +00:00
|
|
|
return UIActivityViewController(activityItems: [account.url, AccountActivityItemSource(account)], applicationActivities: nil)
|
2019-12-14 18:36:05 +00:00
|
|
|
}
|
|
|
|
|
2022-11-30 03:41:36 +00:00
|
|
|
func showMoreOptions(forStatus statusID: String, source: PopoverSource) {
|
2020-01-18 02:29:53 +00:00
|
|
|
let vc = moreOptions(forStatus: statusID)
|
2022-11-30 03:41:36 +00:00
|
|
|
source.apply(to: vc)
|
2020-01-18 02:29:53 +00:00
|
|
|
present(vc, animated: true)
|
2018-09-30 23:29:52 +00:00
|
|
|
}
|
|
|
|
|
2022-11-30 03:41:36 +00:00
|
|
|
func showMoreOptions(forURL url: URL, source: PopoverSource) {
|
2020-01-18 02:29:53 +00:00
|
|
|
let vc = moreOptions(forURL: url)
|
2022-11-30 03:41:36 +00:00
|
|
|
source.apply(to: vc)
|
2020-01-18 02:29:53 +00:00
|
|
|
present(vc, animated: true)
|
2018-10-12 02:04:32 +00:00
|
|
|
}
|
|
|
|
|
2022-11-30 03:41:36 +00:00
|
|
|
func showMoreOptions(forAccount accountID: String, source: PopoverSource) {
|
2020-01-18 02:29:53 +00:00
|
|
|
let vc = moreOptions(forAccount: accountID)
|
2022-11-30 03:41:36 +00:00
|
|
|
source.apply(to: vc)
|
2020-01-18 02:29:53 +00:00
|
|
|
present(vc, animated: true)
|
2019-12-14 18:36:05 +00:00
|
|
|
}
|
|
|
|
|
2018-09-30 23:29:52 +00:00
|
|
|
}
|
2022-11-30 03:41:36 +00:00
|
|
|
|
|
|
|
enum PopoverSource {
|
|
|
|
case none
|
|
|
|
case view(WeakHolder<UIView>)
|
|
|
|
case barButtonItem(WeakHolder<UIBarButtonItem>)
|
|
|
|
|
2024-01-26 16:02:40 +00:00
|
|
|
@MainActor
|
2022-11-30 03:41:36 +00:00
|
|
|
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))
|
|
|
|
}
|
|
|
|
}
|
2023-01-21 18:56:39 +00:00
|
|
|
|
|
|
|
private let statusPathRegex = try! NSRegularExpression(
|
|
|
|
pattern:
|
2023-01-23 21:59:24 +00:00
|
|
|
"(^/@[a-z0-9_]+/\\d{18})" // mastodon
|
2023-05-16 02:01:44 +00:00
|
|
|
+ "|(^/@.+@.+/\\d{18})" // mastodon remote
|
2023-01-23 21:59:24 +00:00
|
|
|
+ "|(^/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
|
2023-01-21 18:56:39 +00:00
|
|
|
,
|
|
|
|
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
|
|
|
|
}
|