From 9e7e16b3fcd61390995628275951e7f4fbd65afc Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sat, 20 Oct 2018 10:54:59 -0400 Subject: [PATCH] Start adding Siri Shortcuts --- Tusker.xcodeproj/project.pbxproj | 20 ++++++ Tusker/AppDelegate.swift | 4 ++ Tusker/Info.plist | 7 ++- .../Compose/ComposeViewController.swift | 15 +++++ .../NotificationsTableViewController.swift | 2 + .../Shortcuts/NSUserActivity+Extensions.swift | 23 +++++++ Tusker/Shortcuts/UserActivityManager.swift | 63 +++++++++++++++++++ Tusker/Shortcuts/UserActivityType.swift | 25 ++++++++ Tusker/XCallbackURL/XCBActions.swift | 8 +-- 9 files changed, 162 insertions(+), 5 deletions(-) create mode 100644 Tusker/Shortcuts/NSUserActivity+Extensions.swift create mode 100644 Tusker/Shortcuts/UserActivityManager.swift create mode 100644 Tusker/Shortcuts/UserActivityType.swift diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 9fbe68f043..6d76fc701e 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -60,6 +60,9 @@ D621544821682A9D0003D87D /* TabsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D621544721682A9D0003D87D /* TabsTableViewController.swift */; }; D621544B21682AD30003D87D /* TabTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D621544A21682AD30003D87D /* TabTableViewCell.swift */; }; D621544D21682AD90003D87D /* TabTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D621544C21682AD90003D87D /* TabTableViewCell.xib */; }; + D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2421217AA7E1005076CC /* UserActivityManager.swift */; }; + D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.swift */; }; + D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2425217ABF63005076CC /* UserActivityType.swift */; }; D6333B372137838300CE884A /* AttributedString+Trim.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B362137838300CE884A /* AttributedString+Trim.swift */; }; D6333B772138D94E00CE884A /* ComposeMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B762138D94E00CE884A /* ComposeMediaView.swift */; }; D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */; }; @@ -249,6 +252,9 @@ D621544721682A9D0003D87D /* TabsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabsTableViewController.swift; sourceTree = ""; }; D621544A21682AD30003D87D /* TabTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabTableViewCell.swift; sourceTree = ""; }; D621544C21682AD90003D87D /* TabTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TabTableViewCell.xib; sourceTree = ""; }; + D62D2421217AA7E1005076CC /* UserActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityManager.swift; sourceTree = ""; }; + D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSUserActivity+Extensions.swift"; sourceTree = ""; }; + D62D2425217ABF63005076CC /* UserActivityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityType.swift; sourceTree = ""; }; D6333B362137838300CE884A /* AttributedString+Trim.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttributedString+Trim.swift"; sourceTree = ""; }; D6333B762138D94E00CE884A /* ComposeMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeMediaView.swift; sourceTree = ""; }; D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+TimeAgo.swift"; sourceTree = ""; }; @@ -483,6 +489,16 @@ path = Tab; sourceTree = ""; }; + D62D241E217AA46B005076CC /* Shortcuts */ = { + isa = PBXGroup; + children = ( + D62D2425217ABF63005076CC /* UserActivityType.swift */, + D62D2421217AA7E1005076CC /* UserActivityManager.swift */, + D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.swift */, + ); + path = Shortcuts; + sourceTree = ""; + }; D641C780213DD7C4004B4513 /* Screens */ = { isa = PBXGroup; children = ( @@ -747,6 +763,7 @@ D6028B9A2150811100F223B9 /* MastodonCache.swift */, D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */, D6757A7A2157E00100721E32 /* XCallbackURL */, + D62D241E217AA46B005076CC /* Shortcuts */, D663626021360A9600C9CBA2 /* Preferences */, D667E5F62135C2ED0057A976 /* Extensions */, D6F953F121251A2F00CF0F2B /* Controllers */, @@ -1107,9 +1124,12 @@ D6C693F92162E4DB007D6A6D /* StatusContentLabel.swift in Sources */, D667E5F52135BCD50057A976 /* ConversationViewController.swift in Sources */, D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */, + D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */, D66362712136338600C9CBA2 /* ComposeViewController.swift in Sources */, D6028B9B2150811100F223B9 /* MastodonCache.swift in Sources */, D67E051521643C77000E0927 /* Tab.swift in Sources */, + D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */, + D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */, D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */, D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */, D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */, diff --git a/Tusker/AppDelegate.swift b/Tusker/AppDelegate.swift index 48390e0f7c..b87034d340 100644 --- a/Tusker/AppDelegate.swift +++ b/Tusker/AppDelegate.swift @@ -34,6 +34,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return false } + func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { + return userActivity.handleResume() + } + func applicationWillResignActive(_ application: UIApplication) { // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. diff --git a/Tusker/Info.plist b/Tusker/Info.plist index 57e31d577f..d5bd98bcb2 100644 --- a/Tusker/Info.plist +++ b/Tusker/Info.plist @@ -2,6 +2,11 @@ + NSUserActivityTypes + + $(PRODUCT_BUNDLE_IDENTIFIER).activity.check-notifications + $(PRODUCT_BUNDLE_IDENTIFIER).activity.new-post + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable @@ -36,7 +41,7 @@ NSCameraUsageDescription Post photos from the camera. NSPhotoLibraryAddUsageDescription - Save photos directly from other people's posts. + Save photos directly from other people's posts. NSPhotoLibraryUsageDescription Post photos from the photo library. UILaunchStoryboardName diff --git a/Tusker/Screens/Compose/ComposeViewController.swift b/Tusker/Screens/Compose/ComposeViewController.swift index 0ac90a15e2..8ef8d2c162 100644 --- a/Tusker/Screens/Compose/ComposeViewController.swift +++ b/Tusker/Screens/Compose/ComposeViewController.swift @@ -8,6 +8,7 @@ import UIKit import Pachyderm +import Intents class ComposeViewController: UIViewController { @@ -121,6 +122,20 @@ class ComposeViewController: UIViewController { updatePlaceholder() progressView.progress = 0 + + if let mentioningAcct = mentioningAcct { + let req = MastodonController.client.searchForAccount(query: mentioningAcct) + MastodonController.client.run(req) { [weak self] (response) in + if case let .success(accounts, _) = response { + self?.userActivity = UserActivityManager.newPostActivity(mentioning: accounts.first) + } else { + self?.userActivity = UserActivityManager.newPostActivity() + } + } + } else { + self.userActivity = UserActivityManager.newPostActivity() + } + } override func viewDidLayoutSubviews() { diff --git a/Tusker/Screens/Notifications/NotificationsTableViewController.swift b/Tusker/Screens/Notifications/NotificationsTableViewController.swift index 7772c76678..3ebdf5966b 100644 --- a/Tusker/Screens/Notifications/NotificationsTableViewController.swift +++ b/Tusker/Screens/Notifications/NotificationsTableViewController.swift @@ -54,6 +54,8 @@ class NotificationsTableViewController: UITableViewController { } registerForPreviewing(with: self, sourceView: view) + + userActivity = UserActivityManager.checkNotificationsActivity() } override func viewWillAppear(_ animated: Bool) { diff --git a/Tusker/Shortcuts/NSUserActivity+Extensions.swift b/Tusker/Shortcuts/NSUserActivity+Extensions.swift new file mode 100644 index 0000000000..edeb6e52a7 --- /dev/null +++ b/Tusker/Shortcuts/NSUserActivity+Extensions.swift @@ -0,0 +1,23 @@ +// +// NSUserActivity+Extensions.swift +// Tusker +// +// Created by Shadowfacts on 10/19/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation + +extension NSUserActivity { + + convenience init(type: UserActivityType) { + self.init(activityType: type.rawValue) + } + + func handleResume() -> Bool { + guard let type = UserActivityType(rawValue: activityType) else { return false } + type.handle(self) + return true + } + +} diff --git a/Tusker/Shortcuts/UserActivityManager.swift b/Tusker/Shortcuts/UserActivityManager.swift new file mode 100644 index 0000000000..13b263d88e --- /dev/null +++ b/Tusker/Shortcuts/UserActivityManager.swift @@ -0,0 +1,63 @@ +// +// UserActivityManager.swift +// Tusker +// +// Created by Shadowfacts on 10/19/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import UIKit +import Pachyderm + +class UserActivityManager { + + // MARK: - Utils + private static func presentModally(_ vc: UIViewController, animated: Bool, completion: (() -> Void)? = nil) { + UIApplication.shared.keyWindow!.rootViewController!.present(vc, animated: animated, completion: completion) + } + + private static func presentNav(_ vc: UIViewController, animated: Bool) { + let tabBarController = UIApplication.shared.keyWindow!.rootViewController! as! UITabBarController + let navController = tabBarController.selectedViewController as! UINavigationController + navController.pushViewController(vc, animated: animated) + } + + // MARK: - New Post + static func newPostActivity(mentioning: Account? = nil) -> NSUserActivity { + let activity = NSUserActivity(type: .newPost) + activity.isEligibleForPrediction = true + if let mentioning = mentioning { + activity.userInfo = ["mentioning": mentioning.acct] + activity.title = "Send a message to \(mentioning.realDisplayName)" + activity.suggestedInvocationPhrase = "Send a message to \(mentioning.realDisplayName)" + } else { + activity.userInfo = [:] + activity.title = "New Post" + activity.suggestedInvocationPhrase = "Post in Tusker" + } + return activity + } + + static func handleNewPost(activity: NSUserActivity) { + // TODO: check not currently showing compose screen + let mentioning = activity.userInfo?["mentioning"] as? String + presentModally(ComposeViewController.create(mentioning: mentioning), animated: true) + } + + // MARK: - Check Notifications + static func checkNotificationsActivity() -> NSUserActivity { + let activity = NSUserActivity(type: .checkNotifications) + activity.isEligibleForPrediction = true + activity.title = "Check Notifications" + activity.suggestedInvocationPhrase = "Check my Tusker notifications" + return activity + } + + static func handleCheckNotifications(activity: NSUserActivity) { + let index = Preferences.shared.tabs[.notifications] ?? -1 + guard index > 0 else { return } + let tabBarController = UIApplication.shared.keyWindow!.rootViewController! as! UITabBarController + tabBarController.selectedIndex = index + } + +} diff --git a/Tusker/Shortcuts/UserActivityType.swift b/Tusker/Shortcuts/UserActivityType.swift new file mode 100644 index 0000000000..fb5216f87f --- /dev/null +++ b/Tusker/Shortcuts/UserActivityType.swift @@ -0,0 +1,25 @@ +// +// UserActivityType.swift +// Tusker +// +// Created by Shadowfacts on 10/19/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation + +enum UserActivityType: String { + case newPost = "net.shadowfacts.tusker.activity.new-post" + case checkNotifications = "net.shadowfacts.tusker.activity.check-notifications" +} + +extension UserActivityType { + var handle: (NSUserActivity) -> Void { + switch self { + case .newPost: + return UserActivityManager.handleNewPost + case .checkNotifications: + return UserActivityManager.handleCheckNotifications + } + } +} diff --git a/Tusker/XCallbackURL/XCBActions.swift b/Tusker/XCallbackURL/XCBActions.swift index f80b63bf6e..a59da720cb 100644 --- a/Tusker/XCallbackURL/XCBActions.swift +++ b/Tusker/XCallbackURL/XCBActions.swift @@ -13,17 +13,17 @@ import SwiftSoup struct XCBActions { // MARK: - Utils - static func presentModally(_ vc: UIViewController, animated: Bool, completion: (() -> Void)? = nil) { + private static func presentModally(_ vc: UIViewController, animated: Bool, completion: (() -> Void)? = nil) { UIApplication.shared.keyWindow!.rootViewController!.present(vc, animated: animated, completion: completion) } - static func presentNav(_ vc: UIViewController, animated: Bool) { + private static func presentNav(_ vc: UIViewController, animated: Bool) { let tabBarController = UIApplication.shared.keyWindow!.rootViewController! as! UITabBarController let navController = tabBarController.selectedViewController as! UINavigationController navController.pushViewController(vc, animated: animated) } - static func getStatus(from request: XCBRequest, session: XCBSession, completion: @escaping (Status) -> Void) { + private static func getStatus(from request: XCBRequest, session: XCBSession, completion: @escaping (Status) -> Void) { if let id = request.arguments["statusID"] { MastodonCache.status(for: id) { (status) in if let status = status { @@ -54,7 +54,7 @@ struct XCBActions { } } - static func getAccount(from request: XCBRequest, session: XCBSession, completion: @escaping (Account) -> Void) { + private static func getAccount(from request: XCBRequest, session: XCBSession, completion: @escaping (Account) -> Void) { if let id = request.arguments["accountID"] { MastodonCache.account(for: id) { (account) in if let account = account {