From 0d902f7d377e96feb18021600a30203def1fd25a Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 23 Sep 2018 12:00:28 -0400 Subject: [PATCH 1/9] Xcode suggestions --- Tusker.xcodeproj/project.pbxproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 0a8db48e..cd5a6d14 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -767,7 +767,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1000; - LastUpgradeCheck = 1000; + LastUpgradeCheck = 1010; ORGANIZATIONNAME = Shadowfacts; TargetAttributes = { D61099AA2144B0CC00432DC2 = { @@ -1034,7 +1034,7 @@ isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; @@ -1064,7 +1064,7 @@ isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; From d74f86418e6fc30c93c1bbcc7a3daf9b7512e610 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 23 Sep 2018 12:01:05 -0400 Subject: [PATCH 2/9] Add initial x-callback-url implementation --- Tusker.xcodeproj/project.pbxproj | 20 ++++++ Tusker/AppDelegate.swift | 10 ++- Tusker/Info.plist | 12 ++-- Tusker/MastodonCache.swift | 14 +++-- .../Compose/ComposeViewController.swift | 31 ++++++++-- .../Profile/ProfileTableViewController.swift | 2 +- Tusker/XCallbackURL/XCBManager.swift | 54 ++++++++++++++++ Tusker/XCallbackURL/XCBSession.swift | 38 ++++++++++++ Tusker/XCallbackURL/XCBSessionType.swift | 13 ++++ Tusker/XCallbackURL/XCallbackURL.swift | 61 +++++++++++++++++++ 10 files changed, 234 insertions(+), 21 deletions(-) create mode 100644 Tusker/XCallbackURL/XCBManager.swift create mode 100644 Tusker/XCallbackURL/XCBSession.swift create mode 100644 Tusker/XCallbackURL/XCBSessionType.swift create mode 100644 Tusker/XCallbackURL/XCallbackURL.swift diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index cd5a6d14..7cb9d954 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -92,6 +92,9 @@ D667E5F32135BC260057A976 /* Conversation.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D667E5F22135BC260057A976 /* Conversation.storyboard */; }; D667E5F52135BCD50057A976 /* ConversationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F42135BCD50057A976 /* ConversationViewController.swift */; }; D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */; }; + D6757A7C2157E01900721E32 /* XCBManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6757A7B2157E01900721E32 /* XCBManager.swift */; }; + D6757A7E2157E02600721E32 /* XCallbackURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6757A7D2157E02600721E32 /* XCallbackURL.swift */; }; + D6757A822157E8FA00721E32 /* XCBSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6757A812157E8FA00721E32 /* XCBSession.swift */; }; D6BED170212663DA00F02DA0 /* SwiftSoup.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; D6BED174212667E900F02DA0 /* StatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BED173212667E900F02DA0 /* StatusTableViewCell.swift */; }; D6C94D852139DFD800CB5196 /* LargeImage.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D6C94D842139DFD800CB5196 /* LargeImage.storyboard */; }; @@ -247,6 +250,9 @@ D667E5F22135BC260057A976 /* Conversation.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Conversation.storyboard; sourceTree = ""; }; D667E5F42135BCD50057A976 /* ConversationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationViewController.swift; sourceTree = ""; }; D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Equatable.swift"; sourceTree = ""; }; + D6757A7B2157E01900721E32 /* XCBManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBManager.swift; sourceTree = ""; }; + D6757A7D2157E02600721E32 /* XCallbackURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCallbackURL.swift; sourceTree = ""; }; + D6757A812157E8FA00721E32 /* XCBSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBSession.swift; sourceTree = ""; }; D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SwiftSoup.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D6BED173212667E900F02DA0 /* StatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTableViewCell.swift; sourceTree = ""; }; D6C94D842139DFD800CB5196 /* LargeImage.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LargeImage.storyboard; sourceTree = ""; }; @@ -566,6 +572,16 @@ path = Extensions; sourceTree = ""; }; + D6757A7A2157E00100721E32 /* XCallbackURL */ = { + isa = PBXGroup; + children = ( + D6757A7B2157E01900721E32 /* XCBManager.swift */, + D6757A812157E8FA00721E32 /* XCBSession.swift */, + D6757A7D2157E02600721E32 /* XCallbackURL.swift */, + ); + path = XCallbackURL; + sourceTree = ""; + }; D6BED1722126661300F02DA0 /* Views */ = { isa = PBXGroup; children = ( @@ -616,6 +632,7 @@ D64D0AAC2128D88B005A6F37 /* LocalData.swift */, 04DACE8D212CC7CC009840C4 /* AvatarCache.swift */, D6028B9A2150811100F223B9 /* MastodonCache.swift */, + D6757A7A2157E00100721E32 /* XCallbackURL */, D663626021360A9600C9CBA2 /* Preferences */, D667E5F62135C2ED0057A976 /* Extensions */, D6F953F121251A2F00CF0F2B /* Controllers */, @@ -921,6 +938,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D6757A822157E8FA00721E32 /* XCBSession.swift in Sources */, 04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */, D667E5F52135BCD50057A976 /* ConversationViewController.swift in Sources */, D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */, @@ -956,7 +974,9 @@ D667E5EB21349EF80057A976 /* ProfileHeaderTableViewCell.swift in Sources */, D641C77D213CB024004B4513 /* FollowNotificationTableViewCell.swift in Sources */, D641C773213CAA25004B4513 /* NotificationsTableViewController.swift in Sources */, + D6757A7C2157E01900721E32 /* XCBManager.swift in Sources */, D64A0CD32132153900640E3B /* HTMLContentLabel.swift in Sources */, + D6757A7E2157E02600721E32 /* XCallbackURL.swift in Sources */, D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */, D663625F2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift in Sources */, D667E5E721349D4C0057A976 /* ProfileTableViewController.swift in Sources */, diff --git a/Tusker/AppDelegate.swift b/Tusker/AppDelegate.swift index 51e3a3e9..f3e41416 100644 --- a/Tusker/AppDelegate.swift +++ b/Tusker/AppDelegate.swift @@ -26,6 +26,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return true } + func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { + if url.host == "x-callback-url" { + return XCBManager.handle(url: url) + } + return false + } + 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. @@ -52,7 +59,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } extension AppDelegate: OnboardingViewControllerDelegate { - func showOnboarding() { if let window = self.window, let onboardingViewController = UIStoryboard(name: "Onboarding", bundle: nil).instantiateInitialViewController() as? OnboardingViewController { @@ -73,6 +79,4 @@ extension AppDelegate: OnboardingViewControllerDelegate { hideOnboarding() LocalData.shared.onboardingComplete = true } - } - diff --git a/Tusker/Info.plist b/Tusker/Info.plist index d23f5ebc..57e31d57 100644 --- a/Tusker/Info.plist +++ b/Tusker/Info.plist @@ -2,12 +2,6 @@ - NSPhotoLibraryAddUsageDescription - Save photos directly from other people's posts. - NSCameraUsageDescription - Post photos from the camera. - NSPhotoLibraryUsageDescription - Post photos from the photo library. CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable @@ -39,6 +33,12 @@ 1 LSRequiresIPhoneOS + NSCameraUsageDescription + Post photos from the camera. + NSPhotoLibraryAddUsageDescription + Save photos directly from other people's posts. + NSPhotoLibraryUsageDescription + Post photos from the photo library. UILaunchStoryboardName LaunchScreen UIMainStoryboardFile diff --git a/Tusker/MastodonCache.swift b/Tusker/MastodonCache.swift index e8e1badd..6ad18eaa 100644 --- a/Tusker/MastodonCache.swift +++ b/Tusker/MastodonCache.swift @@ -11,16 +11,18 @@ import Pachyderm class MastodonCache { - private static let statuses = NSCache() - private static let accounts = NSCache() +// private static let statuses = NSDictionary() +// private static let accounts = NSDictionary() + private static var statuses = [String: Status]() + private static var accounts = [String: Account]() // MARK: - Statuses static func status(for id: String) -> Status? { - return statuses.object(forKey: id as NSString) + return statuses[id] } static func set(status: Status, for id: String) { - statuses.setObject(status, forKey: id as NSString) + statuses[id] = status add(account: status.account) if let reblog = status.reblog { add(account: reblog.account) @@ -49,11 +51,11 @@ class MastodonCache { // MARK: - Accounts static func account(for id: String) -> Account? { - return accounts.object(forKey: id as NSString) + return accounts[id] } static func set(account: Account, for id: String) { - accounts.setObject(account, forKey: id as NSString) + accounts[id] = account } static func account(for id: String, completion: @escaping (Account?) -> Void) { diff --git a/Tusker/Screens/Compose/ComposeViewController.swift b/Tusker/Screens/Compose/ComposeViewController.swift index fb428081..a12c9348 100644 --- a/Tusker/Screens/Compose/ComposeViewController.swift +++ b/Tusker/Screens/Compose/ComposeViewController.swift @@ -11,11 +11,21 @@ import Pachyderm class ComposeViewController: UIViewController { - static func create(inReplyTo inReplyToID: String? = nil, mentioning: Account? = nil) -> UIViewController { + static func create(inReplyTo inReplyToID: String? = nil, mentioning: String? = nil, text: String? = nil) -> UIViewController { guard let navigationController = UIStoryboard(name: "Compose", bundle: nil).instantiateInitialViewController() as? UINavigationController, let composeVC = navigationController.topViewController as? ComposeViewController else { fatalError() } composeVC.inReplyToID = inReplyToID - composeVC.mentioning = mentioning + composeVC.mentioningAcct = mentioning + composeVC.text = text + return navigationController + } + + static func create(for session: XCBSession, mentioning: String? = nil, text: String? = nil) -> UIViewController { + guard let navigationController = UIStoryboard(name: "Compose", bundle: nil).instantiateInitialViewController() as? UINavigationController, + let composeVC = navigationController.topViewController as? ComposeViewController else { fatalError() } + composeVC.mentioningAcct = mentioning + composeVC.text = text + composeVC.xcbSession = session return navigationController } @@ -37,7 +47,10 @@ class ComposeViewController: UIViewController { var scrolled = false var inReplyToID: String? - var mentioning: Account? + var mentioningAcct: String? + var text: String? + + var xcbSession: XCBSession? var contentWarning = false { didSet { @@ -95,8 +108,12 @@ class ComposeViewController: UIViewController { inReplyToContainerView.isHidden = true } - if let mentioning = mentioning { - statusTextView.text += "@\(mentioning.acct) " + if let mentioningAcct = mentioningAcct { + statusTextView.text += "@\(mentioningAcct) " + statusTextView.textViewDidChange(statusTextView) + } + if let text = text { + statusTextView.text += text statusTextView.textViewDidChange(statusTextView) } @@ -128,6 +145,8 @@ class ComposeViewController: UIViewController { let navController = dest.selectedViewController as? UINavigationController, let topVC = navController.topViewController as? StatusTableViewCellDelegate else { return } topVC.selected(status: status.id) + } else if segue.identifier == "cancel" { + xcbSession?.complete(with: .cancel) } } @@ -232,6 +251,8 @@ class ComposeViewController: UIViewController { DispatchQueue.main.async { self.progressView.step() self.performSegue(withIdentifier: "postComplete", sender: self) + + self.xcbSession?.complete(with: .success) } } } diff --git a/Tusker/Screens/Profile/ProfileTableViewController.swift b/Tusker/Screens/Profile/ProfileTableViewController.swift index c51d5170..e8ea68a9 100644 --- a/Tusker/Screens/Profile/ProfileTableViewController.swift +++ b/Tusker/Screens/Profile/ProfileTableViewController.swift @@ -74,7 +74,7 @@ class ProfileTableViewController: UITableViewController, PreferencesAdaptive { func sendMessageMentioning() { guard let account = MastodonCache.account(for: accountID) else { fatalError("Missing cached account \(accountID!)") } - let vc = ComposeViewController.create(mentioning: account) + let vc = ComposeViewController.create(mentioning: account.acct) present(vc, animated: true) } diff --git a/Tusker/XCallbackURL/XCBManager.swift b/Tusker/XCallbackURL/XCBManager.swift new file mode 100644 index 00000000..53c5816a --- /dev/null +++ b/Tusker/XCallbackURL/XCBManager.swift @@ -0,0 +1,54 @@ +// +// XCBManager.swift +// Tusker +// +// Created by Shadowfacts on 9/23/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import UIKit + +class XCBManager { + + static var specs: [XCallbackURLSpec] = [ + XCallbackURLSpec(path: "/postStatus", arguments: ["mentioning": true, "text": true], handle: postStatus) + ] + + static var currentSession: XCBSession? + + static func handle(url: URL) -> Bool { + guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { return false } + if let spec = specs.first(where: { $0.matches(components) }) { + let xcbURL = XCallbackURL(spec: spec, components: components) + return spec.handle(xcbURL) + } + return false + } + + static func didComplete(session type: XCBSessionType, result: XCBSessionResult) { + if let currentSession = currentSession, + currentSession.type == type { + currentSession.complete(with: result) + self.currentSession = nil + } + } + + private static func createSession(type: XCBSessionType, url: XCallbackURL) -> XCBSession { + let session = XCBSession(type: type, success: url.success, error: url.error, cancel: url.cancel) + currentSession = session + return session + } + + static func postStatus(_ url: XCallbackURL) -> Bool { + let mentioning = url.arguments["mentioning"] + let text = url.arguments["text"] + if let window = UIApplication.shared.keyWindow { + let session = createSession(type: .postStatus, url: url) + let vc = ComposeViewController.create(for: session, mentioning: mentioning, text: text) + window.rootViewController?.present(vc, animated: true) + return true + } + return false + } + +} diff --git a/Tusker/XCallbackURL/XCBSession.swift b/Tusker/XCallbackURL/XCBSession.swift new file mode 100644 index 00000000..fa1f156e --- /dev/null +++ b/Tusker/XCallbackURL/XCBSession.swift @@ -0,0 +1,38 @@ +// +// XCBSession.swift +// Tusker +// +// Created by Shadowfacts on 9/23/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import UIKit + +class XCBSession { + let type: XCBSessionType + let success: URL? + let error: URL? + let cancel: URL? + + init(type: XCBSessionType, success: URL?, error: URL?, cancel: URL?) { + self.type = type + self.success = success + self.error = error + self.cancel = cancel + } + + func complete(with result: XCBSessionResult) { + let url = result == .success ? success : result == .error ? error : cancel + if let url = url { + UIApplication.shared.open(url, options: [:]) + } + } +} + +enum XCBSessionType { + case postStatus +} + +enum XCBSessionResult { + case success, error, cancel +} diff --git a/Tusker/XCallbackURL/XCBSessionType.swift b/Tusker/XCallbackURL/XCBSessionType.swift new file mode 100644 index 00000000..871381be --- /dev/null +++ b/Tusker/XCallbackURL/XCBSessionType.swift @@ -0,0 +1,13 @@ +// +// XCBSessionType.swift +// Tusker +// +// Created by Shadowfacts on 9/23/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation + +enum XCBSessionType { + case postStatus +} diff --git a/Tusker/XCallbackURL/XCallbackURL.swift b/Tusker/XCallbackURL/XCallbackURL.swift new file mode 100644 index 00000000..129d233b --- /dev/null +++ b/Tusker/XCallbackURL/XCallbackURL.swift @@ -0,0 +1,61 @@ +// +// XCallbackURL.swift +// Tusker +// +// Created by Shadowfacts on 9/23/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation + +struct XCallbackURLSpec { + + let path: String + let arguments: [String: Bool] + let handle: (XCallbackURL) -> Bool + + init(path: String, arguments: [String: Bool], handle: @escaping (XCallbackURL) -> Bool) { + self.path = path + self.arguments = arguments + self.handle = handle + } + +} + +struct XCallbackURL { + let path: String + let arguments: [String: String] + let success: URL? + let error: URL? + let cancel: URL? + + init(spec: XCallbackURLSpec, components: URLComponents) { + self.path = spec.path + let queryItems = components.queryItems! + self.arguments = spec.arguments.reduce(into: [String: String](), { (result, el) in + if let value = queryItems.first(where: { $0.name == el.key })?.value { + result[el.key] = value + } + }) + success = queryItems.first(where: { $0.name == "x-success" }).flatMap { $0.value }.flatMap { URL(string: $0) } + error = queryItems.first(where: { $0.name == "x-error" }).flatMap { $0.value }.flatMap { URL(string: $0) } + cancel = queryItems.first(where: { $0.name == "x-cancel" }).flatMap { $0.value }.flatMap { URL(string: $0) } + } +} + +extension XCallbackURLSpec { + func matches(_ components: URLComponents) -> Bool { + return path == components.path && arguments.matches(components) + } +} + +extension Dictionary where Key == String, Value == Bool { + func matches(_ components: URLComponents) -> Bool { + for (name, optional) in self { + if (!optional && components.queryItems?.first(where: { $0.name == name }) == nil) { + return false + } + } + return true + } +} From 7af64f8e5a3950d623a60a65faa8167395fab61b Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 23 Sep 2018 18:43:33 -0400 Subject: [PATCH 3/9] Add x-callback-url response data --- Tusker/Screens/Compose/ComposeViewController.swift | 5 ++++- Tusker/XCallbackURL/XCBSession.swift | 10 ++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Tusker/Screens/Compose/ComposeViewController.swift b/Tusker/Screens/Compose/ComposeViewController.swift index a12c9348..a3eb3c85 100644 --- a/Tusker/Screens/Compose/ComposeViewController.swift +++ b/Tusker/Screens/Compose/ComposeViewController.swift @@ -252,7 +252,10 @@ class ComposeViewController: UIViewController { self.progressView.step() self.performSegue(withIdentifier: "postComplete", sender: self) - self.xcbSession?.complete(with: .success) + self.xcbSession?.complete(with: .success, additionalData: [ + "statusURL": status.url!.absoluteString, + "statusURI": status.uri + ]) } } } diff --git a/Tusker/XCallbackURL/XCBSession.swift b/Tusker/XCallbackURL/XCBSession.swift index fa1f156e..8d51eff2 100644 --- a/Tusker/XCallbackURL/XCBSession.swift +++ b/Tusker/XCallbackURL/XCBSession.swift @@ -21,9 +21,15 @@ class XCBSession { self.cancel = cancel } - func complete(with result: XCBSessionResult) { + func complete(with result: XCBSessionResult, additionalData: [String: String?]? = nil) { let url = result == .success ? success : result == .error ? error : cancel - if let url = url { + if var url = url { + if let additionalData = additionalData { + var components = URLComponents(url: url, resolvingAgainstBaseURL: true)! + components.queryItems = components.queryItems ?? [] + components.queryItems!.append(contentsOf: additionalData.map(URLQueryItem.init)) + url = components.url! + } UIApplication.shared.open(url, options: [:]) } } From 6c3ae9ab146d086a4b512ed0b96f845c02edcd27 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 23 Sep 2018 18:45:56 -0400 Subject: [PATCH 4/9] Change ComposeViewController.xcbSession to be weak --- Tusker/Screens/Compose/ComposeViewController.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tusker/Screens/Compose/ComposeViewController.swift b/Tusker/Screens/Compose/ComposeViewController.swift index a3eb3c85..b33547d2 100644 --- a/Tusker/Screens/Compose/ComposeViewController.swift +++ b/Tusker/Screens/Compose/ComposeViewController.swift @@ -50,7 +50,8 @@ class ComposeViewController: UIViewController { var mentioningAcct: String? var text: String? - var xcbSession: XCBSession? + // Weak so that if a new session is initiated (i.e. XCBManager.currentSession is changed) while the current one is in progress, this one will be released + weak var xcbSession: XCBSession? var contentWarning = false { didSet { From af0d0612ba73db0bf01ded82fd1cfaa54547b634 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 23 Sep 2018 19:04:39 -0400 Subject: [PATCH 5/9] Add silent x-callback-url actions User confirmation is required the first time a source app attempts to run an action silently. Rejecting will always display the UI for the given action (as if the silent parameter had been false). --- Tusker.xcodeproj/project.pbxproj | 4 + Tusker/Preferences/Preferences.swift | 8 ++ .../Compose/ComposeViewController.swift | 2 +- Tusker/XCallbackURL/XCBActions.swift | 73 +++++++++++++++++++ Tusker/XCallbackURL/XCBManager.swift | 24 +----- Tusker/XCallbackURL/XCBSession.swift | 5 +- Tusker/XCallbackURL/XCallbackURL.swift | 2 + 7 files changed, 94 insertions(+), 24 deletions(-) create mode 100644 Tusker/XCallbackURL/XCBActions.swift diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 7cb9d954..8bd1a80d 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -95,6 +95,7 @@ D6757A7C2157E01900721E32 /* XCBManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6757A7B2157E01900721E32 /* XCBManager.swift */; }; D6757A7E2157E02600721E32 /* XCallbackURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6757A7D2157E02600721E32 /* XCallbackURL.swift */; }; D6757A822157E8FA00721E32 /* XCBSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6757A812157E8FA00721E32 /* XCBSession.swift */; }; + D679C09F215850EF00DA27FE /* XCBActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D679C09E215850EF00DA27FE /* XCBActions.swift */; }; D6BED170212663DA00F02DA0 /* SwiftSoup.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; D6BED174212667E900F02DA0 /* StatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BED173212667E900F02DA0 /* StatusTableViewCell.swift */; }; D6C94D852139DFD800CB5196 /* LargeImage.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D6C94D842139DFD800CB5196 /* LargeImage.storyboard */; }; @@ -253,6 +254,7 @@ D6757A7B2157E01900721E32 /* XCBManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBManager.swift; sourceTree = ""; }; D6757A7D2157E02600721E32 /* XCallbackURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCallbackURL.swift; sourceTree = ""; }; D6757A812157E8FA00721E32 /* XCBSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBSession.swift; sourceTree = ""; }; + D679C09E215850EF00DA27FE /* XCBActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBActions.swift; sourceTree = ""; }; D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SwiftSoup.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D6BED173212667E900F02DA0 /* StatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTableViewCell.swift; sourceTree = ""; }; D6C94D842139DFD800CB5196 /* LargeImage.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LargeImage.storyboard; sourceTree = ""; }; @@ -578,6 +580,7 @@ D6757A7B2157E01900721E32 /* XCBManager.swift */, D6757A812157E8FA00721E32 /* XCBSession.swift */, D6757A7D2157E02600721E32 /* XCallbackURL.swift */, + D679C09E215850EF00DA27FE /* XCBActions.swift */, ); path = XCallbackURL; sourceTree = ""; @@ -951,6 +954,7 @@ D6333B772138D94E00CE884A /* ComposeMediaView.swift in Sources */, 04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */, D663626421360D2300C9CBA2 /* AvatarStyle.swift in Sources */, + D679C09F215850EF00DA27FE /* XCBActions.swift in Sources */, D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */, D6BED174212667E900F02DA0 /* StatusTableViewCell.swift in Sources */, D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */, diff --git a/Tusker/Preferences/Preferences.swift b/Tusker/Preferences/Preferences.swift index 529726eb..207e3d01 100644 --- a/Tusker/Preferences/Preferences.swift +++ b/Tusker/Preferences/Preferences.swift @@ -39,4 +39,12 @@ class Preferences: Codable { var defaultPostVisibility = Status.Visibility.public + var silentActions: [String: Permission] = [:] + +} + +extension Preferences { + enum Permission: String, Codable { + case undecided, accepted, rejected + } } diff --git a/Tusker/Screens/Compose/ComposeViewController.swift b/Tusker/Screens/Compose/ComposeViewController.swift index b33547d2..0abb4b2d 100644 --- a/Tusker/Screens/Compose/ComposeViewController.swift +++ b/Tusker/Screens/Compose/ComposeViewController.swift @@ -254,7 +254,7 @@ class ComposeViewController: UIViewController { self.performSegue(withIdentifier: "postComplete", sender: self) self.xcbSession?.complete(with: .success, additionalData: [ - "statusURL": status.url!.absoluteString, + "statusURL": status.url?.absoluteString, "statusURI": status.uri ]) } diff --git a/Tusker/XCallbackURL/XCBActions.swift b/Tusker/XCallbackURL/XCBActions.swift new file mode 100644 index 00000000..15cb7399 --- /dev/null +++ b/Tusker/XCallbackURL/XCBActions.swift @@ -0,0 +1,73 @@ +// +// XCBActions.swift +// Tusker +// +// Created by Shadowfacts on 9/23/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import UIKit + +struct XCBActions { + // MARK: - Posts + + static func postStatus(_ url: XCallbackURL) -> Bool { + let session = XCBManager.createSession(type: .postStatus, url: url) + + let mentioning = url.arguments["mentioning"] + let text = url.arguments["text"] + + func postStatusSilently() { + var status = "" + if let mentioning = mentioning { status += mentioning } + if let text = text { status += text } + let request = MastodonController.shared.client.createStatus(text: status) + MastodonController.shared.client.run(request) { response in + if case let .success(status, _) = response { + session.complete(with: .success, additionalData: [ + "statusURL": status.url?.absoluteString, + "statusURI": status.uri + ]) + } else if case let .failure(error) = response { + session.complete(with: .error, additionalData: [ + "error": error.localizedDescription + ]) + } + } + } + + func showComposeStatus() { + let vc = ComposeViewController.create(for: session, mentioning: mentioning, text: text) + UIApplication.shared.keyWindow!.rootViewController!.present(vc, animated: true) + } + + let silent = Bool(url.arguments["silent"] ?? "false") ?? false + if silent { + if let source = url.source { + let permission = Preferences.shared.silentActions[source] ?? .undecided + switch permission { + case .accepted: + postStatusSilently() + case .rejected: + showComposeStatus() + case .undecided: + let alert = UIAlertController(title: "\(source) wants to perform actions silently", message: "Accepting will allow \(source) to perform actions without your confirmation, rejecting will always prompt for confirmation.", preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "Accept", style: .default, handler: { (_) in + Preferences.shared.silentActions[source] = .accepted + postStatusSilently() + })) + alert.addAction(UIAlertAction(title: "Reject", style: .default, handler: { (_) in + Preferences.shared.silentActions[source] = .rejected + showComposeStatus() + })) + UIApplication.shared.keyWindow!.rootViewController!.present(alert, animated: true) + } + } else { + session.complete(with: .error, additionalData: ["error": "Cannot perform silent action without source app, x-source parameter must be specified"]) + } + } else { + showComposeStatus() + } + return true + } +} diff --git a/Tusker/XCallbackURL/XCBManager.swift b/Tusker/XCallbackURL/XCBManager.swift index 53c5816a..7bc1ce0a 100644 --- a/Tusker/XCallbackURL/XCBManager.swift +++ b/Tusker/XCallbackURL/XCBManager.swift @@ -11,7 +11,7 @@ import UIKit class XCBManager { static var specs: [XCallbackURLSpec] = [ - XCallbackURLSpec(path: "/postStatus", arguments: ["mentioning": true, "text": true], handle: postStatus) + XCallbackURLSpec(path: "/postStatus", arguments: ["mentioning": true, "text": true, "silent": true], handle: XCBActions.postStatus) ] static var currentSession: XCBSession? @@ -25,30 +25,10 @@ class XCBManager { return false } - static func didComplete(session type: XCBSessionType, result: XCBSessionResult) { - if let currentSession = currentSession, - currentSession.type == type { - currentSession.complete(with: result) - self.currentSession = nil - } - } - - private static func createSession(type: XCBSessionType, url: XCallbackURL) -> XCBSession { + static func createSession(type: XCBSessionType, url: XCallbackURL) -> XCBSession { let session = XCBSession(type: type, success: url.success, error: url.error, cancel: url.cancel) currentSession = session return session } - static func postStatus(_ url: XCallbackURL) -> Bool { - let mentioning = url.arguments["mentioning"] - let text = url.arguments["text"] - if let window = UIApplication.shared.keyWindow { - let session = createSession(type: .postStatus, url: url) - let vc = ComposeViewController.create(for: session, mentioning: mentioning, text: text) - window.rootViewController?.present(vc, animated: true) - return true - } - return false - } - } diff --git a/Tusker/XCallbackURL/XCBSession.swift b/Tusker/XCallbackURL/XCBSession.swift index 8d51eff2..80c4c558 100644 --- a/Tusker/XCallbackURL/XCBSession.swift +++ b/Tusker/XCallbackURL/XCBSession.swift @@ -24,13 +24,16 @@ class XCBSession { func complete(with result: XCBSessionResult, additionalData: [String: String?]? = nil) { let url = result == .success ? success : result == .error ? error : cancel if var url = url { + XCBManager.currentSession = nil if let additionalData = additionalData { var components = URLComponents(url: url, resolvingAgainstBaseURL: true)! components.queryItems = components.queryItems ?? [] components.queryItems!.append(contentsOf: additionalData.map(URLQueryItem.init)) url = components.url! } - UIApplication.shared.open(url, options: [:]) + DispatchQueue.main.async { + UIApplication.shared.open(url, options: [:]) + } } } } diff --git a/Tusker/XCallbackURL/XCallbackURL.swift b/Tusker/XCallbackURL/XCallbackURL.swift index 129d233b..34d7b4bd 100644 --- a/Tusker/XCallbackURL/XCallbackURL.swift +++ b/Tusker/XCallbackURL/XCallbackURL.swift @@ -25,6 +25,7 @@ struct XCallbackURLSpec { struct XCallbackURL { let path: String let arguments: [String: String] + let source: String? let success: URL? let error: URL? let cancel: URL? @@ -37,6 +38,7 @@ struct XCallbackURL { result[el.key] = value } }) + source = queryItems.first(where: { $0.name == "x-source" }).flatMap { $0.value } success = queryItems.first(where: { $0.name == "x-success" }).flatMap { $0.value }.flatMap { URL(string: $0) } error = queryItems.first(where: { $0.name == "x-error" }).flatMap { $0.value }.flatMap { URL(string: $0) } cancel = queryItems.first(where: { $0.name == "x-cancel" }).flatMap { $0.value }.flatMap { URL(string: $0) } From 68300f930b9d89810f786fe5afa169f295fe42ae Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 23 Sep 2018 21:10:45 -0400 Subject: [PATCH 6/9] Cleanup silent XCB actions code --- Pachyderm/Client.swift | 4 +- .../Compose/ComposeViewController.swift | 2 +- Tusker/XCallbackURL/XCBActions.swift | 41 ++---------- Tusker/XCallbackURL/XCBManager.swift | 4 +- Tusker/XCallbackURL/XCallbackURL.swift | 66 +++++++++++++++---- 5 files changed, 64 insertions(+), 53 deletions(-) diff --git a/Pachyderm/Client.swift b/Pachyderm/Client.swift index 502ede8b..cb9e04f1 100644 --- a/Pachyderm/Client.swift +++ b/Pachyderm/Client.swift @@ -286,14 +286,14 @@ public class Client { media: [Attachment]? = nil, sensitive: Bool? = nil, spoilerText: String? = nil, - visiblity: Status.Visibility? = nil, + visibility: Status.Visibility? = nil, language: String? = nil) -> Request { return Request(method: .post, path: "/api/v1/statuses", body: .parameters([ "status" => text, "in_reply_to_id" => inReplyTo, "sensitive" => sensitive, "spoiler_text" => spoilerText, - "visibility" => visiblity?.rawValue, + "visibility" => visibility?.rawValue, "language" => language ] + "media_ids" => media?.map { $0.id })) } diff --git a/Tusker/Screens/Compose/ComposeViewController.swift b/Tusker/Screens/Compose/ComposeViewController.swift index 0abb4b2d..d1870001 100644 --- a/Tusker/Screens/Compose/ComposeViewController.swift +++ b/Tusker/Screens/Compose/ComposeViewController.swift @@ -244,7 +244,7 @@ class ComposeViewController: UIViewController { media: attachments, sensitive: sensitive, spoilerText: contentWarning, - visiblity: visibility) + visibility: visibility) MastodonController.shared.client.run(request) { response in guard case let .success(status, _) = response else { fatalError() } self.status = status diff --git a/Tusker/XCallbackURL/XCBActions.swift b/Tusker/XCallbackURL/XCBActions.swift index 15cb7399..30217e60 100644 --- a/Tusker/XCallbackURL/XCBActions.swift +++ b/Tusker/XCallbackURL/XCBActions.swift @@ -11,17 +11,15 @@ import UIKit struct XCBActions { // MARK: - Posts - static func postStatus(_ url: XCallbackURL) -> Bool { - let session = XCBManager.createSession(type: .postStatus, url: url) - + static func postStatus(_ url: XCallbackURL, _ session: XCBSession, _ silent: Bool?) { let mentioning = url.arguments["mentioning"] let text = url.arguments["text"] - func postStatusSilently() { + if silent ?? false { var status = "" if let mentioning = mentioning { status += mentioning } if let text = text { status += text } - let request = MastodonController.shared.client.createStatus(text: status) + let request = MastodonController.shared.client.createStatus(text: status, visibility: Preferences.shared.defaultPostVisibility) MastodonController.shared.client.run(request) { response in if case let .success(status, _) = response { session.complete(with: .success, additionalData: [ @@ -34,40 +32,9 @@ struct XCBActions { ]) } } - } - - func showComposeStatus() { + } else { let vc = ComposeViewController.create(for: session, mentioning: mentioning, text: text) UIApplication.shared.keyWindow!.rootViewController!.present(vc, animated: true) } - - let silent = Bool(url.arguments["silent"] ?? "false") ?? false - if silent { - if let source = url.source { - let permission = Preferences.shared.silentActions[source] ?? .undecided - switch permission { - case .accepted: - postStatusSilently() - case .rejected: - showComposeStatus() - case .undecided: - let alert = UIAlertController(title: "\(source) wants to perform actions silently", message: "Accepting will allow \(source) to perform actions without your confirmation, rejecting will always prompt for confirmation.", preferredStyle: .alert) - alert.addAction(UIAlertAction(title: "Accept", style: .default, handler: { (_) in - Preferences.shared.silentActions[source] = .accepted - postStatusSilently() - })) - alert.addAction(UIAlertAction(title: "Reject", style: .default, handler: { (_) in - Preferences.shared.silentActions[source] = .rejected - showComposeStatus() - })) - UIApplication.shared.keyWindow!.rootViewController!.present(alert, animated: true) - } - } else { - session.complete(with: .error, additionalData: ["error": "Cannot perform silent action without source app, x-source parameter must be specified"]) - } - } else { - showComposeStatus() - } - return true } } diff --git a/Tusker/XCallbackURL/XCBManager.swift b/Tusker/XCallbackURL/XCBManager.swift index 7bc1ce0a..373e7200 100644 --- a/Tusker/XCallbackURL/XCBManager.swift +++ b/Tusker/XCallbackURL/XCBManager.swift @@ -11,7 +11,7 @@ import UIKit class XCBManager { static var specs: [XCallbackURLSpec] = [ - XCallbackURLSpec(path: "/postStatus", arguments: ["mentioning": true, "text": true, "silent": true], handle: XCBActions.postStatus) + XCallbackURLSpec(path: "/postStatus", type: .postStatus, arguments: ["mentioning": true, "text": true], canRunSilently: true, action: XCBActions.postStatus) ] static var currentSession: XCBSession? @@ -20,7 +20,7 @@ class XCBManager { guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { return false } if let spec = specs.first(where: { $0.matches(components) }) { let xcbURL = XCallbackURL(spec: spec, components: components) - return spec.handle(xcbURL) + return spec.handle(url: xcbURL) } return false } diff --git a/Tusker/XCallbackURL/XCallbackURL.swift b/Tusker/XCallbackURL/XCallbackURL.swift index 34d7b4bd..cb5cf1a6 100644 --- a/Tusker/XCallbackURL/XCallbackURL.swift +++ b/Tusker/XCallbackURL/XCallbackURL.swift @@ -6,18 +6,61 @@ // Copyright © 2018 Shadowfacts. All rights reserved. // -import Foundation +import UIKit + +typealias XCBAction = (_ url: XCallbackURL, _ session: XCBSession, _ silent: Bool?) -> Void struct XCallbackURLSpec { let path: String + let type: XCBSessionType let arguments: [String: Bool] - let handle: (XCallbackURL) -> Bool + let canRunSilently: Bool + let action: XCBAction - init(path: String, arguments: [String: Bool], handle: @escaping (XCallbackURL) -> Bool) { + init(path: String, type: XCBSessionType, arguments: [String: Bool], canRunSilently: Bool, action: @escaping XCBAction) { self.path = path + self.type = type + self.canRunSilently = canRunSilently + self.action = action + var arguments = arguments + if canRunSilently { + arguments["silent"] = true + } self.arguments = arguments - self.handle = handle + } + + func handle(url: XCallbackURL) -> Bool { + let session = XCBManager.createSession(type: type, url: url) + if canRunSilently && url.silent { + if let source = url.source { + let permission = Preferences.shared.silentActions[source] ?? .undecided + switch permission { + case .accepted: + action(url, session, true) + case .rejected: + action(url, session, false) + case .undecided: + let alert = UIAlertController(title: "\(source) wants to perform actions silently", message: "Accepting will allow \(source) to perform actions without your confirmation, rejecting will always prompt for confirmation.", preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "Accept", style: .default, handler: { (_) in + Preferences.shared.silentActions[source] = .accepted + self.action(url, session, true) + })) + alert.addAction(UIAlertAction(title: "Reject", style: .default, handler: { (_) in + Preferences.shared.silentActions[source] = .rejected + self.action(url, session, false) + })) + UIApplication.shared.keyWindow!.rootViewController!.present(alert, animated: true) + } + } else { + session.complete(with: .error, additionalData: [ + "error": "Cannot perform silent action without source app, x-source parameter must be specified" + ]) + } + } else { + action(url, session, nil) + } + return true } } @@ -25,6 +68,7 @@ struct XCallbackURLSpec { struct XCallbackURL { let path: String let arguments: [String: String] + let silent: Bool let source: String? let success: URL? let error: URL? @@ -38,6 +82,11 @@ struct XCallbackURL { result[el.key] = value } }) + if spec.canRunSilently { + silent = Bool(arguments["silent"] ?? "false") ?? false + } else { + silent = false + } source = queryItems.first(where: { $0.name == "x-source" }).flatMap { $0.value } success = queryItems.first(where: { $0.name == "x-success" }).flatMap { $0.value }.flatMap { URL(string: $0) } error = queryItems.first(where: { $0.name == "x-error" }).flatMap { $0.value }.flatMap { URL(string: $0) } @@ -47,13 +96,8 @@ struct XCallbackURL { extension XCallbackURLSpec { func matches(_ components: URLComponents) -> Bool { - return path == components.path && arguments.matches(components) - } -} - -extension Dictionary where Key == String, Value == Bool { - func matches(_ components: URLComponents) -> Bool { - for (name, optional) in self { + guard path == components.path else { return false } + for (name, optional) in arguments { if (!optional && components.queryItems?.first(where: { $0.name == name }) == nil) { return false } From 9a41d44e9fefb216475c2285928b150603def852 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 23 Sep 2018 21:35:33 -0400 Subject: [PATCH 7/9] Add getCurrentUser XCB action, refactor XCB support some more --- MyPlayground.playground/Contents.swift | 55 ------------------------- Tusker.xcodeproj/project.pbxproj | 6 ++- Tusker/XCallbackURL/XCBActionType.swift | 18 ++++++++ Tusker/XCallbackURL/XCBActions.swift | 17 +++++++- Tusker/XCallbackURL/XCBManager.swift | 7 +++- Tusker/XCallbackURL/XCBSession.swift | 8 +--- Tusker/XCallbackURL/XCallbackURL.swift | 6 +-- 7 files changed, 49 insertions(+), 68 deletions(-) create mode 100644 Tusker/XCallbackURL/XCBActionType.swift diff --git a/MyPlayground.playground/Contents.swift b/MyPlayground.playground/Contents.swift index dc558611..6bfb935c 100644 --- a/MyPlayground.playground/Contents.swift +++ b/MyPlayground.playground/Contents.swift @@ -1,58 +1,3 @@ import UIKit -class Client { - func test(_ thing: A) { - if var thing = thing as? ClientModel { - thing.client = self - } else if var arr = thing as? [ClientModel] { - arr.client = self - } -// } else if let arr = thing as? Array { -// for el in arr { -// if var el = el as? ClientModel { -// el.client = self -// } -// } -// } - } -} -protocol ClientModel { - var client: Client! { get set } -} - -class Something: ClientModel { - var client: Client! -} - -extension Array: ClientModel where Element: ClientModel { - var client: Client! { - get { - return first?.client - } - set { - for var el in self { - el.client = newValue - } - } - } -} -//extension Array: ClientModel where Element == ClientModel { -// var client: Client! { -// get { -// return first?.client -// } -// set { -// for var el in self { -// el.client = newValue -// } -// } -// } -//} - -var array = [Something(), Something()] - -let client = Client() -client.test(array) -array[0].client -array[1].client diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 8bd1a80d..f5cb14f3 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -68,6 +68,7 @@ D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AAC2128D88B005A6F37 /* LocalData.swift */; }; D64D0AAF2128D954005A6F37 /* Onboarding.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D64D0AAE2128D954005A6F37 /* Onboarding.storyboard */; }; D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; }; + D64F80E2215875CC00BEF393 /* XCBActionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64F80E1215875CC00BEF393 /* XCBActionType.swift */; }; D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */; }; D65A37F321472F300087646E /* SwiftSoup.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */; }; D663625D2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D663625C2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib */; }; @@ -228,6 +229,7 @@ D64D0AAC2128D88B005A6F37 /* LocalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalData.swift; sourceTree = ""; }; D64D0AAE2128D954005A6F37 /* Onboarding.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Onboarding.storyboard; sourceTree = ""; }; D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = ""; }; + D64F80E1215875CC00BEF393 /* XCBActionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBActionType.swift; sourceTree = ""; }; D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewSwipeActionProvider.swift; sourceTree = ""; }; D663625C2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ConversationMainStatusTableViewCell.xib; sourceTree = ""; }; D663625E2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationMainStatusTableViewCell.swift; sourceTree = ""; }; @@ -578,8 +580,9 @@ isa = PBXGroup; children = ( D6757A7B2157E01900721E32 /* XCBManager.swift */, - D6757A812157E8FA00721E32 /* XCBSession.swift */, D6757A7D2157E02600721E32 /* XCallbackURL.swift */, + D6757A812157E8FA00721E32 /* XCBSession.swift */, + D64F80E1215875CC00BEF393 /* XCBActionType.swift */, D679C09E215850EF00DA27FE /* XCBActions.swift */, ); path = XCallbackURL; @@ -971,6 +974,7 @@ D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */, D663626821360E2C00C9CBA2 /* PreferencesTableViewController.swift in Sources */, D66362732136FFC600C9CBA2 /* UITextView+Placeholder.swift in Sources */, + D64F80E2215875CC00BEF393 /* XCBActionType.swift in Sources */, D66362752137068A00C9CBA2 /* Visibility+Helpers.swift in Sources */, D646C95A213B5D0500269FB5 /* LargeImageInteractionController.swift in Sources */, D6F953EC212519E700CF0F2B /* TimelineTableViewController.swift in Sources */, diff --git a/Tusker/XCallbackURL/XCBActionType.swift b/Tusker/XCallbackURL/XCBActionType.swift new file mode 100644 index 00000000..1e534fd6 --- /dev/null +++ b/Tusker/XCallbackURL/XCBActionType.swift @@ -0,0 +1,18 @@ +// +// XCBActionType.swift +// Tusker +// +// Created by Shadowfacts on 9/23/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation + +enum XCBActionType: String { + case postStatus + case getCurrentUser + + var path: String { + return "/\(rawValue)" + } +} diff --git a/Tusker/XCallbackURL/XCBActions.swift b/Tusker/XCallbackURL/XCBActions.swift index 30217e60..49f4e74d 100644 --- a/Tusker/XCallbackURL/XCBActions.swift +++ b/Tusker/XCallbackURL/XCBActions.swift @@ -9,8 +9,8 @@ import UIKit struct XCBActions { - // MARK: - Posts + // MARK: - Posts static func postStatus(_ url: XCallbackURL, _ session: XCBSession, _ silent: Bool?) { let mentioning = url.arguments["mentioning"] let text = url.arguments["text"] @@ -37,4 +37,19 @@ struct XCBActions { UIApplication.shared.keyWindow!.rootViewController!.present(vc, animated: true) } } + + // MARK: - Accounts + static func getCurrentUser(_ url: XCallbackURL, _ session: XCBSession, _ silent: Bool?) { + let account = MastodonController.shared.account! + session.complete(with: .success, additionalData: [ + "username": account.acct, + "displayName": account.displayName, + "locked": account.locked.description, + "followers": account.followersCount.description, + "following": account.followingCount.description, + "url": account.url.absoluteString, + "avatarURL": account.avatar.absoluteString, + "headerURL": account.header.absoluteString, + ]) + } } diff --git a/Tusker/XCallbackURL/XCBManager.swift b/Tusker/XCallbackURL/XCBManager.swift index 373e7200..ad7340f4 100644 --- a/Tusker/XCallbackURL/XCBManager.swift +++ b/Tusker/XCallbackURL/XCBManager.swift @@ -11,7 +11,10 @@ import UIKit class XCBManager { static var specs: [XCallbackURLSpec] = [ - XCallbackURLSpec(path: "/postStatus", type: .postStatus, arguments: ["mentioning": true, "text": true], canRunSilently: true, action: XCBActions.postStatus) + // Statuses + XCallbackURLSpec(type: .postStatus, arguments: ["mentioning": true, "text": true], canRunSilently: true, action: XCBActions.postStatus), + // Accounts + XCallbackURLSpec(type: .getCurrentUser, arguments: [:], canRunSilently: false, action: XCBActions.getCurrentUser) ] static var currentSession: XCBSession? @@ -25,7 +28,7 @@ class XCBManager { return false } - static func createSession(type: XCBSessionType, url: XCallbackURL) -> XCBSession { + static func createSession(type: XCBActionType, url: XCallbackURL) -> XCBSession { let session = XCBSession(type: type, success: url.success, error: url.error, cancel: url.cancel) currentSession = session return session diff --git a/Tusker/XCallbackURL/XCBSession.swift b/Tusker/XCallbackURL/XCBSession.swift index 80c4c558..c4179c70 100644 --- a/Tusker/XCallbackURL/XCBSession.swift +++ b/Tusker/XCallbackURL/XCBSession.swift @@ -9,12 +9,12 @@ import UIKit class XCBSession { - let type: XCBSessionType + let type: XCBActionType let success: URL? let error: URL? let cancel: URL? - init(type: XCBSessionType, success: URL?, error: URL?, cancel: URL?) { + init(type: XCBActionType, success: URL?, error: URL?, cancel: URL?) { self.type = type self.success = success self.error = error @@ -38,10 +38,6 @@ class XCBSession { } } -enum XCBSessionType { - case postStatus -} - enum XCBSessionResult { case success, error, cancel } diff --git a/Tusker/XCallbackURL/XCallbackURL.swift b/Tusker/XCallbackURL/XCallbackURL.swift index cb5cf1a6..7e383edc 100644 --- a/Tusker/XCallbackURL/XCallbackURL.swift +++ b/Tusker/XCallbackURL/XCallbackURL.swift @@ -13,13 +13,13 @@ typealias XCBAction = (_ url: XCallbackURL, _ session: XCBSession, _ silent: Boo struct XCallbackURLSpec { let path: String - let type: XCBSessionType + let type: XCBActionType let arguments: [String: Bool] let canRunSilently: Bool let action: XCBAction - init(path: String, type: XCBSessionType, arguments: [String: Bool], canRunSilently: Bool, action: @escaping XCBAction) { - self.path = path + init(type: XCBActionType, arguments: [String: Bool], canRunSilently: Bool, action: @escaping XCBAction) { + self.path = type.path self.type = type self.canRunSilently = canRunSilently self.action = action From 024f55a7a8fa2374c22930c873d64de8e3f7d0d0 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 23 Sep 2018 21:36:20 -0400 Subject: [PATCH 8/9] Remove playground --- .gitignore | 1 + MyPlayground.playground/Contents.swift | 3 --- MyPlayground.playground/contents.xcplayground | 4 ---- Tusker.xcworkspace/contents.xcworkspacedata | 3 --- 4 files changed, 1 insertion(+), 10 deletions(-) delete mode 100644 MyPlayground.playground/Contents.swift delete mode 100644 MyPlayground.playground/contents.xcplayground diff --git a/.gitignore b/.gitignore index 71c83743..fc6431b8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .DS_Store +MyPlayground.playground/ ### Swift ### # Xcode diff --git a/MyPlayground.playground/Contents.swift b/MyPlayground.playground/Contents.swift deleted file mode 100644 index 6bfb935c..00000000 --- a/MyPlayground.playground/Contents.swift +++ /dev/null @@ -1,3 +0,0 @@ -import UIKit - - diff --git a/MyPlayground.playground/contents.xcplayground b/MyPlayground.playground/contents.xcplayground deleted file mode 100644 index 9f5f2f40..00000000 --- a/MyPlayground.playground/contents.xcplayground +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/Tusker.xcworkspace/contents.xcworkspacedata b/Tusker.xcworkspace/contents.xcworkspacedata index d751e971..bb8fa0df 100644 --- a/Tusker.xcworkspace/contents.xcworkspacedata +++ b/Tusker.xcworkspace/contents.xcworkspacedata @@ -1,9 +1,6 @@ - - From c6008bc7c038533734897a1ef0ff10f0a1a061de Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 23 Sep 2018 22:16:25 -0400 Subject: [PATCH 9/9] Add favorite status XCB action --- Tusker/XCallbackURL/XCBActionType.swift | 3 + Tusker/XCallbackURL/XCBActions.swift | 85 ++++++++++++++++++++++++- Tusker/XCallbackURL/XCBManager.swift | 1 + 3 files changed, 87 insertions(+), 2 deletions(-) diff --git a/Tusker/XCallbackURL/XCBActionType.swift b/Tusker/XCallbackURL/XCBActionType.swift index 1e534fd6..238e4510 100644 --- a/Tusker/XCallbackURL/XCBActionType.swift +++ b/Tusker/XCallbackURL/XCBActionType.swift @@ -9,7 +9,10 @@ import Foundation enum XCBActionType: String { + // Statuses case postStatus + case favoriteStatus + // Accounts case getCurrentUser var path: String { diff --git a/Tusker/XCallbackURL/XCBActions.swift b/Tusker/XCallbackURL/XCBActions.swift index 49f4e74d..b897aa2f 100644 --- a/Tusker/XCallbackURL/XCBActions.swift +++ b/Tusker/XCallbackURL/XCBActions.swift @@ -7,10 +7,22 @@ // import UIKit +import Pachyderm struct XCBActions { - // MARK: - Posts + // MARK: - Utils + 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) { + let tabBarController = UIApplication.shared.keyWindow!.rootViewController! as! UITabBarController + let navController = tabBarController.selectedViewController as! UINavigationController + navController.pushViewController(vc, animated: animated) + } + + // MARK: - Statuses static func postStatus(_ url: XCallbackURL, _ session: XCBSession, _ silent: Bool?) { let mentioning = url.arguments["mentioning"] let text = url.arguments["text"] @@ -34,7 +46,76 @@ struct XCBActions { } } else { let vc = ComposeViewController.create(for: session, mentioning: mentioning, text: text) - UIApplication.shared.keyWindow!.rootViewController!.present(vc, animated: true) + presentModally(vc, animated: true) + } + } + + static func favoriteStatus(_ url: XCallbackURL, _ session: XCBSession, _ silent: Bool?) { + func performAction(status: Status, completion: ((Status) -> Void)?) { + let request = Status.favourite(status) + MastodonController.shared.client.run(request) { (response) in + if case let .success(status, _) = response { + MastodonCache.add(status: status) + completion?(status) + session.complete(with: .success, additionalData: [ + "statusURL": status.url?.absoluteString, + "statusURI": status.uri + ]) + } else if case let .failure(error) = response { + session.complete(with: .error, additionalData: [ + "error": error.localizedDescription + ]) + } + } + } + + func favorite(_ status: Status) { + if silent ?? false { + performAction(status: status, completion: nil) + } else { + let vc = ConversationViewController.create(for: status.id) + DispatchQueue.main.async { + presentNav(vc, animated: true) + } + let alertController = UIAlertController(title: "Favorite status?", message: nil, preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: "Ok", style: .default, handler: { (_) in + performAction(status: status, completion: { (status) in + DispatchQueue.main.async { + vc.tableView.reloadData() + } + }) + })) + alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { (_) in + session.complete(with: .cancel) + })) + DispatchQueue.main.async { + presentModally(alertController, animated: true) + } + } + } + + if let id = url.arguments["statusID"] { + MastodonCache.status(for: id) { (status) in + if let status = status { + favorite(status) + } + } + } else if let searchQuery = url.arguments["statusURL"] ?? url.arguments["statusURI"] { + let request = MastodonController.shared.client.search(query: searchQuery) + MastodonController.shared.client.run(request) { (response) in + if case let .success(results, _) = response, + let status = results.statuses.first { + favorite(status) + } else { + session.complete(with: .error, additionalData: [ + "error": "Could not find status by searching '\(searchQuery)'" + ]) + } + } + } else { + session.complete(with: .error, additionalData: [ + "error": "No status provided. Specify either instance-local statusID or remote statusURL/statusURI" + ]) } } diff --git a/Tusker/XCallbackURL/XCBManager.swift b/Tusker/XCallbackURL/XCBManager.swift index ad7340f4..ed90bfa2 100644 --- a/Tusker/XCallbackURL/XCBManager.swift +++ b/Tusker/XCallbackURL/XCBManager.swift @@ -13,6 +13,7 @@ class XCBManager { static var specs: [XCallbackURLSpec] = [ // Statuses XCallbackURLSpec(type: .postStatus, arguments: ["mentioning": true, "text": true], canRunSilently: true, action: XCBActions.postStatus), + XCallbackURLSpec(type: .favoriteStatus, arguments: ["statusID": true, "statusURL": true, "statusURI": true], canRunSilently: true, action: XCBActions.favoriteStatus), // Accounts XCallbackURLSpec(type: .getCurrentUser, arguments: [:], canRunSilently: false, action: XCBActions.getCurrentUser) ]