Compare commits

..

No commits in common. "6df5f7fb088539b9b48fb39ba52ac3eb8a1446dd" and "911e66a159bfb9bef0f99c7fa398590de5146519" have entirely different histories.

21 changed files with 104 additions and 228 deletions

View File

@ -26,7 +26,7 @@ public class Client {
public var timeoutInterval: TimeInterval = 60 public var timeoutInterval: TimeInterval = 60
static let decoder: JSONDecoder = { lazy var 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,16 +36,6 @@ 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
@ -69,12 +59,12 @@ public class Client {
return return
} }
guard response.statusCode == 200 else { guard response.statusCode == 200 else {
let mastodonError = try? Client.decoder.decode(MastodonError.self, from: data) let mastodonError = try? self.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? Client.decoder.decode(Result.self, from: data) else { guard let result = try? self.decoder.decode(Result.self, from: data) else {
completion(.failure(.invalidModel)) completion(.failure(.invalidModel))
return return
} }
@ -102,7 +92,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: ParametersBody([ let request = Request<RegisteredApplication>(method: .post, path: "/api/v1/apps", body: .parameters([
"client_name" => name, "client_name" => name,
"redirect_uris" => redirectURI, "redirect_uris" => redirectURI,
"scopes" => scopes.scopeString, "scopes" => scopes.scopeString,
@ -119,7 +109,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: ParametersBody([ let request = Request<LoginSettings>(method: .post, path: "/oauth/token", body: .parameters([
"client_id" => clientID, "client_id" => clientID,
"client_secret" => clientSecret, "client_secret" => clientSecret,
"grant_type" => "authorization_code", "grant_type" => "authorization_code",
@ -178,13 +168,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: ParametersBody([ return Request<Empty>(method: .post, path: "/api/v1/domain_blocks", body: .parameters([
"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: ParametersBody([ return Request<Empty>(method: .delete, path: "/api/v1/domain_blocks", body: .parameters([
"domain" => domain "domain" => domain
])) ]))
} }
@ -195,7 +185,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: ParametersBody([ return Request<Filter>(method: .post, path: "/api/v1/filters", body: .parameters([
"phrase" => phrase, "phrase" => phrase,
"irreversible" => irreversible, "irreversible" => irreversible,
"whole_word" => wholeWord, "whole_word" => wholeWord,
@ -219,7 +209,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: ParametersBody(["uri" => acct])) return Request<Account>(method: .post, path: "/api/v1/follows", body: .parameters(["uri" => acct]))
} }
// MARK: - Lists // MARK: - Lists
@ -232,12 +222,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: ParametersBody(["title" => title])) return Request<List>(method: .post, path: "/api/v1/lists", body: .parameters(["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: FormDataBody([ return Request<Attachment>(method: .post, path: "/api/v1/media", body: .formData([
"description" => description, "description" => description,
"focus" => focus "focus" => focus
], attachment)) ], attachment))
@ -269,7 +259,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: ParametersBody([ return Request<Report>(method: .post, path: "/api/v1/reports", body: .parameters([
"account_id" => account.id, "account_id" => account.id,
"comment" => comment "comment" => comment
] + "status_ids" => statuses.map { $0.id })) ] + "status_ids" => statuses.map { $0.id }))
@ -297,7 +287,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: ParametersBody([ return Request<Status>(method: .post, path: "/api/v1/statuses", body: .parameters([
"status" => text, "status" => text,
"content_type" => contentType.mimeType, "content_type" => contentType.mimeType,
"in_reply_to_id" => inReplyTo, "in_reply_to_id" => inReplyTo,

View File

@ -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: ParametersBody([ return Request<Relationship>(method: .post, path: "/api/v1/accounts/\(account.id)/mute", body: .parameters([
"notifications" => notifications "notifications" => notifications
])) ]))
} }

View File

@ -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: FormDataBody([ return Request<Attachment>(method: .put, path: "/api/v1/media/\(attachment.id)", body: .formData([
"description" => (description ?? attachment.description), "description" => (description ?? attachment.description),
"focus" => focus "focus" => focus
], nil)) ], nil))

View File

@ -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: ParametersBody([ return Request<Filter>(method: .put, path: "/api/v1/filters/\(filter.id)", body: .parameters([
"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),

View File

@ -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: ParametersBody(["title" => title])) return Request<List>(method: .put, path: "/api/v1/lists/\(list.id)", body: .parameters(["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: ParametersBody( return Request<Empty>(method: .post, path: "/api/v1/lists/\(list.id)/accounts", body: .parameters(
"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: ParametersBody( return Request<Empty>(method: .delete, path: "/api/v1/lists/\(list.id)/accounts", body: .parameters(
"account_ids" => accountIDs "account_ids" => accountIDs
)) ))
} }

View File

@ -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: ParametersBody([ return Request<Empty>(method: .post, path: "/api/v1/notifications/dismiss", body: .parameters([
"id" => notificationID "id" => notificationID
])) ]))
} }

View File

@ -8,82 +8,56 @@
import Foundation import Foundation
protocol Body { enum Body {
var mimeType: String? { get } case parameters([Parameter]?)
var data: Data? { get } case formData([Parameter]?, FormAttachment?)
case empty
} }
struct EmptyBody: Body { extension Body {
var mimeType: String? { nil } private static let boundary: String = "PachydermBoundary"
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("--\(FormDataBody.boundary)\r\n") data.append("--\(Body.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("--\(FormDataBody.boundary)\r\n") data.append("--\(Body.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("--\(FormDataBody.boundary)--\r\n") data.append("--\(Body.boundary)--\r\n")
return data return data
case .empty:
return nil
} }
} }
struct JsonBody<T: Encodable>: Body { var mimeType: String? {
let value: T switch self {
case let .parameters(parameters):
init(_ value: T) { if parameters == nil {
self.value = value 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
}
} }
var mimeType: String? { "application/json" }
var data: Data? { try? Client.encoder.encode(value) }
} }

View File

@ -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 = EmptyBody(), queryParameters: [Parameter] = []) { init(method: Method, path: String, body: Body = .empty, queryParameters: [Parameter] = []) {
self.method = method self.method = method
self.path = path self.path = path
self.body = body self.body = body

View File

@ -146,7 +146,6 @@
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 */; };
@ -475,7 +474,6 @@
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>"; };
@ -1111,7 +1109,6 @@
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>";
@ -1907,7 +1904,6 @@
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 */,

View File

@ -1,30 +0,0 @@
//
// 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)
}
}

View File

@ -55,12 +55,6 @@ 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)
@ -90,8 +84,6 @@ 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)
@ -122,8 +114,6 @@ 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
@ -152,8 +142,6 @@ 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

View File

@ -229,57 +229,36 @@ 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 attachmentDatas = [(Data, String)?]() var anyFailed = false
var uploadedAttachments = [Result<Attachment, Error>?]()
for (index, compAttachment) in draft.attachments.enumerated() { for (index, compAttachment) in draft.attachments.enumerated() {
group.enter() group.enter()
attachmentDatas.append(nil) uploadedAttachments.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.append(.failure(error)) uploadedAttachments[index] = .failure(error)
anyFailed = true anyFailed = true
case let .success(attachment, _): case let .success(attachment, _):
self.postProgress += 1 postProgress += 1
uploadedAttachments.append(.success(attachment)) uploadedAttachments[index] = .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 {
@ -295,7 +274,6 @@ struct ComposeView: View {
} }
completion(.success(uploadedAttachments)) completion(.success(uploadedAttachments))
} }
} }
} }
} }

