Merge branch 'master' of github.com:shadowfacts/Tusker
This commit is contained in:
commit
369ed24997
|
@ -1,4 +1,5 @@
|
|||
.DS_Store
|
||||
MyPlayground.playground/
|
||||
|
||||
### Swift ###
|
||||
# Xcode
|
||||
|
|
|
@ -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
|
|
@ -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>
|
|
@ -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 }))
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,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)"
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
])
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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,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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue