feat(ios): P1-B — W7 UI wave: deep links, timeline, quick-reply, diff, projects, session switcher, thumbnails, lock-screen push
T-iOS-22: DeepLinkRouter (full-field whitelist, cold-start stash, route(from:) for push-tap reuse), 21 tests T-iOS-24: TimelineSheet mirroring web render() order; disabled→empty-state; reuses AwayDigest onExpand T-iOS-25: QuickReply chips (built-ins mirror quick-reply.ts via KeyByteMap; visible iff live gate && !readOnly) T-iOS-27: read-only DiffScreen + App-layer DiffFetcher (RO no-Origin; APIClient fold-in noted for T-iOS-38 owner) T-iOS-26: Projects list/detail — grouping byte-identical to web group keys (prefs-shared collapse state), prefs-clobber defenses (no blind PUT on empty base; adopt server echo), claude\r bootstrap T-iOS-23: UnreadLedger + TitleSanitizer (SessionCore, +15 tests), lastOutputAt decode, list-boundary re-sanitize T-iOS-29: new-in-cwd (untrusted cwd, no bootstrap re-injection) + exited-session reopen; fixes stale-controller SwiftTerm view bug via .id(controller.id) T-iOS-28: offscreen SwiftTerm thumbnail pipeline (LRU 32, concurrency gate 2, 256KiB cap, grid clamp) T-iOS-21: PushRegistrar (WEBTERM_GATE category: Allow=.authenticationRequired, no .foreground) + NotificationActionHandler (whitelisted payload, token never persisted, bg-task-wrapped POST, 403 fallback) CRITICAL fix (verify-found boot crash): @Sendable literals on UN completion closures — MainActor-inherited closures trapped Swift 6 executor check on UN's background queue; boot re-verified (no new crash reports, permission prompt reached, privacy shade correctly covering during system alert) Verified: 261 pkg + 247 app + 10 integration tests green; 7/7 semantics checks; Owns audit clean
This commit is contained in:
44
ios/App/WebTerm/Wiring/UnreadWatermarkStore.swift
Normal file
44
ios/App/WebTerm/Wiring/UnreadWatermarkStore.swift
Normal file
@@ -0,0 +1,44 @@
|
||||
import Foundation
|
||||
|
||||
/// T-iOS-23 · Persistence seam for `SessionCore.UnreadLedger` watermarks.
|
||||
/// The ledger itself is persistence-agnostic (plan §7) — the App layer wires
|
||||
/// UserDefaults here. NON-SECRET UI state only (plan §5.3 split: Keychain =
|
||||
/// secrets, UserDefaults = prefs), same tier as `LastSessionStore`.
|
||||
protocol UnreadWatermarkStore: Sendable {
|
||||
func load() -> [UUID: Int]
|
||||
func save(_ watermarks: [UUID: Int])
|
||||
}
|
||||
|
||||
/// UserDefaults-backed implementation with an injectable suite for tests
|
||||
/// (mirrors `UserDefaultsLastSessionStore`). `@unchecked Sendable`:
|
||||
/// `UserDefaults` is documented thread-safe and this wrapper adds no mutable
|
||||
/// state of its own.
|
||||
struct UserDefaultsUnreadWatermarkStore: UnreadWatermarkStore, @unchecked Sendable {
|
||||
private static let key = "unreadWatermarks"
|
||||
|
||||
private let defaults: UserDefaults
|
||||
|
||||
init(defaults: UserDefaults = .standard) {
|
||||
self.defaults = defaults
|
||||
}
|
||||
|
||||
func load() -> [UUID: Int] {
|
||||
guard let raw = defaults.dictionary(forKey: Self.key) as? [String: Int] else {
|
||||
return [:]
|
||||
}
|
||||
// Stored data crosses a storage boundary — validate every key; garbage
|
||||
// is dropped, never trusted (two case-variant spellings of one UUID
|
||||
// collapse via max, so this can never crash on duplicates).
|
||||
let pairs = raw.compactMap { key, value in
|
||||
UUID(uuidString: key).map { ($0, value) }
|
||||
}
|
||||
return Dictionary(pairs, uniquingKeysWith: max)
|
||||
}
|
||||
|
||||
func save(_ watermarks: [UUID: Int]) {
|
||||
let plist = Dictionary(
|
||||
uniqueKeysWithValues: watermarks.map { ($0.key.uuidString, $0.value) }
|
||||
)
|
||||
defaults.set(plist, forKey: Self.key)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user