forked from shadowfacts/Tusker
Add initial x-callback-url implementation
This commit is contained in:
parent
0d902f7d37
commit
d74f86418e
|
@ -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 = "<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>"; };
|
||||
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; };
|
||||
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>"; };
|
||||
|
@ -566,6 +572,16 @@
|
|||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D6757A7A2157E00100721E32 /* XCallbackURL */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D6757A7B2157E01900721E32 /* XCBManager.swift */,
|
||||
D6757A812157E8FA00721E32 /* XCBSession.swift */,
|
||||
D6757A7D2157E02600721E32 /* XCallbackURL.swift */,
|
||||
);
|
||||
path = XCallbackURL;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 */,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -2,12 +2,6 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>Save photos directly from other people'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>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
|
@ -39,6 +33,12 @@
|
|||
<string>1</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<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>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
|
|
|
@ -11,16 +11,18 @@ import Pachyderm
|
|||
|
||||
class MastodonCache {
|
||||
|
||||
private static let statuses = NSCache<NSString, Status>()
|
||||
private static let accounts = NSCache<NSString, Account>()
|
||||
// private static let statuses = NSDictionary<NSString, Status>()
|
||||
// private static let accounts = NSDictionary<NSString, Account>()
|
||||
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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue