From 973aa208eca68f12ecb6f96c4e3c318ab0c009bb 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 9fbe68f0..6d76fc70 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 48390e0f..b87034d3 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 57e31d57..d5bd98bc 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 0ac90a15..8ef8d2c1 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 7772c766..3ebdf596 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 00000000..edeb6e52 --- /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 00000000..13b263d8 --- /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 00000000..fb5216f8 --- /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 f80b63bf..a59da720 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 {