Merge branch 'master' of github.com:shadowfacts/Tusker

This commit is contained in:
Shadowfacts 2018-09-24 08:51:20 -04:00
commit 3ff80b238e
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
18 changed files with 457 additions and 95 deletions

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
.DS_Store
MyPlayground.playground/
### Swift ###
# Xcode

View File

@ -1,58 +0,0 @@
import UIKit
class Client {
func test<A>(_ 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<Any> {
// 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

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<playground version='5.0' target-platform='ios' executeOnSourceChanges='false'>
<timeline fileName='timeline.xctimeline'/>
</playground>

View File

@ -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<Status> {
return Request<Status>(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 }))
}

View File

@ -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 */; };
@ -92,6 +93,10 @@
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 */; };
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 */; };
@ -224,6 +229,7 @@
D64D0AAC2128D88B005A6F37 /* LocalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalData.swift; sourceTree = "<group>"; };
D64D0AAE2128D954005A6F37 /* Onboarding.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Onboarding.storyboard; sourceTree = "<group>"; };
D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = "<group>"; };
D64F80E1215875CC00BEF393 /* XCBActionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBActionType.swift; sourceTree = "<group>"; };
D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewSwipeActionProvider.swift; sourceTree = "<group>"; };
D663625C2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ConversationMainStatusTableViewCell.xib; sourceTree = "<group>"; };
D663625E2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationMainStatusTableViewCell.swift; sourceTree = "<group>"; };
@ -247,6 +253,10 @@
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>"; };
D679C09E215850EF00DA27FE /* XCBActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBActions.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 +576,18 @@
path = Extensions;
sourceTree = "<group>";
};
D6757A7A2157E00100721E32 /* XCallbackURL */ = {
isa = PBXGroup;
children = (
D6757A7B2157E01900721E32 /* XCBManager.swift */,
D6757A7D2157E02600721E32 /* XCallbackURL.swift */,
D6757A812157E8FA00721E32 /* XCBSession.swift */,
D64F80E1215875CC00BEF393 /* XCBActionType.swift */,
D679C09E215850EF00DA27FE /* XCBActions.swift */,
);
path = XCallbackURL;
sourceTree = "<group>";
};
D6BED1722126661300F02DA0 /* Views */ = {
isa = PBXGroup;
children = (
@ -616,6 +638,7 @@
D64D0AAC2128D88B005A6F37 /* LocalData.swift */,
04DACE8D212CC7CC009840C4 /* AvatarCache.swift */,
D6028B9A2150811100F223B9 /* MastodonCache.swift */,
D6757A7A2157E00100721E32 /* XCallbackURL */,
D663626021360A9600C9CBA2 /* Preferences */,
D667E5F62135C2ED0057A976 /* Extensions */,
D6F953F121251A2F00CF0F2B /* Controllers */,
@ -767,7 +790,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1000;
LastUpgradeCheck = 1000;
LastUpgradeCheck = 1010;
ORGANIZATIONNAME = Shadowfacts;
TargetAttributes = {
D61099AA2144B0CC00432DC2 = {
@ -921,6 +944,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 */,
@ -933,6 +957,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 */,
@ -949,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 */,
@ -956,7 +982,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 */,
@ -1034,7 +1062,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 +1092,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;

View File

@ -1,9 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:MyPlayground.playground">
</FileRef>
<FileRef
location = "container:Tusker.xcodeproj">
</FileRef>

View File

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

View File

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

View File

@ -11,17 +11,20 @@ import Pachyderm
class MastodonCache {
private static let statuses = NSCache<NSString, Status>()
private static let accounts = NSCache<NSString, Account>()
private static let relationships = NSCache<NSString, Relationship>()
// private static let statuses = NSDictionary<NSString, Status>()
// private static let accounts = NSDictionary<NSString, Account>()
// private static let relationships = NSCache<NSString, Relationship>()
private static var statuses = [String: Status]()
private static var accounts = [String: Account]()
private static var relationships = [String: Relationship]()
// 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)
@ -50,11 +53,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) {
@ -79,11 +82,11 @@ class MastodonCache {
// MARK: - Relationships
static func relationship(for id: String) -> Relationship? {
return relationships.object(forKey: id as NSString)
return relationships[id]
}
static func set(relationship: Relationship, id: String) {
relationships.setObject(relationship, forKey: id as NSString)
relationships[id] = relationship
}
static func relationship(for id: String, completion: @escaping (Relationship?) -> Void) {

View File

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

View File

@ -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,11 @@ class ComposeViewController: UIViewController {
var scrolled = false
var inReplyToID: String?
var mentioning: Account?
var mentioningAcct: String?
var text: String?
// 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 {
@ -95,8 +109,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 +146,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)
}
}
@ -224,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
@ -232,6 +252,11 @@ class ComposeViewController: UIViewController {
DispatchQueue.main.async {
self.progressView.step()
self.performSegue(withIdentifier: "postComplete", sender: self)
self.xcbSession?.complete(with: .success, additionalData: [
"statusURL": status.url?.absoluteString,
"statusURI": status.uri
])
}
}
}

View File

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

View File

@ -0,0 +1,21 @@
//
// XCBActionType.swift
// Tusker
//
// Created by Shadowfacts on 9/23/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
enum XCBActionType: String {
// Statuses
case postStatus
case favoriteStatus
// Accounts
case getCurrentUser
var path: String {
return "/\(rawValue)"
}
}

View File

@ -0,0 +1,136 @@
//
// XCBActions.swift
// Tusker
//
// Created by Shadowfacts on 9/23/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
struct XCBActions {
// 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"]
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, visibility: Preferences.shared.defaultPostVisibility)
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
])
}
}
} else {
let vc = ComposeViewController.create(for: session, mentioning: mentioning, text: text)
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"
])
}
}
// 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,
])
}
}

