// // UserActivityManager.swift // Tusker // // Created by Shadowfacts on 10/19/18. // Copyright © 2018 Shadowfacts. All rights reserved. // import UIKit import Intents import Pachyderm import OSLog private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "UserActivityManager") class UserActivityManager { private let scene: UIWindowScene init(scene: UIWindowScene) { self.scene = scene } // MARK: - Utils private static let encoder = PropertyListEncoder() private static let decoder = PropertyListDecoder() private var mastodonController: MastodonController { scene.session.mastodonController! } private func getMainViewController() -> TuskerRootViewController { let window = scene.windows.first { $0.isKeyWindow } ?? scene.windows.first! return window.rootViewController as! TuskerRootViewController } private func present(_ vc: UIViewController, animated: Bool = true) { getMainViewController().present(vc, animated: animated) } static func getAccount(from activity: NSUserActivity) -> LocalData.UserAccountInfo? { guard let id = activity.userInfo?["accountID"] as? String else { return nil } return LocalData.shared.getAccount(id: id) } // MARK: - Main Scene static func mainSceneActivity(accountID: String?) -> NSUserActivity { let activity = NSUserActivity(type: .mainScene) 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) activity.isEligibleForPrediction = true activity.userInfo = [ "accountID": accountID, ] 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) { // TODO: check not currently showing compose screen let mentioning = activity.userInfo?["mentioning"] as? String let draft = mastodonController.createDraft(mentioningAcct: mentioning) // todo: this shouldn't use self.mastodonController, it should get the right one based on the userInfo accountID let composeVC = ComposeHostingController(draft: draft, mastodonController: mastodonController) present(UINavigationController(rootViewController: composeVC)) } static func editDraftActivity(id: UUID, accountID: String) -> NSUserActivity { let activity = NSUserActivity(type: .newPost) activity.userInfo = [ "accountID": accountID, "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 addEditedDraft(to activity: NSUserActivity?, draft: Draft) -> NSUserActivity { if let activity { activity.addUserInfoEntries(from: [ "editedDraftID": draft.id.uuidString ]) return activity } else { return editDraftActivity(id: draft.id, accountID: draft.accountID) } } static func getDraft(from activity: NSUserActivity) -> Draft? { let idStr: String? if activity.activityType == UserActivityType.newPost.rawValue { idStr = activity.userInfo?["draftID"] as? String } else { idStr = activity.userInfo?["duckedDraftID"] as? String ?? activity.userInfo?["editedDraftID"] as? String } guard let idStr, let uuid = UUID(uuidString: idStr) else { return nil } return DraftsManager.shared.getBy(id: uuid) } // MARK: - Check Notifications static func checkNotificationsActivity(mode: NotificationsMode = .allNotifications) -> NSUserActivity { let activity = NSUserActivity(type: .checkNotifications) activity.isEligibleForPrediction = 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) { let mainViewController = getMainViewController() mainViewController.select(tab: .notifications) if let navigationController = mainViewController.getTabController(tab: .notifications) as? UINavigationController, let notificationsPageController = navigationController.viewControllers.first as? NotificationsPageViewController { navigationController.popToRootViewController(animated: false) 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) activity.isEligibleForPrediction = true activity.userInfo = [ "timelineData": timelineData, "accountID": accountID, ] 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 getTimeline(from activity: NSUserActivity) -> Timeline? { guard activity.activityType == UserActivityType.showTimeline.rawValue, let data = activity.userInfo?["timelineData"] as? Data else { return nil } return try? UserActivityManager.decoder.decode(Timeline.self, from: data) } func handleShowTimeline(activity: NSUserActivity) { guard let timeline = Self.getTimeline(from: activity) else { return } let mainViewController = getMainViewController() mainViewController.select(tab: .timelines) guard let navigationController = mainViewController.getTabController(tab: .timelines) as? UINavigationController else { return } if let pinned = PinnedTimeline(timeline: timeline), mastodonController.accountPreferences.pinnedTimelines.contains(pinned) { navigationController.popToRootViewController(animated: false) let rootController = navigationController.viewControllers.first! as! TimelinesPageViewController rootController.selectTimeline(pinned, animated: false) } else { let timeline = TimelineViewController(for: timeline, mastodonController: mastodonController) navigationController.pushViewController(timeline, animated: false) } } // MARK: - Show Conversation static func showConversationActivity(mainStatusID: String, accountID: String, isEligibleForPrediction: Bool = false) -> NSUserActivity { let activity = NSUserActivity(type: .showConversation) activity.userInfo = [ "mainStatusID": mainStatusID, "accountID": accountID, ] activity.isEligibleForPrediction = isEligibleForPrediction return activity } static func getConversationStatus(from activity: NSUserActivity) -> String? { return activity.userInfo?["mainStatusID"] as? String } // MARK: - Explore static func searchActivity() -> NSUserActivity { let activity = NSUserActivity(type: .search) activity.isEligibleForPrediction = true activity.title = NSLocalizedString("Search", comment: "search shortcut title") activity.suggestedInvocationPhrase = NSLocalizedString("Search the fediverse", comment: "search shortcut invocation phrase") return activity } func handleSearch(activity: NSUserActivity) { let mainViewController = getMainViewController() mainViewController.select(tab: .explore) if let navigationController = mainViewController.getTabController(tab: .explore) as? UINavigationController, let exploreController = navigationController.viewControllers.first as? ExploreViewController { navigationController.popToRootViewController(animated: false) exploreController.loadViewIfNeeded() exploreController.searchController.isActive = true exploreController.searchController.searchBar.becomeFirstResponder() } } static func bookmarksActivity() -> NSUserActivity { let activity = NSUserActivity(type: .bookmarks) 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) { let mainViewController = getMainViewController() mainViewController.select(tab: .explore) if let navigationController = mainViewController.getTabController(tab: .explore) as? UINavigationController { navigationController.popToRootViewController(animated: false) navigationController.pushViewController(BookmarksTableViewController(mastodonController: mastodonController), animated: false) } } // MARK: - My Profile static func myProfileActivity() -> NSUserActivity { let activity = NSUserActivity(type: .myProfile) activity.isEligibleForPrediction = 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) { let mainViewController = getMainViewController() mainViewController.select(tab: .myProfile) } // MARK: - Show Profile static func showProfileActivity(id profileID: String, accountID: String) -> NSUserActivity { let activity = NSUserActivity(type: .showProfile) activity.userInfo = [ "profileID": profileID, "accountID": accountID, ] // todo: should this be eligible for prediction? return activity } static func getProfile(from activity: NSUserActivity) -> String? { return activity.userInfo?["profileID"] as? String } }