View File

@ -16,8 +16,7 @@ struct AdvancedPrefsView : View {
formattingSection formattingSection
automationSection automationSection
cachingSection cachingSection
} }.listStyle(GroupedListStyle())
.insetOrGroupedListStyle()
.navigationBarTitle(Text("Advanced")) .navigationBarTitle(Text("Advanced"))
} }
@ -45,7 +44,7 @@ struct AdvancedPrefsView : View {
} }
var cachingSection: some View { var cachingSection: some View {
Section(header: Text("Caching"), footer: Text("Clearing caches will restart the app.")) { Section(header: Text("Caching")) {
Button(action: clearCache) { Button(action: clearCache) {
Text("Clear Mastodon Cache") Text("Clear Mastodon Cache")
}.foregroundColor(.red) }.foregroundColor(.red)

View File

@ -33,7 +33,7 @@ struct AppearancePrefsView : View {
accountsSection accountsSection
postsSection postsSection
} }
.insetOrGroupedListStyle() .listStyle(GroupedListStyle())
.navigationBarTitle(Text("Appearance")) .navigationBarTitle(Text("Appearance"))
} }

View File

@ -14,10 +14,7 @@ struct BehaviorPrefsView: View {
var body: some View { var body: some View {
List { List {
linksSection linksSection
contentWarningsSection }.listStyle(GroupedListStyle()).navigationBarTitle(Text("Behavior"))
}
.insetOrGroupedListStyle()
.navigationBarTitle(Text("Behavior"))
} }
var linksSection: some View { var linksSection: some View {
@ -33,18 +30,6 @@ 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

View File

@ -16,9 +16,7 @@ struct ComposingPrefsView: View {
List { List {
composingSection composingSection
replyingSection replyingSection
} }.listStyle(GroupedListStyle()).navigationBarTitle("Composing")
.insetOrGroupedListStyle()
.navigationBarTitle("Composing")
} }
var composingSection: some View { var composingSection: some View {

View File

@ -14,9 +14,7 @@ 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 {

View File

@ -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(header: Text("Accounts")) { Section {
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 {
} }
} }
} }
.insetOrGroupedListStyle() .listStyle(GroupedListStyle())
.navigationBarTitle(Text("Preferences"), displayMode: .inline) .navigationBarTitle(Text("Preferences"), displayMode: .inline)
// } // }
} }
@ -85,17 +85,6 @@ 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 {

View File

@ -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)
} }
.insetOrGroupedListStyle() .listStyle(GroupedListStyle())
// .navigationBarTitle("Silent Action Permissions") // .navigationBarTitle("Silent Action Permissions")
// see FB6838291 // see FB6838291
} }

View File

@ -15,8 +15,7 @@ struct WellnessPrefsView: View {
List { List {
showFavAndReblogCountSection showFavAndReblogCountSection
notificationsModeSection notificationsModeSection
} }.listStyle(GroupedListStyle())
.insetOrGroupedListStyle()
.navigationBarTitle(Text("Digital Wellness")) .navigationBarTitle(Text("Digital Wellness"))
} }

View File

@ -170,14 +170,26 @@ class BaseStatusTableViewCell: UITableViewCell {
updateStatusIconsForPreferences(status) updateStatusIconsForPreferences(status)
if state.unknown { if state.unknown {
state.resolveFor(status: status, text: contentTextView.text) collapsible = !status.spoilerText.isEmpty
if state.collapsible! && showStatusAutomatically { var shouldCollapse = collapsible
state.collapsed = false if !shouldCollapse,
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