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
|
||||
|
||||
lazy var decoder: JSONDecoder = {
|
||||
static let decoder: JSONDecoder = {
|
||||
let decoder = JSONDecoder()
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SZ"
|
||||
|
@ -36,6 +36,16 @@ public class Client {
|
|||
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) {
|
||||
self.baseURL = baseURL
|
||||
self.accessToken = accessToken
|
||||
|
@ -59,12 +69,12 @@ public class Client {
|
|||
return
|
||||
}
|
||||
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)
|
||||
completion(.failure(error))
|
||||
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))
|
||||
return
|
||||
}
|
||||
|
@ -92,7 +102,7 @@ public class Client {
|
|||
|
||||
// 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([
|
||||
let request = Request<RegisteredApplication>(method: .post, path: "/api/v1/apps", body: ParametersBody([
|
||||
"client_name" => name,
|
||||
"redirect_uris" => redirectURI,
|
||||
"scopes" => scopes.scopeString,
|
||||
|
@ -109,7 +119,7 @@ public class Client {
|
|||
}
|
||||
|
||||
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_secret" => clientSecret,
|
||||
"grant_type" => "authorization_code",
|
||||
|
@ -168,13 +178,13 @@ public class Client {
|
|||
}
|
||||
|
||||
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
|
||||
]))
|
||||
}
|
||||
|
||||
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
|
||||
]))
|
||||
}
|
||||
|
@ -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> {
|
||||
return Request<Filter>(method: .post, path: "/api/v1/filters", body: .parameters([
|
||||
return Request<Filter>(method: .post, path: "/api/v1/filters", body: ParametersBody([
|
||||
"phrase" => phrase,
|
||||
"irreversible" => irreversible,
|
||||
"whole_word" => wholeWord,
|
||||
|
@ -209,7 +219,7 @@ public class Client {
|
|||
}
|
||||
|
||||
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
|
||||
|
@ -222,12 +232,12 @@ public class Client {
|
|||
}
|
||||
|
||||
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
|
||||
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,
|
||||
"focus" => focus
|
||||
], attachment))
|
||||
|
@ -259,7 +269,7 @@ public class Client {
|
|||
}
|
||||
|
||||
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,
|
||||
"comment" => comment
|
||||
] + "status_ids" => statuses.map { $0.id }))
|
||||
|
@ -287,7 +297,7 @@ public class Client {
|
|||
spoilerText: String? = nil,
|
||||
visibility: Status.Visibility? = nil,
|
||||
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,
|
||||
"content_type" => contentType.mimeType,
|
||||
"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> {
|
||||
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
|
||||
]))
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ public class Attachment: Codable {
|
|||
public let blurHash: String?
|
||||
|
||||
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),
|
||||
"focus" => focus
|
||||
], 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> {
|
||||
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),
|
||||
"irreversible" => (irreversible ?? filter.irreversible),
|
||||
"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> {
|
||||
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> {
|
||||
|
@ -39,13 +39,13 @@ public class List: Decodable, Equatable, Hashable {
|
|||
}
|
||||
|
||||
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
|
||||
))
|
||||
}
|
||||
|
||||
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
|
||||
))
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ public class Notification: Decodable {
|
|||
}
|
||||
|
||||
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
|
||||
]))
|
||||
}
|
||||
|
|
|
@ -8,56 +8,82 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
enum Body {
|
||||
case parameters([Parameter]?)
|
||||
case formData([Parameter]?, FormAttachment?)
|
||||
case empty
|
||||
protocol Body {
|
||||
var mimeType: String? { get }
|
||||
var data: Data? { get }
|
||||
}
|
||||
|
||||
extension Body {
|
||||
private static let boundary: String = "PachydermBoundary"
|
||||
struct EmptyBody: Body {
|
||||
var mimeType: String? { nil }
|
||||
var data: Data? { nil }
|
||||
}
|
||||
|
||||
struct ParametersBody: Body {
|
||||
let parameters: [Parameter]?
|
||||
|
||||
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
|
||||
}
|
||||
init(_ parmaeters: [Parameter]?) {
|
||||
self.parameters = parmaeters
|
||||
}
|
||||
|
||||
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:
|
||||
if parameters == nil || parameters!.isEmpty {
|
||||
return nil
|
||||
}
|
||||
return "application/x-www-form-urlencoded; charset=utf-8"
|
||||
}
|
||||
|
||||
var data: Data? {
|
||||
return parameters?.urlEncoded.data(using: .utf8)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
parameters?.forEach { param in
|
||||
guard let value = param.value else { return }
|
||||
data.append("--\(FormDataBody.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("--\(FormDataBody.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("--\(FormDataBody.boundary)--\r\n")
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
struct JsonBody<T: Encodable>: Body {
|
||||
let value: T
|
||||
|
||||
init(_ value: T) {
|
||||
self.value = value
|
||||
}
|
||||
|
||||
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
|
||||
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.path = path
|
||||
self.body = body
|
||||
|
|
|
@ -146,6 +146,7 @@
|
|||
D6531DF0246B867E000F9538 /* GifvAttachmentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DEF246B867E000F9538 /* GifvAttachmentViewController.swift */; };
|
||||
D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.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 */; };
|
||||
D663625F2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663625E2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.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>"; };
|
||||
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; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -1109,6 +1111,7 @@
|
|||
D690797224A4EF9700023A34 /* UIBezierPath+Helpers.swift */,
|
||||
D6D3F4C324FDB6B700EC4A6A /* View+ConditionalModifier.swift */,
|
||||
D6D4CC90250D2C3100FCCF8D /* UIAccessibility.swift */,
|
||||
D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1904,6 +1907,7 @@
|
|||
D677284824ECBCB100C732D3 /* ComposeView.swift in Sources */,
|
||||
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */,
|
||||
04586B4122B2FFB10021BD04 /* PreferencesView.swift in Sources */,
|
||||
D6620ACE2511A0ED00312CA0 /* StatusStateResolver.swift in Sources */,
|
||||
D6BC9DB5232D4CE3002CA326 /* NotificationsMode.swift in Sources */,
|
||||
D68015402401A6BA00D6103B /* ComposingPrefsView.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.useInAppSafari = try container.decode(Bool.self, forKey: .useInAppSafari)
|
||||
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.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(useInAppSafari, forKey: .useInAppSafari)
|
||||
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(defaultNotificationsMode, forKey: .defaultNotificationsType)
|
||||
|
@ -114,6 +122,8 @@ class Preferences: Codable, ObservableObject {
|
|||
@Published var openLinksInApps = true
|
||||
@Published var useInAppSafari = true
|
||||
@Published var inAppSafariAutomaticReaderMode = false
|
||||
@Published var expandAllContentWarnings = false
|
||||
@Published var collapseLongPosts = true
|
||||
|
||||
// MARK: Digital Wellness
|
||||
@Published var showFavoriteAndReblogCounts = true
|
||||
|
@ -142,6 +152,8 @@ class Preferences: Codable, ObservableObject {
|
|||
case openLinksInApps
|
||||
case useInAppSafari
|
||||
case inAppSafariAutomaticReaderMode
|
||||
case expandAllContentWarnings
|
||||
case collapseLongPosts
|
||||
|
||||
case showFavoriteAndReblogCounts
|
||||
case defaultNotificationsType
|
||||
|
|
|
@ -229,36 +229,57 @@ struct ComposeView: View {
|
|||
private func uploadAttachments(_ completion: @escaping (Result<[Attachment], AttachmentUploadError>) -> Void) {
|
||||
let group = DispatchGroup()
|
||||
|
||||
var anyFailed = false
|
||||
var uploadedAttachments = [Result<Attachment, Error>?]()
|
||||
var attachmentDatas = [(Data, String)?]()
|
||||
|
||||
for (index, compAttachment) in draft.attachments.enumerated() {
|
||||
group.enter()
|
||||
|
||||
uploadedAttachments.append(nil)
|
||||
attachmentDatas.append(nil)
|
||||
|
||||
compAttachment.data.getData { (data, mimeType) in
|
||||
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 request = Client.upload(attachment: formAttachment, description: compAttachment.attachmentDescription)
|
||||
self.mastodonController.run(request) { (response) in
|
||||
switch response {
|
||||
case let .failure(error):
|
||||
uploadedAttachments[index] = .failure(error)
|
||||
uploadedAttachments.append(.failure(error))
|
||||
anyFailed = true
|
||||
|
||||
case let .success(attachment, _):
|
||||
postProgress += 1
|
||||
uploadedAttachments[index] = .success(attachment)
|
||||
self.postProgress += 1
|
||||
uploadedAttachments.append(.success(attachment))
|
||||
}
|
||||
|
||||
group.leave()
|
||||
}
|
||||
|
||||
group.wait()
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: .main) {
|
||||
|
||||
|
||||
if anyFailed {
|
||||
let errors = uploadedAttachments.map { (result) -> Error? in
|
||||
if case let .failure(error) = result {
|
||||
|
@ -274,6 +295,7 @@ struct ComposeView: View {
|
|||
}
|
||||
completion(.success(uploadedAttachments))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,8 +16,9 @@ struct AdvancedPrefsView : View {
|
|||
formattingSection
|
||||
automationSection
|
||||
cachingSection
|
||||
}.listStyle(GroupedListStyle())
|
||||
.navigationBarTitle(Text("Advanced"))
|
||||
}
|
||||
.insetOrGroupedListStyle()
|
||||
.navigationBarTitle(Text("Advanced"))
|
||||
}
|
||||
|
||||
var formattingFooter: some View {
|
||||
|
@ -44,7 +45,7 @@ struct AdvancedPrefsView : View {
|
|||
}
|
||||
|
||||
var cachingSection: some View {
|
||||
Section(header: Text("Caching")) {
|
||||
Section(header: Text("Caching"), footer: Text("Clearing caches will restart the app.")) {
|
||||
Button(action: clearCache) {
|
||||
Text("Clear Mastodon Cache")
|
||||
}.foregroundColor(.red)
|
||||
|
|
|
@ -33,8 +33,8 @@ struct AppearancePrefsView : View {
|
|||
accountsSection
|
||||
postsSection
|
||||
}
|
||||
.listStyle(GroupedListStyle())
|
||||
.navigationBarTitle(Text("Appearance"))
|
||||
.insetOrGroupedListStyle()
|
||||
.navigationBarTitle(Text("Appearance"))
|
||||
}
|
||||
|
||||
private var accountsSection: some View {
|
||||
|
|
|
@ -14,7 +14,10 @@ struct BehaviorPrefsView: View {
|
|||
var body: some View {
|
||||
List {
|
||||
linksSection
|
||||
}.listStyle(GroupedListStyle()).navigationBarTitle(Text("Behavior"))
|
||||
contentWarningsSection
|
||||
}
|
||||
.insetOrGroupedListStyle()
|
||||
.navigationBarTitle(Text("Behavior"))
|
||||
}
|
||||
|
||||
var linksSection: some View {
|
||||
|
@ -30,6 +33,18 @@ struct BehaviorPrefsView: View {
|
|||
}.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
|
||||
|
|
|
@ -16,7 +16,9 @@ struct ComposingPrefsView: View {
|
|||
List {
|
||||
composingSection
|
||||
replyingSection
|
||||
}.listStyle(GroupedListStyle()).navigationBarTitle("Composing")
|
||||
}
|
||||
.insetOrGroupedListStyle()
|
||||
.navigationBarTitle("Composing")
|
||||
}
|
||||
|
||||
var composingSection: some View {
|
||||
|
|
|
@ -14,7 +14,9 @@ struct MediaPrefsView: View {
|
|||
var body: some View {
|
||||
List {
|
||||
viewingSection
|
||||
}.listStyle(GroupedListStyle()).navigationBarTitle("Media")
|
||||
}
|
||||
.insetOrGroupedListStyle()
|
||||
.navigationBarTitle("Media")
|
||||
}
|
||||
|
||||
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
|
||||
// NavigationView {
|
||||
List {
|
||||
Section {
|
||||
Section(header: Text("Accounts")) {
|
||||
ForEach(localData.accounts, id: \.accessToken) { (account) in
|
||||
Button(action: {
|
||||
NotificationCenter.default.post(name: .activateAccount, object: nil, userInfo: ["account": account])
|
||||
|
@ -75,8 +75,8 @@ struct PreferencesView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.listStyle(GroupedListStyle())
|
||||
.navigationBarTitle(Text("Preferences"), displayMode: .inline)
|
||||
.insetOrGroupedListStyle()
|
||||
.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
|
||||
struct PreferencesView_Previews : PreviewProvider {
|
||||
static var previews: some View {
|
||||
|
|
|
@ -14,7 +14,7 @@ struct SilentActionPrefs : View {
|
|||
List(Array(preferences.silentActions.keys), id: \.self) { source in
|
||||
SilentActionPermissionCell(source: source)
|
||||
}
|
||||
.listStyle(GroupedListStyle())
|
||||
.insetOrGroupedListStyle()
|
||||
// .navigationBarTitle("Silent Action Permissions")
|
||||
// see FB6838291
|
||||
}
|
||||
|
|
|
@ -15,8 +15,9 @@ struct WellnessPrefsView: View {
|
|||
List {
|
||||
showFavAndReblogCountSection
|
||||
notificationsModeSection
|
||||
}.listStyle(GroupedListStyle())
|
||||
.navigationBarTitle(Text("Digital Wellness"))
|
||||
}
|
||||
.insetOrGroupedListStyle()
|
||||
.navigationBarTitle(Text("Digital Wellness"))
|
||||
}
|
||||
|
||||
var showFavAndReblogCountSection: some View {
|
||||
|
|
|
@ -170,25 +170,13 @@ class BaseStatusTableViewCell: UITableViewCell {
|
|||
updateStatusIconsForPreferences(status)
|
||||
|
||||
if state.unknown {
|
||||
collapsible = !status.spoilerText.isEmpty
|
||||
var shouldCollapse = collapsible
|
||||
if !shouldCollapse,
|
||||
let text = contentTextView.text,
|
||||
text.count > 500 {
|
||||
collapsible = true
|
||||
shouldCollapse = true
|
||||
state.resolveFor(status: status, text: contentTextView.text)
|
||||
if state.collapsible! && showStatusAutomatically {
|
||||
state.collapsed = false
|
||||
}
|
||||
if collapsible && showStatusAutomatically {
|
||||
shouldCollapse = false
|
||||
}
|
||||
setCollapsed(shouldCollapse, animated: false)
|
||||
|
||||
state.collapsible = collapsible
|
||||
state.collapsed = shouldCollapse
|
||||
} else {
|
||||
collapsible = state.collapsible!
|
||||
setCollapsed(state.collapsed!, animated: false)
|
||||
}
|
||||
collapsible = state.collapsible!
|
||||
setCollapsed(state.collapsed!, animated: false)
|
||||
}
|
||||
|
||||
func updateStatusState(status: StatusMO) {
|
||||
|
|
Loading…
Reference in New Issue