diff --git a/.gitmodules b/.gitmodules index 4a73daed..3d032561 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "MastodonKit"] - path = MastodonKit - url = git://github.com/shadowfacts/MastodonKit.git [submodule "SwiftSoup"] path = SwiftSoup url = git://github.com/scinfu/SwiftSoup.git diff --git a/MastodonKit b/MastodonKit deleted file mode 160000 index cfece308..00000000 --- a/MastodonKit +++ /dev/null @@ -1 +0,0 @@ -Subproject commit cfece3083acfeda2f124a84dc35f268682681d49 diff --git a/MyPlayground.playground/Contents.swift b/MyPlayground.playground/Contents.swift index d7b3d126..dc558611 100644 --- a/MyPlayground.playground/Contents.swift +++ b/MyPlayground.playground/Contents.swift @@ -1,13 +1,58 @@ import UIKit -func test(_ nillable: String?) { - defer { - print("defer") +class Client { + func test(_ 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 { +// for el in arr { +// if var el = el as? ClientModel { +// el.client = self +// } +// } +// } } - guard let value = nillable else { return } - print(value) } -test("test") -print("------") -test(nil) +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 diff --git a/Pachyderm/Client.swift b/Pachyderm/Client.swift new file mode 100644 index 00000000..ce461504 --- /dev/null +++ b/Pachyderm/Client.swift @@ -0,0 +1,346 @@ +// +// Client.swift +// Pachyderm +// +// Created by Shadowfacts on 9/8/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation + +/** + The base Mastodon API client. + */ +public class Client { + + public typealias Callback = (Response) -> Void + + let baseURL: URL + let session: URLSession + + public var accessToken: String? + + public var appID: String? + public var clientID: String? + public var clientSecret: String? + + public var timeoutInterval: TimeInterval = 60 + + lazy var decoder: JSONDecoder = { + let decoder = JSONDecoder() + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SZ" + formatter.timeZone = TimeZone(abbreviation: "UTC") + formatter.locale = Locale(identifier: "en_US_POSIX") + decoder.dateDecodingStrategy = .formatted(formatter) + return decoder + }() + + public init(baseURL: URL, accessToken: String? = nil, session: URLSession = .shared) { + self.baseURL = baseURL + self.accessToken = accessToken + self.session = session + } + + // MARK: - Internal Helpers + func run(_ request: Request, completion: @escaping Callback) { + guard let request = createURLRequest(request: request) else { + completion(.failure(Error.invalidRequest)) + return + } + + let task = session.dataTask(with: request) { data, response, error in + if let error = error { + completion(.failure(error)) + return + } + guard let data = data, + let response = response as? HTTPURLResponse else { + completion(.failure(Error.invalidResponse)) + return + } + guard response.statusCode == 200 else { + let mastodonError = try? self.decoder.decode(MastodonError.self, from: data) + let error = mastodonError.flatMap { Error.mastodonError($0.description) } ?? Error.unknownError + completion(.failure(error)) + return + } + guard let result = try? self.decoder.decode(Result.self, from: data) else { + completion(.failure(Error.invalidModel)) + return + } + if var result = result as? ClientModel { + result.client = self + } else if var result = result as? [ClientModel] { + result.client = self + } + let pagination = response.allHeaderFields["Link"].flatMap { $0 as? String }.flatMap(Pagination.init) + + completion(.success(result, pagination)) + } + task.resume() + } + + func createURLRequest(request: Request) -> URLRequest? { + guard var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: true) else { return nil } + components.path = request.path + components.queryItems = request.queryParameters.queryItems + guard let url = components.url else { return nil } + var urlRequest = URLRequest(url: url, timeoutInterval: timeoutInterval) + urlRequest.httpMethod = request.method.name + urlRequest.httpBody = request.body.data + urlRequest.setValue(request.body.mimeType, forHTTPHeaderField: "Content-Type") + if let accessToken = accessToken { + urlRequest.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") + } + return urlRequest + } + + // MARK: - Authorization + public func registerApp(name: String, redirectURI: String, scopes: [Scope], website: URL? = nil, completion: @escaping Callback) { + let request = Request(method: .post, path: "/api/v1/apps", body: .parameters([ + "client_name" => name, + "redirect_uris" => redirectURI, + "scopes" => scopes.scopeString, + "website" => website?.absoluteString + ])) + run(request) { result in + defer { completion(result) } + guard case let .success(application, _) = result else { return } + + self.appID = application.id + self.clientID = application.clientID + self.clientSecret = application.clientSecret + } + } + + public func getAccessToken(authorizationCode: String, redirectURI: String, completion: @escaping Callback) { + let request = Request(method: .post, path: "/oauth/token", body: .parameters([ + "client_id" => clientID, + "client_secret" => clientSecret, + "grant_type" => "authorization_code", + "code" => authorizationCode, + "redirect_uri" => redirectURI + ])) + run(request) { result in + defer { completion(result) } + guard case let .success(loginSettings, _) = result else { return } + + self.accessToken = loginSettings.accessToken + } + } + + // MARK: - Self + public func getSelfAccount(completion: @escaping Callback) { + let request = Request(method: .get, path: "/api/v1/accounts/verify_credentials") + run(request, completion: completion) + } + + public func getFavourites(completion: @escaping Callback<[Status]>) { + let request = Request<[Status]>(method: .get, path: "/api/v1/favourites") + run(request, completion: completion) + } + + public func getRelationships(accounts: [Account]? = nil, completion: @escaping Callback<[Relationship]>) { + let request = Request<[Relationship]>(method: .get, path: "/api/v1/accounts/relationships", queryParameters: "id" => accounts?.map { $0.id }) + run(request, completion: completion) + } + + public func getInstance(completion: @escaping Callback) { + let request = Request(method: .get, path: "/api/v1/instance") + run(request, completion: completion) + } + + public func getCustomEmoji(completion: @escaping Callback<[Emoji]>) { + let request = Request<[Emoji]>(method: .get, path: "/api/v1/custom_emojis") + run(request, completion: completion) + } + + // MARK: - Accounts + public func getAccount(id: String, completion: @escaping Callback) { + let request = Request(method: .get, path: "/api/v1/accounts/\(id)") + run(request, completion: completion) + } + + public func searchForAccount(query: String, limit: Int? = nil, following: Bool? = nil, completion: @escaping Callback<[Account]>) { + let request = Request<[Account]>(method: .get, path: "/api/v1/accounts/search", queryParameters: [ + "q" => query, + "limit" => limit, + "following" => following + ]) + run(request, completion: completion) + } + + // MARK: - Blocks + public func getBlocks(completion: @escaping Callback<[Account]>) { + let request = Request<[Account]>(method: .get, path: "/api/v1/blocks") + run(request, completion: completion) + } + + public func getDomainBlocks(completion: @escaping Callback<[String]>) { + let request = Request<[String]>(method: .get, path: "api/v1/domain_blocks") + run(request, completion: completion) + } + + public func block(domain: String, completion: @escaping Callback) { + let request = Request(method: .post, path: "/api/v1/domain_blocks", body: .parameters([ + "domain" => domain + ])) + run(request, completion: completion) + } + + public func unblock(domain: String, completion: @escaping Callback) { + let request = Request(method: .delete, path: "/api/v1/domain_blocks", body: .parameters([ + "domain" => domain + ])) + run(request, completion: completion) + } + + // MARK: - Filters + public func getFilters(completion: @escaping Callback<[Filter]>) { + let request = Request<[Filter]>(method: .get, path: "/api/v1/filters") + run(request, completion: completion) + } + + public func createFilter(phrase: String, context: [Filter.Context], irreversible: Bool? = nil, wholeWord: Bool? = nil, expiresAt: Date? = nil, completion: @escaping Callback) { + let request = Request(method: .post, path: "/api/v1/filters", body: .parameters([ + "phrase" => phrase, + "irreversible" => irreversible, + "whole_word" => wholeWord, + "expires_at" => expiresAt + ] + "context" => context.contextStrings)) + run(request, completion: completion) + } + + public func getFilter(id: String, completion: @escaping Callback) { + let request = Request(method: .get, path: "/api/v1/filters/\(id)") + run(request, completion: completion) + } + + // MARK: - Follows + public func getFollowRequests(range: RequestRange = .default, completion: @escaping Callback<[Account]>) { + var request = Request<[Account]>(method: .get, path: "/api/v1/follow_requests") + request.range = range + run(request, completion: completion) + } + + public func getFollowSuggestions(completion: @escaping Callback<[Account]>) { + let request = Request<[Account]>(method: .get, path: "/api/v1/suggestions") + run(request, completion: completion) + } + + public func followRemote(acct: String, completion: @escaping Callback) { + let request = Request(method: .post, path: "/api/v1/follows", body: .parameters(["uri" => acct])) + run(request, completion: completion) + } + + // MARK: - Lists + public func getLists(completion: @escaping Callback<[List]>) { + let request = Request<[List]>(method: .get, path: "/api/v1/lists") + run(request, completion: completion) + } + + public func getList(id: String, completion: @escaping Callback) { + let request = Request(method: .get, path: "/api/v1/lists/\(id)") + run(request, completion: completion) + } + + public func createList(title: String, completion: @escaping Callback) { + let request = Request(method: .post, path: "/api/v1/lists", body: .parameters(["title" => title])) + run(request, completion: completion) + } + + // MARK: - Media + public func upload(attachment: FormAttachment, description: String? = nil, focus: (Float, Float)? = nil, completion: @escaping Callback) { + let request = Request(method: .post, path: "/api/v1/media", body: .formData([ + "description" => description, + "focus" => focus + ], attachment)) + run(request, completion: completion) + } + + // MARK: - Mutes + public func getMutes(range: RequestRange, completion: @escaping Callback<[Account]>) { + var request = Request<[Account]>(method: .get, path: "/api/v1/mutes") + request.range = range + run(request, completion: completion) + } + + // MARK: - Notifications + public func getNotifications(range: RequestRange = .default, completion: @escaping Callback<[Notification]>) { + var request = Request<[Notification]>(method: .get, path: "/api/v1/notifications") + request.range = range + run(request, completion: completion) + } + + public func clearNotifications(completion: @escaping Callback) { + let request = Request(method: .post, path: "/api/v1/notifications/clear") + run(request, completion: completion) + } + + // MARK: - Reports + public func getReports(completion: @escaping Callback<[Report]>) { + let request = Request<[Report]>(method: .get, path: "/api/v1/reports") + run(request, completion: completion) + } + + public func report(account: Account, statuses: [Status], comment: String, completion: @escaping Callback) { + let request = Request(method: .post, path: "/api/v1/reports", body: .parameters([ + "account_id" => account.id, + "comment" => comment + ] + "status_ids" => statuses.map { $0.id })) + run(request, completion: completion) + } + + // MARK: - Search + public func search(query: String, resolve: Bool? = nil, completion: @escaping Callback) { + let request = Request(method: .get, path: "/api/v2/search", queryParameters: [ + "q" => query, + "resolve" => resolve + ]) + run(request, completion: completion) + } + + // MARK: - Statuses + public func getStatus(id: String, completion: @escaping Callback) { + let request = Request(method: .get, path: "/api/v1/statuses/\(id)") + run(request, completion: completion) + } + + public func createStatus(text: String, + inReplyTo: Status? = nil, + media: [Attachment]? = nil, + sensitive: Bool? = nil, + spoilerText: String? = nil, + visiblity: Status.Visibility? = nil, + language: String? = nil, + completion: @escaping Callback) { + let request = Request(method: .post, path: "/api/v1/statuses", body: .parameters([ + "status" => text, + "in_reply_to_id" => inReplyTo?.id, + "sensitive" => sensitive, + "spoiler_text" => spoilerText, + "visibility" => visiblity?.rawValue, + "language" => language + ] + "media" => media?.map { $0.id })) + run(request, completion: completion) + } + + // MARK: - Timelines + public func getStatuses(timeline: Timeline, range: RequestRange = .default, completion: @escaping Callback<[Status]>) { + let request = timeline.request(range: range) + run(request, completion: completion) + } + +} + +extension Client { + public enum Error: Swift.Error { + case unknownError + case invalidRequest + case invalidResponse + case invalidModel + case mastodonError(String) + + } +} diff --git a/Pachyderm/ClientModel.swift b/Pachyderm/ClientModel.swift new file mode 100644 index 00000000..f1328260 --- /dev/null +++ b/Pachyderm/ClientModel.swift @@ -0,0 +1,39 @@ +// +// ClientModel.swift +// Pachyderm +// +// Created by Shadowfacts on 9/9/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation + +protocol ClientModel { + var client: Client! { get set } +} + +extension Array where Element == ClientModel { + var client: Client! { + get { + return first?.client + } + set { + for var el in self { + el.client = newValue + } + } + } +} + +extension Array where Element: ClientModel { + var client: Client! { + get { + return first?.client + } + set { + for var el in self { + el.client = newValue + } + } + } +} diff --git a/Pachyderm/Extensions/Data.swift b/Pachyderm/Extensions/Data.swift new file mode 100644 index 00000000..2a6d9ee6 --- /dev/null +++ b/Pachyderm/Extensions/Data.swift @@ -0,0 +1,16 @@ +// +// Data.swift +// Pachyderm +// +// Created by Shadowfacts on 9/8/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation + +extension Data { + mutating func append(_ string: String, encoding: String.Encoding = .utf8) { + guard let data = string.data(using: encoding) else { return } + append(data) + } +} diff --git a/Pachyderm/Info.plist b/Pachyderm/Info.plist new file mode 100644 index 00000000..e1fe4cfb --- /dev/null +++ b/Pachyderm/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/Pachyderm/Model/Account.swift b/Pachyderm/Model/Account.swift new file mode 100644 index 00000000..04cd78e7 --- /dev/null +++ b/Pachyderm/Model/Account.swift @@ -0,0 +1,146 @@ +// +// Account.swift +// Pachyderm +// +// Created by Shadowfacts on 9/8/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation + +public class Account: Decodable, ClientModel { + var client: Client! { + didSet { + emojis.client = client + } + } + + public let id: String + public let username: String + public let acct: String + public let displayName: String + public let locked: Bool + public let createdAt: Date + public let followersCount: Int + public let followingCount: Int + public let statusesCount: Int + public let note: String + public let url: URL + public let avatar: URL + public let avatarStatic: URL + public let header: URL + public let headerStatic: URL + public private(set) var emojis: [Emoji] + public let moved: Bool? + public let fields: [Field]? + public let bot: Bool? + + public func authorizeFollowRequest(completion: @escaping Client.Callback) { + let request = Request(method: .post, path: "/api/v1/follow_requests/\(id)/authorize") + client.run(request, completion: completion) + } + + public func rejectFollowRequest(completion: @escaping Client.Callback) { + let request = Request(method: .post, path: "/api/v1/follow_requests/\(id)/reject") + client.run(request, completion: completion) + } + + public func removeFromFollowRequests(completion: @escaping Client.Callback) { + let request = Request(method: .delete, path: "/api/v1/suggestions/\(id)") + client.run(request, completion: completion) + } + + public func getFollowers(range: RequestRange = .default, completion: @escaping Client.Callback<[Account]>) { + var request = Request<[Account]>(method: .get, path: "/api/v1/accounts/\(id)/followers") + request.range = range + client.run(request, completion: completion) + } + + public func getFollowing(range: RequestRange = .default, completion: @escaping Client.Callback<[Account]>) { + var request = Request<[Account]>(method: .get, path: "/api/v1/accounts/\(id)/following") + request.range = range + client.run(request, completion: completion) + } + + public func getStatuses(range: RequestRange = .default, onlyMedia: Bool? = nil, pinned: Bool? = nil, excludeReplies: Bool? = nil, completion: @escaping Client.Callback<[Status]>) { + var request = Request<[Status]>(method: .get, path: "/api/v1/accounts/\(id)/statuses", queryParameters: [ + "only_media" => onlyMedia, + "pinned" => pinned, + "exclude_replies" => excludeReplies + ]) + request.range = range + client.run(request, completion: completion) + } + + public func follow(completion: @escaping Client.Callback) { + let request = Request(method: .post, path: "/api/v1/accounts/\(id)/follow") + client.run(request, completion: completion) + } + + public func unfollow(completion: @escaping Client.Callback) { + let request = Request(method: .post, path: "/api/v1/accounts/\(id)/unfollow") + client.run(request, completion: completion) + } + + public func block(completion: @escaping Client.Callback) { + let request = Request(method: .post, path: "/api/v1/accounts/\(id)/block") + client.run(request, completion: completion) + } + + public func unblock(completion: @escaping Client.Callback) { + let request = Request(method: .post, path: "/api/v1/accounts/\(id)/unblock") + client.run(request, completion: completion) + } + + public func mute(notifications: Bool? = nil, completion: @escaping Client.Callback) { + let request = Request(method: .post, path: "/api/v1/accounts/\(id)/mute", body: .parameters([ + "notifications" => notifications + ])) + client.run(request, completion: completion) + } + + public func unmute(completion: @escaping Client.Callback) { + let request = Request(method: .post, path: "/api/v1/accounts/\(id)/unmute") + client.run(request, completion: completion) + } + + public func getLists(completion: @escaping Client.Callback<[List]>) { + let request = Request<[List]>(method: .get, path: "/api/v1/accounts/\(id)/lists") + client.run(request, completion: completion) + } + + private enum CodingKeys: String, CodingKey { + case id + case username + case acct + case displayName = "display_name" + case locked + case createdAt = "created_at" + case followersCount = "followers_count" + case followingCount = "following_count" + case statusesCount = "statuses_count" + case note + case url + case avatar + case avatarStatic = "avatar_static" + case header + case headerStatic = "header_static" + case emojis + case moved + case fields + case bot + } +} + +extension Account: CustomDebugStringConvertible { + public var debugDescription: String { + return "Account(\(id), \(acct))" + } +} + +extension Account { + public struct Field: Codable { + let name: String + let value: String + } +} diff --git a/Pachyderm/Model/Application.swift b/Pachyderm/Model/Application.swift new file mode 100644 index 00000000..56079185 --- /dev/null +++ b/Pachyderm/Model/Application.swift @@ -0,0 +1,21 @@ +// +// Application.swift +// Pachyderm +// +// Created by Shadowfacts on 9/9/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation + +public class Application: Decodable, ClientModel { + var client: Client! + + public let name: String + public let website: URL? + + private enum CodingKeys: String, CodingKey { + case name + case website + } +} diff --git a/Pachyderm/Model/Attachment.swift b/Pachyderm/Model/Attachment.swift new file mode 100644 index 00000000..83700a90 --- /dev/null +++ b/Pachyderm/Model/Attachment.swift @@ -0,0 +1,100 @@ +// +// Attachment.swift +// Pachyderm +// +// Created by Shadowfacts on 9/9/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation + +public class Attachment: Decodable, ClientModel { + var client: Client! + + public let id: String + public let kind: Kind + public let url: URL + public let remoteURL: URL? + public let previewURL: URL + public let textURL: URL? + public let meta: Metadata? + public var description: String? + + public func update(focus: (Float, Float)?, completion: Client.Callback?) { + let request = Request(method: .put, path: "/api/v1/media/\(id)", body: .formData([ + "description" => description, + "focus" => focus + ], nil)) + client.run(request) { result in + completion?(result) + } + } + + private enum CodingKeys: String, CodingKey { + case id + case kind = "type" + case url + case remoteURL = "remote_url" + case previewURL = "preview_url" + case textURL = "text_url" + case meta + case description + } +} + +extension Attachment { + public enum Kind: String, Decodable { + case image + case video + case gifv + case audio + case unknown + } +} + +extension Attachment { + public class Metadata: Decodable { + public let length: String? + public let duration: Float? + public let audioEncoding: String? + public let audioBitrate: String? + public let audioChannels: String? + public let fps: Float? + public let width: Int? + public let height: Int? + public let size: String? + public let aspect: Float? + + public let small: ImageMetadata? + public let original: ImageMetadata? + + private enum CodingKeys: String, CodingKey { + case length + case duration + case audioEncoding = "audio_encode" + case audioBitrate = "audio_bitrate" + case audioChannels = "audio_channels" + case fps + case width + case height + case size + case aspect + case small + case original + } + } + + public class ImageMetadata: Decodable { + public let width: Int? + public let height: Int? + public let size: String? + public let aspect: Float? + + private enum CodingKeys: String, CodingKey { + case width + case height + case size + case aspect + } + } +} diff --git a/Pachyderm/Model/Card.swift b/Pachyderm/Model/Card.swift new file mode 100644 index 00000000..e75f3589 --- /dev/null +++ b/Pachyderm/Model/Card.swift @@ -0,0 +1,50 @@ +// +// Card.swift +// Pachyderm +// +// Created by Shadowfacts on 9/9/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation + +public class Card: Decodable, ClientModel { + var client: Client! + + public let url: URL + public let title: String + public let description: String + public let image: URL? + public let kind: Kind + public let authorName: String? + public let authorURL: URL? + public let providerName: String? + public let providerURL: URL? + public let html: String? + public let width: Int? + public let height: Int? + + private enum CodingKeys: String, CodingKey { + case url + case title + case description + case image + case kind = "type" + case authorName = "author_name" + case authorURL = "author_url" + case providerName = "provider_name" + case providerURL = "provider_url" + case html + case width + case height + } +} + +extension Card { + public enum Kind: String, Decodable { + case link + case photo + case video + case rich + } +} diff --git a/Pachyderm/Model/ConversationContext.swift b/Pachyderm/Model/ConversationContext.swift new file mode 100644 index 00000000..4fa407b2 --- /dev/null +++ b/Pachyderm/Model/ConversationContext.swift @@ -0,0 +1,26 @@ +// +// Context.swift +// Pachyderm +// +// Created by Shadowfacts on 9/9/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation + +public class ConversationContext: Decodable, ClientModel { + var client: Client! { + didSet { + ancestors.client = client + descendants.client = client + } + } + + public private(set) var ancestors: [Status] + public private(set) var descendants: [Status] + + private enum CodingKeys: String, CodingKey { + case ancestors + case descendants + } +} diff --git a/Pachyderm/Model/Emoji.swift b/Pachyderm/Model/Emoji.swift new file mode 100644 index 00000000..4bf47fec --- /dev/null +++ b/Pachyderm/Model/Emoji.swift @@ -0,0 +1,32 @@ +// +// Emoji.swift +// Pachyderm +// +// Created by Shadowfacts on 9/8/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation + +public class Emoji: Decodable, ClientModel { + var client: Client! + + let shortcode: String + let url: URL + let staticURL: URL + // TODO: missing in pleroma +// let visibleInPicker: Bool + + private enum CodingKeys: String, CodingKey { + case shortcode + case url + case staticURL = "static_url" +// case visibleInPicker = "visible_in_picker" + } +} + +extension Emoji: CustomDebugStringConvertible { + public var debugDescription: String { + return ":\(shortcode):" + } +} diff --git a/Pachyderm/Model/Filter.swift b/Pachyderm/Model/Filter.swift new file mode 100644 index 00000000..17decf54 --- /dev/null +++ b/Pachyderm/Model/Filter.swift @@ -0,0 +1,70 @@ +// +// Filter.swift +// Pachyderm +// +// Created by Shadowfacts on 9/9/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation + +public class Filter: Decodable, ClientModel { + var client: Client! + + public let id: String + public var phrase: String + private var context: [String] + public var expiresAt: Date? + public var irreversible: Bool + public var wholeWord: Bool + + public var contexts: [Context] { + get { + return context.compactMap(Context.init) + } + set { + context = contexts.contextStrings + } + } + + public func update(completion: Client.Callback?) { + let request = Request(method: .put, path: "/api/v1/filters/\(id)", body: .parameters([ + "phrase" => phrase, + "irreversible" => irreversible, + "whole_word" => wholeWord, + "expires_at" => expiresAt + ] + "context" => context)) + client.run(request) { result in + completion?(result) + } + } + + public func delete(completion: @escaping Client.Callback) { + let request = Request(method: .delete, path: "/api/v1/filters/\(id)") + client.run(request, completion: completion) + } + + private enum CodingKeys: String, CodingKey { + case id + case phrase + case context + case expiresAt = "expires_at" + case irreversible + case wholeWord = "whole_word" + } +} + +extension Filter { + public enum Context: String, Decodable { + case home + case notifications + case `public` + case thread + } +} + +extension Array where Element == Filter.Context { + var contextStrings: [String] { + return map { $0.rawValue } + } +} diff --git a/Pachyderm/Model/Hashtag.swift b/Pachyderm/Model/Hashtag.swift new file mode 100644 index 00000000..be93e062 --- /dev/null +++ b/Pachyderm/Model/Hashtag.swift @@ -0,0 +1,43 @@ +// +// Hashtag.swift +// Pachyderm +// +// Created by Shadowfacts on 9/9/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation + +public class Hashtag: Decodable, ClientModel { + var client: Client! + + public let name: String + public let url: URL + public let history: [History]? + + public init(name: String, url: URL) { + self.name = name + self.url = url + self.history = nil + } + + private enum CodingKeys: String, CodingKey { + case name + case url + case history + } +} + +extension Hashtag { + public class History: Decodable { + public let day: Date + public let uses: Int + public let accounts: Int + + private enum CodingKeys: String, CodingKey { + case day + case uses + case accounts + } + } +} diff --git a/Pachyderm/Model/Instance.swift b/Pachyderm/Model/Instance.swift new file mode 100644 index 00000000..8adfa99f --- /dev/null +++ b/Pachyderm/Model/Instance.swift @@ -0,0 +1,60 @@ +// +// Instance.swift +// Pachyderm +// +// Created by Shadowfacts on 9/9/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation + +public class Instance: Decodable, ClientModel { + var client: Client! { + didSet { + contactAccount.client = client + } + } + + public let uri: String + public let title: String + public let description: String + public let email: String + public let version: String + public let urls: [String: URL] + public let languages: [String] + public let contactAccount: Account + + // MARK: Unofficial additions to the Mastodon API. + public let stats: Stats? + public let thumbnail: URL? + public let maxStatusCharacters: Int? + + private enum CodingKeys: String, CodingKey { + case uri + case title + case description + case email + case version + case urls + case languages + case contactAccount = "contact_account" + + case stats + case thumbnail + case maxStatusCharacters = "max_toot_chars" + } +} + +extension Instance { + public class Stats: Decodable { + public let domainCount: Int? + public let statusCount: Int? + public let userCount: Int? + + private enum CodingKeys: String, CodingKey { + case domainCount = "domain_count" + case statusCount = "status_count" + case userCount = "user_count" + } + } +} diff --git a/Pachyderm/Model/List.swift b/Pachyderm/Model/List.swift new file mode 100644 index 00000000..f723d475 --- /dev/null +++ b/Pachyderm/Model/List.swift @@ -0,0 +1,53 @@ +// +// List.swift +// Pachyderm +// +// Created by Shadowfacts on 9/9/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation + +public class List: Decodable, ClientModel { + var client: Client! + + public let id: String + public var title: String + + public func getAccounts(range: RequestRange = .default, completion: @escaping Client.Callback<[Account]>) { + var request = Request<[Account]>(method: .get, path: "/api/v1/lists/\(id)/accounts") + request.range = range + client.run(request, completion: completion) + } + + public func update(completion: Client.Callback?) { + let request = Request(method: .put, path: "/api/v1/lists/\(id)", body: .parameters(["title" => title])) + client.run(request) { result in + completion?(result) + } + } + + public func delete(completion: @escaping Client.Callback) { + let request = Request(method: .delete, path: "/api/v1/lists/\(id)") + client.run(request, completion: completion) + } + + public func add(accounts: [Account], completion: @escaping Client.Callback) { + let request = Request(method: .post, path: "/api/v1/lists/\(id)/accounts", body: .parameters( + "account_ids" => accounts.map { $0.id } + )) + client.run(request, completion: completion) + } + + public func remove(accounts: [Account], completion: @escaping Client.Callback) { + let request = Request(method: .delete, path: "/api/v1/lists/\(id)/accounts", body: .parameters( + "account_ids" => accounts.map { $0.id } + )) + client.run(request, completion: completion) + } + + private enum CodingKeys: String, CodingKey { + case id + case title + } +} diff --git a/Pachyderm/Model/LoginSettings.swift b/Pachyderm/Model/LoginSettings.swift new file mode 100644 index 00000000..096345e4 --- /dev/null +++ b/Pachyderm/Model/LoginSettings.swift @@ -0,0 +1,23 @@ +// +// LoginSettings.swift +// Pachyderm +// +// Created by Shadowfacts on 9/9/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation + +public class LoginSettings: Decodable { + public let accessToken: String + private let scope: String + + public var scopes: [Scope] { + return scope.components(separatedBy: .whitespaces).compactMap(Scope.init) + } + + private enum CodingKeys: String, CodingKey { + case accessToken = "access_token" + case scope + } +} diff --git a/Pachyderm/Model/MastodonError.swift b/Pachyderm/Model/MastodonError.swift new file mode 100644 index 00000000..78e99041 --- /dev/null +++ b/Pachyderm/Model/MastodonError.swift @@ -0,0 +1,17 @@ +// +// MastodonError.swift +// Pachyderm +// +// Created by Shadowfacts on 9/8/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation + +struct MastodonError: Decodable, CustomStringConvertible { + var description: String + + private enum CodingKeys: String, CodingKey { + case description = "error" + } +} diff --git a/Pachyderm/Model/Mention.swift b/Pachyderm/Model/Mention.swift new file mode 100644 index 00000000..1935e89b --- /dev/null +++ b/Pachyderm/Model/Mention.swift @@ -0,0 +1,25 @@ +// +// Mention.swift +// Pachyderm +// +// Created by Shadowfacts on 9/9/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation + +public class Mention: Decodable, ClientModel { + var client: Client! + + public let url: URL + public let username: String + public let acct: String + public let id: String + + private enum CodingKeys: String, CodingKey { + case url + case username + case acct + case id + } +} diff --git a/Pachyderm/Model/Notification.swift b/Pachyderm/Model/Notification.swift new file mode 100644 index 00000000..b282b417 --- /dev/null +++ b/Pachyderm/Model/Notification.swift @@ -0,0 +1,48 @@ +// +// Notification.swift +// Pachyderm +// +// Created by Shadowfacts on 9/9/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation + +public class Notification: Decodable, ClientModel { + var client: Client! { + didSet { + account.client = client + status?.client = client + } + } + + public let id: String + public let kind: Kind + public let createdAt: Date + public let account: Account + public let status: Status? + + public func dismiss(completion: @escaping Client.Callback) { + let request = Request(method: .post, path: "/api/v1/notifications/dismiss", body: .parameters([ + "id" => id + ])) + client.run(request, completion: completion) + } + + private enum CodingKeys: String, CodingKey { + case id + case kind = "type" + case createdAt = "created_at" + case account + case status + } +} + +extension Notification { + public enum Kind: String, Decodable { + case mention + case reblog + case favourite + case follow + } +} diff --git a/Pachyderm/Model/PushSubscription.swift b/Pachyderm/Model/PushSubscription.swift new file mode 100644 index 00000000..95a8ddbf --- /dev/null +++ b/Pachyderm/Model/PushSubscription.swift @@ -0,0 +1,26 @@ +// +// PushSubscription.swift +// Pachyderm +// +// Created by Shadowfacts on 9/9/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation + +public class PushSubscription: Decodable, ClientModel { + var client: Client! + + public let id: String + public let endpoint: URL + public let serverKey: String + // TODO: WTF is this? +// public let alerts + + private enum CodingKeys: String, CodingKey { + case id + case endpoint + case serverKey = "server_key" +// case alerts + } +} diff --git a/Pachyderm/Model/RegisteredApplication.swift b/Pachyderm/Model/RegisteredApplication.swift new file mode 100644 index 00000000..c0db4ed0 --- /dev/null +++ b/Pachyderm/Model/RegisteredApplication.swift @@ -0,0 +1,21 @@ +// +// RegisteredApplication.swift +// Pachyderm +// +// Created by Shadowfacts on 9/9/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation + +public class RegisteredApplication: Decodable { + public let id: String + public let clientID: String + public let clientSecret: String + + private enum CodingKeys: String, CodingKey { + case id + case clientID = "client_id" + case clientSecret = "client_secret" + } +} diff --git a/Pachyderm/Model/Relationship.swift b/Pachyderm/Model/Relationship.swift new file mode 100644 index 00000000..53eb90d6 --- /dev/null +++ b/Pachyderm/Model/Relationship.swift @@ -0,0 +1,35 @@ +// +// Relationship.swift +// Pachyderm +// +// Created by Shadowfacts on 9/9/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation + +public class Relationship: Decodable, ClientModel { + var client: Client! + + public let id: String + public let following: Bool + public let followedBy: Bool + public let blocked: Bool + public let muting: Bool + public let mutingNotifications: Bool + public let followRequested: Bool + public let domainBlocking: Bool + public let showingReblogs: Bool + + private enum CodingKeys: String, CodingKey { + case id + case following + case followedBy = "followed_by" + case blocked + case muting + case mutingNotifications = "muting_notifications" + case followRequested = "requested" + case domainBlocking = "domain_blocking" + case showingReblogs = "showing_reblogs" + } +} diff --git a/Pachyderm/Model/Report.swift b/Pachyderm/Model/Report.swift new file mode 100644 index 00000000..2748f6da --- /dev/null +++ b/Pachyderm/Model/Report.swift @@ -0,0 +1,21 @@ +// +// Report.swift +// Pachyderm +// +// Created by Shadowfacts on 9/9/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation + +public class Report: Decodable, ClientModel { + var client: Client! + + public let id: String + public let actionTaken: Bool + + private enum CodingKeys: String, CodingKey { + case id + case actionTaken = "action_taken" + } +} diff --git a/Pachyderm/Model/Scope.swift b/Pachyderm/Model/Scope.swift new file mode 100644 index 00000000..8f8736ea --- /dev/null +++ b/Pachyderm/Model/Scope.swift @@ -0,0 +1,21 @@ +// +// Scope.swift +// Pachyderm +// +// Created by Shadowfacts on 9/9/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation + +public enum Scope: String { + case read + case write + case follow +} + +extension Array where Element == Scope { + var scopeString: String { + return map { $0.rawValue }.joined(separator: " ") + } +} diff --git a/Pachyderm/Model/SearchResults.swift b/Pachyderm/Model/SearchResults.swift new file mode 100644 index 00000000..4494b684 --- /dev/null +++ b/Pachyderm/Model/SearchResults.swift @@ -0,0 +1,28 @@ +// +// SearchResults.swift +// Pachyderm +// +// Created by Shadowfacts on 9/9/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation + +public class SearchResults: Decodable, ClientModel { + var client: Client! { + didSet { + accounts.client = client + statuses.client = client + } + } + + public private(set) var accounts: [Account] + public private(set) var statuses: [Status] + public let hashtags: [String] + + private enum CodingKeys: String, CodingKey { + case accounts + case statuses + case hashtags + } +} diff --git a/Pachyderm/Model/Status.swift b/Pachyderm/Model/Status.swift new file mode 100644 index 00000000..28b5bc3a --- /dev/null +++ b/Pachyderm/Model/Status.swift @@ -0,0 +1,222 @@ +// +// Status.swift +// Pachyderm +// +// Created by Shadowfacts on 9/9/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation + +public class Status: Decodable, ClientModel { + var client: Client! { + didSet { + didSetClient() + } + } + // when reblog.client is set directly from self.client didSet, reblog.client didSet is never called + private func didSetClient() { + account.client = client + reblog?.client = client + emojis.client = client + attachments.client = client + mentions.client = client + hashtags.client = client + application?.client = client + } + + public let id: String + public let uri: String + public let url: URL? + public let account: Account + public let inReplyToID: String? + public let inReplyToAccountID: String? + public private(set) var reblog: Status? + public let content: String + public let createdAt: Date + public private(set) var emojis: [Emoji] + // TODO: missing from pleroma +// public let repliesCount: Int + public let reblogsCount: Int + public let favouritesCount: Int + public var reblogged: Bool? + public var favourited: Bool? + public var muted: Bool? + public let sensitive: Bool + public let spoilerText: String + public let visibility: Visibility + public private(set) var attachments: [Attachment] + public private(set) var mentions: [Mention] + public private(set) var hashtags: [Hashtag] + public private(set) var application: Application? + public let language: String? + public var pinned: Bool? + + public func getContext(completion: @escaping Client.Callback) { + let request = Request(method: .get, path: "/api/v1/statuses/\(id)/context") + client.run(request, completion: completion) + } + + public func getCard(completion: @escaping Client.Callback) { + let request = Request(method: .get, path: "/api/v1/statuses/\(id)/card") + client.run(request, completion: completion) + } + + public func getFavourites(range: RequestRange = .default, completion: @escaping Client.Callback<[Account]>) { + var request = Request<[Account]>(method: .get, path: "/api/v1/statuses/\(id)/favourited_by") + request.range = range + client.run(request, completion: completion) + } + + public func getReblogs(range: RequestRange = .default, completion: @escaping Client.Callback<[Account]>) { + var request = Request<[Account]>(method: .get, path: "/api/v1/statuses/\(id)/reblogged_by") + request.range = range + client.run(request, completion: completion) + } + + public func delete(completion: @escaping Client.Callback) { + let request = Request(method: .delete, path: "/api/v1/statuses/\(id)") + client.run(request, completion: completion) + } + + public func reblog(completion: @escaping Client.Callback) { + let oldValue = reblogged + let request = Request(method: .post, path: "/api/v1/statuses/\(id)/reblog") + client.run(request) { response in + if case .success = response { + self.reblogged = true + } else { + self.reblogged = oldValue + } + completion(response) + } + } + + public func unreblog(completion: @escaping Client.Callback) { + let oldValue = reblogged + let request = Request(method: .post, path: "/api/v1/statuses/\(id)/unreblog") + client.run(request) { response in + if case .success = response { + self.reblogged = false + } else { + self.reblogged = oldValue + } + completion(response) + } + } + + public func favourite(completion: @escaping Client.Callback) { + let oldValue = favourited + let request = Request(method: .post, path: "/api/v1/statuses/\(id)/favourite") + client.run(request) { response in + if case .success = response { + self.favourited = true + } else { + self.favourited = oldValue + } + completion(response) + } + } + + public func unfavourite(completion: @escaping Client.Callback) { + let oldValue = favourited + let request = Request(method: .post, path: "/api/v1/statuses/\(id)/unfavourite") + client.run(request) { response in + if case .success = response { + self.favourited = false + } else { + self.favourited = oldValue + } + completion(response) + } + } + + public func pin(completion: @escaping Client.Callback) { + let oldValue = pinned + let request = Request(method: .post, path: "/api/v1/statuses/\(id)/pin") + client.run(request) { response in + if case .success = response { + self.pinned = true + } else { + self.pinned = oldValue + } + completion(response) + } + } + + public func unpin(completion: @escaping Client.Callback) { + let oldValue = pinned + let request = Request(method: .post, path: "/api/v1/statuses/\(id)/unpin") + client.run(request) { response in + if case .success = response { + self.pinned = false + } else { + self.pinned = oldValue + } + completion(response) + } + } + + public func muteConversation(completion: @escaping Client.Callback) { + let oldValue = muted + let request = Request(method: .post, path: "/api/v1/statuses/\(id)/mute") + client.run(request) { response in + if case .success = response { + self.muted = true + } else { + self.muted = oldValue + } + completion(response) + } + } + + public func unmuteConversation(completion: @escaping Client.Callback) { + let oldValue = muted + let request = Request(method: .post, path: "/api/v1/statuses/\(id)/unmute") + client.run(request) { response in + if case .success = response { + self.muted = false + } else { + self.muted = oldValue + } + completion(response) + } + } + + private enum CodingKeys: String, CodingKey { + case id + case uri + case url + case account + case inReplyToID = "in_reply_to_id" + case inReplyToAccountID = "in_reply_to_account_id" + case reblog + case content + case createdAt = "created_at" + case emojis +// case repliesCount = "replies_count" + case reblogsCount = "reblogs_count" + case favouritesCount = "favourites_count" + case reblogged + case favourited + case muted + case sensitive + case spoilerText = "spoiler_text" + case visibility + case attachments = "media_attachments" + case mentions + case hashtags = "tags" + case application + case language + case pinned + } +} + +extension Status { + public enum Visibility: String, Codable, CaseIterable { + case `public` + case unlisted + case `private` + case direct + } +} diff --git a/Pachyderm/Model/Timeline.swift b/Pachyderm/Model/Timeline.swift new file mode 100644 index 00000000..e1eb7794 --- /dev/null +++ b/Pachyderm/Model/Timeline.swift @@ -0,0 +1,40 @@ +// +// Timeline.swift +// Pachyderm +// +// Created by Shadowfacts on 9/9/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation + +public enum Timeline { + case home + case `public`(local: Bool) + case tag(hashtag: String) + case list(id: String) + case direct +} + +extension Timeline { + func request(range: RequestRange) -> Request<[Status]> { + var request: Request<[Status]> + switch self { + case .home: + request = Request(method: .get, path: "/api/v1/timelines/home") + case let .public(local): + request = Request(method: .get, path: "/api/v1/timelines/public") + if local { + request.queryParameters.append("local" => true) + } + case let .tag(hashtag): + request = Request(method: .get, path: "/api/v1/timeliens/tag/\(hashtag)") + case let .list(id): + request = Request(method: .get, path: "/api/v1/timelines/list/\(id)") + case .direct: + request = Request(method: .get, path: "/api/v1/timelines/direct") + } + request.range = range + return request + } +} diff --git a/Pachyderm/Pachyderm.h b/Pachyderm/Pachyderm.h new file mode 100644 index 00000000..85604eeb --- /dev/null +++ b/Pachyderm/Pachyderm.h @@ -0,0 +1,19 @@ +// +// Pachyderm.h +// Pachyderm +// +// Created by Shadowfacts on 9/8/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +#import + +//! Project version number for Pachyderm. +FOUNDATION_EXPORT double PachydermVersionNumber; + +//! Project version string for Pachyderm. +FOUNDATION_EXPORT const unsigned char PachydermVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/Pachyderm/Request/Body.swift b/Pachyderm/Request/Body.swift new file mode 100644 index 00000000..bb1b74dd --- /dev/null +++ b/Pachyderm/Request/Body.swift @@ -0,0 +1,63 @@ +// +// Body.swift +// Pachyderm +// +// Created by Shadowfacts on 9/8/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation + +enum Body { + case parameters([Parameter]?) + case formData([Parameter]?, FormAttachment?) + case empty +} + +extension Body { + private static let boundary: String = "PachydermBoundary" + + var data: Data? { + switch self { + case let .parameters(parameters): + return parameters?.urlEncoded.data(using: .utf8) + case let .formData(parameters, attachment): + var data = Data() + parameters?.forEach { param in + guard let value = param.value else { return } + data.append("--\(Body.boundary)\r\n") + data.append("Content-Disposition: form-data; name=\"\(param.name)\"\r\n\r\n") + data.append("\(value)\r\n") + } + if let attachment = attachment { + data.append("--\(Body.boundary)\r\n") + data.append("Content-Disposition: form-data; name=\"file\"; filename=\"\(attachment.fileName)\"\r\n") + data.append("Content-Type: \(attachment.mimeType)\r\n\r\n") + data.append(attachment.data) + data.append("\r\n") + } + + data.append("--\(Body.boundary)--\r\n") + return data + case .empty: + return nil + } + } + + var mimeType: String? { + switch self { + case let .parameters(parameters): + if parameters == nil { + return nil + } + return "application/x-www-form-urlencoded; charset=utf-8" + case let .formData(parameters, attachment): + if parameters == nil && attachment == nil { + return nil + } + return "multipart/form-data; boundary=\(Body.boundary)" + case .empty: + return nil + } + } +} diff --git a/Pachyderm/Request/FormAttachment.swift b/Pachyderm/Request/FormAttachment.swift new file mode 100644 index 00000000..76cc80c0 --- /dev/null +++ b/Pachyderm/Request/FormAttachment.swift @@ -0,0 +1,35 @@ +// +// Attachment.swift +// Pachyderm +// +// Created by Shadowfacts on 9/8/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation + +public struct FormAttachment { + let mimeType: String + let data: Data + let fileName: String + + public init(mimeType: String, data: Data, fileName: String) { + self.mimeType = mimeType + self.data = data + self.fileName = fileName + } +} + +extension FormAttachment { + public init(jepgData data: Data, fileName: String = "file.jpg") { + self.init(mimeType: "image/jpg", data: data, fileName: fileName) + } + + public init(pngData data: Data, fileName: String = "file.png") { + self.init(mimeType: "image/png", data: data, fileName: fileName) + } + + public init(gifData data: Data, fileName: String = "file.gif") { + self.init(mimeType: "image/gif", data: data, fileName: fileName) + } +} diff --git a/Pachyderm/Request/Method.swift b/Pachyderm/Request/Method.swift new file mode 100644 index 00000000..82285d8e --- /dev/null +++ b/Pachyderm/Request/Method.swift @@ -0,0 +1,30 @@ +// +// Method.swift +// Pachyderm +// +// Created by Shadowfacts on 9/8/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation + +enum Method { + case get, post, put, patch, delete +} + +extension Method { + var name: String { + switch self { + case .get: + return "GET" + case .post: + return "POST" + case .put: + return "PUT" + case .patch: + return "PATCH" + case .delete: + return "DELETE" + } + } +} diff --git a/Pachyderm/Request/Parameter.swift b/Pachyderm/Request/Parameter.swift new file mode 100644 index 00000000..c6362765 --- /dev/null +++ b/Pachyderm/Request/Parameter.swift @@ -0,0 +1,80 @@ +// +// Parameter.swift +// Pachyderm +// +// Created by Shadowfacts on 9/8/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation + +struct Parameter { + let name: String + let value: String? +} +precedencegroup ParameterizationPrecedence { + associativity: left + higherThan: AdditionPrecedence +} +infix operator => : ParameterizationPrecedence + +extension String { + static func =>(name: String, value: String?) -> Parameter { + return Parameter(name: name, value: value) + } + + static func =>(name: String, value: Bool?) -> Parameter { + return Parameter(name: name, value: value?.description) + } + + static func =>(name: String, value: Int?) -> Parameter { + return Parameter(name: name, value: value?.description) + } + + static func =>(name: String, value: Date?) -> Parameter { + if let value = value { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + let string = formatter.string(from: value) + return Parameter(name: name, value: string) + } else { + return Parameter(name: name, value: nil) + } + } + + static func =>(name: String, focus: (Float, Float)?) -> Parameter { + guard let focus = focus else { return Parameter(name: name, value: nil) } + return Parameter(name: name, value: "\(focus.0),\(focus.1)") + } + + static func =>(name: String, values: [String]?) -> [Parameter] { + guard let values = values else { return [] } + let name = "\(name)[]" + return values.map { Parameter(name: name, value: $0) } + } +} + +extension Parameter: CustomStringConvertible { + var description: String { + if let value = value { + return "\(name)=\(value)" + } else { + return name + } + } +} + +extension Array where Element == Parameter { + var urlEncoded: String { + return compactMap { + guard let value = $0.value else { return nil } + return "\($0.name)=\(value)" + }.joined(separator: "&") + } + + var queryItems: [URLQueryItem] { + return compactMap { + URLQueryItem(name: $0.name, value: $0.value) + } + } +} diff --git a/Pachyderm/Request/Request.swift b/Pachyderm/Request/Request.swift new file mode 100644 index 00000000..c9b3a1b0 --- /dev/null +++ b/Pachyderm/Request/Request.swift @@ -0,0 +1,57 @@ +// +// Request.swift +// Pachyderm +// +// Created by Shadowfacts on 9/8/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation + +struct Request { + let method: Method + let path: String + let body: Body + var queryParameters: [Parameter] + + init(method: Method, path: String, body: Body = .empty, queryParameters: [Parameter] = []) { + self.method = method + self.path = path + self.body = body + self.queryParameters = queryParameters + } +} + +extension Request { + var range: RequestRange { + get { + let max = queryParameters.first { $0.name == "max_id" } + let since = queryParameters.first { $0.name == "since_id" } + let count = queryParameters.first { $0.name == "count" } + if let max = max, let count = count { + return .before(id: max.value!, count: Int(count.value!)!) + } else if let since = since, let count = count { + return .after(id: since.value!, count: Int(count.value!)!) + } else if let count = count { + return .count(Int(count.value!)!) + } else { + return .default + } + } + set { + let rangeParams = newValue.queryParameters + let max = rangeParams.first { $0.name == "max_id" } + let since = rangeParams.first { $0.name == "since_id" } + let count = rangeParams.first { $0.name == "count" } + if let max = max, let i = queryParameters.firstIndex(where: { $0.name == "max_id" }) { + queryParameters[i] = max + } + if let since = since, let i = queryParameters.firstIndex(where: { $0.name == "since_id" }) { + queryParameters[i] = since + } + if let count = count, let i = queryParameters.firstIndex(where: { $0.name == "count" }) { + queryParameters[i] = count + } + } + } +} diff --git a/Pachyderm/Request/RequestRange.swift b/Pachyderm/Request/RequestRange.swift new file mode 100644 index 00000000..9e6d66b1 --- /dev/null +++ b/Pachyderm/Request/RequestRange.swift @@ -0,0 +1,31 @@ +// +// RequestRange.swift +// Pachyderm +// +// Created by Shadowfacts on 9/9/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation + +public enum RequestRange { + case `default` + case count(Int) + case before(id: String, count: Int?) + case after(id: String, count: Int?) +} + +extension RequestRange { + var queryParameters: [Parameter] { + switch self { + case .default: + return [] + case let .count(count): + return ["limit" => count] + case let .before(id, count): + return ["max_id" => id, "count" => count] + case let .after(id, count): + return ["since_id" => id, "count" => count] + } + } +} diff --git a/Pachyderm/Response/Empty.swift b/Pachyderm/Response/Empty.swift new file mode 100644 index 00000000..754b8745 --- /dev/null +++ b/Pachyderm/Response/Empty.swift @@ -0,0 +1,13 @@ +// +// Empty.swift +// Pachyderm +// +// Created by Shadowfacts on 9/9/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation + +public struct Empty: Decodable { + +} diff --git a/Pachyderm/Response/Pagination.swift b/Pachyderm/Response/Pagination.swift new file mode 100644 index 00000000..c0ec4cec --- /dev/null +++ b/Pachyderm/Response/Pagination.swift @@ -0,0 +1,73 @@ +// +// Pagination.swift +// Pachyderm +// +// Created by Shadowfacts on 9/9/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation + +public struct Pagination { + public let older: RequestRange? + public let newer: RequestRange? +} + +extension Pagination { + init(string: String) { + let links = string.components(separatedBy: ",").compactMap(Item.init) + self.older = links.first(where: { $0.kind == .next })?.range + self.newer = links.first(where: { $0.kind == .prev })?.range + } +} + +extension Pagination { + struct Item { + let kind: Kind + let id: String + let limit: Int? + + var range: RequestRange { + switch kind { + case .next: + return .after(id: id, count: limit) + case .prev: + return .before(id: id, count: limit) + } + } + + init?(string: String) { + let segments = string.components(separatedBy: .whitespaces).filter { !$0.isEmpty }.joined().components(separatedBy: ";") + + let url = segments.first.flatMap { str in + String(str[str.index(after: str.startIndex).. { + case success(Result, Pagination?) + case failure(Error) +} diff --git a/PachydermTests/Info.plist b/PachydermTests/Info.plist new file mode 100644 index 00000000..6c40a6cd --- /dev/null +++ b/PachydermTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/PachydermTests/PachydermTests.swift b/PachydermTests/PachydermTests.swift new file mode 100644 index 00000000..aa685099 --- /dev/null +++ b/PachydermTests/PachydermTests.swift @@ -0,0 +1,34 @@ +// +// PachydermTests.swift +// PachydermTests +// +// Created by Shadowfacts on 9/8/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import XCTest +@testable import Pachyderm + +class PachydermTests: XCTestCase { + + override func setUp() { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testPerformanceExample() { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index f3f6a8f2..75553265 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -7,9 +7,47 @@ objects = { /* Begin PBXBuildFile section */ - 04DACE8A212CA6B7009840C4 /* Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE89212CA6B7009840C4 /* Timeline.swift */; }; 04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */; }; 04DACE8E212CC7CC009840C4 /* AvatarCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8D212CC7CC009840C4 /* AvatarCache.swift */; }; + D61099B42144B0CC00432DC2 /* Pachyderm.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D61099AB2144B0CC00432DC2 /* Pachyderm.framework */; }; + D61099BB2144B0CC00432DC2 /* PachydermTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61099BA2144B0CC00432DC2 /* PachydermTests.swift */; }; + D61099BD2144B0CC00432DC2 /* Pachyderm.h in Headers */ = {isa = PBXBuildFile; fileRef = D61099AD2144B0CC00432DC2 /* Pachyderm.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D61099C02144B0CC00432DC2 /* Pachyderm.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D61099AB2144B0CC00432DC2 /* Pachyderm.framework */; }; + D61099C12144B0CC00432DC2 /* Pachyderm.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D61099AB2144B0CC00432DC2 /* Pachyderm.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D61099C92144B13C00432DC2 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61099C82144B13C00432DC2 /* Client.swift */; }; + D61099CB2144B20500432DC2 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61099CA2144B20500432DC2 /* Request.swift */; }; + D61099D02144B2D700432DC2 /* Method.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61099CF2144B2D700432DC2 /* Method.swift */; }; + D61099D22144B2E600432DC2 /* Body.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61099D12144B2E600432DC2 /* Body.swift */; }; + D61099D42144B32E00432DC2 /* Parameter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61099D32144B32E00432DC2 /* Parameter.swift */; }; + D61099D62144B4B200432DC2 /* FormAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61099D52144B4B200432DC2 /* FormAttachment.swift */; }; + D61099D92144B76400432DC2 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61099D82144B76400432DC2 /* Data.swift */; }; + D61099DC2144BDBF00432DC2 /* Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61099DB2144BDBF00432DC2 /* Response.swift */; }; + D61099DF2144C11400432DC2 /* MastodonError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61099DE2144C11400432DC2 /* MastodonError.swift */; }; + D61099E12144C1DC00432DC2 /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61099E02144C1DC00432DC2 /* Account.swift */; }; + D61099E32144C38900432DC2 /* Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61099E22144C38900432DC2 /* Emoji.swift */; }; + D61099E5214561AB00432DC2 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61099E4214561AB00432DC2 /* Application.swift */; }; + D61099E7214561FF00432DC2 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61099E6214561FF00432DC2 /* Attachment.swift */; }; + D61099E92145658300432DC2 /* Card.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61099E82145658300432DC2 /* Card.swift */; }; + D61099EB2145661700432DC2 /* ConversationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61099EA2145661700432DC2 /* ConversationContext.swift */; }; + D61099ED2145664800432DC2 /* Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61099EC2145664800432DC2 /* Filter.swift */; }; + D61099EF214566C000432DC2 /* Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61099EE214566C000432DC2 /* Instance.swift */; }; + D61099F12145686D00432DC2 /* List.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61099F02145686D00432DC2 /* List.swift */; }; + D61099F32145688600432DC2 /* Mention.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61099F22145688600432DC2 /* Mention.swift */; }; + D61099F5214568C300432DC2 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61099F4214568C300432DC2 /* Notification.swift */; }; + D61099F72145693500432DC2 /* PushSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61099F62145693500432DC2 /* PushSubscription.swift */; }; + D61099F92145698900432DC2 /* Relationship.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61099F82145698900432DC2 /* Relationship.swift */; }; + D61099FB214569F600432DC2 /* Report.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61099FA214569F600432DC2 /* Report.swift */; }; + D61099FD21456A1D00432DC2 /* SearchResults.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61099FC21456A1D00432DC2 /* SearchResults.swift */; }; + D61099FF21456A4C00432DC2 /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61099FE21456A4C00432DC2 /* Status.swift */; }; + D6109A0121456B0800432DC2 /* Hashtag.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6109A0021456B0800432DC2 /* Hashtag.swift */; }; + D6109A032145722C00432DC2 /* RegisteredApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6109A022145722C00432DC2 /* RegisteredApplication.swift */; }; + D6109A05214572BF00432DC2 /* Scope.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6109A04214572BF00432DC2 /* Scope.swift */; }; + D6109A072145756700432DC2 /* LoginSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6109A062145756700432DC2 /* LoginSettings.swift */; }; + D6109A0921458C4A00432DC2 /* Empty.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6109A0821458C4A00432DC2 /* Empty.swift */; }; + D6109A0B2145953C00432DC2 /* ClientModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6109A0A2145953C00432DC2 /* ClientModel.swift */; }; + D6109A0D214599E100432DC2 /* RequestRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6109A0C214599E100432DC2 /* RequestRange.swift */; }; + D6109A0F21459B6900432DC2 /* Pagination.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6109A0E21459B6900432DC2 /* Pagination.swift */; }; + D6109A11214607D500432DC2 /* Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6109A10214607D500432DC2 /* Timeline.swift */; }; D6333B372137838300CE884A /* AttributedString+Trim.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B362137838300CE884A /* AttributedString+Trim.swift */; }; D6333B772138D94E00CE884A /* ComposeMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B762138D94E00CE884A /* ComposeMediaView.swift */; }; D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */; }; @@ -28,6 +66,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 */; }; + D65A37F321472F300087646E /* SwiftSoup.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */; }; D663625D2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D663625C2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib */; }; D663625F2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663625E2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift */; }; D663626221360B1900C9CBA2 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663626121360B1900C9CBA2 /* Preferences.swift */; }; @@ -50,7 +89,6 @@ 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 */; }; - D6BED16F212663DA00F02DA0 /* SwiftSoup.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */; }; 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 */; }; @@ -62,14 +100,33 @@ D6D4DDDA212518A200E1C4BB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D6D4DDD8212518A200E1C4BB /* LaunchScreen.storyboard */; }; D6D4DDE5212518A200E1C4BB /* TuskerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDE4212518A200E1C4BB /* TuskerTests.swift */; }; D6D4DDF0212518A200E1C4BB /* TuskerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDEF212518A200E1C4BB /* TuskerUITests.swift */; }; - D6F953E7212519A400CF0F2B /* MastodonKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D6F953E6212519A400CF0F2B /* MastodonKit.framework */; }; - D6F953E8212519A400CF0F2B /* MastodonKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D6F953E6212519A400CF0F2B /* MastodonKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; D6F953EC212519E700CF0F2B /* TimelineTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F953EB212519E700CF0F2B /* TimelineTableViewController.swift */; }; D6F953EE21251A0700CF0F2B /* Timeline.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D6F953ED21251A0700CF0F2B /* Timeline.storyboard */; }; D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F953EF21251A2900CF0F2B /* MastodonController.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + D61099B52144B0CC00432DC2 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D6D4DDC4212518A000E1C4BB /* Project object */; + proxyType = 1; + remoteGlobalIDString = D61099AA2144B0CC00432DC2; + remoteInfo = Pachyderm; + }; + D61099B72144B0CC00432DC2 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D6D4DDC4212518A000E1C4BB /* Project object */; + proxyType = 1; + remoteGlobalIDString = D6D4DDCB212518A000E1C4BB; + remoteInfo = Tusker; + }; + D61099BE2144B0CC00432DC2 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D6D4DDC4212518A000E1C4BB /* Project object */; + proxyType = 1; + remoteGlobalIDString = D61099AA2144B0CC00432DC2; + remoteInfo = Pachyderm; + }; D6D4DDE1212518A200E1C4BB /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = D6D4DDC4212518A000E1C4BB /* Project object */; @@ -93,7 +150,7 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - D6F953E8212519A400CF0F2B /* MastodonKit.framework in Embed Frameworks */, + D61099C12144B0CC00432DC2 /* Pachyderm.framework in Embed Frameworks */, D6BED170212663DA00F02DA0 /* SwiftSoup.framework in Embed Frameworks */, ); name = "Embed Frameworks"; @@ -102,9 +159,48 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 04DACE89212CA6B7009840C4 /* Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timeline.swift; sourceTree = ""; }; 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarViewController.swift; sourceTree = ""; }; 04DACE8D212CC7CC009840C4 /* AvatarCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarCache.swift; sourceTree = ""; }; + D61099AB2144B0CC00432DC2 /* Pachyderm.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pachyderm.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D61099AD2144B0CC00432DC2 /* Pachyderm.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Pachyderm.h; sourceTree = ""; }; + D61099AE2144B0CC00432DC2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D61099B32144B0CC00432DC2 /* PachydermTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PachydermTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + D61099BA2144B0CC00432DC2 /* PachydermTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PachydermTests.swift; sourceTree = ""; }; + D61099BC2144B0CC00432DC2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D61099C82144B13C00432DC2 /* Client.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Client.swift; sourceTree = ""; }; + D61099CA2144B20500432DC2 /* Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; }; + D61099CF2144B2D700432DC2 /* Method.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Method.swift; sourceTree = ""; }; + D61099D12144B2E600432DC2 /* Body.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Body.swift; sourceTree = ""; }; + D61099D32144B32E00432DC2 /* Parameter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Parameter.swift; sourceTree = ""; }; + D61099D52144B4B200432DC2 /* FormAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormAttachment.swift; sourceTree = ""; }; + D61099D82144B76400432DC2 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = ""; }; + D61099DB2144BDBF00432DC2 /* Response.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Response.swift; sourceTree = ""; }; + D61099DE2144C11400432DC2 /* MastodonError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonError.swift; sourceTree = ""; }; + D61099E02144C1DC00432DC2 /* Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = ""; }; + D61099E22144C38900432DC2 /* Emoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emoji.swift; sourceTree = ""; }; + D61099E4214561AB00432DC2 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; + D61099E6214561FF00432DC2 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = ""; }; + D61099E82145658300432DC2 /* Card.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Card.swift; sourceTree = ""; }; + D61099EA2145661700432DC2 /* ConversationContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationContext.swift; sourceTree = ""; }; + D61099EC2145664800432DC2 /* Filter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Filter.swift; sourceTree = ""; }; + D61099EE214566C000432DC2 /* Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instance.swift; sourceTree = ""; }; + D61099F02145686D00432DC2 /* List.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = List.swift; sourceTree = ""; }; + D61099F22145688600432DC2 /* Mention.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mention.swift; sourceTree = ""; }; + D61099F4214568C300432DC2 /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = ""; }; + D61099F62145693500432DC2 /* PushSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushSubscription.swift; sourceTree = ""; }; + D61099F82145698900432DC2 /* Relationship.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Relationship.swift; sourceTree = ""; }; + D61099FA214569F600432DC2 /* Report.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Report.swift; sourceTree = ""; }; + D61099FC21456A1D00432DC2 /* SearchResults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResults.swift; sourceTree = ""; }; + D61099FE21456A4C00432DC2 /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = ""; }; + D6109A0021456B0800432DC2 /* Hashtag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Hashtag.swift; sourceTree = ""; }; + D6109A022145722C00432DC2 /* RegisteredApplication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisteredApplication.swift; sourceTree = ""; }; + D6109A04214572BF00432DC2 /* Scope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Scope.swift; sourceTree = ""; }; + D6109A062145756700432DC2 /* LoginSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginSettings.swift; sourceTree = ""; }; + D6109A0821458C4A00432DC2 /* Empty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Empty.swift; sourceTree = ""; }; + D6109A0A2145953C00432DC2 /* ClientModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientModel.swift; sourceTree = ""; }; + D6109A0C214599E100432DC2 /* RequestRange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestRange.swift; sourceTree = ""; }; + D6109A0E21459B6900432DC2 /* Pagination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pagination.swift; sourceTree = ""; }; + D6109A10214607D500432DC2 /* Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timeline.swift; sourceTree = ""; }; D6333B362137838300CE884A /* AttributedString+Trim.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttributedString+Trim.swift"; sourceTree = ""; }; D6333B762138D94E00CE884A /* ComposeMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeMediaView.swift; sourceTree = ""; }; D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+TimeAgo.swift"; sourceTree = ""; }; @@ -169,12 +265,27 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + D61099A82144B0CC00432DC2 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D61099B02144B0CC00432DC2 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D61099B42144B0CC00432DC2 /* Pachyderm.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; D6D4DDC9212518A000E1C4BB /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D6F953E7212519A400CF0F2B /* MastodonKit.framework in Frameworks */, - D6BED16F212663DA00F02DA0 /* SwiftSoup.framework in Frameworks */, + D61099C02144B0CC00432DC2 /* Pachyderm.framework in Frameworks */, + D65A37F321472F300087646E /* SwiftSoup.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -195,6 +306,90 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + D61099AC2144B0CC00432DC2 /* Pachyderm */ = { + isa = PBXGroup; + children = ( + D61099AD2144B0CC00432DC2 /* Pachyderm.h */, + D61099AE2144B0CC00432DC2 /* Info.plist */, + D61099C82144B13C00432DC2 /* Client.swift */, + D6109A0A2145953C00432DC2 /* ClientModel.swift */, + D61099D72144B74500432DC2 /* Extensions */, + D61099CC2144B2C300432DC2 /* Request */, + D61099DA2144BDB600432DC2 /* Response */, + D61099DD2144C10C00432DC2 /* Model */, + ); + path = Pachyderm; + sourceTree = ""; + }; + D61099B92144B0CC00432DC2 /* PachydermTests */ = { + isa = PBXGroup; + children = ( + D61099BA2144B0CC00432DC2 /* PachydermTests.swift */, + D61099BC2144B0CC00432DC2 /* Info.plist */, + ); + path = PachydermTests; + sourceTree = ""; + }; + D61099CC2144B2C300432DC2 /* Request */ = { + isa = PBXGroup; + children = ( + D61099CA2144B20500432DC2 /* Request.swift */, + D6109A0C214599E100432DC2 /* RequestRange.swift */, + D61099CF2144B2D700432DC2 /* Method.swift */, + D61099D12144B2E600432DC2 /* Body.swift */, + D61099D32144B32E00432DC2 /* Parameter.swift */, + D61099D52144B4B200432DC2 /* FormAttachment.swift */, + ); + path = Request; + sourceTree = ""; + }; + D61099D72144B74500432DC2 /* Extensions */ = { + isa = PBXGroup; + children = ( + D61099D82144B76400432DC2 /* Data.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + D61099DA2144BDB600432DC2 /* Response */ = { + isa = PBXGroup; + children = ( + D61099DB2144BDBF00432DC2 /* Response.swift */, + D6109A0821458C4A00432DC2 /* Empty.swift */, + D6109A0E21459B6900432DC2 /* Pagination.swift */, + ); + path = Response; + sourceTree = ""; + }; + D61099DD2144C10C00432DC2 /* Model */ = { + isa = PBXGroup; + children = ( + D61099DE2144C11400432DC2 /* MastodonError.swift */, + D6109A04214572BF00432DC2 /* Scope.swift */, + D61099E02144C1DC00432DC2 /* Account.swift */, + D61099E4214561AB00432DC2 /* Application.swift */, + D61099E6214561FF00432DC2 /* Attachment.swift */, + D61099E82145658300432DC2 /* Card.swift */, + D61099EA2145661700432DC2 /* ConversationContext.swift */, + D61099E22144C38900432DC2 /* Emoji.swift */, + D61099EC2145664800432DC2 /* Filter.swift */, + D6109A0021456B0800432DC2 /* Hashtag.swift */, + D61099EE214566C000432DC2 /* Instance.swift */, + D61099F02145686D00432DC2 /* List.swift */, + D6109A062145756700432DC2 /* LoginSettings.swift */, + D61099F22145688600432DC2 /* Mention.swift */, + D61099F4214568C300432DC2 /* Notification.swift */, + D61099F62145693500432DC2 /* PushSubscription.swift */, + D6109A022145722C00432DC2 /* RegisteredApplication.swift */, + D61099F82145698900432DC2 /* Relationship.swift */, + D61099FA214569F600432DC2 /* Report.swift */, + D61099FC21456A1D00432DC2 /* SearchResults.swift */, + D61099FE21456A4C00432DC2 /* Status.swift */, + D6109A10214607D500432DC2 /* Timeline.swift */, + ); + path = Model; + sourceTree = ""; + }; D641C780213DD7C4004B4513 /* Screens */ = { isa = PBXGroup; children = ( @@ -335,6 +530,13 @@ path = Transitions; sourceTree = ""; }; + D65A37F221472F300087646E /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; D663626021360A9600C9CBA2 /* Preferences */ = { isa = PBXGroup; children = ( @@ -379,10 +581,13 @@ children = ( D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */, D6F953E6212519A400CF0F2B /* MastodonKit.framework */, + D61099AC2144B0CC00432DC2 /* Pachyderm */, + D61099B92144B0CC00432DC2 /* PachydermTests */, D6D4DDCE212518A000E1C4BB /* Tusker */, D6D4DDE3212518A200E1C4BB /* TuskerTests */, D6D4DDEE212518A200E1C4BB /* TuskerUITests */, D6D4DDCD212518A000E1C4BB /* Products */, + D65A37F221472F300087646E /* Frameworks */, ); sourceTree = ""; }; @@ -392,6 +597,8 @@ D6D4DDCC212518A000E1C4BB /* Tusker.app */, D6D4DDE0212518A200E1C4BB /* TuskerTests.xctest */, D6D4DDEB212518A200E1C4BB /* TuskerUITests.xctest */, + D61099AB2144B0CC00432DC2 /* Pachyderm.framework */, + D61099B32144B0CC00432DC2 /* PachydermTests.xctest */, ); name = Products; sourceTree = ""; @@ -400,7 +607,6 @@ isa = PBXGroup; children = ( D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */, - 04DACE89212CA6B7009840C4 /* Timeline.swift */, D64D0AAC2128D88B005A6F37 /* LocalData.swift */, 04DACE8D212CC7CC009840C4 /* AvatarCache.swift */, D663626021360A9600C9CBA2 /* Preferences */, @@ -443,7 +649,55 @@ }; /* End PBXGroup section */ +/* Begin PBXHeadersBuildPhase section */ + D61099A62144B0CC00432DC2 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + D61099BD2144B0CC00432DC2 /* Pachyderm.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + /* Begin PBXNativeTarget section */ + D61099AA2144B0CC00432DC2 /* Pachyderm */ = { + isa = PBXNativeTarget; + buildConfigurationList = D61099C22144B0CC00432DC2 /* Build configuration list for PBXNativeTarget "Pachyderm" */; + buildPhases = ( + D61099A62144B0CC00432DC2 /* Headers */, + D61099A72144B0CC00432DC2 /* Sources */, + D61099A82144B0CC00432DC2 /* Frameworks */, + D61099A92144B0CC00432DC2 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Pachyderm; + productName = Pachyderm; + productReference = D61099AB2144B0CC00432DC2 /* Pachyderm.framework */; + productType = "com.apple.product-type.framework"; + }; + D61099B22144B0CC00432DC2 /* PachydermTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = D61099C52144B0CC00432DC2 /* Build configuration list for PBXNativeTarget "PachydermTests" */; + buildPhases = ( + D61099AF2144B0CC00432DC2 /* Sources */, + D61099B02144B0CC00432DC2 /* Frameworks */, + D61099B12144B0CC00432DC2 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D61099B62144B0CC00432DC2 /* PBXTargetDependency */, + D61099B82144B0CC00432DC2 /* PBXTargetDependency */, + ); + name = PachydermTests; + productName = PachydermTests; + productReference = D61099B32144B0CC00432DC2 /* PachydermTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; D6D4DDCB212518A000E1C4BB /* Tusker */ = { isa = PBXNativeTarget; buildConfigurationList = D6D4DDF4212518A200E1C4BB /* Build configuration list for PBXNativeTarget "Tusker" */; @@ -456,6 +710,7 @@ buildRules = ( ); dependencies = ( + D61099BF2144B0CC00432DC2 /* PBXTargetDependency */, ); name = Tusker; productName = Tusker; @@ -508,6 +763,14 @@ LastUpgradeCheck = 1000; ORGANIZATIONNAME = Shadowfacts; TargetAttributes = { + D61099AA2144B0CC00432DC2 = { + CreatedOnToolsVersion = 10.0; + LastSwiftMigration = 1000; + }; + D61099B22144B0CC00432DC2 = { + CreatedOnToolsVersion = 10.0; + TestTargetID = D6D4DDCB212518A000E1C4BB; + }; D6D4DDCB212518A000E1C4BB = { CreatedOnToolsVersion = 10.0; }; @@ -537,11 +800,27 @@ D6D4DDCB212518A000E1C4BB /* Tusker */, D6D4DDDF212518A200E1C4BB /* TuskerTests */, D6D4DDEA212518A200E1C4BB /* TuskerUITests */, + D61099AA2144B0CC00432DC2 /* Pachyderm */, + D61099B22144B0CC00432DC2 /* PachydermTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + D61099A92144B0CC00432DC2 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D61099B12144B0CC00432DC2 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; D6D4DDCA212518A000E1C4BB /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -582,11 +861,59 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + D61099A72144B0CC00432DC2 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D61099E5214561AB00432DC2 /* Application.swift in Sources */, + D61099FF21456A4C00432DC2 /* Status.swift in Sources */, + D61099E32144C38900432DC2 /* Emoji.swift in Sources */, + D6109A0D214599E100432DC2 /* RequestRange.swift in Sources */, + D61099D92144B76400432DC2 /* Data.swift in Sources */, + D61099EB2145661700432DC2 /* ConversationContext.swift in Sources */, + D61099C92144B13C00432DC2 /* Client.swift in Sources */, + D61099D42144B32E00432DC2 /* Parameter.swift in Sources */, + D61099CB2144B20500432DC2 /* Request.swift in Sources */, + D6109A05214572BF00432DC2 /* Scope.swift in Sources */, + D6109A11214607D500432DC2 /* Timeline.swift in Sources */, + D61099E7214561FF00432DC2 /* Attachment.swift in Sources */, + D61099D02144B2D700432DC2 /* Method.swift in Sources */, + D61099FB214569F600432DC2 /* Report.swift in Sources */, + D61099F92145698900432DC2 /* Relationship.swift in Sources */, + D61099E12144C1DC00432DC2 /* Account.swift in Sources */, + D6109A0B2145953C00432DC2 /* ClientModel.swift in Sources */, + D61099E92145658300432DC2 /* Card.swift in Sources */, + D61099F32145688600432DC2 /* Mention.swift in Sources */, + D6109A0F21459B6900432DC2 /* Pagination.swift in Sources */, + D6109A032145722C00432DC2 /* RegisteredApplication.swift in Sources */, + D6109A0921458C4A00432DC2 /* Empty.swift in Sources */, + D61099DF2144C11400432DC2 /* MastodonError.swift in Sources */, + D61099D62144B4B200432DC2 /* FormAttachment.swift in Sources */, + D6109A072145756700432DC2 /* LoginSettings.swift in Sources */, + D61099ED2145664800432DC2 /* Filter.swift in Sources */, + D61099DC2144BDBF00432DC2 /* Response.swift in Sources */, + D61099F72145693500432DC2 /* PushSubscription.swift in Sources */, + D61099F5214568C300432DC2 /* Notification.swift in Sources */, + D61099EF214566C000432DC2 /* Instance.swift in Sources */, + D61099D22144B2E600432DC2 /* Body.swift in Sources */, + D6109A0121456B0800432DC2 /* Hashtag.swift in Sources */, + D61099FD21456A1D00432DC2 /* SearchResults.swift in Sources */, + D61099F12145686D00432DC2 /* List.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D61099AF2144B0CC00432DC2 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D61099BB2144B0CC00432DC2 /* PachydermTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; D6D4DDC8212518A000E1C4BB /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 04DACE8A212CA6B7009840C4 /* Timeline.swift in Sources */, 04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */, D667E5F52135BCD50057A976 /* ConversationViewController.swift in Sources */, D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */, @@ -646,6 +973,21 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + D61099B62144B0CC00432DC2 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D61099AA2144B0CC00432DC2 /* Pachyderm */; + targetProxy = D61099B52144B0CC00432DC2 /* PBXContainerItemProxy */; + }; + D61099B82144B0CC00432DC2 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D6D4DDCB212518A000E1C4BB /* Tusker */; + targetProxy = D61099B72144B0CC00432DC2 /* PBXContainerItemProxy */; + }; + D61099BF2144B0CC00432DC2 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D61099AA2144B0CC00432DC2 /* Pachyderm */; + targetProxy = D61099BE2144B0CC00432DC2 /* PBXContainerItemProxy */; + }; D6D4DDE2212518A200E1C4BB /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = D6D4DDCB212518A000E1C4BB /* Tusker */; @@ -678,6 +1020,105 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + D61099C32144B0CC00432DC2 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = HGYVAQA9FW; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Pachyderm/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.Tusker.Pachyderm; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + D61099C42144B0CC00432DC2 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = HGYVAQA9FW; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Pachyderm/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.Tusker.Pachyderm; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + D61099C62144B0CC00432DC2 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = HGYVAQA9FW; + INFOPLIST_FILE = PachydermTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.Tusker.PachydermTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Tusker.app/Tusker"; + }; + name = Debug; + }; + D61099C72144B0CC00432DC2 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = HGYVAQA9FW; + INFOPLIST_FILE = PachydermTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.Tusker.PachydermTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Tusker.app/Tusker"; + }; + name = Release; + }; D6D4DDF2212518A200E1C4BB /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -797,6 +1238,7 @@ D6D4DDF5212518A200E1C4BB /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; @@ -817,6 +1259,7 @@ D6D4DDF6212518A200E1C4BB /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; @@ -919,6 +1362,24 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + D61099C22144B0CC00432DC2 /* Build configuration list for PBXNativeTarget "Pachyderm" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D61099C32144B0CC00432DC2 /* Debug */, + D61099C42144B0CC00432DC2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D61099C52144B0CC00432DC2 /* Build configuration list for PBXNativeTarget "PachydermTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D61099C62144B0CC00432DC2 /* Debug */, + D61099C72144B0CC00432DC2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; D6D4DDC7212518A000E1C4BB /* Build configuration list for PBXProject "Tusker" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Tusker.xcodeproj/xcuserdata/shadowfacts.xcuserdatad/xcschemes/xcschememanagement.plist b/Tusker.xcodeproj/xcuserdata/shadowfacts.xcuserdatad/xcschemes/xcschememanagement.plist index 0d45b3f4..bf24477a 100644 --- a/Tusker.xcodeproj/xcuserdata/shadowfacts.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Tusker.xcodeproj/xcuserdata/shadowfacts.xcuserdatad/xcschemes/xcschememanagement.plist @@ -4,6 +4,11 @@ SchemeUserState + Pachyderm.xcscheme + + orderHint + 9 + Tusker.xcscheme orderHint diff --git a/Tusker.xcworkspace/contents.xcworkspacedata b/Tusker.xcworkspace/contents.xcworkspacedata index 043d424d..d751e971 100644 --- a/Tusker.xcworkspace/contents.xcworkspacedata +++ b/Tusker.xcworkspace/contents.xcworkspacedata @@ -7,9 +7,6 @@ - - diff --git a/Tusker/Controllers/MastodonController.swift b/Tusker/Controllers/MastodonController.swift index 19c1bd86..9925e0db 100644 --- a/Tusker/Controllers/MastodonController.swift +++ b/Tusker/Controllers/MastodonController.swift @@ -7,7 +7,7 @@ // import Foundation -import MastodonKit +import Pachyderm class MastodonController { @@ -37,30 +37,25 @@ class MastodonController { return } - let registerRequest = Clients.register(clientName: "Tusker", redirectURI: "tusker://oauth", scopes: [.read, .write, .follow]) - - client.run(registerRequest) { result in - guard case let .success(application, _) = result else { fatalError() } - LocalData.shared.clientID = application.clientID - LocalData.shared.clientSecret = application.clientSecret + client.registerApp(name: "Tusker", redirectURI: "tusker://oauth", scopes: [.read, .write, .follow]) { response in + guard case let .success(app, _) = response else { fatalError() } + LocalData.shared.clientID = app.clientID + LocalData.shared.clientSecret = app.clientSecret completion() } } func authorize(authorizationCode: String, completion: @escaping () -> Void) { - let authorizeRequest = Login.authorize(code: authorizationCode, clientID: LocalData.shared.clientID!, clientSecret: LocalData.shared.clientSecret!, redirectURI: "tusker://oauth") - client.run(authorizeRequest) { result in - guard case let .success(settings, _) = result else { fatalError() } + client.getAccessToken(authorizationCode: authorizationCode, redirectURI: "tusker://oauth") { response in + guard case let .success(settings, _) = response else { fatalError() } LocalData.shared.accessToken = settings.accessToken - self.client.accessToken = settings.accessToken completion() } } func getOwnAccount() { - let req = Accounts.currentUser() - client.run(req) { result in - guard case let .success(account, _) = result else { fatalError() } + client.getSelfAccount { response in + guard case let .success(account, _) = response else { fatalError() } self.account = account } } diff --git a/Tusker/Extensions/Account+Preferences.swift b/Tusker/Extensions/Account+Preferences.swift index 69c533c0..3622c182 100644 --- a/Tusker/Extensions/Account+Preferences.swift +++ b/Tusker/Extensions/Account+Preferences.swift @@ -7,7 +7,7 @@ // import Foundation -import MastodonKit +import Pachyderm extension Account { diff --git a/Tusker/Extensions/Mastodon+Equatable.swift b/Tusker/Extensions/Mastodon+Equatable.swift index 492de48f..3ee2f750 100644 --- a/Tusker/Extensions/Mastodon+Equatable.swift +++ b/Tusker/Extensions/Mastodon+Equatable.swift @@ -6,7 +6,7 @@ // Copyright © 2018 Shadowfacts. All rights reserved. // -import MastodonKit +import Pachyderm extension Status: Equatable { public static func ==(lhs: Status, rhs: Status) -> Bool { diff --git a/Tusker/Extensions/UIViewController+Delegates.swift b/Tusker/Extensions/UIViewController+Delegates.swift index d9463f4f..1a9b478e 100644 --- a/Tusker/Extensions/UIViewController+Delegates.swift +++ b/Tusker/Extensions/UIViewController+Delegates.swift @@ -7,7 +7,7 @@ // import UIKit -import MastodonKit +import Pachyderm import SafariServices extension StatusTableViewCellDelegate where Self: UIViewController { @@ -30,7 +30,7 @@ extension StatusTableViewCellDelegate where Self: UIViewController { } - func selected(tag: Tag) { + func selected(tag: Hashtag) { } diff --git a/Tusker/Extensions/Visibility+Helpers.swift b/Tusker/Extensions/Visibility+Helpers.swift index 188f4e21..bde6b28a 100644 --- a/Tusker/Extensions/Visibility+Helpers.swift +++ b/Tusker/Extensions/Visibility+Helpers.swift @@ -6,13 +6,9 @@ // Copyright © 2018 Shadowfacts. All rights reserved. // -import MastodonKit +import Pachyderm -extension Visibility { - - static var allCases: [Visibility] { - return [.public, .unlisted, .private, .direct] - } +extension Status.Visibility { var displayName: String { switch self { diff --git a/Tusker/LocalData.swift b/Tusker/LocalData.swift index d2cf95ac..7d7fef28 100644 --- a/Tusker/LocalData.swift +++ b/Tusker/LocalData.swift @@ -25,9 +25,9 @@ class LocalData { } private let instanceURLKey = "instanceURL" - var instanceURL: String? { + var instanceURL: URL? { get { - return defaults.string(forKey: instanceURLKey) + return defaults.url(forKey: instanceURLKey) } set { defaults.set(newValue, forKey: instanceURLKey) diff --git a/Tusker/Preferences/Preferences.swift b/Tusker/Preferences/Preferences.swift index 67ebec1e..529726eb 100644 --- a/Tusker/Preferences/Preferences.swift +++ b/Tusker/Preferences/Preferences.swift @@ -7,7 +7,7 @@ // import Foundation -import MastodonKit +import Pachyderm class Preferences: Codable { @@ -37,6 +37,6 @@ class Preferences: Codable { var hideCustomEmojiInUsernames = false - var defaultPostVisibility = Visibility.public + var defaultPostVisibility = Status.Visibility.public } diff --git a/Tusker/Screens/Compose/ComposeViewController.swift b/Tusker/Screens/Compose/ComposeViewController.swift index 31c9ed0b..3f763a5e 100644 --- a/Tusker/Screens/Compose/ComposeViewController.swift +++ b/Tusker/Screens/Compose/ComposeViewController.swift @@ -7,7 +7,7 @@ // import UIKit -import MastodonKit +import Pachyderm class ComposeViewController: UIViewController { @@ -73,11 +73,9 @@ class ComposeViewController: UIViewController { inReplyToAvatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: inReplyToAvatarImageView) inReplyToAvatarImageView.layer.masksToBounds = true inReplyToAvatarImageView.image = nil - if let url = URL(string: inReplyTo.account.avatar) { - AvatarCache.shared.get(url) { image in - DispatchQueue.main.async { - self.inReplyToAvatarImageView.image = image - } + AvatarCache.shared.get(inReplyTo.account.avatar) { image in + DispatchQueue.main.async { + self.inReplyToAvatarImageView.image = image } } inReplyToLabel.text = "In reply to \(inReplyTo.account.realDisplayName)" @@ -86,7 +84,7 @@ class ComposeViewController: UIViewController { } statusTextView.text += inReplyTo.mentions.filter({ $0.id != MastodonController.shared.account.id }).map({ "@\($0.acct) " }).joined() statusTextView.textViewDidChange(statusTextView) - contentWarning = inReplyTo.sensitive ?? false + contentWarning = inReplyTo.sensitive contentWarningTextField.text = inReplyTo.spoilerText visibility = inReplyTo.visibility } else { @@ -132,7 +130,7 @@ class ComposeViewController: UIViewController { @IBAction func visibilityPressed(_ sender: Any) { let alertController = UIAlertController(title: "Post Visibility", message: nil, preferredStyle: .actionSheet) - for visibility in Visibility.allCases { + for visibility in Status.Visibility.allCases { let action = UIAlertAction(title: visibility.displayName, style: .default, handler: { _ in UIView.performWithoutAnimation { self.visibility = visibility @@ -177,8 +175,6 @@ class ComposeViewController: UIViewController { guard let text = statusTextView.text, !text.isEmpty else { return } - let inReplyToID = inReplyTo?.id - let contentWarning: String? if self.contentWarning, let text = contentWarningTextField.text, @@ -200,26 +196,23 @@ class ComposeViewController: UIViewController { let index = attachments.count attachments.append(nil) group.enter() - let req = Media.upload(media: .png(data), description: mediaView.mediaDescription) - MastodonController.shared.client.run(req) { result in - guard case let .success(attachment, _) = result else { fatalError() } + MastodonController.shared.client.upload(attachment: FormAttachment(pngData: data), description: mediaView.mediaDescription) { response in + guard case let .success(attachment, _) = response else { fatalError() } attachments[index] = attachment group.leave() } } group.notify(queue: .main) { - let mediaIDs = attachments.map { $0!.id } + let attachments = attachments.compactMap { $0 } - let req = Statuses.create(status: text, - replyToID: inReplyToID, - mediaIDs: mediaIDs, - sensitive: sensitive, - spoilerText: contentWarning, - visibility: visibility) - - MastodonController.shared.client.run(req) { result in - guard case let .success(status, _) = result else { fatalError() } + MastodonController.shared.client.createStatus(text: text, + inReplyTo: self.inReplyTo, + media: attachments, + sensitive: sensitive, + spoilerText: contentWarning, + visiblity: visibility) { response in + guard case let .success(status, _) = response else { fatalError() } self.status = status DispatchQueue.main.async { self.performSegue(withIdentifier: "postComplete", sender: self) diff --git a/Tusker/Screens/Conversation/ConversationViewController.swift b/Tusker/Screens/Conversation/ConversationViewController.swift index a948b688..99c4f6cd 100644 --- a/Tusker/Screens/Conversation/ConversationViewController.swift +++ b/Tusker/Screens/Conversation/ConversationViewController.swift @@ -7,7 +7,7 @@ // import UIKit -import MastodonKit +import Pachyderm class ConversationViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { @@ -40,9 +40,8 @@ class ConversationViewController: UIViewController, UITableViewDataSource, UITab statuses = [mainStatus] - let req = Statuses.context(id: mainStatus.id) - MastodonController.shared.client.run(req) { result in - guard case let .success(context, _) = result else { fatalError() } + mainStatus.getContext { response in + guard case let .success(context, _) = response else { fatalError() } var statuses = self.getDirectParents(of: self.mainStatus, from: context.ancestors) statuses.append(self.mainStatus) statuses.append(contentsOf: context.descendants) diff --git a/Tusker/Screens/Main/MainTabBarViewController.swift b/Tusker/Screens/Main/MainTabBarViewController.swift index 8cd36ef1..d963a681 100644 --- a/Tusker/Screens/Main/MainTabBarViewController.swift +++ b/Tusker/Screens/Main/MainTabBarViewController.swift @@ -15,8 +15,8 @@ class MainTabBarViewController: UITabBarController { viewControllers = [ TimelineTableViewController.create(for: .home), - TimelineTableViewController.create(for: .federated), - TimelineTableViewController.create(for: .local), + TimelineTableViewController.create(for: .public(local: false)), + TimelineTableViewController.create(for: .public(local: true)), NotificationsTableViewController.create(), PreferencesTableViewController.create() ] diff --git a/Tusker/Screens/Notifications/NotificationsTableViewController.swift b/Tusker/Screens/Notifications/NotificationsTableViewController.swift index 4f96c833..c1e785d1 100644 --- a/Tusker/Screens/Notifications/NotificationsTableViewController.swift +++ b/Tusker/Screens/Notifications/NotificationsTableViewController.swift @@ -7,7 +7,7 @@ // import UIKit -import MastodonKit +import Pachyderm class NotificationsTableViewController: UITableViewController { @@ -16,7 +16,7 @@ class NotificationsTableViewController: UITableViewController { return navigationController } - var notifications: [MastodonKit.Notification] = [] { + var notifications: [Pachyderm.Notification] = [] { didSet { DispatchQueue.main.async { self.tableView.reloadData() @@ -43,12 +43,11 @@ class NotificationsTableViewController: UITableViewController { tableView.register(UINib(nibName: "ActionNotificationTableViewCell", bundle: nil), forCellReuseIdentifier: "actionCell") tableView.register(UINib(nibName: "FollowNotificationTableViewCell", bundle: nil), forCellReuseIdentifier: "followCell") - let req = Notifications.all() - MastodonController.shared.client.run(req) { result in + MastodonController.shared.client.getNotifications() { result in guard case let .success(notifications, pagination) = result else { fatalError() } self.notifications = notifications - self.newer = pagination?.previous - self.older = pagination?.next + self.newer = pagination?.newer + self.older = pagination?.older } } @@ -86,7 +85,7 @@ class NotificationsTableViewController: UITableViewController { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let notification = notifications[indexPath.row] - switch notification.type { + switch notification.kind { case .mention: guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? StatusTableViewCell else { fatalError() } let status = notification.status! @@ -111,10 +110,9 @@ class NotificationsTableViewController: UITableViewController { if indexPath.row == notifications.count - 1 { guard let older = older else { return } - let req = Notifications.all(range: older) - MastodonController.shared.client.run(req) { result in + MastodonController.shared.client.getNotifications(range: older) { result in guard case let .success(newNotifications, pagination) = result else { fatalError() } - self.older = pagination?.next + self.older = pagination?.older self.notifications.append(contentsOf: newNotifications) } } @@ -123,10 +121,9 @@ class NotificationsTableViewController: UITableViewController { @IBAction func refreshNotifications(_ sender: Any) { guard let newer = newer else { return } - let req = Notifications.all(range: newer) - MastodonController.shared.client.run(req) { result in + MastodonController.shared.client.getNotifications(range: newer) { result in guard case let .success(newNotifications, pagination) = result else { fatalError() } - self.newer = pagination?.previous + self.newer = pagination?.newer self.notifications.insert(contentsOf: newNotifications, at: 0) DispatchQueue.main.async { self.refreshControl?.endRefreshing() diff --git a/Tusker/Screens/Onboarding/OnboardingViewController.swift b/Tusker/Screens/Onboarding/OnboardingViewController.swift index 54939233..292daeae 100644 --- a/Tusker/Screens/Onboarding/OnboardingViewController.swift +++ b/Tusker/Screens/Onboarding/OnboardingViewController.swift @@ -29,9 +29,10 @@ class OnboardingViewController: UIViewController { @IBAction func loginPressed(_ sender: Any) { guard let text = urlTextField.text, + let url = URL(string: text), var components = URLComponents(string: text) else { return } - LocalData.shared.instanceURL = text + LocalData.shared.instanceURL = url MastodonController.shared.createClient() MastodonController.shared.registerApp { let clientID = LocalData.shared.clientID! diff --git a/Tusker/Screens/Preferences/VisibilityTableViewController.swift b/Tusker/Screens/Preferences/VisibilityTableViewController.swift index 851e3226..d76c0de6 100644 --- a/Tusker/Screens/Preferences/VisibilityTableViewController.swift +++ b/Tusker/Screens/Preferences/VisibilityTableViewController.swift @@ -7,7 +7,7 @@ // import UIKit -import MastodonKit +import Pachyderm class VisibilityTableViewController: UITableViewController { @@ -24,11 +24,11 @@ class VisibilityTableViewController: UITableViewController { } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return Visibility.allCases.count + return Status.Visibility.allCases.count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let visibility = Visibility.allCases[indexPath.row] + let visibility = Status.Visibility.allCases[indexPath.row] let cell = tableView.dequeueReusableCell(withIdentifier: "visibilityCell", for: indexPath) cell.textLabel!.text = visibility.displayName @@ -38,9 +38,9 @@ class VisibilityTableViewController: UITableViewController { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let oldVisibility = Preferences.shared.defaultPostVisibility - let oldIndexPath = IndexPath(row: Visibility.allCases.firstIndex(of: oldVisibility)!, section: 0) + let oldIndexPath = IndexPath(row: Status.Visibility.allCases.firstIndex(of: oldVisibility)!, section: 0) - let visibility = Visibility.allCases[indexPath.row] + let visibility = Status.Visibility.allCases[indexPath.row] Preferences.shared.defaultPostVisibility = visibility tableView.reloadRows(at: [indexPath], with: .automatic) diff --git a/Tusker/Screens/Profile/ProfileTableViewController.swift b/Tusker/Screens/Profile/ProfileTableViewController.swift index 4b32dcdf..4927442e 100644 --- a/Tusker/Screens/Profile/ProfileTableViewController.swift +++ b/Tusker/Screens/Profile/ProfileTableViewController.swift @@ -7,7 +7,7 @@ // import UIKit -import MastodonKit +import Pachyderm import SafariServices class ProfileTableViewController: UITableViewController, PreferencesAdaptive { @@ -28,12 +28,11 @@ class ProfileTableViewController: UITableViewController, PreferencesAdaptive { } } - var newer: RequestRange? var older: RequestRange? + var newer: RequestRange? - func request(for range: RequestRange? = .default) -> Request<[Status]> { - let range = range ?? .default - return Accounts.statuses(id: account.id, mediaOnly: false, pinnedOnly: false, excludeReplies: !Preferences.shared.showRepliesInProfiles, range: range) + func getStatuses(for range: RequestRange = .default, completion: @escaping Client.Callback<[Status]>) { + account.getStatuses(range: range, onlyMedia: false, pinned: false, excludeReplies: !Preferences.shared.showRepliesInProfiles, completion: completion) } override func viewDidLoad() { @@ -47,13 +46,11 @@ class ProfileTableViewController: UITableViewController, PreferencesAdaptive { updateUIForPreferences() - - - MastodonController.shared.client.run(request()) { result in - guard case let .success(statuses, pagination) = result else { fatalError() } + getStatuses { response in + guard case let .success(statuses, pagination) = response else { fatalError() } self.statuses = statuses - self.newer = pagination?.previous - self.older = pagination?.next + self.older = pagination?.older + self.newer = pagination?.newer } } @@ -127,9 +124,9 @@ class ProfileTableViewController: UITableViewController, PreferencesAdaptive { if indexPath.section == 1 && indexPath.row == statuses.count - 1 { guard let older = older else { return } - MastodonController.shared.client.run(request(for: older)) { result in - guard case let .success(newStatuses, pagination) = result else { fatalError() } - self.older = pagination?.next + getStatuses(for: older) { response in + guard case let .success(newStatuses, pagination) = response else { fatalError() } + self.older = pagination?.older self.statuses.append(contentsOf: newStatuses) } } @@ -138,9 +135,9 @@ class ProfileTableViewController: UITableViewController, PreferencesAdaptive { @IBAction func refreshStatuses(_ sender: Any) { guard let newer = newer else { return } - MastodonController.shared.client.run(request(for: newer)) { result in - guard case let .success(newStatuses, pagination) = result else { fatalError() } - self.newer = pagination?.previous + getStatuses(for: newer) { response in + guard case let .success(newStatuses, pagination) = response else { fatalError() } + self.newer = pagination?.newer self.statuses.insert(contentsOf: newStatuses, at: 0) DispatchQueue.main.async { self.refreshControl?.endRefreshing() @@ -159,11 +156,11 @@ extension ProfileTableViewController: ProfileHeaderTableViewCellDelegate { func showMoreOptions() { let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) alert.addAction(UIAlertAction(title: "Open in Safari...", style: .default, handler: { _ in - let vc = SFSafariViewController(url: URL(string: self.account.url)!) + let vc = SFSafariViewController(url: self.account.url) self.present(vc, animated: true) })) alert.addAction(UIAlertAction(title: "Share...", style: .default, handler: { _ in - let vc = UIActivityViewController(activityItems: [URL(string: self.account.url)!], applicationActivities: nil) + let vc = UIActivityViewController(activityItems: [self.account.url], applicationActivities: nil) self.present(vc, animated: true) })) alert.addAction(UIAlertAction(title: "Send Message...", style: .default, handler: { _ in diff --git a/Tusker/Screens/Timeline/TimelineTableViewController.swift b/Tusker/Screens/Timeline/TimelineTableViewController.swift index 0e19b485..2a6a7aa5 100644 --- a/Tusker/Screens/Timeline/TimelineTableViewController.swift +++ b/Tusker/Screens/Timeline/TimelineTableViewController.swift @@ -7,7 +7,7 @@ // import UIKit -import MastodonKit +import Pachyderm class TimelineTableViewController: UITableViewController { @@ -15,17 +15,22 @@ class TimelineTableViewController: UITableViewController { guard let navigationController = UIStoryboard(name: "Timeline", bundle: nil).instantiateInitialViewController() as? UINavigationController, let timelineController = navigationController.topViewController as? TimelineTableViewController else { fatalError() } timelineController.timeline = timeline + + let title: String switch timeline { case .home: - navigationController.tabBarItem.title = "Home" - timelineController.navigationItem.title = "Home" - case .local: - navigationController.tabBarItem.title = "Local" - timelineController.navigationItem.title = "Local" - case .federated: - navigationController.tabBarItem.title = "Federated" - timelineController.navigationItem.title = "Federated" + title = "Home" + case let .public(local): + title = local ? "Local" : "Federated" + case let .tag(hashtag): + title = "#\(hashtag)" + case .list: + title = "List" + case .direct: + title = "Direct" } + navigationController.tabBarItem.title = title + timelineController.navigationItem.title = title return navigationController } @@ -52,11 +57,11 @@ class TimelineTableViewController: UITableViewController { tableView.register(UINib(nibName: "StatusTableViewCell", bundle: nil), forCellReuseIdentifier: "statusCell") guard MastodonController.shared.client?.accessToken != nil else { return } - MastodonController.shared.client.run(timeline.request()) { result in - guard case let .success(statuses, pagination) = result else { fatalError() } + MastodonController.shared.client.getStatuses(timeline: timeline) { response in + guard case let .success(statuses, pagination) = response else { fatalError() } self.statuses = statuses - self.newer = pagination?.previous - self.older = pagination?.next + self.newer = pagination?.newer + self.older = pagination?.older } } @@ -107,9 +112,9 @@ class TimelineTableViewController: UITableViewController { if indexPath.row == statuses.count - 1 { guard let older = older else { return } - MastodonController.shared.client.run(timeline.request(range: older)) { result in - guard case let .success(newStatuses, pagination) = result else { fatalError() } - self.older = pagination?.next + MastodonController.shared.client.getStatuses(timeline: timeline, range: older) { response in + guard case let .success(newStatuses, pagination) = response else { fatalError() } + self.older = pagination?.older self.statuses.append(contentsOf: newStatuses) } } @@ -118,9 +123,9 @@ class TimelineTableViewController: UITableViewController { @IBAction func refreshStatuses(_ sender: Any) { guard let newer = newer else { return } - MastodonController.shared.client.run(timeline.request(range: newer)) { result in - guard case let .success(newStatuses, pagination) = result else { fatalError() } - self.newer = pagination?.previous + MastodonController.shared.client.getStatuses(timeline: timeline, range: newer) { response in + guard case let .success(newStatuses, pagination) = response else { fatalError() } + self.newer = pagination?.newer self.statuses.insert(contentsOf: newStatuses, at: 0) DispatchQueue.main.async { self.refreshControl?.endRefreshing() diff --git a/Tusker/Timeline.swift b/Tusker/Timeline.swift deleted file mode 100644 index bbd33e04..00000000 --- a/Tusker/Timeline.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// Timeline.swift -// Tusker -// -// Created by Shadowfactson 8/21/18. -// Copyright © 2018 Shadowfacts. All rights reserved. -// - -import Foundation -import MastodonKit - -enum Timeline { - - case home, local, federated - - func request(range: RequestRange = .default) -> Request<[Status]> { - switch self { - case .home: - return Timelines.home(range: range) - case .local: - return Timelines.public(local: true, range: range) - case .federated: - return Timelines.public(local: false, range: range) - } - } - -} diff --git a/Tusker/Views/AttachmentView.swift b/Tusker/Views/AttachmentView.swift index cdf4d7ab..ab3abaf5 100644 --- a/Tusker/Views/AttachmentView.swift +++ b/Tusker/Views/AttachmentView.swift @@ -7,7 +7,7 @@ // import UIKit -import MastodonKit +import Pachyderm protocol AttachmentViewDelegate { func showLargeAttachment(for attachmentView: AttachmentView) @@ -45,8 +45,7 @@ class AttachmentView: UIImageView { } func loadImage() { - guard let url = URL(string: attachment.url) else { fatalError("Invalid URL: \(attachment.url)") } - task = URLSession.shared.dataTask(with: url) { data, response, error in + task = URLSession.shared.dataTask(with: attachment.url) { data, response, error in guard error == nil, let data = data, let image = UIImage(data: data) else { return } DispatchQueue.main.async { self.image = image diff --git a/Tusker/Views/HTMLContentLabel.swift b/Tusker/Views/HTMLContentLabel.swift index 507688b8..5c6781db 100644 --- a/Tusker/Views/HTMLContentLabel.swift +++ b/Tusker/Views/HTMLContentLabel.swift @@ -7,14 +7,14 @@ // import UIKit -import MastodonKit +import Pachyderm import SwiftSoup protocol HTMLContentLabelDelegate { func selected(mention: Mention) - func selected(tag: MastodonKit.Tag) + func selected(tag: Hashtag) func selected(url: URL) @@ -192,10 +192,10 @@ class HTMLContentLabel: UILabel { return nil } - func getTag(for url: URL, text: String) -> MastodonKit.Tag? { + func getTag(for url: URL, text: String) -> Hashtag? { if text.starts(with: "#") { let tag = String(text.dropFirst()) - return MastodonKit.Tag(name: tag, url: url.absoluteString) + return Hashtag(name: tag, url: url) } else { return nil } diff --git a/Tusker/Views/Notifications/ActionNotificationTableViewCell.swift b/Tusker/Views/Notifications/ActionNotificationTableViewCell.swift index 23214b3a..a09febf7 100644 --- a/Tusker/Views/Notifications/ActionNotificationTableViewCell.swift +++ b/Tusker/Views/Notifications/ActionNotificationTableViewCell.swift @@ -7,7 +7,7 @@ // import UIKit -import MastodonKit +import Pachyderm class ActionNotificationTableViewCell: UITableViewCell, PreferencesAdaptive { @@ -22,7 +22,7 @@ class ActionNotificationTableViewCell: UITableViewCell, PreferencesAdaptive { @IBOutlet weak var timestampLabel: UILabel! @IBOutlet weak var attachmentsView: UIStackView! - var notification: MastodonKit.Notification! + var notification: Pachyderm.Notification! var status: Status! var opAvatarURL: URL? @@ -49,20 +49,20 @@ class ActionNotificationTableViewCell: UITableViewCell, PreferencesAdaptive { displayNameLabel.text = status.account.realDisplayName let verb: String - switch notification.type { + switch notification.kind { case .favourite: verb = "Liked" case .reblog: verb = "Reblogged" default: - fatalError("Invalid notification type \(notification.type) for ActionNotificationTableViewCell") + fatalError("Invalid notification type \(notification.kind) for ActionNotificationTableViewCell") } actionLabel.text = "\(verb) by \(notification.account.realDisplayName)" } - func updateUI(for notification: MastodonKit.Notification) { - guard notification.type == .favourite || notification.type == .reblog else { - fatalError("Invalid notification type \(notification.type) for ActionNotificationTableViewCell") + func updateUI(for notification: Pachyderm.Notification) { + guard notification.kind == .favourite || notification.kind == .reblog else { + fatalError("Invalid notification type \(notification.kind) for ActionNotificationTableViewCell") } self.notification = notification self.status = notification.status! @@ -71,31 +71,27 @@ class ActionNotificationTableViewCell: UITableViewCell, PreferencesAdaptive { usernameLabel.text = "@\(status.account.acct)" opAvatarImageView.image = nil - if let url = URL(string: status.account.avatar) { - opAvatarURL = url - AvatarCache.shared.get(url) { image in - DispatchQueue.main.async { - self.opAvatarImageView.image = image - self.opAvatarURL = nil - } + opAvatarURL = status.account.avatar + AvatarCache.shared.get(status.account.avatar) { image in + DispatchQueue.main.async { + self.opAvatarImageView.image = image + self.opAvatarURL = nil } } actionAvatarImageView.image = nil - if let url = URL(string: notification.account.avatar) { - actionAvatarURL = url - AvatarCache.shared.get(url) { image in - DispatchQueue.main.async { - self.actionAvatarImageView.image = image - self.actionAvatarURL = nil - } + actionAvatarURL = notification.account.avatar + AvatarCache.shared.get(notification.account.avatar) { image in + DispatchQueue.main.async { + self.actionAvatarImageView.image = image + self.actionAvatarURL = nil } } updateTimestamp() - let attachments = status.mediaAttachments.filter({ $0.type == .image }) + let attachments = status.attachments.filter({ $0.kind == .image }) if attachments.count > 0 { attachmentsView.isHidden = false for attachment in attachments { - guard let url = URL(string: attachment.textURL ?? attachment.url) else { continue } + let url = attachment.textURL ?? attachment.url let label = UILabel() label.textColor = .darkGray @@ -190,7 +186,7 @@ extension ActionNotificationTableViewCell: HTMLContentLabelDelegate { delegate?.selected(mention: mention) } - func selected(tag: Tag) { + func selected(tag: Hashtag) { delegate?.selected(tag: tag) } diff --git a/Tusker/Views/Notifications/FollowNotificationTableViewCell.swift b/Tusker/Views/Notifications/FollowNotificationTableViewCell.swift index 7681404b..b7ac327d 100644 --- a/Tusker/Views/Notifications/FollowNotificationTableViewCell.swift +++ b/Tusker/Views/Notifications/FollowNotificationTableViewCell.swift @@ -7,7 +7,7 @@ // import UIKit -import MastodonKit +import Pachyderm class FollowNotificationTableViewCell: UITableViewCell, PreferencesAdaptive { @@ -19,7 +19,7 @@ class FollowNotificationTableViewCell: UITableViewCell, PreferencesAdaptive { @IBOutlet weak var displayNameLabel: UILabel! @IBOutlet weak var usernameLabel: UILabel! - var notification: MastodonKit.Notification! + var notification: Pachyderm.Notification! var account: Account! var avatarURL: URL? @@ -37,7 +37,7 @@ class FollowNotificationTableViewCell: UITableViewCell, PreferencesAdaptive { displayNameLabel.text = account.realDisplayName } - func updateUI(for notification: MastodonKit.Notification) { + func updateUI(for notification: Pachyderm.Notification) { self.notification = notification self.account = notification.account @@ -45,13 +45,11 @@ class FollowNotificationTableViewCell: UITableViewCell, PreferencesAdaptive { usernameLabel.text = "@\(account.acct)" avatarImageView.image = nil - if let url = URL(string: account.avatar) { - avatarURL = url - AvatarCache.shared.get(url) { image in - DispatchQueue.main.async { - self.avatarImageView.image = image - self.avatarURL = nil - } + avatarURL = account.avatar + AvatarCache.shared.get(account.avatar) { image in + DispatchQueue.main.async { + self.avatarImageView.image = image + self.avatarURL = nil } } updateTimestamp() diff --git a/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift b/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift index 47d7b5bb..ac3ba648 100644 --- a/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift +++ b/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift @@ -7,7 +7,7 @@ // import UIKit -import MastodonKit +import Pachyderm protocol ProfileHeaderTableViewCellDelegate: StatusTableViewCellDelegate { @@ -55,25 +55,21 @@ class ProfileHeaderTableViewCell: UITableViewCell, PreferencesAdaptive { usernameLabel.text = "@\(account.acct)" avatarImageView.image = nil - if let url = URL(string: account.avatar) { - avatarURL = url - AvatarCache.shared.get(url) { image in - DispatchQueue.main.async { - self.avatarImageView.image = image - self.avatarURL = nil - } + avatarURL = account.avatar + AvatarCache.shared.get(account.avatar) { image in + DispatchQueue.main.async { + self.avatarImageView.image = image + self.avatarURL = nil } } - if let url = URL(string: account.header) { - headerImageDownloadTask = URLSession.shared.dataTask(with: url) { data, response, error in - guard error == nil, let data = data, let image = UIImage(data: data) else { return } - DispatchQueue.main.async { - self.headerImageView.image = image - self.headerImageDownloadTask = nil - } + headerImageDownloadTask = URLSession.shared.dataTask(with: account.header) { data, response, error in + guard error == nil, let data = data, let image = UIImage(data: data) else { return } + DispatchQueue.main.async { + self.headerImageView.image = image + self.headerImageDownloadTask = nil } - headerImageDownloadTask!.resume() } + headerImageDownloadTask!.resume() // todo: HTML parsing noteLabel.text = account.note @@ -106,7 +102,7 @@ extension ProfileHeaderTableViewCell: HTMLContentLabelDelegate { delegate?.selected(mention: mention) } - func selected(tag: Tag) { + func selected(tag: Hashtag) { delegate?.selected(tag: tag) } diff --git a/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift b/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift index 771e64f9..fa3332eb 100644 --- a/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift +++ b/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift @@ -7,7 +7,7 @@ // import UIKit -import MastodonKit +import Pachyderm class ConversationMainStatusTableViewCell: UITableViewCell, PreferencesAdaptive { @@ -72,18 +72,16 @@ class ConversationMainStatusTableViewCell: UITableViewCell, PreferencesAdaptive usernameLabel.text = "@\(account.acct)" avatarImageView.image = nil - if let url = URL(string: account.avatar) { - avatarURL = url - AvatarCache.shared.get(url) { image in - DispatchQueue.main.async { - self.avatarImageView.image = image - self.avatarURL = nil - } + avatarURL = account.avatar + AvatarCache.shared.get(account.avatar) { image in + DispatchQueue.main.async { + self.avatarImageView.image = image + self.avatarURL = nil } } attachmentsView.subviews.forEach { $0.removeFromSuperview() } - let attachments = status.mediaAttachments.filter({ $0.type == .image }) + let attachments = status.attachments.filter({ $0.kind == .image }) if attachments.count > 0 { attachmentsView.isHidden = false let width = attachmentsView.bounds.width @@ -164,55 +162,40 @@ class ConversationMainStatusTableViewCell: UITableViewCell, PreferencesAdaptive } @IBAction func favoritePressed(_ sender: Any) { - let oldValue = favorited favorited = !favorited let realStatus: Status = status.reblog ?? status - let req = favorited ? Statuses.favourite(id: realStatus.id) : Statuses.unfavourite(id: realStatus.id) - MastodonController.shared.client.run(req) { result in - guard case .success = result else { - print("Couldn't favorite status \(realStatus.id)") - // todo: display error message - DispatchQueue.main.async { - self.favorited = oldValue - - let generator = UINotificationFeedbackGenerator() - generator.notificationOccurred(.error) - } - return - } - + (favorited ? realStatus.favourite : realStatus.unfavourite)() { response in + self.favorited = realStatus.favourited ?? false DispatchQueue.main.async { - let generator = UIImpactFeedbackGenerator(style: .light) - generator.impactOccurred() + if case .success = response { + UIImpactFeedbackGenerator(style: .light).impactOccurred() + } else { + print("Couldn't favorite status \(realStatus.id)") + // todo: display error message + UINotificationFeedbackGenerator().notificationOccurred(.error) + return + } } } } @IBAction func reblogPressed(_ sender: Any) { - let oldValue = reblogged reblogged = !reblogged let realStatus: Status = status.reblog ?? status - let req = reblogged ? Statuses.reblog(id: realStatus.id) : Statuses.unreblog(id: realStatus.id) - MastodonController.shared.client.run(req) { result in - guard case .success = result else { - print("Couldn't reblog status \(realStatus.id)") - // todo: display error message - DispatchQueue.main.async { - self.reblogged = oldValue - - let generator = UINotificationFeedbackGenerator() - generator.notificationOccurred(.error) - } - return - } - + (reblogged ? realStatus.reblog : realStatus.unreblog)() { response in + self.reblogged = realStatus.reblogged ?? false DispatchQueue.main.async { - let generator = UIImpactFeedbackGenerator(style: .light) - generator.impactOccurred() + if case .success = response { + UIImpactFeedbackGenerator(style: .light).impactOccurred() + } else { + print("Couldn't reblog status \(realStatus.id)") + // todo: display error message + UINotificationFeedbackGenerator().notificationOccurred(.error) + } } } } @@ -225,7 +208,7 @@ extension ConversationMainStatusTableViewCell: HTMLContentLabelDelegate { delegate?.selected(mention: mention) } - func selected(tag: MastodonKit.Tag) { + func selected(tag: Hashtag) { delegate?.selected(tag: tag) } diff --git a/Tusker/Views/Status/StatusTableViewCell.swift b/Tusker/Views/Status/StatusTableViewCell.swift index 357a49d1..c133e47b 100644 --- a/Tusker/Views/Status/StatusTableViewCell.swift +++ b/Tusker/Views/Status/StatusTableViewCell.swift @@ -7,7 +7,7 @@ // import UIKit -import MastodonKit +import Pachyderm protocol StatusTableViewCellDelegate { @@ -15,7 +15,7 @@ protocol StatusTableViewCellDelegate { func selected(mention: Mention) - func selected(tag: MastodonKit.Tag) + func selected(tag: Hashtag) func selected(url: URL) @@ -105,18 +105,16 @@ class StatusTableViewCell: UITableViewCell, PreferencesAdaptive { usernameLabel.text = "@\(account.acct)" avatarImageView.image = nil - if let url = URL(string: account.avatar) { - avatarURL = url - AvatarCache.shared.get(url) { image in - DispatchQueue.main.async { - self.avatarImageView.image = image - self.avatarURL = nil - } + avatarURL = account.avatar + AvatarCache.shared.get(account.avatar) { image in + DispatchQueue.main.async { + self.avatarImageView.image = image + self.avatarURL = nil } } updateTimestamp() - let attachments = status.mediaAttachments.filter({ $0.type == .image }) + let attachments = status.attachments.filter({ $0.kind == .image }) if attachments.count > 0 { attachmentsView.isHidden = false let width = attachmentsView.bounds.width @@ -212,55 +210,40 @@ class StatusTableViewCell: UITableViewCell, PreferencesAdaptive { } @IBAction func favoritePressed(_ sender: Any) { - let oldValue = favorited favorited = !favorited let realStatus: Status = status.reblog ?? status - let req = favorited ? Statuses.favourite(id: realStatus.id) : Statuses.unfavourite(id: realStatus.id) - MastodonController.shared.client.run(req) { result in - guard case .success = result else { - print("Couldn't favorite status \(realStatus.id)") - // todo: display error message - DispatchQueue.main.async { - self.favorited = oldValue - - let generator = UINotificationFeedbackGenerator() - generator.notificationOccurred(.error) - } - return - } - + (favorited ? realStatus.favourite : realStatus.unfavourite)() { response in + self.favorited = realStatus.favourited ?? false DispatchQueue.main.async { - let generator = UIImpactFeedbackGenerator(style: .light) - generator.impactOccurred() + if case .success = response { + UIImpactFeedbackGenerator(style: .light).impactOccurred() + } else { + print("Couldn't favorite status \(realStatus.id)") + // todo: display error message + UINotificationFeedbackGenerator().notificationOccurred(.error) + return + } } } } @IBAction func reblogPressed(_ sender: Any) { - let oldValue = reblogged reblogged = !reblogged let realStatus: Status = status.reblog ?? status - let req = reblogged ? Statuses.reblog(id: realStatus.id) : Statuses.unreblog(id: realStatus.id) - MastodonController.shared.client.run(req) { result in - guard case .success = result else { - print("Couldn't reblog status \(realStatus.id)") - // todo: display error message - DispatchQueue.main.async { - self.reblogged = oldValue - - let generator = UINotificationFeedbackGenerator() - generator.notificationOccurred(.error) - } - return - } - + (reblogged ? realStatus.reblog : realStatus.unreblog)() { response in + self.reblogged = realStatus.reblogged ?? false DispatchQueue.main.async { - let generator = UIImpactFeedbackGenerator(style: .light) - generator.impactOccurred() + if case .success = response { + UIImpactFeedbackGenerator(style: .light).impactOccurred() + } else { + print("Couldn't reblog status \(realStatus.id)") + // todo: display error message + UINotificationFeedbackGenerator().notificationOccurred(.error) + } } } } @@ -273,7 +256,7 @@ extension StatusTableViewCell: HTMLContentLabelDelegate { delegate?.selected(mention: mention) } - func selected(tag: MastodonKit.Tag) { + func selected(tag: Hashtag) { delegate?.selected(tag: tag) } diff --git a/Tusker/Views/StatusContentLabel.swift b/Tusker/Views/StatusContentLabel.swift index 6eab953c..a30f071b 100644 --- a/Tusker/Views/StatusContentLabel.swift +++ b/Tusker/Views/StatusContentLabel.swift @@ -7,7 +7,7 @@ // import UIKit -import MastodonKit +import Pachyderm class StatusContentLabel: HTMLContentLabel { @@ -19,14 +19,12 @@ class StatusContentLabel: HTMLContentLabel { override func getMention(for url: URL, text: String) -> Mention? { return status.mentions.first(where: { mention -> Bool in - (text.dropFirst() == mention.username || text == mention.username) && url.host == URL(string: mention.url)!.host + (text.dropFirst() == mention.username || text == mention.username) && url.host == mention.url.host }) ?? super.getMention(for: url, text: text) } - override func getTag(for url: URL, text: String) -> MastodonKit.Tag? { - if let tag = status.tags.first(where: { tag -> Bool in - tag.url == url.absoluteString - }) { + override func getTag(for url: URL, text: String) -> Hashtag? { + if let tag = status.hashtags.first(where: { $0.url == url }) { return tag } else { return super.getTag(for: url, text: text)