forked from shadowfacts/Tusker
Compare commits
1 Commits
develop
...
simple-swi
Author | SHA1 | Date |
---|---|---|
Shadowfacts | 279b7868e3 |
|
@ -7,6 +7,7 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
/**
|
/**
|
||||||
The base Mastodon API client.
|
The base Mastodon API client.
|
||||||
|
@ -80,6 +81,43 @@ public class Client {
|
||||||
task.resume()
|
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? {
|
func createURLRequest<Result>(request: Request<Result>) -> URLRequest? {
|
||||||
guard var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: true) else { return nil }
|
guard var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: true) else { return nil }
|
||||||
components.path = request.path
|
components.path = request.path
|
||||||
|
@ -325,6 +363,6 @@ extension Client {
|
||||||
case invalidResponse
|
case invalidResponse
|
||||||
case invalidModel
|
case invalidModel
|
||||||
case mastodonError(String)
|
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 */; };
|
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */; };
|
||||||
D67C57B221E28FAD00C3118B /* ComposeStatusReplyView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D67C57B121E28FAD00C3118B /* ComposeStatusReplyView.xib */; };
|
D67C57B221E28FAD00C3118B /* ComposeStatusReplyView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D67C57B121E28FAD00C3118B /* ComposeStatusReplyView.xib */; };
|
||||||
D67C57B421E2910700C3118B /* ComposeStatusReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */; };
|
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 */; };
|
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */; };
|
||||||
D6945C2F23AC47C3005C403C /* SavedDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C2E23AC47C3005C403C /* SavedDataManager.swift */; };
|
D6945C2F23AC47C3005C403C /* SavedDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C2E23AC47C3005C403C /* SavedDataManager.swift */; };
|
||||||
D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3123AC4D36005C403C /* HashtagTimelineViewController.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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineViewController.swift; sourceTree = "<group>"; };
|
||||||
|
@ -594,6 +598,7 @@
|
||||||
D61099AD2144B0CC00432DC2 /* Pachyderm.h */,
|
D61099AD2144B0CC00432DC2 /* Pachyderm.h */,
|
||||||
D61099AE2144B0CC00432DC2 /* Info.plist */,
|
D61099AE2144B0CC00432DC2 /* Info.plist */,
|
||||||
D61099C82144B13C00432DC2 /* Client.swift */,
|
D61099C82144B13C00432DC2 /* Client.swift */,
|
||||||
|
D683418523F79BFC00D06703 /* Promise.swift */,
|
||||||
D6109A0A2145953C00432DC2 /* ClientModel.swift */,
|
D6109A0A2145953C00432DC2 /* ClientModel.swift */,
|
||||||
D6A3BC7223218C6E00FD64D5 /* Utilities */,
|
D6A3BC7223218C6E00FD64D5 /* Utilities */,
|
||||||
D61099D72144B74500432DC2 /* Extensions */,
|
D61099D72144B74500432DC2 /* Extensions */,
|
||||||
|
@ -609,6 +614,7 @@
|
||||||
children = (
|
children = (
|
||||||
D61099BA2144B0CC00432DC2 /* PachydermTests.swift */,
|
D61099BA2144B0CC00432DC2 /* PachydermTests.swift */,
|
||||||
D6E6F26421604242006A8599 /* CharacterCounterTests.swift */,
|
D6E6F26421604242006A8599 /* CharacterCounterTests.swift */,
|
||||||
|
D683418923F7A3BB00D06703 /* PromiseTests.swift */,
|
||||||
D61099BC2144B0CC00432DC2 /* Info.plist */,
|
D61099BC2144B0CC00432DC2 /* Info.plist */,
|
||||||
);
|
);
|
||||||
path = PachydermTests;
|
path = PachydermTests;
|
||||||
|
@ -1537,6 +1543,7 @@
|
||||||
D61099E92145658300432DC2 /* Card.swift in Sources */,
|
D61099E92145658300432DC2 /* Card.swift in Sources */,
|
||||||
D61099F32145688600432DC2 /* Mention.swift in Sources */,
|
D61099F32145688600432DC2 /* Mention.swift in Sources */,
|
||||||
D6109A0F21459B6900432DC2 /* Pagination.swift in Sources */,
|
D6109A0F21459B6900432DC2 /* Pagination.swift in Sources */,
|
||||||
|
D683418623F79BFC00D06703 /* Promise.swift in Sources */,
|
||||||
D6109A032145722C00432DC2 /* RegisteredApplication.swift in Sources */,
|
D6109A032145722C00432DC2 /* RegisteredApplication.swift in Sources */,
|
||||||
D6109A0921458C4A00432DC2 /* Empty.swift in Sources */,
|
D6109A0921458C4A00432DC2 /* Empty.swift in Sources */,
|
||||||
D6285B4F21EA695800FE4B39 /* StatusContentType.swift in Sources */,
|
D6285B4F21EA695800FE4B39 /* StatusContentType.swift in Sources */,
|
||||||
|
@ -1562,6 +1569,7 @@
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
D683418A23F7A3BB00D06703 /* PromiseTests.swift in Sources */,
|
||||||
D6E6F26521604242006A8599 /* CharacterCounterTests.swift in Sources */,
|
D6E6F26521604242006A8599 /* CharacterCounterTests.swift in Sources */,
|
||||||
D61099BB2144B0CC00432DC2 /* PachydermTests.swift in Sources */,
|
D61099BB2144B0CC00432DC2 /* PachydermTests.swift in Sources */,
|
||||||
);
|
);
|
||||||
|
|
|
@ -28,14 +28,11 @@ class FollowAccountActivity: AccountActivity {
|
||||||
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
|
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
|
||||||
|
|
||||||
let request = Account.follow(account.id)
|
let request = Account.follow(account.id)
|
||||||
mastodonController.run(request) { (response) in
|
mastodonController.run(request).then { (relationship, _) -> Void in
|
||||||
if case let .success(relationship, _) = response {
|
self.mastodonController.cache.add(relationship: relationship)
|
||||||
self.mastodonController.cache.add(relationship: relationship)
|
}.catch { (error) -> Void in
|
||||||
} else {
|
print("could not follow account")
|
||||||
// todo: display error message
|
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||||
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,14 +28,11 @@ class UnfollowAccountActivity: AccountActivity {
|
||||||
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
|
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
|
||||||
|
|
||||||
let request = Account.unfollow(account.id)
|
let request = Account.unfollow(account.id)
|
||||||
mastodonController.run(request) { (response) in
|
mastodonController.run(request).then { (relationship, _) -> Void in
|
||||||
if case let .success(relationship, _) = response {
|
self.mastodonController.cache.add(relationship: relationship)
|
||||||
self.mastodonController.cache.add(relationship: relationship)
|
}.catch { (error) -> Void in
|
||||||
} else {
|
print("could not unfollow account: \(error)")
|
||||||
// todo: display error message
|
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||||
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,14 +27,11 @@ class BookmarkStatusActivity: StatusActivity {
|
||||||
guard let status = status else { return }
|
guard let status = status else { return }
|
||||||
|
|
||||||
let request = Status.bookmark(status)
|
let request = Status.bookmark(status)
|
||||||
mastodonController.run(request) { (response) in
|
mastodonController.run(request).then { (status, _) -> Void in
|
||||||
if case let .success(status, _) = response {
|
self.mastodonController.cache.add(status: status)
|
||||||
self.mastodonController.cache.add(status: status)
|
}.catch { (error) -> Void in
|
||||||
} else {
|
print("could not bookmark status: \(error)")
|
||||||
// todo: display error message
|
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||||
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,14 +26,11 @@ class PinStatusActivity: StatusActivity {
|
||||||
guard let status = status else { return }
|
guard let status = status else { return }
|
||||||
|
|
||||||
let request = Status.pin(status)
|
let request = Status.pin(status)
|
||||||
mastodonController.run(request) { (response) in
|
mastodonController.run(request).then { (status, _) -> Void in
|
||||||
if case let .success(status, _) = response {
|
self.mastodonController.cache.add(status: status)
|
||||||
self.mastodonController.cache.add(status: status)
|
}.catch { (error) -> Void in
|
||||||
} else {
|
print("could not pin status: \(error)")
|
||||||
// todo: display error message
|
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||||
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,14 +27,11 @@ class UnbookmarkStatusActivity: StatusActivity {
|
||||||
guard let status = status else { return }
|
guard let status = status else { return }
|
||||||
|
|
||||||
let request = Status.unbookmark(status)
|
let request = Status.unbookmark(status)
|
||||||
mastodonController.run(request) { (response) in
|
mastodonController.run(request).then { (status, _) -> Void in
|
||||||
if case let .success(status, _) = response {
|
self.mastodonController.cache.add(status: status)
|
||||||
self.mastodonController.cache.add(status: status)
|
}.catch { (error) -> Void in
|
||||||
} else {
|
print("could not unbookmark status: \(error)")
|
||||||
// todo: display error message
|
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||||
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,14 +26,11 @@ class UnpinStatusActivity: StatusActivity {
|
||||||
guard let status = status else { return }
|
guard let status = status else { return }
|
||||||
|
|
||||||
let request = Status.unpin(status)
|
let request = Status.unpin(status)
|
||||||
mastodonController.run(request) { (response) in
|
mastodonController.run(request).then { (status, _) -> Void in
|
||||||
if case let .success(status, _) = response {
|
self.mastodonController.cache.add(status: status)
|
||||||
self.mastodonController.cache.add(status: status)
|
}.catch { (error) -> Void in
|
||||||
} else {
|
print("could not unpin status: \(error)")
|
||||||
// todo: display error message
|
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||||
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Cache
|
import Cache
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
class ImageCache {
|
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? {
|
func get(_ url: URL) -> Data? {
|
||||||
return try? cache.object(forKey: url.absoluteString)
|
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 Foundation
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
import Combine
|
||||||
|
|
||||||
class MastodonController {
|
class MastodonController {
|
||||||
|
|
||||||
|
@ -50,6 +51,14 @@ class MastodonController {
|
||||||
client.run(request, completion: completion)
|
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) {
|
func registerApp(completion: @escaping (_ clientID: String, _ clientSecret: String) -> Void) {
|
||||||
guard client.clientID == nil,
|
guard client.clientID == nil,
|
||||||
client.clientSecret == nil else {
|
client.clientSecret == nil else {
|
||||||
|
@ -79,20 +88,22 @@ class MastodonController {
|
||||||
completion?(account)
|
completion?(account)
|
||||||
} else {
|
} else {
|
||||||
let request = Client.getSelfAccount()
|
let request = Client.getSelfAccount()
|
||||||
run(request) { response in
|
run(request).then { (account, _) -> Void in
|
||||||
guard case let .success(account, _) = response else { fatalError() }
|
|
||||||
self.account = account
|
self.account = account
|
||||||
self.cache.add(account: account)
|
self.cache.add(account: account)
|
||||||
completion?(account)
|
completion?(account)
|
||||||
|
}.catch { (error) -> Void in
|
||||||
|
fatalError("couldn't get own account: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getOwnInstance() {
|
func getOwnInstance() {
|
||||||
let request = Client.getInstance()
|
let request = Client.getInstance()
|
||||||
run(request) { (response) in
|
run(request).then { (instance, _) -> Void in
|
||||||
guard case let .success(instance, _) = response else { fatalError() }
|
|
||||||
self.instance = instance
|
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) {
|
func add(status: Status) {
|
||||||
set(status: status, for: status.id)
|
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) {
|
func add(account: Account) {
|
||||||
set(account: account, for: account.id)
|
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) {
|
func add(relationship: Relationship) {
|
||||||
set(relationship: relationship, id: relationship.id)
|
set(relationship: relationship, id: relationship.id)
|
||||||
}
|
}
|
||||||
|
|
|
@ -490,71 +490,52 @@ class ComposeViewController: UIViewController {
|
||||||
}
|
}
|
||||||
let sensitive = contentWarning != nil
|
let sensitive = contentWarning != nil
|
||||||
let visibility = self.visibility!
|
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()
|
let attachmentPromises = selectedAttachments.map { (attachment) -> Promise<Attachment> in
|
||||||
|
let view = attachmentsStackView.arrangedSubviews.first { ($0 as! ComposeMediaView).attachment == attachment } as! ComposeMediaView
|
||||||
var attachments: [Attachment?] = []
|
let description = view.description
|
||||||
for compAttachment in selectedAttachments {
|
|
||||||
let index = attachments.count
|
return attachment.getData().then { (data, mimeType) -> Promise<(Attachment, Pagination?)> in
|
||||||
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 request = Client.upload(attachment: FormAttachment(mimeType: mimeType, data: data, fileName: "file"), description: description)
|
let request = Client.upload(attachment: FormAttachment(mimeType: mimeType, data: data, fileName: "file"), description: description)
|
||||||
self.mastodonController.run(request) { (response) in
|
return self.mastodonController.run(request)
|
||||||
guard case let .success(attachment, _) = response else { fatalError() }
|
}.then { (attachment, _) -> Attachment in
|
||||||
|
self.postProgressView.step()
|
||||||
attachments[index] = attachment
|
return attachment
|
||||||
|
|
||||||
self.postProgressView.step()
|
|
||||||
|
|
||||||
group.leave()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
postProgressView.steps = 2 + (attachments.count * 2) // 2 steps (request data, then upload) for each attachment
|
Promise<[Attachment]>.all(attachmentPromises).then { (attachments) -> Promise<(Status, Pagination?)> in
|
||||||
postProgressView.currentStep = 1
|
|
||||||
|
|
||||||
group.notify(queue: .main) {
|
|
||||||
let attachments = attachments.compactMap { $0 }
|
|
||||||
|
|
||||||
let request = Client.createStatus(text: text,
|
let request = Client.createStatus(text: text,
|
||||||
contentType: Preferences.shared.statusContentType,
|
contentType: Preferences.shared.statusContentType,
|
||||||
inReplyTo: self.inReplyToID,
|
inReplyTo: self.inReplyToID,
|
||||||
media: attachments,
|
media: attachments,
|
||||||
sensitive: sensitive,
|
sensitive: sensitive,
|
||||||
spoilerText: contentWarning,
|
spoilerText: contentWarning,
|
||||||
visibility: visibility,
|
visibility: visibility,
|
||||||
language: nil)
|
language: nil)
|
||||||
self.mastodonController.run(request) { (response) in
|
return self.mastodonController.run(request)
|
||||||
guard case let .success(status, _) = response else { fatalError() }
|
}.then { (status, _) -> Status in
|
||||||
self.postedStatus = status
|
self.postedStatus = status
|
||||||
self.mastodonController.cache.add(status: status)
|
self.mastodonController.cache.add(status: status)
|
||||||
|
|
||||||
if let draft = self.currentDraft {
|
if let draft = self.currentDraft {
|
||||||
DraftsManager.shared.remove(draft)
|
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
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 UIKit
|
||||||
import Photos
|
import Photos
|
||||||
import MobileCoreServices
|
import MobileCoreServices
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
enum CompositionAttachment {
|
enum CompositionAttachment {
|
||||||
case asset(PHAsset)
|
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) {
|
private static func exportVideoData(session: AVAssetExportSession, completion: @escaping (Data, String) -> Void) {
|
||||||
session.outputFileType = .mp4
|
session.outputFileType = .mp4
|
||||||
session.outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("exported_video_\(UUID())").appendingPathExtension("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) {
|
func dismissNotificationsInGroup(at indexPath: IndexPath, completion: (() -> Void)? = nil) {
|
||||||
let group = DispatchGroup()
|
let dismissPromises = groups[indexPath.row].notificationIDs.map { (id) -> Promise<(Empty, Pagination?)> in
|
||||||
groups[indexPath.row].notificationIDs
|
let req = Pachyderm.Notification.dismiss(id: id)
|
||||||
.map(Pachyderm.Notification.dismiss(id:))
|
return self.mastodonController.run(req)
|
||||||
.forEach { (request) in
|
|
||||||
group.enter()
|
|
||||||
mastodonController.run(request) { (response) in
|
|
||||||
group.leave()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
group.notify(queue: .main) {
|
Promise<[(Empty, Pagination?)]>.all(dismissPromises).handle(on: .main).then { (_) in
|
||||||
self.groups.remove(at: indexPath.row)
|
self.groups.remove(at: indexPath.row)
|
||||||
self.tableView.deleteRows(at: [indexPath], with: .automatic)
|
self.tableView.deleteRows(at: [indexPath], with: .automatic)
|
||||||
completion?()
|
completion?()
|
||||||
|
|
|
@ -43,33 +43,27 @@ class ContentTextView: LinkTextView {
|
||||||
func setEmojis(_ emojis: [Emoji]) {
|
func setEmojis(_ emojis: [Emoji]) {
|
||||||
guard !emojis.isEmpty else { return }
|
guard !emojis.isEmpty else { return }
|
||||||
|
|
||||||
let emojiImages = CachedDictionary<UIImage>(name: "ContentTextView Emoji Images")
|
let emojiPromises = emojis.map { (emoji) -> Promise<(String, UIImage)> in
|
||||||
|
ImageCache.emojis.get(emoji.url).then { (data) -> Promise<(String, UIImage)> in
|
||||||
let group = DispatchGroup()
|
if let image = UIImage(data: data) {
|
||||||
|
return Promise<(String, UIImage)>.resolve((emoji.shortcode, image))
|
||||||
for emoji in emojis {
|
} else {
|
||||||
group.enter()
|
return Promise<(String, UIImage)>.reject(ImageCache.Error.unknown)
|
||||||
_ = ImageCache.emojis.get(emoji.url) { (data) in
|
|
||||||
defer { group.leave() }
|
|
||||||
guard let data = data, let image = UIImage(data: data) else {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
emojiImages[emoji.shortcode] = image
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Promise<[(String, UIImage)]>.all(emojiPromises).then { (emojis) in
|
||||||
group.notify(queue: .main) {
|
|
||||||
let mutAttrString = NSMutableAttributedString(attributedString: self.attributedText!)
|
let mutAttrString = NSMutableAttributedString(attributedString: self.attributedText!)
|
||||||
let string = mutAttrString.string
|
let string = mutAttrString.string
|
||||||
let matches = emojiRegex.matches(in: string, options: [], range: mutAttrString.fullRange)
|
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() {
|
for match in matches.reversed() {
|
||||||
let shortcode = (string as NSString).substring(with: match.range(at: 1))
|
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
|
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)
|
let attachmentStr = NSAttributedString(attachment: attachment)
|
||||||
mutAttrString.replaceCharacters(in: match.range, with: attachmentStr)
|
mutAttrString.replaceCharacters(in: match.range, with: attachmentStr)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue