forked from shadowfacts/Tusker
parent
dfc8234908
commit
6e5e0c3bb5
|
@ -81,6 +81,7 @@ public class DraftsPersistentContainer: NSPersistentContainer {
|
|||
contentWarning: String,
|
||||
inReplyToID: String?,
|
||||
visibility: Visibility,
|
||||
language: String?,
|
||||
localOnly: Bool
|
||||
) -> Draft {
|
||||
let draft = Draft(context: viewContext)
|
||||
|
@ -92,6 +93,7 @@ public class DraftsPersistentContainer: NSPersistentContainer {
|
|||
draft.contentWarningEnabled = !contentWarning.isEmpty
|
||||
draft.inReplyToID = inReplyToID
|
||||
draft.visibility = visibility
|
||||
draft.language = language
|
||||
draft.localOnly = localOnly
|
||||
save()
|
||||
return draft
|
||||
|
|
|
@ -171,6 +171,10 @@ public class InstanceFeatures: ObservableObject {
|
|||
hasMastodonVersion(4, 2, 0)
|
||||
}
|
||||
|
||||
public var hasServerPreferences: Bool {
|
||||
hasMastodonVersion(2, 8, 0)
|
||||
}
|
||||
|
||||
public init() {
|
||||
}
|
||||
|
||||
|
|
|
@ -105,6 +105,20 @@ public class Client {
|
|||
return task
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func run<Result: Sendable>(_ request: Request<Result>) async throws -> (Result, Pagination?) {
|
||||
return try await withCheckedThrowingContinuation { continuation in
|
||||
run(request) { response in
|
||||
switch response {
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
case .success(let result, let pagination):
|
||||
continuation.resume(returning: (result, pagination))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func createURLRequest<Result>(request: Request<Result>) -> URLRequest? {
|
||||
guard var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: true) else { return nil }
|
||||
components.path = request.endpoint.path
|
||||
|
@ -225,6 +239,10 @@ public class Client {
|
|||
return Request<[Emoji]>(method: .get, path: "/api/v1/custom_emojis")
|
||||
}
|
||||
|
||||
public static func getPreferences() -> Request<Preferences> {
|
||||
return Request(method: .get, path: "/api/v1/preferences")
|
||||
}
|
||||
|
||||
// MARK: - Accounts
|
||||
public static func getAccount(id: String) -> Request<Account> {
|
||||
return Request<Account>(method: .get, path: "/api/v1/accounts/\(id)")
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
//
|
||||
// Preferences.swift
|
||||
// Pachyderm
|
||||
//
|
||||
// Created by Shadowfacts on 10/26/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct Preferences: Codable, Sendable {
|
||||
public let postingDefaultVisibility: Visibility
|
||||
public let postingDefaultSensitive: Bool
|
||||
public let postingDefaultLanguage: String
|
||||
public let readingExpandMedia: ExpandMedia
|
||||
public let readingExpandSpoilers: Bool
|
||||
public let readingAutoplayGifs: Bool
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case postingDefaultVisibility = "posting:default:visibility"
|
||||
case postingDefaultSensitive = "posting:default:sensitive"
|
||||
case postingDefaultLanguage = "posting:default:language"
|
||||
case readingExpandMedia = "reading:expand:media"
|
||||
case readingExpandSpoilers = "reading:expand:spoilers"
|
||||
case readingAutoplayGifs = "reading:autoplay:gifs"
|
||||
}
|
||||
}
|
||||
|
||||
extension Preferences {
|
||||
public enum ExpandMedia: String, Codable, Sendable {
|
||||
case `default`
|
||||
case always = "show_all"
|
||||
case never = "hide_all"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
//
|
||||
// PostVisibility.swift
|
||||
// TuskerPreferences
|
||||
//
|
||||
// Created by Shadowfacts on 10/26/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Pachyderm
|
||||
|
||||
public enum PostVisibility: Codable, Hashable, CaseIterable {
|
||||
case serverDefault
|
||||
case visibility(Visibility)
|
||||
|
||||
public static var allCases: [PostVisibility] = [.serverDefault] + Visibility.allCases.map { .visibility($0) }
|
||||
|
||||
public func resolved(withServerDefault serverDefault: Visibility?) -> Visibility {
|
||||
switch self {
|
||||
case .serverDefault:
|
||||
// If the server doesn't have a default visibility preference, we fallback to public.
|
||||
// This isn't ideal, but I don't want to add a separate preference for "Default Post Visibility Fallback" :/
|
||||
serverDefault ?? .public
|
||||
case .visibility(let vis):
|
||||
vis
|
||||
}
|
||||
}
|
||||
|
||||
public func resolved(withServerDefault serverDefault: () async -> Visibility?) async -> Visibility {
|
||||
switch self {
|
||||
case .serverDefault:
|
||||
await serverDefault() ?? .public
|
||||
case .visibility(let vis):
|
||||
vis
|
||||
}
|
||||
}
|
||||
|
||||
public var displayName: String {
|
||||
switch self {
|
||||
case .serverDefault:
|
||||
return "Account Default"
|
||||
case .visibility(let vis):
|
||||
return vis.displayName
|
||||
}
|
||||
}
|
||||
|
||||
public var imageName: String? {
|
||||
switch self {
|
||||
case .serverDefault:
|
||||
return nil
|
||||
case .visibility(let vis):
|
||||
return vis.imageName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum ReplyVisibility: Codable, Hashable, CaseIterable {
|
||||
case sameAsPost
|
||||
case visibility(Visibility)
|
||||
|
||||
public static var allCases: [ReplyVisibility] = [.sameAsPost] + Visibility.allCases.map { .visibility($0) }
|
||||
|
||||
public func resolved(withServerDefault serverDefault: Visibility?) -> Visibility {
|
||||
switch self {
|
||||
case .sameAsPost:
|
||||
Preferences.shared.defaultPostVisibility.resolved(withServerDefault: serverDefault)
|
||||
case .visibility(let vis):
|
||||
vis
|
||||
}
|
||||
}
|
||||
|
||||
public func resolved(withServerDefault serverDefault: () async -> Visibility?) async -> Visibility {
|
||||
switch self {
|
||||
case .sameAsPost:
|
||||
await Preferences.shared.defaultPostVisibility.resolved(withServerDefault: serverDefault)
|
||||
case .visibility(let vis):
|
||||
vis
|
||||
}
|
||||
}
|
||||
|
||||
public var displayName: String {
|
||||
switch self {
|
||||
case .sameAsPost:
|
||||
return "Same as Default"
|
||||
case .visibility(let vis):
|
||||
return vis.displayName
|
||||
}
|
||||
}
|
||||
|
||||
public var imageName: String? {
|
||||
switch self {
|
||||
case .sameAsPost:
|
||||
return nil
|
||||
case .visibility(let vis):
|
||||
return vis.imageName
|
||||
}
|
||||
}
|
||||
}
|
|
@ -63,7 +63,11 @@ public final class Preferences: Codable, ObservableObject {
|
|||
self.widescreenNavigationMode = try container.decodeIfPresent(WidescreenNavigationMode.self, forKey: .widescreenNavigationMode) ?? Self.defaultWidescreenNavigationMode
|
||||
self.underlineTextLinks = try container.decodeIfPresent(Bool.self, forKey: .underlineTextLinks) ?? false
|
||||
|
||||
self.defaultPostVisibility = try container.decode(Visibility.self, forKey: .defaultPostVisibility)
|
||||
if let existing = try? container.decode(Visibility.self, forKey: .defaultPostVisibility) {
|
||||
self.defaultPostVisibility = .visibility(existing)
|
||||
} else {
|
||||
self.defaultPostVisibility = try container.decode(PostVisibility.self, forKey: .defaultPostVisibility)
|
||||
}
|
||||
self.defaultReplyVisibility = try container.decodeIfPresent(ReplyVisibility.self, forKey: .defaultReplyVisibility) ?? .sameAsPost
|
||||
self.requireAttachmentDescriptions = try container.decode(Bool.self, forKey: .requireAttachmentDescriptions)
|
||||
self.contentWarningCopyMode = try container.decode(ContentWarningCopyMode.self, forKey: .contentWarningCopyMode)
|
||||
|
@ -180,7 +184,7 @@ public final class Preferences: Codable, ObservableObject {
|
|||
@Published public var underlineTextLinks = false
|
||||
|
||||
// MARK: Composing
|
||||
@Published public var defaultPostVisibility = Visibility.public
|
||||
@Published public var defaultPostVisibility = PostVisibility.serverDefault
|
||||
@Published public var defaultReplyVisibility = ReplyVisibility.sameAsPost
|
||||
@Published public var requireAttachmentDescriptions = false
|
||||
@Published public var contentWarningCopyMode = ContentWarningCopyMode.asIs
|
||||
|
@ -292,42 +296,6 @@ public final class Preferences: Codable, ObservableObject {
|
|||
|
||||
}
|
||||
|
||||
extension Preferences {
|
||||
public enum ReplyVisibility: Codable, Hashable, CaseIterable {
|
||||
case sameAsPost
|
||||
case visibility(Visibility)
|
||||
|
||||
public static var allCases: [Preferences.ReplyVisibility] = [.sameAsPost] + Visibility.allCases.map { .visibility($0) }
|
||||
|
||||
public var resolved: Visibility {
|
||||
switch self {
|
||||
case .sameAsPost:
|
||||
return Preferences.shared.defaultPostVisibility
|
||||
case .visibility(let vis):
|
||||
return vis
|
||||
}
|
||||
}
|
||||
|
||||
public var displayName: String {
|
||||
switch self {
|
||||
case .sameAsPost:
|
||||
return "Same as Default"
|
||||
case .visibility(let vis):
|
||||
return vis.displayName
|
||||
}
|
||||
}
|
||||
|
||||
public var imageName: String? {
|
||||
switch self {
|
||||
case .sameAsPost:
|
||||
return nil
|
||||
case .visibility(let vis):
|
||||
return vis.imageName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Preferences {
|
||||
public enum AttachmentBlurMode: Codable, Hashable, CaseIterable {
|
||||
case useStatusSetting
|
||||
|
|
|
@ -12,6 +12,7 @@ import ComposeUI
|
|||
import UniformTypeIdentifiers
|
||||
import TuskerPreferences
|
||||
import Combine
|
||||
import Pachyderm
|
||||
|
||||
class ShareViewController: UIViewController {
|
||||
|
||||
|
@ -50,21 +51,26 @@ class ShareViewController: UIViewController {
|
|||
}
|
||||
|
||||
private func createDraft(account: UserAccountInfo) async -> Draft {
|
||||
let (text, attachments) = await getDraftConfigurationFromExtensionContext()
|
||||
async let (text, attachments) = getDraftConfigurationFromExtensionContext()
|
||||
|
||||
// TODO: I really don't like that there's a network request in the hot path here, but we don't have easy access to AccountPreferences :/
|
||||
let serverPrefs = try? await Client(baseURL: account.instanceURL, accessToken: account.accessToken).run(Client.getPreferences()).0
|
||||
let visibility = Preferences.shared.defaultPostVisibility.resolved(withServerDefault: serverPrefs?.postingDefaultVisibility)
|
||||
|
||||
let draft = DraftsPersistentContainer.shared.createDraft(
|
||||
accountID: account.id,
|
||||
text: text,
|
||||
text: await text,
|
||||
contentWarning: "",
|
||||
inReplyToID: nil,
|
||||
visibility: Preferences.shared.defaultPostVisibility,
|
||||
visibility: visibility,
|
||||
language: serverPrefs?.postingDefaultLanguage,
|
||||
localOnly: false
|
||||
)
|
||||
|
||||
for attachment in attachments {
|
||||
for attachment in await attachments {
|
||||
DraftsPersistentContainer.shared.viewContext.insert(attachment)
|
||||
}
|
||||
draft.draftAttachments = attachments
|
||||
draft.draftAttachments = await attachments
|
||||
|
||||
return draft
|
||||
}
|
||||
|
|
|
@ -193,6 +193,8 @@ class MastodonController: ObservableObject {
|
|||
|
||||
@MainActor
|
||||
func initialize() {
|
||||
precondition(!transient, "Cannot initialize transient MastodonController")
|
||||
|
||||
// we want this to happen immediately, and synchronously so that the filters (which don't change that often)
|
||||
// are available when Filterers are constructed
|
||||
loadCachedFilters()
|
||||
|
@ -217,6 +219,7 @@ class MastodonController: ObservableObject {
|
|||
|
||||
loadLists()
|
||||
_ = await loadFilters()
|
||||
await loadServerPreferences()
|
||||
} catch {
|
||||
Logging.general.error("MastodonController initialization failed: \(String(describing: error))")
|
||||
}
|
||||
|
@ -358,6 +361,17 @@ class MastodonController: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
// MainActor because the accountPreferences instance is bound to the view context
|
||||
@MainActor
|
||||
private func loadServerPreferences() async {
|
||||
guard instanceFeatures.hasServerPreferences,
|
||||
let (prefs, _) = try? await run(Client.getPreferences()) else {
|
||||
return
|
||||
}
|
||||
accountPreferences!.serverDefaultLanguage = prefs.postingDefaultLanguage
|
||||
accountPreferences!.serverDefaultVisibility = prefs.postingDefaultVisibility
|
||||
}
|
||||
|
||||
private func updateActiveInstance(from instance: Instance) {
|
||||
persistentContainer.performBackgroundTask { context in
|
||||
if let existing = try? context.fetch(ActiveInstance.fetchRequest()).first {
|
||||
|
@ -519,7 +533,11 @@ class MastodonController: ObservableObject {
|
|||
func createDraft(inReplyToID: String? = nil, mentioningAcct: String? = nil, text: String? = nil) -> Draft {
|
||||
var acctsToMention = [String]()
|
||||
|
||||
var visibility = inReplyToID != nil ? Preferences.shared.defaultReplyVisibility.resolved : Preferences.shared.defaultPostVisibility
|
||||
var visibility = if inReplyToID != nil {
|
||||
Preferences.shared.defaultReplyVisibility.resolved(withServerDefault: accountPreferences!.serverDefaultVisibility)
|
||||
} else {
|
||||
Preferences.shared.defaultPostVisibility.resolved(withServerDefault: accountPreferences!.serverDefaultVisibility)
|
||||
}
|
||||
var localOnly = false
|
||||
var contentWarning = ""
|
||||
|
||||
|
@ -559,6 +577,7 @@ class MastodonController: ObservableObject {
|
|||
contentWarning: contentWarning,
|
||||
inReplyToID: inReplyToID,
|
||||
visibility: visibility,
|
||||
language: accountPreferences!.serverDefaultLanguage,
|
||||
localOnly: localOnly
|
||||
)
|
||||
}
|
||||
|
|
|
@ -24,10 +24,21 @@ public final class AccountPreferences: NSManagedObject {
|
|||
@NSManaged public var accountID: String
|
||||
@NSManaged var createdAt: Date
|
||||
@NSManaged var pinnedTimelinesData: Data?
|
||||
@NSManaged var serverDefaultLanguage: String?
|
||||
@NSManaged private var serverDefaultVisibilityString: String?
|
||||
|
||||
@LazilyDecoding(from: \AccountPreferences.pinnedTimelinesData, fallback: AccountPreferences.defaultPinnedTimelines)
|
||||
var pinnedTimelines: [PinnedTimeline]
|
||||
|
||||
var serverDefaultVisibility: Visibility? {
|
||||
get {
|
||||
serverDefaultVisibilityString.flatMap(Visibility.init(rawValue:))
|
||||
}
|
||||
set {
|
||||
serverDefaultVisibilityString = newValue?.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
static func `default`(account: UserAccountInfo, context: NSManagedObjectContext) -> AccountPreferences {
|
||||
let prefs = AccountPreferences(context: context)
|
||||
prefs.accountID = account.id
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21754" systemVersion="22D49" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22225" systemVersion="23A344" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Account" representedClassName="AccountMO" syncable="YES">
|
||||
<attribute name="acct" attributeType="String"/>
|
||||
<attribute name="active" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
|
@ -33,6 +33,8 @@
|
|||
<attribute name="accountID" optional="YES" attributeType="String"/>
|
||||
<attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="pinnedTimelinesData" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="serverDefaultLanguage" optional="YES" attributeType="String"/>
|
||||
<attribute name="serverDefaultVisibilityString" optional="YES" attributeType="String"/>
|
||||
</entity>
|
||||
<entity name="ActiveInstance" representedClassName="ActiveInstance" syncable="YES">
|
||||
<attribute name="configurationData" optional="YES" attributeType="Binary"/>
|
||||
|
@ -155,4 +157,4 @@
|
|||
<memberEntity name="Account"/>
|
||||
<memberEntity name="ActiveInstance"/>
|
||||
</configuration>
|
||||
</model>
|
||||
</model>
|
||||
|
|
|
@ -28,9 +28,11 @@ struct ComposingPrefsView: View {
|
|||
var visibilitySection: some View {
|
||||
Section {
|
||||
Picker(selection: $preferences.defaultPostVisibility, label: Text("Default Visibility")) {
|
||||
ForEach(Visibility.allCases, id: \.self) { visibility in
|
||||
ForEach(PostVisibility.allCases, id: \.self) { visibility in
|
||||
HStack {
|
||||
Image(systemName: visibility.imageName)
|
||||
if let imageName = visibility.imageName {
|
||||
Image(systemName: imageName)
|
||||
}
|
||||
Text(visibility.displayName)
|
||||
}
|
||||
.tag(visibility)
|
||||
|
@ -38,7 +40,7 @@ struct ComposingPrefsView: View {
|
|||
// navbar title on the ForEach is currently incorrectly applied when the picker is not expanded, see FB6838291
|
||||
}
|
||||
Picker(selection: $preferences.defaultReplyVisibility, label: Text("Reply Visibility")) {
|
||||
ForEach(Preferences.ReplyVisibility.allCases, id: \.self) { visibility in
|
||||
ForEach(ReplyVisibility.allCases, id: \.self) { visibility in
|
||||
HStack {
|
||||
if let imageName = visibility.imageName {
|
||||
Image(systemName: imageName)
|
||||
|
|
Loading…
Reference in New Issue