Test with simple swift promises
See https://shadowfacts.net/2020/simple-swift-promises/
This commit is contained in:
parent
8be7480755
commit
279b7868e3
|
@ -7,6 +7,7 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
/**
|
||||
The base Mastodon API client.
|
||||
|
@ -80,6 +81,43 @@ public class Client {
|
|||
task.resume()
|
||||
}
|
||||
|
||||
public func run<Result>(_ request: Request<Result>) -> Promise<(Result, Pagination?)> {
|
||||
return Promise { (resolve, reject) in
|
||||
self.run(request) { (response) in
|
||||
switch response {
|
||||
case let .success(result, pagination):
|
||||
resolve((result, pagination))
|
||||
case let .failure(error):
|
||||
reject(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func run<Result: Decodable>(_ request: Request<Result>) -> AnyPublisher<(Result, Pagination?), Swift.Error> {
|
||||
guard let request = createURLRequest(request: request) else {
|
||||
return Fail(error: Error.invalidRequest).eraseToAnyPublisher()
|
||||
}
|
||||
return session.dataTaskPublisher(for: request)
|
||||
.mapError { Error.urlError($0) }
|
||||
.tryMap {
|
||||
guard let response = $0.response as? HTTPURLResponse else {
|
||||
throw Error.invalidResponse
|
||||
}
|
||||
guard response.statusCode == 200 else {
|
||||
if let mastodonError = try? self.decoder.decode(MastodonError.self, from: $0.data) {
|
||||
throw Error.mastodonError(mastodonError.description)
|
||||
} else {
|
||||
throw Error.unknownError
|
||||
}
|
||||
}
|
||||
let result = try self.decoder.decode(Result.self, from: $0.data)
|
||||
let pagination = response.allHeaderFields["Link"].flatMap { $0 as? String }.flatMap(Pagination.init)
|
||||
return (result, pagination)
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func createURLRequest<Result>(request: Request<Result>) -> URLRequest? {
|
||||
guard var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: true) else { return nil }
|
||||
components.path = request.path
|
||||
|
@ -325,6 +363,6 @@ extension Client {
|
|||
case invalidResponse
|
||||
case invalidModel
|
||||
case mastodonError(String)
|
||||
|
||||
case urlError(URLError)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,170 @@
|
|||
//
|
||||
// Promise.swift
|
||||
// Pachyderm
|
||||
//
|
||||
// Created by Shadowfacts on 2/14/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public class Promise<Result> {
|
||||
private var handlers: [(Result) -> Void] = []
|
||||
private var result: Result?
|
||||
private var catchers: [(Error) -> Void] = []
|
||||
private var error: Error?
|
||||
|
||||
func resolve(_ result: Result) {
|
||||
self.result = result
|
||||
self.handlers.forEach { $0(result) }
|
||||
}
|
||||
|
||||
func reject(_ error: Error) {
|
||||
self.error = error
|
||||
self.catchers.forEach { $0(error) }
|
||||
}
|
||||
|
||||
func addHandler(_ handler: @escaping (Result) -> Void) {
|
||||
if let result = result {
|
||||
handler(result)
|
||||
} else {
|
||||
handlers.append(handler)
|
||||
}
|
||||
}
|
||||
|
||||
func addCatcher(_ catcher: @escaping (Error) -> Void) {
|
||||
if let error = error {
|
||||
catcher(error)
|
||||
} else {
|
||||
catchers.append(catcher)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension Promise {
|
||||
static func resolve<Result>(_ value: Result) -> Promise<Result> {
|
||||
let promise = Promise<Result>()
|
||||
promise.resolve(value)
|
||||
return promise
|
||||
}
|
||||
|
||||
static func reject<Result>(_ error: Error) -> Promise<Result> {
|
||||
let promise = Promise<Result>()
|
||||
promise.reject(error)
|
||||
return promise
|
||||
}
|
||||
|
||||
static func all<Result>(_ promises: [Promise<Result>], queue: DispatchQueue = .main) -> Promise<[Result]> {
|
||||
let group = DispatchGroup()
|
||||
|
||||
var results = [Result?](repeating: nil, count: promises.count)
|
||||
var firstError: Error?
|
||||
|
||||
for (index, promise) in promises.enumerated() {
|
||||
group.enter()
|
||||
promise.then { (res) in
|
||||
queue.async {
|
||||
results[index] = res
|
||||
group.leave()
|
||||
}
|
||||
}.catch { (err) -> Void in
|
||||
if firstError == nil {
|
||||
firstError = err
|
||||
}
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
|
||||
return Promise<[Result]> { (resolve, reject) in
|
||||
group.notify(queue: queue) {
|
||||
if let firstError = firstError {
|
||||
reject(firstError)
|
||||
} else {
|
||||
resolve(results.compactMap { $0 })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
convenience init(resultProvider: @escaping (_ resolve: @escaping (Result) -> Void, _ reject: @escaping (Error) -> Void) -> Void) {
|
||||
self.init()
|
||||
resultProvider(self.resolve, self.reject)
|
||||
}
|
||||
|
||||
convenience init<ErrorType>(_ resultProvider: @escaping ((Swift.Result<Result, ErrorType>) -> Void) -> Void) {
|
||||
self.init { (resolve, reject) in
|
||||
resultProvider { (result) in
|
||||
switch result {
|
||||
case let .success(res):
|
||||
resolve(res)
|
||||
case let .failure(error):
|
||||
reject(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func then(_ func: @escaping (Result) -> Void) -> Promise<Result> {
|
||||
addHandler(`func`)
|
||||
return self
|
||||
}
|
||||
|
||||
func then<Next>(_ mapper: @escaping (Result) -> Promise<Next>) -> Promise<Next> {
|
||||
let next = Promise<Next>()
|
||||
addHandler { (parentResult) in
|
||||
let newPromise = mapper(parentResult)
|
||||
newPromise.addHandler(next.resolve)
|
||||
newPromise.addCatcher(next.reject)
|
||||
}
|
||||
addCatcher(next.reject)
|
||||
return next
|
||||
}
|
||||
|
||||
func then<Next>(_ mapper: @escaping (Result) -> Next) -> Promise<Next> {
|
||||
let next = Promise<Next>()
|
||||
addHandler { (parentResult) in
|
||||
let newResult = mapper(parentResult)
|
||||
next.resolve(newResult)
|
||||
}
|
||||
addCatcher(next.reject)
|
||||
return next
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func `catch`(_ catcher: @escaping (Error) -> Void) -> Promise<Result> {
|
||||
addCatcher(catcher)
|
||||
return self
|
||||
}
|
||||
|
||||
func `catch`(_ catcher: @escaping (Error) -> Promise<Result>) -> Promise<Result> {
|
||||
let next = Promise<Result>()
|
||||
addHandler(next.resolve)
|
||||
addCatcher { (error) in
|
||||
let newPromise = catcher(error)
|
||||
newPromise.addHandler(next.resolve)
|
||||
newPromise.addCatcher(next.reject)
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
func `catch`(_ catcher: @escaping (Error) -> Result) -> Promise<Result> {
|
||||
let next = Promise<Result>()
|
||||
addHandler(next.resolve)
|
||||
addCatcher { (error) in
|
||||
let newResult = catcher(error)
|
||||
next.resolve(newResult)
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
func handle(on queue: DispatchQueue) -> Promise<Result> {
|
||||
return self.then { (result) in
|
||||
return Promise { (resolve, reject) in
|
||||
queue.async {
|
||||
resolve(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
//
|
||||
// PromiseTests.swift
|
||||
// PachydermTests
|
||||
//
|
||||
// Created by Shadowfacts on 2/14/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Pachyderm
|
||||
|
||||
class PromiseTests: XCTestCase {
|
||||
|
||||
override func setUpWithError() throws {
|
||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
func assertResultEqual<Result: Equatable>(_ promise: Promise<Result>, _ value: Result, message: String? = nil) {
|
||||
let expectation = self.expectation(description: message ?? "promise result assertion")
|
||||
promise.then {
|
||||
XCTAssertEqual($0, value)
|
||||
expectation.fulfill()
|
||||
}
|
||||
self.waitForExpectations(timeout: 2) { (error) in
|
||||
if let error = error {
|
||||
XCTFail("didn't resolve promise: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testResolveImmediate() {
|
||||
assertResultEqual(Promise<String>.resolve("blah"), "blah")
|
||||
}
|
||||
|
||||
func testResolveImmediateMapped() {
|
||||
let promise = Promise<String>.resolve("foo").then {
|
||||
"test \($0)"
|
||||
}.then {
|
||||
Promise<String>.resolve("\($0) bar")
|
||||
}
|
||||
assertResultEqual(promise, "test foo bar")
|
||||
}
|
||||
|
||||
func testContinueAfterReject() {
|
||||
let promise = Promise<String>.reject(TestError()).then { (res) in
|
||||
XCTFail("then on rejected promise is unreachable")
|
||||
}.catch { (error) -> String in
|
||||
XCTAssertTrue(error is TestError)
|
||||
return "caught"
|
||||
}.then {
|
||||
"\($0) error"
|
||||
}
|
||||
assertResultEqual(promise, "caught error")
|
||||
}
|
||||
|
||||
func testResolveDelayed() {
|
||||
let promise = Promise<String> { (resolve, reject) in
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
resolve("blah")
|
||||
}
|
||||
}
|
||||
assertResultEqual(promise, "blah")
|
||||
}
|
||||
|
||||
func testResolveMappedDelayed() {
|
||||
let promise = Promise<String> { (resolve, reject) in
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
resolve("foo")
|
||||
}
|
||||
}.then {
|
||||
"\($0) bar"
|
||||
}.then { (result) in
|
||||
Promise<String> { (resolve, reject) in
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
resolve("\(result) baz")
|
||||
}
|
||||
}
|
||||
}
|
||||
assertResultEqual(promise, "foo bar baz")
|
||||
}
|
||||
|
||||
func testResolveAll() {
|
||||
let promise = Promise<[String]>.all([
|
||||
Promise<String>.resolve("a"),
|
||||
Promise<String>.resolve("b"),
|
||||
Promise<String>.resolve("c"),
|
||||
])
|
||||
assertResultEqual(promise, ["a", "b", "c"])
|
||||
}
|
||||
|
||||
func testIntermediateReject() {
|
||||
let promise = Promise<String>.resolve("foo").then { (_) -> Promise<String> in
|
||||
Promise<String>.reject(TestError())
|
||||
}.catch { (error) -> String in
|
||||
XCTAssertTrue(error is TestError)
|
||||
return "caught"
|
||||
}.then { (result) -> String in
|
||||
"\(result) error"
|
||||
}
|
||||
assertResultEqual(promise, "caught error")
|
||||
}
|
||||
|
||||
func testResultHelper() {
|
||||
let success = Promise<String> { (handler) in
|
||||
handler(Result<String, Never>.success("asdf"))
|
||||
}
|
||||
assertResultEqual(success, "asdf")
|
||||
let failure = Promise<String> { (handler) in
|
||||
handler(Result<String, TestError>.failure(TestError()))
|
||||
}.catch { (error) -> String in
|
||||
"blah"
|
||||
}
|
||||
assertResultEqual(failure, "blah")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct TestError: Error {
|
||||
var localizedDescription: String {
|
||||
"test error"
|
||||
}
|
||||
}
|
|
@ -156,6 +156,8 @@
|
|||
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */; };
|
||||
D67C57B221E28FAD00C3118B /* ComposeStatusReplyView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D67C57B121E28FAD00C3118B /* ComposeStatusReplyView.xib */; };
|
||||
D67C57B421E2910700C3118B /* ComposeStatusReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */; };
|
||||
D683418623F79BFC00D06703 /* Promise.swift in Sources */ = {isa = PBXBuildFile; fileRef = D683418523F79BFC00D06703 /* Promise.swift */; };
|
||||
D683418A23F7A3BB00D06703 /* PromiseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D683418923F7A3BB00D06703 /* PromiseTests.swift */; };
|
||||
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */; };
|
||||
D6945C2F23AC47C3005C403C /* SavedDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C2E23AC47C3005C403C /* SavedDataManager.swift */; };
|
||||
D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */; };
|
||||
|
@ -431,6 +433,8 @@
|
|||
D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Uniques.swift"; sourceTree = "<group>"; };
|
||||
D67C57B121E28FAD00C3118B /* ComposeStatusReplyView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ComposeStatusReplyView.xib; sourceTree = "<group>"; };
|
||||
D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusReplyView.swift; sourceTree = "<group>"; };
|
||||
D683418523F79BFC00D06703 /* Promise.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Promise.swift; sourceTree = "<group>"; };
|
||||
D683418923F7A3BB00D06703 /* PromiseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromiseTests.swift; sourceTree = "<group>"; };
|
||||
D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedPageViewController.swift; sourceTree = "<group>"; };
|
||||
D6945C2E23AC47C3005C403C /* SavedDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedDataManager.swift; sourceTree = "<group>"; };
|
||||
D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineViewController.swift; sourceTree = "<group>"; };
|
||||
|
@ -594,6 +598,7 @@
|
|||
D61099AD2144B0CC00432DC2 /* Pachyderm.h */,
|
||||
D61099AE2144B0CC00432DC2 /* Info.plist */,
|
||||
D61099C82144B13C00432DC2 /* Client.swift */,
|
||||
D683418523F79BFC00D06703 /* Promise.swift */,
|
||||
D6109A0A2145953C00432DC2 /* ClientModel.swift */,
|
||||
D6A3BC7223218C6E00FD64D5 /* Utilities */,
|
||||
D61099D72144B74500432DC2 /* Extensions */,
|
||||
|
@ -609,6 +614,7 @@
|
|||
children = (
|
||||
D61099BA2144B0CC00432DC2 /* PachydermTests.swift */,
|
||||
D6E6F26421604242006A8599 /* CharacterCounterTests.swift */,
|
||||
D683418923F7A3BB00D06703 /* PromiseTests.swift */,
|
||||
D61099BC2144B0CC00432DC2 /* Info.plist */,
|
||||
);
|
||||
path = PachydermTests;
|
||||
|
@ -1537,6 +1543,7 @@
|
|||
D61099E92145658300432DC2 /* Card.swift in Sources */,
|
||||
D61099F32145688600432DC2 /* Mention.swift in Sources */,
|
||||
D6109A0F21459B6900432DC2 /* Pagination.swift in Sources */,
|
||||
D683418623F79BFC00D06703 /* Promise.swift in Sources */,
|
||||
D6109A032145722C00432DC2 /* RegisteredApplication.swift in Sources */,
|
||||
D6109A0921458C4A00432DC2 /* Empty.swift in Sources */,
|
||||
D6285B4F21EA695800FE4B39 /* StatusContentType.swift in Sources */,
|
||||
|
@ -1562,6 +1569,7 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D683418A23F7A3BB00D06703 /* PromiseTests.swift in Sources */,
|
||||
D6E6F26521604242006A8599 /* CharacterCounterTests.swift in Sources */,
|
||||
D61099BB2144B0CC00432DC2 /* PachydermTests.swift in Sources */,
|
||||
);
|
||||
|
|
|
@ -28,14 +28,11 @@ class FollowAccountActivity: AccountActivity {
|
|||
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
|
||||
|
||||
let request = Account.follow(account.id)
|
||||
mastodonController.run(request) { (response) in
|
||||
if case let .success(relationship, _) = response {
|
||||
self.mastodonController.cache.add(relationship: relationship)
|
||||
} else {
|
||||
// todo: display error message
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||
fatalError()
|
||||
}
|
||||
mastodonController.run(request).then { (relationship, _) -> Void in
|
||||
self.mastodonController.cache.add(relationship: relationship)
|
||||
}.catch { (error) -> Void in
|
||||
print("could not follow account")
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -28,14 +28,11 @@ class UnfollowAccountActivity: AccountActivity {
|
|||
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
|
||||
|
||||
let request = Account.unfollow(account.id)
|
||||
mastodonController.run(request) { (response) in
|
||||
if case let .success(relationship, _) = response {
|
||||
self.mastodonController.cache.add(relationship: relationship)
|
||||
} else {
|
||||
// todo: display error message
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||
fatalError()
|
||||
}
|
||||
mastodonController.run(request).then { (relationship, _) -> Void in
|
||||
self.mastodonController.cache.add(relationship: relationship)
|
||||
}.catch { (error) -> Void in
|
||||
print("could not unfollow account: \(error)")
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -27,14 +27,11 @@ class BookmarkStatusActivity: StatusActivity {
|
|||
guard let status = status else { return }
|
||||
|
||||
let request = Status.bookmark(status)
|
||||
mastodonController.run(request) { (response) in
|
||||
if case let .success(status, _) = response {
|
||||
self.mastodonController.cache.add(status: status)
|
||||
} else {
|
||||
// todo: display error message
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||
fatalError()
|
||||
}
|
||||
mastodonController.run(request).then { (status, _) -> Void in
|
||||
self.mastodonController.cache.add(status: status)
|
||||
}.catch { (error) -> Void in
|
||||
print("could not bookmark status: \(error)")
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -26,14 +26,11 @@ class PinStatusActivity: StatusActivity {
|
|||
guard let status = status else { return }
|
||||
|
||||
let request = Status.pin(status)
|
||||
mastodonController.run(request) { (response) in
|
||||
if case let .success(status, _) = response {
|
||||
self.mastodonController.cache.add(status: status)
|
||||
} else {
|
||||
// todo: display error message
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||
fatalError()
|
||||
}
|
||||
mastodonController.run(request).then { (status, _) -> Void in
|
||||
self.mastodonController.cache.add(status: status)
|
||||
}.catch { (error) -> Void in
|
||||
print("could not pin status: \(error)")
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,14 +27,11 @@ class UnbookmarkStatusActivity: StatusActivity {
|
|||
guard let status = status else { return }
|
||||
|
||||
let request = Status.unbookmark(status)
|
||||
mastodonController.run(request) { (response) in
|
||||
if case let .success(status, _) = response {
|
||||
self.mastodonController.cache.add(status: status)
|
||||
} else {
|
||||
// todo: display error message
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||
fatalError()
|
||||
}
|
||||
mastodonController.run(request).then { (status, _) -> Void in
|
||||
self.mastodonController.cache.add(status: status)
|
||||
}.catch { (error) -> Void in
|
||||
print("could not unbookmark status: \(error)")
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -26,14 +26,11 @@ class UnpinStatusActivity: StatusActivity {
|
|||
guard let status = status else { return }
|
||||
|
||||
let request = Status.unpin(status)
|
||||
mastodonController.run(request) { (response) in
|
||||
if case let .success(status, _) = response {
|
||||
self.mastodonController.cache.add(status: status)
|
||||
} else {
|
||||
// todo: display error message
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||
fatalError()
|
||||
}
|
||||
mastodonController.run(request).then { (status, _) -> Void in
|
||||
self.mastodonController.cache.add(status: status)
|
||||
}.catch { (error) -> Void in
|
||||
print("could not unpin status: \(error)")
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import UIKit
|
||||
import Cache
|
||||
import Pachyderm
|
||||
|
||||
class ImageCache {
|
||||
|
||||
|
@ -56,6 +57,18 @@ class ImageCache {
|
|||
}
|
||||
}
|
||||
|
||||
func get(_ url: URL) -> Promise<Data> {
|
||||
return Promise<Data> { (resolve, reject) in
|
||||
_ = self.get(url) { (data) in
|
||||
if let data = data {
|
||||
resolve(data)
|
||||
} else {
|
||||
reject(Error.unknown)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func get(_ url: URL) -> Data? {
|
||||
return try? cache.object(forKey: url.absoluteString)
|
||||
}
|
||||
|
@ -142,4 +155,8 @@ class ImageCache {
|
|||
}
|
||||
}
|
||||
|
||||
enum Error: Swift.Error {
|
||||
case unknown
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import Foundation
|
||||
import Pachyderm
|
||||
import Combine
|
||||
|
||||
class MastodonController {
|
||||
|
||||
|
@ -50,6 +51,14 @@ class MastodonController {
|
|||
client.run(request, completion: completion)
|
||||
}
|
||||
|
||||
func run<Result: Decodable>(_ request: Request<Result>) -> Promise<(Result, Pagination?)> {
|
||||
return client.run(request)
|
||||
}
|
||||
|
||||
func run<Result: Decodable>(_ request: Request<Result>) -> AnyPublisher<(Result, Pagination?), Error> {
|
||||
return client.run(request)
|
||||
}
|
||||
|
||||
func registerApp(completion: @escaping (_ clientID: String, _ clientSecret: String) -> Void) {
|
||||
guard client.clientID == nil,
|
||||
client.clientSecret == nil else {
|
||||
|
@ -79,20 +88,22 @@ class MastodonController {
|
|||
completion?(account)
|
||||
} else {
|
||||
let request = Client.getSelfAccount()
|
||||
run(request) { response in
|
||||
guard case let .success(account, _) = response else { fatalError() }
|
||||
run(request).then { (account, _) -> Void in
|
||||
self.account = account
|
||||
self.cache.add(account: account)
|
||||
completion?(account)
|
||||
}.catch { (error) -> Void in
|
||||
fatalError("couldn't get own account: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getOwnInstance() {
|
||||
let request = Client.getInstance()
|
||||
run(request) { (response) in
|
||||
guard case let .success(instance, _) = response else { fatalError() }
|
||||
run(request).then { (instance, _) -> Void in
|
||||
self.instance = instance
|
||||
}.catch { (error) -> Void in
|
||||
fatalError("couldn't get own instance: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -57,6 +57,16 @@ class MastodonCache {
|
|||
}
|
||||
}
|
||||
|
||||
func status(for id: String) -> Promise<Status> {
|
||||
guard let mastodonController = mastodonController else {
|
||||
fatalError("The MastodonController for this cache has been deinitialized, so this cache should no longer exist. Are you storing a strong reference to it?")
|
||||
}
|
||||
let request = Client.getStatus(id: id)
|
||||
return mastodonController.run(request).then { (status, _) in
|
||||
status
|
||||
}.then(self.add(status:))
|
||||
}
|
||||
|
||||
func add(status: Status) {
|
||||
set(status: status, for: status.id)
|
||||
}
|
||||
|
@ -90,6 +100,16 @@ class MastodonCache {
|
|||
}
|
||||
}
|
||||
|
||||
func account(for id: String) -> Promise<Account> {
|
||||
guard let mastodonController = mastodonController else {
|
||||
fatalError("The MastodonController for this cache has been deinitialized, so this cache should no longer exist. Are you storing a strong reference to it?")
|
||||
}
|
||||
let request = Client.getAccount(id: id)
|
||||
return mastodonController.run(request).then { (account, _) in
|
||||
account
|
||||
}.then(self.add(account:))
|
||||
}
|
||||
|
||||
func add(account: Account) {
|
||||
set(account: account, for: account.id)
|
||||
}
|
||||
|
@ -123,6 +143,16 @@ class MastodonCache {
|
|||
}
|
||||
}
|
||||
|
||||
func relationship(for id: String) -> Promise<Relationship> {
|
||||
guard let mastodonController = mastodonController else {
|
||||
fatalError("The MastodonController for this cache has been deinitialized, so this cache should no longer exist. Are you storing a strong reference to it?")
|
||||
}
|
||||
let request = Client.getRelationships(accounts: [id])
|
||||
return mastodonController.run(request).then { (relationships, _) in
|
||||
relationships.first!
|
||||
}.then(self.add(relationship:))
|
||||
}
|
||||
|
||||
func add(relationship: Relationship) {
|
||||
set(relationship: relationship, id: relationship.id)
|
||||
}
|
||||
|
|
|
@ -490,71 +490,52 @@ class ComposeViewController: UIViewController {
|
|||
}
|
||||
let sensitive = contentWarning != nil
|
||||
let visibility = self.visibility!
|
||||
|
||||
postProgressView.steps = 2 + (selectedAttachments.count * 2) // 2 steps (request data, then upload) for each attachment
|
||||
postProgressView.currentStep = 1
|
||||
|
||||
let group = DispatchGroup()
|
||||
|
||||
var attachments: [Attachment?] = []
|
||||
for compAttachment in selectedAttachments {
|
||||
let index = attachments.count
|
||||
attachments.append(nil)
|
||||
|
||||
let mediaView = attachmentsStackView.arrangedSubviews[index] as! ComposeMediaView
|
||||
let description = mediaView.descriptionTextView.text
|
||||
|
||||
group.enter()
|
||||
|
||||
compAttachment.getData { (data, mimeType) in
|
||||
self.postProgressView.step()
|
||||
|
||||
let attachmentPromises = selectedAttachments.map { (attachment) -> Promise<Attachment> in
|
||||
let view = attachmentsStackView.arrangedSubviews.first { ($0 as! ComposeMediaView).attachment == attachment } as! ComposeMediaView
|
||||
let description = view.description
|
||||
|
||||
return attachment.getData().then { (data, mimeType) -> Promise<(Attachment, Pagination?)> in
|
||||
let request = Client.upload(attachment: FormAttachment(mimeType: mimeType, data: data, fileName: "file"), description: description)
|
||||
self.mastodonController.run(request) { (response) in
|
||||
guard case let .success(attachment, _) = response else { fatalError() }
|
||||
|
||||
attachments[index] = attachment
|
||||
|
||||
self.postProgressView.step()
|
||||
|
||||
group.leave()
|
||||
}
|
||||
return self.mastodonController.run(request)
|
||||
}.then { (attachment, _) -> Attachment in
|
||||
self.postProgressView.step()
|
||||
return attachment
|
||||
}
|
||||
}
|
||||
|
||||
postProgressView.steps = 2 + (attachments.count * 2) // 2 steps (request data, then upload) for each attachment
|
||||
postProgressView.currentStep = 1
|
||||
|
||||
group.notify(queue: .main) {
|
||||
let attachments = attachments.compactMap { $0 }
|
||||
|
||||
Promise<[Attachment]>.all(attachmentPromises).then { (attachments) -> Promise<(Status, Pagination?)> in
|
||||
let request = Client.createStatus(text: text,
|
||||
contentType: Preferences.shared.statusContentType,
|
||||
inReplyTo: self.inReplyToID,
|
||||
media: attachments,
|
||||
sensitive: sensitive,
|
||||
spoilerText: contentWarning,
|
||||
visibility: visibility,
|
||||
language: nil)
|
||||
self.mastodonController.run(request) { (response) in
|
||||
guard case let .success(status, _) = response else { fatalError() }
|
||||
self.postedStatus = status
|
||||
self.mastodonController.cache.add(status: status)
|
||||
|
||||
if let draft = self.currentDraft {
|
||||
DraftsManager.shared.remove(draft)
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.postProgressView.step()
|
||||
self.dismiss(animated: true)
|
||||
|
||||
let conversationVC = ConversationTableViewController(for: status.id, mastodonController: self.mastodonController)
|
||||
self.show(conversationVC, sender: self)
|
||||
|
||||
self.xcbSession?.complete(with: .success, additionalData: [
|
||||
"statusURL": status.url?.absoluteString,
|
||||
"statusURI": status.uri
|
||||
])
|
||||
}
|
||||
contentType: Preferences.shared.statusContentType,
|
||||
inReplyTo: self.inReplyToID,
|
||||
media: attachments,
|
||||
sensitive: sensitive,
|
||||
spoilerText: contentWarning,
|
||||
visibility: visibility,
|
||||
language: nil)
|
||||
return self.mastodonController.run(request)
|
||||
}.then { (status, _) -> Status in
|
||||
self.postedStatus = status
|
||||
self.mastodonController.cache.add(status: status)
|
||||
|
||||
if let draft = self.currentDraft {
|
||||
DraftsManager.shared.remove(draft)
|
||||
}
|
||||
|
||||
return status
|
||||
}.handle(on: DispatchQueue.main).then { (status) in
|
||||
self.postProgressView.step()
|
||||
self.dismiss(animated: true)
|
||||
|
||||
self.xcbSession?.complete(with: .success, additionalData: [
|
||||
"statusURL": status.url?.absoluteString,
|
||||
"statusURI": status.uri
|
||||
])
|
||||
}.catch { (error) -> Void in
|
||||
fatalError("couldn't create post: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import UIKit
|
||||
import Photos
|
||||
import MobileCoreServices
|
||||
import Pachyderm
|
||||
|
||||
enum CompositionAttachment {
|
||||
case asset(PHAsset)
|
||||
|
@ -93,6 +94,14 @@ enum CompositionAttachment {
|
|||
}
|
||||
}
|
||||
|
||||
func getData() -> Promise<(Data, String)> {
|
||||
return Promise { (resolve, reject) in
|
||||
self.getData { (data, mimeType) in
|
||||
resolve((data, mimeType))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func exportVideoData(session: AVAssetExportSession, completion: @escaping (Data, String) -> Void) {
|
||||
session.outputFileType = .mp4
|
||||
session.outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("exported_video_\(UUID())").appendingPathExtension("mp4")
|
||||
|
|
|
@ -187,16 +187,11 @@ class NotificationsTableViewController: EnhancedTableViewController {
|
|||
}
|
||||
|
||||
func dismissNotificationsInGroup(at indexPath: IndexPath, completion: (() -> Void)? = nil) {
|
||||
let group = DispatchGroup()
|
||||
groups[indexPath.row].notificationIDs
|
||||
.map(Pachyderm.Notification.dismiss(id:))
|
||||
.forEach { (request) in
|
||||
group.enter()
|
||||
mastodonController.run(request) { (response) in
|
||||
group.leave()
|
||||
}
|
||||
let dismissPromises = groups[indexPath.row].notificationIDs.map { (id) -> Promise<(Empty, Pagination?)> in
|
||||
let req = Pachyderm.Notification.dismiss(id: id)
|
||||
return self.mastodonController.run(req)
|
||||
}
|
||||
group.notify(queue: .main) {
|
||||
Promise<[(Empty, Pagination?)]>.all(dismissPromises).handle(on: .main).then { (_) in
|
||||
self.groups.remove(at: indexPath.row)
|
||||
self.tableView.deleteRows(at: [indexPath], with: .automatic)
|
||||
completion?()
|
||||
|
|
|
@ -43,33 +43,27 @@ class ContentTextView: LinkTextView {
|
|||
func setEmojis(_ emojis: [Emoji]) {
|
||||
guard !emojis.isEmpty else { return }
|
||||
|
||||
let emojiImages = CachedDictionary<UIImage>(name: "ContentTextView Emoji Images")
|
||||
|
||||
let group = DispatchGroup()
|
||||
|
||||
for emoji in emojis {
|
||||
group.enter()
|
||||
_ = ImageCache.emojis.get(emoji.url) { (data) in
|
||||
defer { group.leave() }
|
||||
guard let data = data, let image = UIImage(data: data) else {
|
||||
return
|
||||
let emojiPromises = emojis.map { (emoji) -> Promise<(String, UIImage)> in
|
||||
ImageCache.emojis.get(emoji.url).then { (data) -> Promise<(String, UIImage)> in
|
||||
if let image = UIImage(data: data) {
|
||||
return Promise<(String, UIImage)>.resolve((emoji.shortcode, image))
|
||||
} else {
|
||||
return Promise<(String, UIImage)>.reject(ImageCache.Error.unknown)
|
||||
}
|
||||
emojiImages[emoji.shortcode] = image
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: .main) {
|
||||
Promise<[(String, UIImage)]>.all(emojiPromises).then { (emojis) in
|
||||
let mutAttrString = NSMutableAttributedString(attributedString: self.attributedText!)
|
||||
let string = mutAttrString.string
|
||||
let matches = emojiRegex.matches(in: string, options: [], range: mutAttrString.fullRange)
|
||||
// replaces the emojis started from the end of the string as to not alter the indexes of the other emojis
|
||||
// replaces emojis starting from the end of the string as to not alter the indices of earlier emojis
|
||||
for match in matches.reversed() {
|
||||
let shortcode = (string as NSString).substring(with: match.range(at: 1))
|
||||
guard let emojiImage = emojiImages[shortcode] else {
|
||||
guard let emojiImage = emojis.first(where: { $0.0 == shortcode }) else {
|
||||
continue
|
||||
}
|
||||
|
||||
let attachment = self.createEmojiTextAttachment(image: emojiImage, index: match.range.location)
|
||||
let attachment = self.createEmojiTextAttachment(image: emojiImage.1, index: match.range.location)
|
||||
let attachmentStr = NSAttributedString(attachment: attachment)
|
||||
mutAttrString.replaceCharacters(in: match.range, with: attachmentStr)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue