~ knotless Search/
↑↓ move↵ open⇧↵ all results

grain.social/grain-ios

1 collaborator
0
1
main
61 lines · 2.5 KB Raw History
import Foundation

/// Namespacing for local state that belongs to one signed-in account.
///
/// Several accounts share a single app sandbox, so anything scoped to "the
/// signed-in user" — viewed stories, recent searches, cached feeds — has the
/// owning DID folded into its key. Without it, switching accounts would show
/// the previous account's cached feed and read state.
enum AccountScopedStorage {
    /// Non-sensitive mirror of `TokenStorage.activeDID`. The Keychain is the
    /// source of truth, but it's too slow to consult from the launch path — the
    /// feed cache is read before the first frame — and the DID is public data.
    static var activeAccountID: String? {
        get { StorageEnvironment.defaults.string(forKey: "activeAccountID") }
        set {
            if let newValue {
                StorageEnvironment.defaults.set(newValue, forKey: "activeAccountID")
            } else {
                StorageEnvironment.defaults.removeObject(forKey: "activeAccountID")
            }
        }
    }

    /// UserDefaults keys holding per-account state, unsuffixed.
    private static let scopedDefaultsKeys = [
        "viewedStoryUris",
        "viewedStoryAuthors",
        "viewedStoryPending",
        "recentSearchProfiles",
        "recentSearchText",
    ]

    /// Suffix `base` with the DID that owns it. A nil DID yields the bare key,
    /// which is also what pre-multi-account builds wrote.
    static func key(_ base: String, did: String?) -> String {
        guard let did else { return base }
        return "\(base)::\(did)"
    }

    /// Claim any unsuffixed state left by a pre-multi-account build for `did`.
    /// Runs once — after the move there's nothing left at the bare key.
    static func migrateLegacyState(to did: String) {
        let defaults = StorageEnvironment.defaults
        for base in scopedDefaultsKeys {
            guard let legacy = defaults.object(forKey: base) else { continue }
            if defaults.object(forKey: key(base, did: did)) == nil {
                defaults.set(legacy, forKey: key(base, did: did))
            }
            defaults.removeObject(forKey: base)
        }
        FeedCache.shared.adoptLegacyEntries(did: did)
    }

    /// Delete everything account-scoped for `did`, leaving other accounts alone.
    static func purge(did: String) {
        for base in scopedDefaultsKeys {
            StorageEnvironment.defaults.removeObject(forKey: key(base, did: did))
        }
        FeedCache.shared.purge(did: did)
    }
}