View File

@ -0,0 +1,38 @@
//
// XCBManager.swift
// Tusker
//
// Created by Shadowfacts on 9/23/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import UIKit
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)
]
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(url: xcbURL)
}
return false
}
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
}
}

View File

@ -0,0 +1,43 @@
//
// XCBSession.swift
// Tusker
//
// Created by Shadowfacts on 9/23/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import UIKit
class XCBSession {
let type: XCBActionType
let success: URL?
let error: URL?
let cancel: URL?
init(type: XCBActionType, success: URL?, error: URL?, cancel: URL?) {
self.type = type
self.success = success
self.error = error
self.cancel = cancel
}
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!
}
DispatchQueue.main.async {
UIApplication.shared.open(url, options: [:])
}
}
}
}
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,107 @@
//
// XCallbackURL.swift
// Tusker
//
// Created by Shadowfacts on 9/23/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import UIKit
typealias XCBAction = (_ url: XCallbackURL, _ session: XCBSession, _ silent: Bool?) -> Void
struct XCallbackURLSpec {
let path: String
let type: XCBActionType
let arguments: [String: Bool]
let canRunSilently: Bool
let action: XCBAction
init(type: XCBActionType, arguments: [String: Bool], canRunSilently: Bool, action: @escaping XCBAction) {
self.path = type.path
self.type = type
self.canRunSilently = canRunSilently
self.action = action
var arguments = arguments
if canRunSilently {
arguments["silent"] = true
}
self.arguments = arguments
}
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
}
}
struct XCallbackURL {
let path: String
let arguments: [String: String]
let silent: Bool
let source: 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
}
})
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) }
cancel = queryItems.first(where: { $0.name == "x-cancel" }).flatMap { $0.value }.flatMap { URL(string: $0) }
}
}
extension XCallbackURLSpec {
func matches(_ components: URLComponents) -> Bool {
guard path == components.path else { return false }
for (name, optional) in arguments {
if (!optional && components.queryItems?.first(where: { $0.name == name }) == nil) {
return false
}
}
return true
}
}