Compare commits
4 Commits
911e66a159
...
6df5f7fb08
Author | SHA1 | Date |
---|---|---|
Shadowfacts | 6df5f7fb08 | |
Shadowfacts | 02135aa0de | |
Shadowfacts | be5a4c03a6 | |
Shadowfacts | 2c1ba7926e |
|
@ -26,7 +26,7 @@ public class Client {
|
||||||
|
|
||||||
public var timeoutInterval: TimeInterval = 60
|
public var timeoutInterval: TimeInterval = 60
|
||||||
|
|
||||||
lazy var decoder: JSONDecoder = {
|
static let decoder: JSONDecoder = {
|
||||||
let decoder = JSONDecoder()
|
let decoder = JSONDecoder()
|
||||||
let formatter = DateFormatter()
|
let formatter = DateFormatter()
|
||||||
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SZ"
|
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SZ"
|
||||||
|
@ -36,6 +36,16 @@ public class Client {
|
||||||
return decoder
|
return decoder
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
static let encoder: JSONEncoder = {
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
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")
|
||||||
|
encoder.dateEncodingStrategy = .formatted(formatter)
|
||||||
|
return encoder
|
||||||
|
}()
|
||||||
|
|
||||||
public init(baseURL: URL, accessToken: String? = nil, session: URLSession = .shared) {
|
public init(baseURL: URL, accessToken: String? = nil, session: URLSession = .shared) {
|
||||||
self.baseURL = baseURL
|
self.baseURL = baseURL
|
||||||
self.accessToken = accessToken
|
self.accessToken = accessToken
|
||||||
|
@ -59,12 +69,12 @@ public class Client {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard response.statusCode == 200 else {
|
guard response.statusCode == 200 else {
|
||||||
let mastodonError = try? self.decoder.decode(MastodonError.self, from: data)
|
let mastodonError = try? Client.decoder.decode(MastodonError.self, from: data)
|
||||||
let error: Error = mastodonError.flatMap { .mastodonError($0.description) } ?? .unexpectedStatus(response.statusCode)
|
let error: Error = mastodonError.flatMap { .mastodonError($0.description) } ?? .unexpectedStatus(response.statusCode)
|
||||||
completion(.failure(error))
|
completion(.failure(error))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let result = try? self.decoder.decode(Result.self, from: data) else {
|
guard let result = try? Client.decoder.decode(Result.self, from: data) else {
|
||||||
completion(.failure(.invalidModel))
|
completion(.failure(.invalidModel))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -92,7 +102,7 @@ public class Client {
|
||||||
|
|
||||||
// MARK: - Authorization
|
// MARK: - Authorization
|
||||||
public func registerApp(name: String, redirectURI: String, scopes: [Scope], website: URL? = nil, completion: @escaping Callback<RegisteredApplication>) {
|
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([
|
let request = Request<RegisteredApplication>(method: .post, path: "/api/v1/apps", body: ParametersBody([
|
||||||
"client_name" => name,
|
"client_name" => name,
|
||||||
"redirect_uris" => redirectURI,
|
"redirect_uris" => redirectURI,
|
||||||
"scopes" => scopes.scopeString,
|
"scopes" => scopes.scopeString,
|
||||||
|
@ -109,7 +119,7 @@ public class Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
public func getAccessToken(authorizationCode: String, redirectURI: String, completion: @escaping Callback<LoginSettings>) {
|
public func getAccessToken(authorizationCode: String, redirectURI: String, completion: @escaping Callback<LoginSettings>) {
|
||||||
let request = Request<LoginSettings>(method: .post, path: "/oauth/token", body: .parameters([
|
let request = Request<LoginSettings>(method: .post, path: "/oauth/token", body: ParametersBody([
|
||||||
"client_id" => clientID,
|
"client_id" => clientID,
|
||||||
"client_secret" => clientSecret,
|
"client_secret" => clientSecret,
|
||||||
"grant_type" => "authorization_code",
|
"grant_type" => "authorization_code",
|
||||||
|
@ -168,13 +178,13 @@ public class Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func block(domain: String) -> Request<Empty> {
|
public static func block(domain: String) -> Request<Empty> {
|
||||||
return Request<Empty>(method: .post, path: "/api/v1/domain_blocks", body: .parameters([
|
return Request<Empty>(method: .post, path: "/api/v1/domain_blocks", body: ParametersBody([
|
||||||
"domain" => domain
|
"domain" => domain
|
||||||
]))
|
]))
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func unblock(domain: String) -> Request<Empty> {
|
public static func unblock(domain: String) -> Request<Empty> {
|
||||||
return Request<Empty>(method: .delete, path: "/api/v1/domain_blocks", body: .parameters([
|
return Request<Empty>(method: .delete, path: "/api/v1/domain_blocks", body: ParametersBody([
|
||||||
"domain" => domain
|
"domain" => domain
|
||||||
]))
|
]))
|
||||||
}
|
}
|
||||||
|
@ -185,7 +195,7 @@ public class Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func createFilter(phrase: String, context: [Filter.Context], irreversible: Bool? = nil, wholeWord: Bool? = nil, expiresAt: Date? = nil) -> Request<Filter> {
|
public static func createFilter(phrase: String, context: [Filter.Context], irreversible: Bool? = nil, wholeWord: Bool? = nil, expiresAt: Date? = nil) -> Request<Filter> {
|
||||||
return Request<Filter>(method: .post, path: "/api/v1/filters", body: .parameters([
|
return Request<Filter>(method: .post, path: "/api/v1/filters", body: ParametersBody([
|
||||||
"phrase" => phrase,
|
"phrase" => phrase,
|
||||||
"irreversible" => irreversible,
|
"irreversible" => irreversible,
|
||||||
"whole_word" => wholeWord,
|
"whole_word" => wholeWord,
|
||||||
|
@ -209,7 +219,7 @@ public class Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func followRemote(acct: String) -> Request<Account> {
|
public static func followRemote(acct: String) -> Request<Account> {
|
||||||
return Request<Account>(method: .post, path: "/api/v1/follows", body: .parameters(["uri" => acct]))
|
return Request<Account>(method: .post, path: "/api/v1/follows", body: ParametersBody(["uri" => acct]))
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Lists
|
// MARK: - Lists
|
||||||
|
@ -222,12 +232,12 @@ public class Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func createList(title: String) -> Request<List> {
|
public static func createList(title: String) -> Request<List> {
|
||||||
return Request<List>(method: .post, path: "/api/v1/lists", body: .parameters(["title" => title]))
|
return Request<List>(method: .post, path: "/api/v1/lists", body: ParametersBody(["title" => title]))
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Media
|
// MARK: - Media
|
||||||
public static func upload(attachment: FormAttachment, description: String? = nil, focus: (Float, Float)? = nil) -> Request<Attachment> {
|
public static func upload(attachment: FormAttachment, description: String? = nil, focus: (Float, Float)? = nil) -> Request<Attachment> {
|
||||||
return Request<Attachment>(method: .post, path: "/api/v1/media", body: .formData([
|
return Request<Attachment>(method: .post, path: "/api/v1/media", body: FormDataBody([
|
||||||
"description" => description,
|
"description" => description,
|
||||||
"focus" => focus
|
"focus" => focus
|
||||||
], attachment))
|
], attachment))
|
||||||
|
@ -259,7 +269,7 @@ public class Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func report(account: Account, statuses: [Status], comment: String) -> Request<Report> {
|
public static func report(account: Account, statuses: [Status], comment: String) -> Request<Report> {
|
||||||
return Request<Report>(method: .post, path: "/api/v1/reports", body: .parameters([
|
return Request<Report>(method: .post, path: "/api/v1/reports", body: ParametersBody([
|
||||||
"account_id" => account.id,
|
"account_id" => account.id,
|
||||||
"comment" => comment
|
"comment" => comment
|
||||||
] + "status_ids" => statuses.map { $0.id }))
|
] + "status_ids" => statuses.map { $0.id }))
|
||||||
|
@ -287,7 +297,7 @@ public class Client {
|
||||||
spoilerText: String? = nil,
|
spoilerText: String? = nil,
|
||||||
visibility: Status.Visibility? = nil,
|
visibility: Status.Visibility? = nil,
|
||||||
language: String? = nil) -> Request<Status> {
|
language: String? = nil) -> Request<Status> {
|
||||||
return Request<Status>(method: .post, path: "/api/v1/statuses", body: .parameters([
|
return Request<Status>(method: .post, path: "/api/v1/statuses", body: ParametersBody([
|
||||||
"status" => text,
|
"status" => text,
|
||||||
"content_type" => contentType.mimeType,
|
"content_type" => contentType.mimeType,
|
||||||
"in_reply_to_id" => inReplyTo,
|
"in_reply_to_id" => inReplyTo,
|
||||||
|
|
|
@ -115,7 +115,7 @@ public final class Account: AccountProtocol, Decodable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func mute(_ account: Account, notifications: Bool? = nil) -> Request<Relationship> {
|
public static func mute(_ account: Account, notifications: Bool? = nil) -> Request<Relationship> {
|
||||||
return Request<Relationship>(method: .post, path: "/api/v1/accounts/\(account.id)/mute", body: .parameters([
|
return Request<Relationship>(method: .post, path: "/api/v1/accounts/\(account.id)/mute", body: ParametersBody([
|
||||||
"notifications" => notifications
|
"notifications" => notifications
|
||||||
]))
|
]))
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ public class Attachment: Codable {
|
||||||
public let blurHash: String?
|
public let blurHash: String?
|
||||||
|
|
||||||
public static func update(_ attachment: Attachment, focus: (Float, Float)?, description: String?) -> Request<Attachment> {
|
public static func update(_ attachment: Attachment, focus: (Float, Float)?, description: String?) -> Request<Attachment> {
|
||||||
return Request<Attachment>(method: .put, path: "/api/v1/media/\(attachment.id)", body: .formData([
|
return Request<Attachment>(method: .put, path: "/api/v1/media/\(attachment.id)", body: FormDataBody([
|
||||||
"description" => (description ?? attachment.description),
|
"description" => (description ?? attachment.description),
|
||||||
"focus" => focus
|
"focus" => focus
|
||||||
], nil))
|
], nil))
|
||||||
|
|
|
@ -23,7 +23,7 @@ public class Filter: Decodable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func update(_ filter: Filter, phrase: String? = nil, context: [Context]? = nil, irreversible: Bool? = nil, wholeWord: Bool? = nil, expiresAt: Date? = nil) -> Request<Filter> {
|
public static func update(_ filter: Filter, phrase: String? = nil, context: [Context]? = nil, irreversible: Bool? = nil, wholeWord: Bool? = nil, expiresAt: Date? = nil) -> Request<Filter> {
|
||||||
return Request<Filter>(method: .put, path: "/api/v1/filters/\(filter.id)", body: .parameters([
|
return Request<Filter>(method: .put, path: "/api/v1/filters/\(filter.id)", body: ParametersBody([
|
||||||
"phrase" => (phrase ?? filter.phrase),
|
"phrase" => (phrase ?? filter.phrase),
|
||||||
"irreversible" => (irreversible ?? filter.irreversible),
|
"irreversible" => (irreversible ?? filter.irreversible),
|
||||||
"whole_word" => (wholeWord ?? filter.wholeWord),
|
"whole_word" => (wholeWord ?? filter.wholeWord),
|
||||||
|
|
|
@ -31,7 +31,7 @@ public class List: Decodable, Equatable, Hashable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func update(_ list: List, title: String) -> Request<List> {
|
public static func update(_ list: List, title: String) -> Request<List> {
|
||||||
return Request<List>(method: .put, path: "/api/v1/lists/\(list.id)", body: .parameters(["title" => title]))
|
return Request<List>(method: .put, path: "/api/v1/lists/\(list.id)", body: ParametersBody(["title" => title]))
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func delete(_ list: List) -> Request<Empty> {
|
public static func delete(_ list: List) -> Request<Empty> {
|
||||||
|
@ -39,13 +39,13 @@ public class List: Decodable, Equatable, Hashable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func add(_ list: List, accounts accountIDs: [String]) -> Request<Empty> {
|
public static func add(_ list: List, accounts accountIDs: [String]) -> Request<Empty> {
|
||||||
return Request<Empty>(method: .post, path: "/api/v1/lists/\(list.id)/accounts", body: .parameters(
|
return Request<Empty>(method: .post, path: "/api/v1/lists/\(list.id)/accounts", body: ParametersBody(
|
||||||
"account_ids" => accountIDs
|
"account_ids" => accountIDs
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func remove(_ list: List, accounts accountIDs: [String]) -> Request<Empty> {
|
public static func remove(_ list: List, accounts accountIDs: [String]) -> Request<Empty> {
|
||||||
return Request<Empty>(method: .delete, path: "/api/v1/lists/\(list.id)/accounts", body: .parameters(
|
return Request<Empty>(method: .delete, path: "/api/v1/lists/\(list.id)/accounts", body: ParametersBody(
|
||||||
"account_ids" => accountIDs
|
"account_ids" => accountIDs
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ public class Notification: Decodable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func dismiss(id notificationID: String) -> Request<Empty> {
|
public static func dismiss(id notificationID: String) -> Request<Empty> {
|
||||||
return Request<Empty>(method: .post, path: "/api/v1/notifications/dismiss", body: .parameters([
|
return Request<Empty>(method: .post, path: "/api/v1/notifications/dismiss", body: ParametersBody([
|
||||||
"id" => notificationID
|
"id" => notificationID
|
||||||
]))
|
]))
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,56 +8,82 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
enum Body {
|
protocol Body {
|
||||||
case parameters([Parameter]?)
|
var mimeType: String? { get }
|
||||||
case formData([Parameter]?, FormAttachment?)
|
var data: Data? { get }
|
||||||
case empty
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Body {
|
struct EmptyBody: Body {
|
||||||
private static let boundary: String = "PachydermBoundary"
|
var mimeType: String? { nil }
|
||||||
|
var data: Data? { nil }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ParametersBody: Body {
|
||||||
|
let parameters: [Parameter]?
|
||||||
|
|
||||||
|
init(_ parmaeters: [Parameter]?) {
|
||||||
|
self.parameters = parmaeters
|
||||||
|
}
|
||||||
|
|
||||||
|
var mimeType: String? {
|
||||||
|
if parameters == nil || parameters!.isEmpty {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return "application/x-www-form-urlencoded; charset=utf-8"
|
||||||
|
}
|
||||||
|
|
||||||
var data: Data? {
|
var data: Data? {
|
||||||
switch self {
|
|
||||||
case let .parameters(parameters):
|
|
||||||
return parameters?.urlEncoded.data(using: .utf8)
|
return parameters?.urlEncoded.data(using: .utf8)
|
||||||
case let .formData(parameters, attachment):
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FormDataBody: Body {
|
||||||
|
private static let boundary = "PachydermBoundary"
|
||||||
|
|
||||||
|
let parameters: [Parameter]?
|
||||||
|
let attachment: FormAttachment?
|
||||||
|
|
||||||
|
init(_ parameters: [Parameter]?, _ attachment: FormAttachment?) {
|
||||||
|
self.parameters = parameters
|
||||||
|
self.attachment = attachment
|
||||||
|
}
|
||||||
|
|
||||||
|
var mimeType: String? {
|
||||||
|
if parameters == nil && attachment == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return "multipart/form-data; boundary=\(FormDataBody.boundary)"
|
||||||
|
}
|
||||||
|
|
||||||
|
var data: Data? {
|
||||||
var data = Data()
|
var data = Data()
|
||||||
parameters?.forEach { param in
|
parameters?.forEach { param in
|
||||||
guard let value = param.value else { return }
|
guard let value = param.value else { return }
|
||||||
data.append("--\(Body.boundary)\r\n")
|
data.append("--\(FormDataBody.boundary)\r\n")
|
||||||
data.append("Content-Disposition: form-data; name=\"\(param.name)\"\r\n\r\n")
|
data.append("Content-Disposition: form-data; name=\"\(param.name)\"\r\n\r\n")
|
||||||
data.append("\(value)\r\n")
|
data.append("\(value)\r\n")
|
||||||
}
|
}
|
||||||
if let attachment = attachment {
|
if let attachment = attachment {
|
||||||
data.append("--\(Body.boundary)\r\n")
|
data.append("--\(FormDataBody.boundary)\r\n")
|
||||||
data.append("Content-Disposition: form-data; name=\"file\"; filename=\"\(attachment.fileName)\"\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("Content-Type: \(attachment.mimeType)\r\n\r\n")
|
||||||
data.append(attachment.data)
|
data.append(attachment.data)
|
||||||
data.append("\r\n")
|
data.append("\r\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
data.append("--\(Body.boundary)--\r\n")
|
data.append("--\(FormDataBody.boundary)--\r\n")
|
||||||
return data
|
return data
|
||||||
case .empty:
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var mimeType: String? {
|
struct JsonBody<T: Encodable>: Body {
|
||||||
switch self {
|
let value: T
|
||||||
case let .parameters(parameters):
|
|
||||||
if parameters == nil {
|
init(_ value: T) {
|
||||||
return nil
|
self.value = value
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var mimeType: String? { "application/json" }
|
||||||
|
|
||||||
|
var data: Data? { try? Client.encoder.encode(value) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ public struct Request<ResultType: Decodable> {
|
||||||
let body: Body
|
let body: Body
|
||||||
var queryParameters: [Parameter]
|
var queryParameters: [Parameter]
|
||||||
|
|
||||||
init(method: Method, path: String, body: Body = .empty, queryParameters: [Parameter] = []) {
|
init(method: Method, path: String, body: Body = EmptyBody(), queryParameters: [Parameter] = []) {
|
||||||
self.method = method
|
self.method = method
|
||||||
self.path = path
|
self.path = path
|
||||||
self.body = body
|
self.body = body
|
||||||
|
|
|
@ -146,6 +146,7 @@
|
||||||
D6531DF0246B867E000F9538 /* GifvAttachmentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DEF246B867E000F9538 /* GifvAttachmentViewController.swift */; };
|
D6531DF0246B867E000F9538 /* GifvAttachmentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DEF246B867E000F9538 /* GifvAttachmentViewController.swift */; };
|
||||||
D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */; };
|
D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */; };
|
||||||
D65F613423AEAB6600F3CFD3 /* OnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65F613323AEAB6600F3CFD3 /* OnboardingTests.swift */; };
|
D65F613423AEAB6600F3CFD3 /* OnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65F613323AEAB6600F3CFD3 /* OnboardingTests.swift */; };
|
||||||
|
D6620ACE2511A0ED00312CA0 /* StatusStateResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */; };
|
||||||
D663625D2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D663625C2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib */; };
|
D663625D2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D663625C2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib */; };
|
||||||
D663625F2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663625E2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift */; };
|
D663625F2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663625E2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift */; };
|
||||||
D663626221360B1900C9CBA2 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663626121360B1900C9CBA2 /* Preferences.swift */; };
|
D663626221360B1900C9CBA2 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663626121360B1900C9CBA2 /* Preferences.swift */; };
|
||||||
|
@ -474,6 +475,7 @@
|
||||||
D65F613323AEAB6600F3CFD3 /* OnboardingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingTests.swift; sourceTree = "<group>"; };
|
D65F613323AEAB6600F3CFD3 /* OnboardingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingTests.swift; sourceTree = "<group>"; };
|
||||||
D65F613523AFD65900F3CFD3 /* Ambassador.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Ambassador.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
D65F613523AFD65900F3CFD3 /* Ambassador.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Ambassador.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
D65F613723AFD65D00F3CFD3 /* Embassy.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Embassy.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
D65F613723AFD65D00F3CFD3 /* Embassy.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Embassy.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusStateResolver.swift; sourceTree = "<group>"; };
|
||||||
D663625C2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ConversationMainStatusTableViewCell.xib; sourceTree = "<group>"; };
|
D663625C2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ConversationMainStatusTableViewCell.xib; sourceTree = "<group>"; };
|
||||||
D663625E2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationMainStatusTableViewCell.swift; sourceTree = "<group>"; };
|
D663625E2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationMainStatusTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
D663626121360B1900C9CBA2 /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
|
D663626121360B1900C9CBA2 /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1109,6 +1111,7 @@
|
||||||
D690797224A4EF9700023A34 /* UIBezierPath+Helpers.swift */,
|
D690797224A4EF9700023A34 /* UIBezierPath+Helpers.swift */,
|
||||||
D6D3F4C324FDB6B700EC4A6A /* View+ConditionalModifier.swift */,
|
D6D3F4C324FDB6B700EC4A6A /* View+ConditionalModifier.swift */,
|
||||||
D6D4CC90250D2C3100FCCF8D /* UIAccessibility.swift */,
|
D6D4CC90250D2C3100FCCF8D /* UIAccessibility.swift */,
|
||||||
|
D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */,
|
||||||
);
|
);
|
||||||
path = Extensions;
|
path = Extensions;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1904,6 +1907,7 @@
|
||||||
D677284824ECBCB100C732D3 /* ComposeView.swift in Sources */,
|
D677284824ECBCB100C732D3 /* ComposeView.swift in Sources */,
|
||||||
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */,
|
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */,
|
||||||
04586B4122B2FFB10021BD04 /* PreferencesView.swift in Sources */,
|
04586B4122B2FFB10021BD04 /* PreferencesView.swift in Sources */,
|
||||||
|
D6620ACE2511A0ED00312CA0 /* StatusStateResolver.swift in Sources */,
|
||||||
D6BC9DB5232D4CE3002CA326 /* NotificationsMode.swift in Sources */,
|
D6BC9DB5232D4CE3002CA326 /* NotificationsMode.swift in Sources */,
|
||||||
D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */,
|
D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */,
|
||||||
D67895BC24671E6D00D4CD9E /* PKDrawing+Render.swift in Sources */,
|
D67895BC24671E6D00D4CD9E /* PKDrawing+Render.swift in Sources */,
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
//
|
||||||
|
// StatusStateResolver.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 9/15/20.
|
||||||
|
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
|
extension StatusState {
|
||||||
|
|
||||||
|
func resolveFor(status: StatusMO, text: String?) {
|
||||||
|
let longEnoughToCollapse: Bool
|
||||||
|
if Preferences.shared.collapseLongPosts,
|
||||||
|
let text = text,
|
||||||
|
text.count > 500 {
|
||||||
|
longEnoughToCollapse = true
|
||||||
|
} else {
|
||||||
|
longEnoughToCollapse = false
|
||||||
|
}
|
||||||
|
|
||||||
|
let contentWarningCollapsible = !status.spoilerText.isEmpty
|
||||||
|
|
||||||
|
self.collapsible = contentWarningCollapsible || longEnoughToCollapse
|
||||||
|
self.collapsed = longEnoughToCollapse || (!Preferences.shared.expandAllContentWarnings && contentWarningCollapsible)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -55,6 +55,12 @@ class Preferences: Codable, ObservableObject {
|
||||||
self.openLinksInApps = try container.decode(Bool.self, forKey: .openLinksInApps)
|
self.openLinksInApps = try container.decode(Bool.self, forKey: .openLinksInApps)
|
||||||
self.useInAppSafari = try container.decode(Bool.self, forKey: .useInAppSafari)
|
self.useInAppSafari = try container.decode(Bool.self, forKey: .useInAppSafari)
|
||||||
self.inAppSafariAutomaticReaderMode = try container.decode(Bool.self, forKey: .inAppSafariAutomaticReaderMode)
|
self.inAppSafariAutomaticReaderMode = try container.decode(Bool.self, forKey: .inAppSafariAutomaticReaderMode)
|
||||||
|
if container.contains(.expandAllContentWarnings) {
|
||||||
|
self.expandAllContentWarnings = try container.decode(Bool.self, forKey: .expandAllContentWarnings)
|
||||||
|
}
|
||||||
|
if container.contains(.collapseLongPosts) {
|
||||||
|
self.collapseLongPosts = try container.decode(Bool.self, forKey: .collapseLongPosts)
|
||||||
|
}
|
||||||
|
|
||||||
self.showFavoriteAndReblogCounts = try container.decode(Bool.self, forKey: .showFavoriteAndReblogCounts)
|
self.showFavoriteAndReblogCounts = try container.decode(Bool.self, forKey: .showFavoriteAndReblogCounts)
|
||||||
self.defaultNotificationsMode = try container.decode(NotificationsMode.self, forKey: .defaultNotificationsType)
|
self.defaultNotificationsMode = try container.decode(NotificationsMode.self, forKey: .defaultNotificationsType)
|
||||||
|
@ -84,6 +90,8 @@ class Preferences: Codable, ObservableObject {
|
||||||
try container.encode(openLinksInApps, forKey: .openLinksInApps)
|
try container.encode(openLinksInApps, forKey: .openLinksInApps)
|
||||||
try container.encode(useInAppSafari, forKey: .useInAppSafari)
|
try container.encode(useInAppSafari, forKey: .useInAppSafari)
|
||||||
try container.encode(inAppSafariAutomaticReaderMode, forKey: .inAppSafariAutomaticReaderMode)
|
try container.encode(inAppSafariAutomaticReaderMode, forKey: .inAppSafariAutomaticReaderMode)
|
||||||
|
try container.encode(expandAllContentWarnings, forKey: .expandAllContentWarnings)
|
||||||
|
try container.encode(collapseLongPosts, forKey: .collapseLongPosts)
|
||||||
|
|
||||||
try container.encode(showFavoriteAndReblogCounts, forKey: .showFavoriteAndReblogCounts)
|
try container.encode(showFavoriteAndReblogCounts, forKey: .showFavoriteAndReblogCounts)
|
||||||
try container.encode(defaultNotificationsMode, forKey: .defaultNotificationsType)
|
try container.encode(defaultNotificationsMode, forKey: .defaultNotificationsType)
|
||||||
|
@ -114,6 +122,8 @@ class Preferences: Codable, ObservableObject {
|
||||||
@Published var openLinksInApps = true
|
@Published var openLinksInApps = true
|
||||||
@Published var useInAppSafari = true
|
@Published var useInAppSafari = true
|
||||||
@Published var inAppSafariAutomaticReaderMode = false
|
@Published var inAppSafariAutomaticReaderMode = false
|
||||||
|
@Published var expandAllContentWarnings = false
|
||||||
|
@Published var collapseLongPosts = true
|
||||||
|
|
||||||
// MARK: Digital Wellness
|
// MARK: Digital Wellness
|
||||||
@Published var showFavoriteAndReblogCounts = true
|
@Published var showFavoriteAndReblogCounts = true
|
||||||
|
@ -142,6 +152,8 @@ class Preferences: Codable, ObservableObject {
|
||||||
case openLinksInApps
|
case openLinksInApps
|
||||||
case useInAppSafari
|
case useInAppSafari
|
||||||
case inAppSafariAutomaticReaderMode
|
case inAppSafariAutomaticReaderMode
|
||||||
|
case expandAllContentWarnings
|
||||||
|
case collapseLongPosts
|
||||||
|
|
||||||
case showFavoriteAndReblogCounts
|
case showFavoriteAndReblogCounts
|
||||||
case defaultNotificationsType
|
case defaultNotificationsType
|
||||||
|
|
|
@ -229,36 +229,57 @@ struct ComposeView: View {
|
||||||
private func uploadAttachments(_ completion: @escaping (Result<[Attachment], AttachmentUploadError>) -> Void) {
|
private func uploadAttachments(_ completion: @escaping (Result<[Attachment], AttachmentUploadError>) -> Void) {
|
||||||
let group = DispatchGroup()
|
let group = DispatchGroup()
|
||||||
|
|
||||||
var anyFailed = false
|
var attachmentDatas = [(Data, String)?]()
|
||||||
var uploadedAttachments = [Result<Attachment, Error>?]()
|
|
||||||
|
|
||||||
for (index, compAttachment) in draft.attachments.enumerated() {
|
for (index, compAttachment) in draft.attachments.enumerated() {
|
||||||
group.enter()
|
group.enter()
|
||||||
|
|
||||||
uploadedAttachments.append(nil)
|
attachmentDatas.append(nil)
|
||||||
|
|
||||||
compAttachment.data.getData { (data, mimeType) in
|
compAttachment.data.getData { (data, mimeType) in
|
||||||
postProgress += 1
|
postProgress += 1
|
||||||
|
|
||||||
|
attachmentDatas[index] = (data, mimeType)
|
||||||
|
group.leave()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
group.notify(queue: .global(qos: .userInitiated)) {
|
||||||
|
|
||||||
|
var anyFailed = false
|
||||||
|
var uploadedAttachments = [Result<Attachment, Error>?]()
|
||||||
|
|
||||||
|
// Mastodon does not respect the order of the `media_ids` parameter in the create post request,
|
||||||
|
// it determines attachment order by which was uploaded first. Since the upload attachment request
|
||||||
|
// does not include any timestamp data, and requests may arrive at the server out-of-order,
|
||||||
|
// attachments need to be uploaded serially in order to ensure the order of attachments in the
|
||||||
|
// posted status reflects order the user set.
|
||||||
|
// Pleroma does respect the order of the `media_ids` parameter.
|
||||||
|
|
||||||
|
for (index, (data, mimeType)) in attachmentDatas.map(\.unsafelyUnwrapped).enumerated() {
|
||||||
|
group.enter()
|
||||||
|
|
||||||
|
let compAttachment = draft.attachments[index]
|
||||||
let formAttachment = FormAttachment(mimeType: mimeType, data: data, fileName: "file")
|
let formAttachment = FormAttachment(mimeType: mimeType, data: data, fileName: "file")
|
||||||
let request = Client.upload(attachment: formAttachment, description: compAttachment.attachmentDescription)
|
let request = Client.upload(attachment: formAttachment, description: compAttachment.attachmentDescription)
|
||||||
self.mastodonController.run(request) { (response) in
|
self.mastodonController.run(request) { (response) in
|
||||||
switch response {
|
switch response {
|
||||||
case let .failure(error):
|
case let .failure(error):
|
||||||
uploadedAttachments[index] = .failure(error)
|
uploadedAttachments.append(.failure(error))
|
||||||
anyFailed = true
|
anyFailed = true
|
||||||
|
|
||||||
case let .success(attachment, _):
|
case let .success(attachment, _):
|
||||||
postProgress += 1
|
self.postProgress += 1
|
||||||
uploadedAttachments[index] = .success(attachment)
|
uploadedAttachments.append(.success(attachment))
|
||||||
}
|
}
|
||||||
|
|
||||||
group.leave()
|
group.leave()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
group.wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
group.notify(queue: .main) {
|
|
||||||
if anyFailed {
|
if anyFailed {
|
||||||
let errors = uploadedAttachments.map { (result) -> Error? in
|
let errors = uploadedAttachments.map { (result) -> Error? in
|
||||||
if case let .failure(error) = result {
|
if case let .failure(error) = result {
|
||||||
|
@ -274,6 +295,7 @@ struct ComposeView: View {
|
||||||
}
|
}
|
||||||
completion(.success(uploadedAttachments))
|
completion(.success(uploadedAttachments))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,8 @@ struct AdvancedPrefsView : View {
|
||||||
formattingSection
|
formattingSection
|
||||||
automationSection
|
automationSection
|
||||||
cachingSection
|
cachingSection
|
||||||
}.listStyle(GroupedListStyle())
|
}
|
||||||
|
.insetOrGroupedListStyle()
|
||||||
.navigationBarTitle(Text("Advanced"))
|
.navigationBarTitle(Text("Advanced"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,7 +45,7 @@ struct AdvancedPrefsView : View {
|
||||||
}
|
}
|
||||||
|
|
||||||
var cachingSection: some View {
|
var cachingSection: some View {
|
||||||
Section(header: Text("Caching")) {
|
Section(header: Text("Caching"), footer: Text("Clearing caches will restart the app.")) {
|
||||||
Button(action: clearCache) {
|
Button(action: clearCache) {
|
||||||
Text("Clear Mastodon Cache")
|
Text("Clear Mastodon Cache")
|
||||||
}.foregroundColor(.red)
|
}.foregroundColor(.red)
|
||||||
|
|
|
@ -33,7 +33,7 @@ struct AppearancePrefsView : View {
|
||||||
accountsSection
|
accountsSection
|
||||||
postsSection
|
postsSection
|
||||||
}
|
}
|
||||||
.listStyle(GroupedListStyle())
|
.insetOrGroupedListStyle()
|
||||||
.navigationBarTitle(Text("Appearance"))
|
.navigationBarTitle(Text("Appearance"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,10 @@ struct BehaviorPrefsView: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
linksSection
|
linksSection
|
||||||
}.listStyle(GroupedListStyle()).navigationBarTitle(Text("Behavior"))
|
contentWarningsSection
|
||||||
|
}
|
||||||
|
.insetOrGroupedListStyle()
|
||||||
|
.navigationBarTitle(Text("Behavior"))
|
||||||
}
|
}
|
||||||
|
|
||||||
var linksSection: some View {
|
var linksSection: some View {
|
||||||
|
@ -30,6 +33,18 @@ struct BehaviorPrefsView: View {
|
||||||
}.disabled(!preferences.useInAppSafari)
|
}.disabled(!preferences.useInAppSafari)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var contentWarningsSection: some View {
|
||||||
|
Section(header: Text("Content Warnings")) {
|
||||||
|
Toggle(isOn: $preferences.expandAllContentWarnings) {
|
||||||
|
Text("Expand All Content Warnings")
|
||||||
|
}
|
||||||
|
|
||||||
|
Toggle(isOn: $preferences.collapseLongPosts) {
|
||||||
|
Text("Collapse Long Posts")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
|
|
|
@ -16,7 +16,9 @@ struct ComposingPrefsView: View {
|
||||||
List {
|
List {
|
||||||
composingSection
|
composingSection
|
||||||
replyingSection
|
replyingSection
|
||||||
}.listStyle(GroupedListStyle()).navigationBarTitle("Composing")
|
}
|
||||||
|
.insetOrGroupedListStyle()
|
||||||
|
.navigationBarTitle("Composing")
|
||||||
}
|
}
|
||||||
|
|
||||||
var composingSection: some View {
|
var composingSection: some View {
|
||||||
|
|
|
@ -14,7 +14,9 @@ struct MediaPrefsView: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
viewingSection
|
viewingSection
|
||||||
}.listStyle(GroupedListStyle()).navigationBarTitle("Media")
|
}
|
||||||
|
.insetOrGroupedListStyle()
|
||||||
|
.navigationBarTitle("Media")
|
||||||
}
|
}
|
||||||
|
|
||||||
var viewingSection: some View {
|
var viewingSection: some View {
|
||||||
|
|
|
@ -15,7 +15,7 @@ struct PreferencesView: View {
|
||||||
// workaround: the navigation view is provided by MyProfileTableViewController so that it can inject the Done button
|
// workaround: the navigation view is provided by MyProfileTableViewController so that it can inject the Done button
|
||||||
// NavigationView {
|
// NavigationView {
|
||||||
List {
|
List {
|
||||||
Section {
|
Section(header: Text("Accounts")) {
|
||||||
ForEach(localData.accounts, id: \.accessToken) { (account) in
|
ForEach(localData.accounts, id: \.accessToken) { (account) in
|
||||||
Button(action: {
|
Button(action: {
|
||||||
NotificationCenter.default.post(name: .activateAccount, object: nil, userInfo: ["account": account])
|
NotificationCenter.default.post(name: .activateAccount, object: nil, userInfo: ["account": account])
|
||||||
|
@ -75,7 +75,7 @@ struct PreferencesView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.listStyle(GroupedListStyle())
|
.insetOrGroupedListStyle()
|
||||||
.navigationBarTitle(Text("Preferences"), displayMode: .inline)
|
.navigationBarTitle(Text("Preferences"), displayMode: .inline)
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
@ -85,6 +85,17 @@ struct PreferencesView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
@ViewBuilder
|
||||||
|
func insetOrGroupedListStyle() -> some View {
|
||||||
|
if #available(iOS 14.0, *) {
|
||||||
|
self.listStyle(InsetGroupedListStyle())
|
||||||
|
} else {
|
||||||
|
self.listStyle(GroupedListStyle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
struct PreferencesView_Previews : PreviewProvider {
|
struct PreferencesView_Previews : PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
|
|
|
@ -14,7 +14,7 @@ struct SilentActionPrefs : View {
|
||||||
List(Array(preferences.silentActions.keys), id: \.self) { source in
|
List(Array(preferences.silentActions.keys), id: \.self) { source in
|
||||||
SilentActionPermissionCell(source: source)
|
SilentActionPermissionCell(source: source)
|
||||||
}
|
}
|
||||||
.listStyle(GroupedListStyle())
|
.insetOrGroupedListStyle()
|
||||||
// .navigationBarTitle("Silent Action Permissions")
|
// .navigationBarTitle("Silent Action Permissions")
|
||||||
// see FB6838291
|
// see FB6838291
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,8 @@ struct WellnessPrefsView: View {
|
||||||
List {
|
List {
|
||||||
showFavAndReblogCountSection
|
showFavAndReblogCountSection
|
||||||
notificationsModeSection
|
notificationsModeSection
|
||||||
}.listStyle(GroupedListStyle())
|
}
|
||||||
|
.insetOrGroupedListStyle()
|
||||||
.navigationBarTitle(Text("Digital Wellness"))
|
.navigationBarTitle(Text("Digital Wellness"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -170,26 +170,14 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
updateStatusIconsForPreferences(status)
|
updateStatusIconsForPreferences(status)
|
||||||
|
|
||||||
if state.unknown {
|
if state.unknown {
|
||||||
collapsible = !status.spoilerText.isEmpty
|
state.resolveFor(status: status, text: contentTextView.text)
|
||||||
var shouldCollapse = collapsible
|
if state.collapsible! && showStatusAutomatically {
|
||||||
if !shouldCollapse,
|
state.collapsed = false
|
||||||
let text = contentTextView.text,
|
|
||||||
text.count > 500 {
|
|
||||||
collapsible = true
|
|
||||||
shouldCollapse = true
|
|
||||||
}
|
}
|
||||||
if collapsible && showStatusAutomatically {
|
|
||||||
shouldCollapse = false
|
|
||||||
}
|
}
|
||||||
setCollapsed(shouldCollapse, animated: false)
|
|
||||||
|
|
||||||
state.collapsible = collapsible
|
|
||||||
state.collapsed = shouldCollapse
|
|
||||||
} else {
|
|
||||||
collapsible = state.collapsible!
|
collapsible = state.collapsible!
|
||||||
setCollapsed(state.collapsed!, animated: false)
|
setCollapsed(state.collapsed!, animated: false)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func updateStatusState(status: StatusMO) {
|
func updateStatusState(status: StatusMO) {
|
||||||
favorited = status.favourited
|
favorited = status.favourited
|
||||||
|
|
Loading…
Reference in New Issue