All preference toggles showed as off regardless of server state, and saving notificationBadge 400'd with "notificationBadge: Invalid Input" — the guessed wire keys/values from the earlier speculative commit were wrong. Fixed against a live network capture of Outline's own web app toggling every one of these settings: - separateEditing -> seamlessEdit, and inverted (seamlessEdit is separate editing's negation, confirmed by toggling it live) - showCommentMarker -> commentsInGutter - smartText -> enableSmartText - notificationBadge values -> "disabled"/"indicator"/"count", not the guessed "none"/"unread"/"all" - rememberLastPath/useCursorPointer/codeBlockLineNumbers were already correct Also preserves fullWidthDocuments (a real preference this app has no UI for) on round-trip, since the client always sends the whole preferences object back on save — dropping an unrecognized key during decode would otherwise silently clear it the next time any toggle here gets saved.
110 lines
5.1 KiB
Swift
110 lines
5.1 KiB
Swift
import Foundation
|
|
|
|
/// `User.preferences` — a free-form JSON blob on Outline's own `User` row,
|
|
/// not a fixed-shape API resource. Wire key names below are confirmed
|
|
/// against a live server's own web app traffic (captured toggling every
|
|
/// Preferences setting one at a time), not guessed — see the `CodingKeys`
|
|
/// mapping for the two that don't match this struct's own property names.
|
|
///
|
|
/// The server also validates `preferences` against a known key allowlist
|
|
/// and rejects the whole `users.update` call (not just the bad field) if
|
|
/// any key it doesn't recognize is present — confirmed live via a
|
|
/// `"notificationBadge: Invalid Input"` error when an earlier, wrong value
|
|
/// was sent. That's also why `fullWidthDocuments` is kept here even though
|
|
/// this app has no UI for it yet: this app always sends the *whole*
|
|
/// preferences object back on every save (see
|
|
/// `UpdateUserPreferencesRequest`), so silently dropping an unknown key
|
|
/// during decode would permanently clear it the next time any other
|
|
/// preference here gets saved.
|
|
public struct OutlineUserPreferences: Codable, Hashable, Sendable {
|
|
public var rememberLastPath: Bool?
|
|
/// App-facing polarity: `true` means "separate editing mode is on" —
|
|
/// the opposite of the wire's own `seamlessEdit` (seamless editing and
|
|
/// separate editing modes are each other's negation), inverted in
|
|
/// `init(from:)`/`encode(to:)` so nothing outside this file has to
|
|
/// remember that.
|
|
public var separateEditing: Bool?
|
|
public var useCursorPointer: Bool?
|
|
public var codeBlockLineNumbers: Bool?
|
|
public var showCommentMarker: Bool?
|
|
public var smartText: Bool?
|
|
/// One of `NotificationBadgeStyle`'s raw values.
|
|
public var notificationBadge: String?
|
|
/// No UI in this app yet — preserved purely so saving any other
|
|
/// preference here doesn't clobber it. See the type doc comment.
|
|
public var fullWidthDocuments: Bool?
|
|
|
|
public init(
|
|
rememberLastPath: Bool? = nil,
|
|
separateEditing: Bool? = nil,
|
|
useCursorPointer: Bool? = nil,
|
|
codeBlockLineNumbers: Bool? = nil,
|
|
showCommentMarker: Bool? = nil,
|
|
smartText: Bool? = nil,
|
|
notificationBadge: String? = nil,
|
|
fullWidthDocuments: Bool? = nil
|
|
) {
|
|
self.rememberLastPath = rememberLastPath
|
|
self.separateEditing = separateEditing
|
|
self.useCursorPointer = useCursorPointer
|
|
self.codeBlockLineNumbers = codeBlockLineNumbers
|
|
self.showCommentMarker = showCommentMarker
|
|
self.smartText = smartText
|
|
self.notificationBadge = notificationBadge
|
|
self.fullWidthDocuments = fullWidthDocuments
|
|
}
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case rememberLastPath
|
|
case seamlessEdit
|
|
case useCursorPointer
|
|
case codeBlockLineNumbers
|
|
case commentsInGutter
|
|
case enableSmartText
|
|
case notificationBadge
|
|
case fullWidthDocuments
|
|
}
|
|
|
|
public init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
rememberLastPath = try container.decodeIfPresent(Bool.self, forKey: .rememberLastPath)
|
|
separateEditing = try container.decodeIfPresent(Bool.self, forKey: .seamlessEdit).map { !$0 }
|
|
useCursorPointer = try container.decodeIfPresent(Bool.self, forKey: .useCursorPointer)
|
|
codeBlockLineNumbers = try container.decodeIfPresent(Bool.self, forKey: .codeBlockLineNumbers)
|
|
showCommentMarker = try container.decodeIfPresent(Bool.self, forKey: .commentsInGutter)
|
|
smartText = try container.decodeIfPresent(Bool.self, forKey: .enableSmartText)
|
|
notificationBadge = try container.decodeIfPresent(String.self, forKey: .notificationBadge)
|
|
fullWidthDocuments = try container.decodeIfPresent(Bool.self, forKey: .fullWidthDocuments)
|
|
}
|
|
|
|
public func encode(to encoder: Encoder) throws {
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
try container.encodeIfPresent(rememberLastPath, forKey: .rememberLastPath)
|
|
try container.encodeIfPresent(separateEditing.map { !$0 }, forKey: .seamlessEdit)
|
|
try container.encodeIfPresent(useCursorPointer, forKey: .useCursorPointer)
|
|
try container.encodeIfPresent(codeBlockLineNumbers, forKey: .codeBlockLineNumbers)
|
|
try container.encodeIfPresent(showCommentMarker, forKey: .commentsInGutter)
|
|
try container.encodeIfPresent(smartText, forKey: .enableSmartText)
|
|
try container.encodeIfPresent(notificationBadge, forKey: .notificationBadge)
|
|
try container.encodeIfPresent(fullWidthDocuments, forKey: .fullWidthDocuments)
|
|
}
|
|
}
|
|
|
|
/// App-icon unread indicator style. Wire values confirmed live (captured
|
|
/// setting all three from Outline's own web app).
|
|
public enum NotificationBadgeStyle: String, CaseIterable, Identifiable, Sendable {
|
|
case none = "disabled"
|
|
case unreadIndicator = "indicator"
|
|
case unreadCount = "count"
|
|
|
|
public var id: String { rawValue }
|
|
|
|
public var label: String {
|
|
switch self {
|
|
case .none: return "None"
|
|
case .unreadIndicator: return "Unread Indicator"
|
|
case .unreadCount: return "Unread Count"
|
|
}
|
|
}
|
|
}
|