Replace MastodonKit with Pachyderm

This commit is contained in:
Shadowfacts 2018-09-11 10:52:21 -04:00
parent d4a451fadb
commit 1119a861d8
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
68 changed files with 2754 additions and 319 deletions

3
.gitmodules vendored
View File

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

@ -1 +0,0 @@
Subproject commit cfece3083acfeda2f124a84dc35f268682681d49

View File

@ -1,13 +1,58 @@
import UIKit
func test(_ nillable: String?) {
defer {
print("defer")
class Client {
func test<A>(_ thing: A) {
if var thing = thing as? ClientModel {
thing.client = self
} else if var arr = thing as? [ClientModel] {
arr.client = self
}
// } else if let arr = thing as? Array<Any> {
// for el in arr {
// if var el = el as? ClientModel {
// el.client = self
// }
// }
// }
}
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

346
Pachyderm/Client.swift Normal file
View File

@ -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<Result: Decodable> = (Response<Result>) -> 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<Result>(_ request: Request<Result>, completion: @escaping Callback<Result>) {
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<Result>(request: Request<Result>) -> 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<RegisteredApplication>) {
let request = Request<RegisteredApplication>(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<LoginSettings>) {
let request = Request<LoginSettings>(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<Account>) {
let request = Request<Account>(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<Instance>) {
let request = Request<Instance>(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<Account>) {
let request = Request<Account>(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<Empty>) {
let request = Request<Empty>(method: .post, path: "/api/v1/domain_blocks", body: .parameters([
"domain" => domain
]))
run(request, completion: completion)
}
public func unblock(domain: String, completion: @escaping Callback<Empty>) {
let request = Request<Empty>(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<Filter>) {
let request = Request<Filter>(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<Filter>) {
let request = Request<Filter>(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<Account>) {
let request = Request<Account>(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<List>) {
let request = Request<List>(method: .get, path: "/api/v1/lists/\(id)")
run(request, completion: completion)
}
public func createList(title: String, completion: @escaping Callback<List>) {
let request = Request<List>(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<Attachment>) {
let request = Request<Attachment>(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<Empty>) {
let request = Request<Empty>(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<Report>) {
let request = Request<Report>(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<SearchResults>) {
let request = Request<SearchResults>(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<Status>) {
let request = Request<Status>(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<Status>) {
let request = Request<Status>(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)
}
}

View File

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

View File

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

22
Pachyderm/Info.plist Normal file
View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
</plist>

View File

@ -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<Empty>) {
let request = Request<Empty>(method: .post, path: "/api/v1/follow_requests/\(id)/authorize")
client.run(request, completion: completion)
}
public func rejectFollowRequest(completion: @escaping Client.Callback<Empty>) {
let request = Request<Empty>(method: .post, path: "/api/v1/follow_requests/\(id)/reject")
client.run(request, completion: completion)
}
public func removeFromFollowRequests(completion: @escaping Client.Callback<Empty>) {
let request = Request<Empty>(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<Relationship>) {
let request = Request<Relationship>(method: .post, path: "/api/v1/accounts/\(id)/follow")
client.run(request, completion: completion)
}
public func unfollow(completion: @escaping Client.Callback<Relationship>) {
let request = Request<Relationship>(method: .post, path: "/api/v1/accounts/\(id)/unfollow")
client.run(request, completion: completion)
}
public func block(completion: @escaping Client.Callback<Relationship>) {
let request = Request<Relationship>(method: .post, path: "/api/v1/accounts/\(id)/block")
client.run(request, completion: completion)
}
public func unblock(completion: @escaping Client.Callback<Relationship>) {
let request = Request<Relationship>(method: .post, path: "/api/v1/accounts/\(id)/unblock")
client.run(request, completion: completion)
}
public func mute(notifications: Bool? = nil, completion: @escaping Client.Callback<Relationship>) {
let request = Request<Relationship>(method: .post, path: "/api/v1/accounts/\(id)/mute", body: .parameters([
"notifications" => notifications
]))
client.run(request, completion: completion)
}
public func unmute(completion: @escaping Client.Callback<Relationship>) {
let request = Request<Relationship>(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
}
}

View File

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

View File

@ -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<Attachment>?) {
let request = Request<Attachment>(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
}
}
}

View File

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

View File

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

View File

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

View File

@ -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<Filter>?) {
let request = Request<Filter>(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<Empty>) {
let request = Request<Empty>(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 }
}
}

View File

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

View File

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

View File

@ -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<List>?) {
let request = Request<List>(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<Empty>) {
let request = Request<Empty>(method: .delete, path: "/api/v1/lists/\(id)")
client.run(request, completion: completion)
}
public func add(accounts: [Account], completion: @escaping Client.Callback<Empty>) {
let request = Request<Empty>(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<Empty>) {
let request = Request<Empty>(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
}
}

View File

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

View File

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

View File

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

View File

@ -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<Empty>) {
let request = Request<Empty>(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
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<ConversationContext>) {
let request = Request<ConversationContext>(method: .get, path: "/api/v1/statuses/\(id)/context")
client.run(request, completion: completion)
}
public func getCard(completion: @escaping Client.Callback<Card>) {
let request = Request<Card>(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<Empty>) {
let request = Request<Empty>(method: .delete, path: "/api/v1/statuses/\(id)")
client.run(request, completion: completion)
}
public func reblog(completion: @escaping Client.Callback<Status>) {
let oldValue = reblogged
let request = Request<Status>(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<Status>) {
let oldValue = reblogged
let request = Request<Status>(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<Status>) {
let oldValue = favourited
let request = Request<Status>(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<Status>) {
let oldValue = favourited
let request = Request<Status>(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<Status>) {
let oldValue = pinned
let request = Request<Status>(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<Status>) {
let oldValue = pinned
let request = Request<Status>(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<Status>) {
let oldValue = muted
let request = Request<Status>(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<Status>) {
let oldValue = muted
let request = Request<Status>(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
}
}

View File

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

19
Pachyderm/Pachyderm.h Normal file
View File

@ -0,0 +1,19 @@
//
// Pachyderm.h
// Pachyderm
//
// Created by Shadowfacts on 9/8/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
#import <UIKit/UIKit.h>
//! 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 <Pachyderm/PublicHeader.h>

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,57 @@
//
// Request.swift
// Pachyderm
//
// Created by Shadowfacts on 9/8/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
struct Request<ResultType: Decodable> {
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
}
}
}
}

View File

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

View File

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

View File

@ -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)..<str.index(before: str.endIndex)])
}
let rel = segments.last?.replacingOccurrences(of: "\"", with: "").trimmingCharacters(in: .whitespaces).components(separatedBy: "=")
guard let validURL = url,
let key = rel?.first,
key == "rel",
let value = rel?.last,
let kind = Kind(rawValue: value),
let components = URLComponents(string: validURL),
let queryItems = components.queryItems else { return nil }
let since = queryItems.first { $0.name == "since_id" }?.value
let max = queryItems.first { $0.name == "max_id" }?.value
guard let id = since ?? max else { return nil }
let limit = queryItems.first { $0.name == "limit" }.flatMap { $0.value }.flatMap { Int($0) }
self.kind = kind
self.id = id
self.limit = limit
}
}
}
extension Pagination.Item {
enum Kind: String {
case next, prev
}
}

View File

@ -0,0 +1,14 @@
//
// Response.swift
// Pachyderm
//
// Created by Shadowfacts on 9/8/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
public enum Response<Result: Decodable> {
case success(Result, Pagination?)
case failure(Error)
}

22
PachydermTests/Info.plist Normal file
View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>

View File

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

View File

@ -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 = "<group>"; };
04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarViewController.swift; sourceTree = "<group>"; };
04DACE8D212CC7CC009840C4 /* AvatarCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarCache.swift; sourceTree = "<group>"; };
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 = "<group>"; };
D61099AE2144B0CC00432DC2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
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 = "<group>"; };
D61099BC2144B0CC00432DC2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
D61099C82144B13C00432DC2 /* Client.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Client.swift; sourceTree = "<group>"; };
D61099CA2144B20500432DC2 /* Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = "<group>"; };
D61099CF2144B2D700432DC2 /* Method.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Method.swift; sourceTree = "<group>"; };
D61099D12144B2E600432DC2 /* Body.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Body.swift; sourceTree = "<group>"; };
D61099D32144B32E00432DC2 /* Parameter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Parameter.swift; sourceTree = "<group>"; };
D61099D52144B4B200432DC2 /* FormAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormAttachment.swift; sourceTree = "<group>"; };
D61099D82144B76400432DC2 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = "<group>"; };
D61099DB2144BDBF00432DC2 /* Response.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Response.swift; sourceTree = "<group>"; };
D61099DE2144C11400432DC2 /* MastodonError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonError.swift; sourceTree = "<group>"; };
D61099E02144C1DC00432DC2 /* Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = "<group>"; };
D61099E22144C38900432DC2 /* Emoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emoji.swift; sourceTree = "<group>"; };
D61099E4214561AB00432DC2 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
D61099E6214561FF00432DC2 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = "<group>"; };
D61099E82145658300432DC2 /* Card.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Card.swift; sourceTree = "<group>"; };
D61099EA2145661700432DC2 /* ConversationContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationContext.swift; sourceTree = "<group>"; };
D61099EC2145664800432DC2 /* Filter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Filter.swift; sourceTree = "<group>"; };
D61099EE214566C000432DC2 /* Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instance.swift; sourceTree = "<group>"; };
D61099F02145686D00432DC2 /* List.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = List.swift; sourceTree = "<group>"; };
D61099F22145688600432DC2 /* Mention.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mention.swift; sourceTree = "<group>"; };
D61099F4214568C300432DC2 /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = "<group>"; };
D61099F62145693500432DC2 /* PushSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushSubscription.swift; sourceTree = "<group>"; };
D61099F82145698900432DC2 /* Relationship.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Relationship.swift; sourceTree = "<group>"; };
D61099FA214569F600432DC2 /* Report.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Report.swift; sourceTree = "<group>"; };
D61099FC21456A1D00432DC2 /* SearchResults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResults.swift; sourceTree = "<group>"; };
D61099FE21456A4C00432DC2 /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = "<group>"; };
D6109A0021456B0800432DC2 /* Hashtag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Hashtag.swift; sourceTree = "<group>"; };
D6109A022145722C00432DC2 /* RegisteredApplication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisteredApplication.swift; sourceTree = "<group>"; };
D6109A04214572BF00432DC2 /* Scope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Scope.swift; sourceTree = "<group>"; };
D6109A062145756700432DC2 /* LoginSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginSettings.swift; sourceTree = "<group>"; };
D6109A0821458C4A00432DC2 /* Empty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Empty.swift; sourceTree = "<group>"; };
D6109A0A2145953C00432DC2 /* ClientModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientModel.swift; sourceTree = "<group>"; };
D6109A0C214599E100432DC2 /* RequestRange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestRange.swift; sourceTree = "<group>"; };
D6109A0E21459B6900432DC2 /* Pagination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pagination.swift; sourceTree = "<group>"; };
D6109A10214607D500432DC2 /* Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timeline.swift; sourceTree = "<group>"; };
D6333B362137838300CE884A /* AttributedString+Trim.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttributedString+Trim.swift"; sourceTree = "<group>"; };
D6333B762138D94E00CE884A /* ComposeMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeMediaView.swift; sourceTree = "<group>"; };
D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+TimeAgo.swift"; sourceTree = "<group>"; };
@ -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 = "<group>";
};
D61099B92144B0CC00432DC2 /* PachydermTests */ = {
isa = PBXGroup;
children = (
D61099BA2144B0CC00432DC2 /* PachydermTests.swift */,
D61099BC2144B0CC00432DC2 /* Info.plist */,
);
path = PachydermTests;
sourceTree = "<group>";
};
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 = "<group>";
};
D61099D72144B74500432DC2 /* Extensions */ = {
isa = PBXGroup;
children = (
D61099D82144B76400432DC2 /* Data.swift */,
);
path = Extensions;
sourceTree = "<group>";
};
D61099DA2144BDB600432DC2 /* Response */ = {
isa = PBXGroup;
children = (
D61099DB2144BDBF00432DC2 /* Response.swift */,
D6109A0821458C4A00432DC2 /* Empty.swift */,
D6109A0E21459B6900432DC2 /* Pagination.swift */,
);
path = Response;
sourceTree = "<group>";
};
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 = "<group>";
};
D641C780213DD7C4004B4513 /* Screens */ = {
isa = PBXGroup;
children = (
@ -335,6 +530,13 @@
path = Transitions;
sourceTree = "<group>";
};
D65A37F221472F300087646E /* Frameworks */ = {
isa = PBXGroup;
children = (
);
name = Frameworks;
sourceTree = "<group>";
};
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 = "<group>";
};
@ -392,6 +597,8 @@
D6D4DDCC212518A000E1C4BB /* Tusker.app */,
D6D4DDE0212518A200E1C4BB /* TuskerTests.xctest */,
D6D4DDEB212518A200E1C4BB /* TuskerUITests.xctest */,
D61099AB2144B0CC00432DC2 /* Pachyderm.framework */,
D61099B32144B0CC00432DC2 /* PachydermTests.xctest */,
);
name = Products;
sourceTree = "<group>";
@ -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 = (

View File

@ -4,6 +4,11 @@
<dict>
<key>SchemeUserState</key>
<dict>
<key>Pachyderm.xcscheme</key>
<dict>
<key>orderHint</key>
<integer>9</integer>
</dict>
<key>Tusker.xcscheme</key>
<dict>
<key>orderHint</key>

View File

@ -7,9 +7,6 @@
<FileRef
location = "container:Tusker.xcodeproj">
</FileRef>
<FileRef
location = "group:MastodonKit/MastodonKit.xcodeproj">
</FileRef>
<FileRef
location = "group:SwiftSoup/SwiftSoup.xcodeproj">
</FileRef>

View File

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

View File

@ -7,7 +7,7 @@
//
import Foundation
import MastodonKit
import Pachyderm
extension Account {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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()
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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