Add initial x-callback-url implementation

This commit is contained in:
Shadowfacts 2018-09-23 12:01:05 -04:00
parent 48923977c5
commit 97fe450d1f
10 changed files with 234 additions and 21 deletions

View File

@ -92,6 +92,9 @@
D667E5F32135BC260057A976 /* Conversation.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D667E5F22135BC260057A976 /* Conversation.storyboard */; }; D667E5F32135BC260057A976 /* Conversation.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D667E5F22135BC260057A976 /* Conversation.storyboard */; };
D667E5F52135BCD50057A976 /* ConversationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F42135BCD50057A976 /* ConversationViewController.swift */; }; D667E5F52135BCD50057A976 /* ConversationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F42135BCD50057A976 /* ConversationViewController.swift */; };
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F72135C3040057A976 /* Mastodon+Equatable.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, ); }; }; 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 */; }; D6BED174212667E900F02DA0 /* StatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BED173212667E900F02DA0 /* StatusTableViewCell.swift */; };
D6C94D852139DFD800CB5196 /* LargeImage.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D6C94D842139DFD800CB5196 /* LargeImage.storyboard */; }; 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 = "<group>"; }; D667E5F22135BC260057A976 /* Conversation.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Conversation.storyboard; sourceTree = "<group>"; };
D667E5F42135BCD50057A976 /* ConversationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationViewController.swift; sourceTree = "<group>"; }; D667E5F42135BCD50057A976 /* ConversationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationViewController.swift; sourceTree = "<group>"; };
D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Equatable.swift"; sourceTree = "<group>"; }; D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Equatable.swift"; sourceTree = "<group>"; };
D6757A7B2157E01900721E32 /* XCBManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBManager.swift; sourceTree = "<group>"; };
D6757A7D2157E02600721E32 /* XCallbackURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCallbackURL.swift; sourceTree = "<group>"; };
D6757A812157E8FA00721E32 /* XCBSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBSession.swift; sourceTree = "<group>"; };
D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SwiftSoup.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = "<group>"; }; D6BED173212667E900F02DA0 /* StatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTableViewCell.swift; sourceTree = "<group>"; };
D6C94D842139DFD800CB5196 /* LargeImage.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LargeImage.storyboard; sourceTree = "<group>"; }; D6C94D842139DFD800CB5196 /* LargeImage.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LargeImage.storyboard; sourceTree = "<group>"; };
@ -566,6 +572,16 @@
path = Extensions; path = Extensions;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
D6757A7A2157E00100721E32 /* XCallbackURL */ = {
isa = PBXGroup;
children = (
D6757A7B2157E01900721E32 /* XCBManager.swift */,
D6757A812157E8FA00721E32 /* XCBSession.swift */,
D6757A7D2157E02600721E32 /* XCallbackURL.swift */,
);
path = XCallbackURL;
sourceTree = "<group>";
};
D6BED1722126661300F02DA0 /* Views */ = { D6BED1722126661300F02DA0 /* Views */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -616,6 +632,7 @@
D64D0AAC2128D88B005A6F37 /* LocalData.swift */, D64D0AAC2128D88B005A6F37 /* LocalData.swift */,
04DACE8D212CC7CC009840C4 /* AvatarCache.swift */, 04DACE8D212CC7CC009840C4 /* AvatarCache.swift */,
D6028B9A2150811100F223B9 /* MastodonCache.swift */, D6028B9A2150811100F223B9 /* MastodonCache.swift */,
D6757A7A2157E00100721E32 /* XCallbackURL */,
D663626021360A9600C9CBA2 /* Preferences */, D663626021360A9600C9CBA2 /* Preferences */,
D667E5F62135C2ED0057A976 /* Extensions */, D667E5F62135C2ED0057A976 /* Extensions */,
D6F953F121251A2F00CF0F2B /* Controllers */, D6F953F121251A2F00CF0F2B /* Controllers */,
@ -921,6 +938,7 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
D6757A822157E8FA00721E32 /* XCBSession.swift in Sources */,
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */, 04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */,
D667E5F52135BCD50057A976 /* ConversationViewController.swift in Sources */, D667E5F52135BCD50057A976 /* ConversationViewController.swift in Sources */,
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */, D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */,
@ -956,7 +974,9 @@
D667E5EB21349EF80057A976 /* ProfileHeaderTableViewCell.swift in Sources */, D667E5EB21349EF80057A976 /* ProfileHeaderTableViewCell.swift in Sources */,
D641C77D213CB024004B4513 /* FollowNotificationTableViewCell.swift in Sources */, D641C77D213CB024004B4513 /* FollowNotificationTableViewCell.swift in Sources */,
D641C773213CAA25004B4513 /* NotificationsTableViewController.swift in Sources */, D641C773213CAA25004B4513 /* NotificationsTableViewController.swift in Sources */,
D6757A7C2157E01900721E32 /* XCBManager.swift in Sources */,
D64A0CD32132153900640E3B /* HTMLContentLabel.swift in Sources */, D64A0CD32132153900640E3B /* HTMLContentLabel.swift in Sources */,
D6757A7E2157E02600721E32 /* XCallbackURL.swift in Sources */,
D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */, D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */,
D663625F2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift in Sources */, D663625F2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift in Sources */,
D667E5E721349D4C0057A976 /* ProfileTableViewController.swift in Sources */, D667E5E721349D4C0057A976 /* ProfileTableViewController.swift in Sources */,

View File

@ -26,6 +26,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
return true 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) { 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. // 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. // 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 { extension AppDelegate: OnboardingViewControllerDelegate {
func showOnboarding() { func showOnboarding() {
if let window = self.window, if let window = self.window,
let onboardingViewController = UIStoryboard(name: "Onboarding", bundle: nil).instantiateInitialViewController() as? OnboardingViewController { let onboardingViewController = UIStoryboard(name: "Onboarding", bundle: nil).instantiateInitialViewController() as? OnboardingViewController {
@ -73,6 +79,4 @@ extension AppDelegate: OnboardingViewControllerDelegate {
hideOnboarding() hideOnboarding()
LocalData.shared.onboardingComplete = true LocalData.shared.onboardingComplete = true
} }
} }

View File

@ -2,12 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Save photos directly from other people&apos;s posts.</string>
<key>NSCameraUsageDescription</key>
<string>Post photos from the camera.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Post photos from the photo library.</string>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
@ -39,6 +33,12 @@
<string>1</string> <string>1</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>NSCameraUsageDescription</key>
<string>Post photos from the camera.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Save photos directly from other people's posts.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Post photos from the photo library.</string>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>

View File

@ -11,16 +11,18 @@ import Pachyderm
class MastodonCache { class MastodonCache {
private static let statuses = NSCache<NSString, Status>() // private static let statuses = NSDictionary<NSString, Status>()
private static let accounts = NSCache<NSString, Account>() // private static let accounts = NSDictionary<NSString, Account>()
private static var statuses = [String: Status]()
private static var accounts = [String: Account]()
// MARK: - Statuses // MARK: - Statuses
static func status(for id: String) -> Status? { 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) { static func set(status: Status, for id: String) {
statuses.setObject(status, forKey: id as NSString) statuses[id] = status
add(account: status.account) add(account: status.account)
if let reblog = status.reblog { if let reblog = status.reblog {
add(account: reblog.account) add(account: reblog.account)
@ -49,11 +51,11 @@ class MastodonCache {
// MARK: - Accounts // MARK: - Accounts
static func account(for id: String) -> Account? { 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) { 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) { static func account(for id: String, completion: @escaping (Account?) -> Void) {

View File

@ -11,11 +11,21 @@ import Pachyderm
class ComposeViewController: UIViewController { 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, guard let navigationController = UIStoryboard(name: "Compose", bundle: nil).instantiateInitialViewController() as? UINavigationController,
let composeVC = navigationController.topViewController as? ComposeViewController else { fatalError() } let composeVC = navigationController.topViewController as? ComposeViewController else { fatalError() }
composeVC.inReplyToID = inReplyToID 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 return navigationController
} }
@ -37,7 +47,10 @@ class ComposeViewController: UIViewController {
var scrolled = false var scrolled = false
var inReplyToID: String? var inReplyToID: String?
var mentioning: Account? var mentioningAcct: String?
var text: String?
var xcbSession: XCBSession?
var contentWarning = false { var contentWarning = false {
didSet { didSet {
@ -95,8 +108,12 @@ class ComposeViewController: UIViewController {
inReplyToContainerView.isHidden = true inReplyToContainerView.isHidden = true
} }
if let mentioning = mentioning { if let mentioningAcct = mentioningAcct {
statusTextView.text += "@\(mentioning.acct) " statusTextView.text += "@\(mentioningAcct) "
statusTextView.textViewDidChange(statusTextView)
}
if let text = text {
statusTextView.text += text
statusTextView.textViewDidChange(statusTextView) statusTextView.textViewDidChange(statusTextView)
} }
@ -128,6 +145,8 @@ class ComposeViewController: UIViewController {
let navController = dest.selectedViewController as? UINavigationController, let navController = dest.selectedViewController as? UINavigationController,
let topVC = navController.topViewController as? StatusTableViewCellDelegate else { return } let topVC = navController.topViewController as? StatusTableViewCellDelegate else { return }
topVC.selected(status: status.id) topVC.selected(status: status.id)
} else if segue.identifier == "cancel" {
xcbSession?.complete(with: .cancel)
} }
} }
@ -232,6 +251,8 @@ class ComposeViewController: UIViewController {
DispatchQueue.main.async { DispatchQueue.main.async {
self.progressView.step() self.progressView.step()
self.performSegue(withIdentifier: "postComplete", sender: self) self.performSegue(withIdentifier: "postComplete", sender: self)
self.xcbSession?.complete(with: .success)
} }
} }
} }

View File

@ -74,7 +74,7 @@ class ProfileTableViewController: UITableViewController, PreferencesAdaptive {
func sendMessageMentioning() { func sendMessageMentioning() {
guard let account = MastodonCache.account(for: accountID) else { fatalError("Missing cached account \(accountID!)") } 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) present(vc, animated: true)
} }

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}
}