forked from shadowfacts/Tusker
373 lines
16 KiB
Swift
373 lines
16 KiB
Swift
//
|
|
// UserActivityManager.swift
|
|
// Tusker
|
|
//
|
|
// Created by Shadowfacts on 10/19/18.
|
|
// Copyright © 2018 Shadowfacts. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
import Intents
|
|
import Pachyderm
|
|
import OSLog
|
|
import UserAccounts
|
|
import ComposeUI
|
|
import TuskerPreferences
|
|
|
|
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "UserActivityManager")
|
|
|
|
@MainActor
|
|
class UserActivityManager {
|
|
|
|
private let scene: UIWindowScene
|
|
private let context: any UserActivityHandlingContext
|
|
|
|
init(scene: UIWindowScene, context: any UserActivityHandlingContext) {
|
|
self.scene = scene
|
|
self.context = context
|
|
}
|
|
|
|
// MARK: - Utils
|
|
private static let encoder = PropertyListEncoder()
|
|
private static let decoder = PropertyListDecoder()
|
|
|
|
private var mastodonController: MastodonController {
|
|
scene.session.mastodonController!
|
|
}
|
|
|
|
static func getAccount(from activity: NSUserActivity) -> UserAccountInfo? {
|
|
guard let id = activity.userInfo?["accountID"] as? String else {
|
|
return nil
|
|
}
|
|
return UserAccountsManager.shared.getAccount(id: id)
|
|
}
|
|
|
|
// MARK: - Main Scene
|
|
static func mainSceneActivity(accountID: String?) -> NSUserActivity {
|
|
let activity = NSUserActivity(activityType: UserActivityType.mainScene.rawValue)
|
|
if let accountID {
|
|
activity.userInfo = [
|
|
"accountID": accountID,
|
|
]
|
|
}
|
|
return activity
|
|
}
|
|
|
|
// MARK: - New Post
|
|
static func newPostActivity(mentioning: Account? = nil, accountID: String) -> NSUserActivity {
|
|
// todo: update to use managed objects
|
|
let activity = NSUserActivity(type: .newPost, accountID: accountID)
|
|
activity.isEligibleForPrediction = true
|
|
if let mentioning = mentioning {
|
|
activity.userInfo!["mentioning"] = mentioning.acct
|
|
activity.title = "Send a message to \(mentioning.displayName)"
|
|
activity.suggestedInvocationPhrase = "Send a message to \(mentioning.displayName)"
|
|
} else {
|
|
activity.title = "New Post"
|
|
activity.suggestedInvocationPhrase = "Post in Tusker"
|
|
}
|
|
return activity
|
|
}
|
|
|
|
func handleNewPost(activity: NSUserActivity) {
|
|
if let draft = Self.getDraft(from: activity) {
|
|
context.compose(editing: draft)
|
|
} else {
|
|
let mentioning = activity.userInfo?["mentioning"] as? String
|
|
let draft = mastodonController.createDraft(mentioningAcct: mentioning)
|
|
context.compose(editing: draft)
|
|
}
|
|
}
|
|
|
|
static func editDraftActivity(id: UUID, accountID: String) -> NSUserActivity {
|
|
let activity = NSUserActivity(type: .newPost, accountID: accountID)
|
|
activity.addUserInfoEntries(from: [
|
|
"draftID": id.uuidString,
|
|
])
|
|
return activity
|
|
}
|
|
|
|
static func addDuckedDraft(to activity: NSUserActivity?, draft: Draft) -> NSUserActivity {
|
|
if let activity {
|
|
activity.addUserInfoEntries(from: [
|
|
"duckedDraftID": draft.id.uuidString
|
|
])
|
|
return activity
|
|
} else {
|
|
return editDraftActivity(id: draft.id, accountID: draft.accountID)
|
|
}
|
|
}
|
|
|
|
static func getDraft(from activity: NSUserActivity) -> Draft? {
|
|
guard let idStr = activity.userInfo?["draftID"] as? String,
|
|
let uuid = UUID(uuidString: idStr) else {
|
|
return nil
|
|
}
|
|
return DraftsPersistentContainer.shared.getDraft(id: uuid)
|
|
}
|
|
|
|
static func getDuckedDraft(from activity: NSUserActivity) -> Draft? {
|
|
guard let idStr = activity.userInfo?["duckedDraftID"] as? String,
|
|
let uuid = UUID(uuidString: idStr) else {
|
|
return nil
|
|
}
|
|
return DraftsPersistentContainer.shared.getDraft(id: uuid)
|
|
}
|
|
|
|
// MARK: - Check Notifications
|
|
static func checkNotificationsActivity(mode: NotificationsMode = .allNotifications, accountID: String) -> NSUserActivity {
|
|
let activity = NSUserActivity(type: .checkNotifications, accountID: accountID)
|
|
activity.isEligibleForPrediction = true
|
|
activity.isEligibleForHandoff = true
|
|
activity.addUserInfoEntries(from: [
|
|
"notificationsMode": mode.rawValue
|
|
])
|
|
switch mode {
|
|
case .allNotifications:
|
|
activity.title = NSLocalizedString("Check Notifications", comment: "check notifications shortcut title")
|
|
activity.suggestedInvocationPhrase = NSLocalizedString("Check my Tusker notifications", comment: "check notifications shortcut invocation phrase")
|
|
case .mentionsOnly:
|
|
activity.title = NSLocalizedString("Check Mentions", comment: "check mentions shortcut title")
|
|
activity.suggestedInvocationPhrase = NSLocalizedString("Check my mentions", comment: "check mentions shortcut invocation phrase")
|
|
}
|
|
return activity
|
|
}
|
|
|
|
func handleCheckNotifications(activity: NSUserActivity) {
|
|
context.select(route: .notifications)
|
|
context.popToRoot()
|
|
if let notificationsPageController = context.topViewController as? NotificationsPageViewController {
|
|
notificationsPageController.loadViewIfNeeded()
|
|
notificationsPageController.selectMode(Self.getNotificationsMode(from: activity) ?? Preferences.shared.defaultNotificationsMode)
|
|
}
|
|
}
|
|
|
|
static func getNotificationsMode(from activity: NSUserActivity) -> NotificationsMode? {
|
|
guard let str = activity.userInfo?["notificationsMode"] as? String else {
|
|
return nil
|
|
}
|
|
return NotificationsMode(rawValue: str)
|
|
}
|
|
|
|
// MARK: - Show Timeline
|
|
static func showTimelineActivity(timeline: Timeline, accountID: String) -> NSUserActivity? {
|
|
guard let timelineData = try? encoder.encode(timeline) else { return nil }
|
|
|
|
let activity = NSUserActivity(type: .showTimeline, accountID: accountID)
|
|
activity.isEligibleForPrediction = true
|
|
activity.isEligibleForHandoff = true
|
|
activity.addUserInfoEntries(from: [
|
|
"timelineData": timelineData,
|
|
])
|
|
switch timeline {
|
|
case .home:
|
|
activity.title = NSLocalizedString("Show Home Timeline", comment: "home timeline shortcut title")
|
|
activity.suggestedInvocationPhrase = NSLocalizedString("Show my home timeline", comment: "home timeline shortcut invocation phrase")
|
|
case .public(local: true):
|
|
activity.title = NSLocalizedString("Show Local Timeline", comment: "local timeline shortcut title")
|
|
activity.suggestedInvocationPhrase = NSLocalizedString("Show my local timeline", comment: "local timeline shortcut invocation phrase")
|
|
case .public(local: false):
|
|
activity.title = NSLocalizedString("Show Federated Timeline", comment: "federated timeline shortcut title")
|
|
activity.suggestedInvocationPhrase = NSLocalizedString("Show my federated timeline", comment: "federated timeline invocation phrase")
|
|
case let .tag(hashtag):
|
|
activity.title = String(format: NSLocalizedString("Show #%@", comment: "show hashtag shortcut title"), hashtag)
|
|
activity.suggestedInvocationPhrase = String(format: NSLocalizedString("Show the %@ hashtag", comment: "hashtag shortcut invocation phrase"), hashtag)
|
|
case .list:
|
|
// todo: add title to list
|
|
activity.title = NSLocalizedString("Show List", comment: "list timeline shortcut title")
|
|
activity.suggestedInvocationPhrase = NSLocalizedString("Show my list", comment: "list timeline invocation phrase")
|
|
case .direct:
|
|
activity.title = NSLocalizedString("Show Direct Messages", comment: "direct message timeline shortcut title")
|
|
activity.suggestedInvocationPhrase = NSLocalizedString("Show my direct messages", comment: "direct message timeline invocation phrase")
|
|
}
|
|
return activity
|
|
}
|
|
|
|
static func addTimelinePositionInfo(to activity: NSUserActivity, statusIDs: [String], centerStatusID: String) {
|
|
activity.addUserInfoEntries(from: [
|
|
"statusIDs": statusIDs,
|
|
"centerStatusID": centerStatusID
|
|
])
|
|
}
|
|
|
|
static func getTimeline(from activity: NSUserActivity) -> (Timeline, (statusIDs: [String], centerStatusID: String)?)? {
|
|
guard activity.activityType == UserActivityType.showTimeline.rawValue,
|
|
let data = activity.userInfo?["timelineData"] as? Data,
|
|
let timeline = try? UserActivityManager.decoder.decode(Timeline.self, from: data) else {
|
|
return nil
|
|
}
|
|
var positionInfo: ([String], String)?
|
|
if let ids = activity.userInfo?["statusIDs"] as? [String],
|
|
let center = activity.userInfo?["centerStatusID"] as? String {
|
|
positionInfo = (ids, center)
|
|
}
|
|
return (timeline, positionInfo)
|
|
}
|
|
|
|
func handleShowTimeline(activity: NSUserActivity) {
|
|
guard let (timeline, positionInfo) = Self.getTimeline(from: activity) else { return }
|
|
|
|
var timelineVC: TimelineViewController?
|
|
if let pinned = PinnedTimeline(timeline: timeline),
|
|
mastodonController.accountPreferences.pinnedTimelines.contains(pinned) {
|
|
context.select(route: .timelines)
|
|
context.popToRoot()
|
|
let pageController = context.topViewController as! TimelinesPageViewController
|
|
pageController.selectTimeline(pinned, animated: false)
|
|
timelineVC = pageController.currentViewController as? TimelineViewController
|
|
} else if case .list(let id) = timeline {
|
|
context.select(route: .list(id: id))
|
|
timelineVC = context.topViewController as? TimelineViewController
|
|
} else {
|
|
context.select(route: .explore)
|
|
context.popToRoot()
|
|
timelineVC = TimelineViewController(for: timeline, mastodonController: mastodonController)
|
|
context.push(timelineVC!)
|
|
}
|
|
|
|
if let timelineVC,
|
|
let positionInfo,
|
|
context.isHandoff {
|
|
Task {
|
|
await timelineVC.restoreStateFromHandoff(statusIDs: positionInfo.statusIDs, centerStatusID: positionInfo.centerStatusID)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Show Conversation
|
|
static func showConversationActivity(mainStatusID: String, accountID: String, isEligibleForPrediction: Bool = false) -> NSUserActivity {
|
|
let activity = NSUserActivity(type: .showConversation, accountID: accountID)
|
|
activity.addUserInfoEntries(from: [
|
|
"mainStatusID": mainStatusID,
|
|
])
|
|
activity.isEligibleForPrediction = isEligibleForPrediction
|
|
activity.isEligibleForHandoff = true
|
|
return activity
|
|
}
|
|
|
|
static func getConversationStatus(from activity: NSUserActivity) -> String? {
|
|
return activity.userInfo?["mainStatusID"] as? String
|
|
}
|
|
|
|
func handleShowConversation(activity: NSUserActivity) {
|
|
guard let mainStatusID = Self.getConversationStatus(from: activity) else {
|
|
return
|
|
}
|
|
context.select(route: .timelines)
|
|
context.push(ConversationViewController(for: mainStatusID, state: .unknown, mastodonController: mastodonController))
|
|
}
|
|
|
|
// MARK: - Explore
|
|
|
|
static func searchActivity(query: String?, accountID: String) -> NSUserActivity {
|
|
let activity = NSUserActivity(type: .search, accountID: accountID)
|
|
if let query {
|
|
activity.userInfo!["query"] = query
|
|
}
|
|
activity.isEligibleForPrediction = true
|
|
activity.title = NSLocalizedString("Search", comment: "search shortcut title")
|
|
activity.suggestedInvocationPhrase = NSLocalizedString("Search the fediverse", comment: "search shortcut invocation phrase")
|
|
return activity
|
|
}
|
|
|
|
static func getSearchQuery(from activity: NSUserActivity) -> String? {
|
|
return activity.userInfo?["query"] as? String
|
|
}
|
|
|
|
func handleSearch(activity: NSUserActivity) {
|
|
context.select(route: .explore)
|
|
context.popToRoot()
|
|
|
|
let searchController: UISearchController
|
|
let resultsController: SearchResultsViewController
|
|
if let explore = context.topViewController as? ExploreViewController {
|
|
explore.loadViewIfNeeded()
|
|
explore.searchControllerStatusOnAppearance = true
|
|
searchController = explore.searchController
|
|
resultsController = explore.resultsController
|
|
} else if let inlineTrends = context.topViewController as? InlineTrendsViewController {
|
|
inlineTrends.loadViewIfNeeded()
|
|
inlineTrends.searchControllerStatusOnAppearance = true
|
|
searchController = inlineTrends.searchController
|
|
resultsController = inlineTrends.resultsController
|
|
} else {
|
|
return
|
|
}
|
|
|
|
if let query = Self.getSearchQuery(from: activity),
|
|
!query.isEmpty {
|
|
searchController.searchBar.text = query
|
|
resultsController.performSearch(query: query)
|
|
} else {
|
|
searchController.searchBar.becomeFirstResponder()
|
|
}
|
|
}
|
|
|
|
static func bookmarksActivity(accountID: String) -> NSUserActivity {
|
|
let activity = NSUserActivity(type: .bookmarks, accountID: accountID)
|
|
activity.isEligibleForPrediction = true
|
|
activity.title = NSLocalizedString("View Bookmarks", comment: "bookmarks shortcut title")
|
|
activity.suggestedInvocationPhrase = NSLocalizedString("Show my bookmarks in Tusker", comment: "bookmarks shortcut invocation phrase")
|
|
return activity
|
|
}
|
|
|
|
func handleBookmarks(activity: NSUserActivity) {
|
|
context.select(route: .bookmarks)
|
|
}
|
|
|
|
// MARK: - My Profile
|
|
static func myProfileActivity(accountID: String) -> NSUserActivity {
|
|
let activity = NSUserActivity(type: .myProfile, accountID: accountID)
|
|
activity.isEligibleForPrediction = true
|
|
activity.isEligibleForHandoff = true
|
|
activity.title = NSLocalizedString("My Profile", comment: "my profile shortcut title")
|
|
activity.suggestedInvocationPhrase = NSLocalizedString("Show my Mastodon profile", comment: "my profile shortuct invocation phrase")
|
|
return activity
|
|
}
|
|
|
|
func handleMyProfile(activity: NSUserActivity) {
|
|
context.select(route: .myProfile)
|
|
}
|
|
|
|
// MARK: - Show Profile
|
|
static func showProfileActivity(id profileID: String, accountID: String) -> NSUserActivity {
|
|
let activity = NSUserActivity(type: .showProfile, accountID: accountID)
|
|
activity.addUserInfoEntries(from: [
|
|
"profileID": profileID,
|
|
])
|
|
activity.isEligibleForPrediction = true
|
|
activity.isEligibleForHandoff = true
|
|
return activity
|
|
}
|
|
|
|
static func getProfile(from activity: NSUserActivity) -> String? {
|
|
return activity.userInfo?["profileID"] as? String
|
|
}
|
|
|
|
func handleShowProfile(activity: NSUserActivity) {
|
|
guard let accountID = Self.getProfile(from: activity) else {
|
|
return
|
|
}
|
|
context.select(route: .timelines)
|
|
context.push(ProfileViewController(accountID: accountID, mastodonController: mastodonController))
|
|
}
|
|
|
|
// MARK: - Show Notification
|
|
static func showNotificationActivity(id notificationID: String, accountID: String) -> NSUserActivity {
|
|
let activity = NSUserActivity(type: .showNotification, accountID: accountID)
|
|
activity.addUserInfoEntries(from: [
|
|
"notificationID": notificationID,
|
|
])
|
|
return activity
|
|
}
|
|
|
|
func handleShowNotification(activity: NSUserActivity) {
|
|
guard let notificationID = activity.userInfo?["notificationID"] as? String else {
|
|
return
|
|
}
|
|
context.select(route: .notifications)
|
|
context.push(NotificationLoadingViewController(notificationID: notificationID, mastodonController: mastodonController))
|
|
}
|
|
|
|
}
|