diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 7cb9d9543a..8bd1a80db5 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 529726eb6d..207e3d01f9 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 b33547d265..0abb4b2d5d 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 0000000000..15cb739919 --- /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 53c5816a9a..7bc1ce0a18 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 8d51eff2ee..80c4c55884 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 129d233b19..34d7b4bdee 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) }