Compare commits

...

4 Commits

21 changed files with 228 additions and 104 deletions

View File

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

View File

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

View File

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

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> {
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),

View File

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

View File

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

View File

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

View File

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

View File

@ -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 */,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,7 +14,9 @@ struct MediaPrefsView: View {
var body: some View {
List {
viewingSection
}.listStyle(GroupedListStyle()).navigationBarTitle("Media")
}
.insetOrGroupedListStyle()
.navigationBarTitle("Media")
}
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
// 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 {

View File

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

View File

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

View File

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