Mirrors the existing name/avatar/id fields — Settings' Preferences page needs somewhere to read current values from and applyUpdatedProfile already refreshes everything else on save. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
120 lines
4.8 KiB
Swift
120 lines
4.8 KiB
Swift
import Foundation
|
|
import Observation
|
|
import OutlineKit
|
|
|
|
@MainActor
|
|
@Observable
|
|
final class SessionStore {
|
|
private static let serverURLDefaultsKey = "outline.serverURL"
|
|
|
|
private let tokenStore: TokenStoring
|
|
private let defaults: UserDefaults
|
|
|
|
var isSignedIn: Bool
|
|
private(set) var userId: String?
|
|
var userName: String?
|
|
var userEmail: String?
|
|
var userAvatarURL: URL?
|
|
var userLanguage: String?
|
|
var userPreferences: OutlineUserPreferences?
|
|
var teamName: String?
|
|
var teamAvatarURL: URL?
|
|
private(set) var apiClient: OutlineAPIClient?
|
|
/// Same object as `apiClient` when the offline cache is available — kept
|
|
/// as a separately-typed reference so Settings can reach cache/sync-queue
|
|
/// specific methods (`flushPendingOperations`, `performFullSync`, etc.)
|
|
/// without downcasting the protocol-typed `apiClient` everywhere.
|
|
private(set) var cachingClient: CachingOutlineAPIClient?
|
|
let networkMonitor = NetworkMonitor()
|
|
/// `nil` only if the on-disk SwiftData store failed to open (e.g. disk
|
|
/// full) — in that case `apiClient` falls back to talking to the server
|
|
/// directly with no offline cache, rather than failing sign-in outright.
|
|
private let cacheStore: OfflineCacheStore?
|
|
|
|
var serverURL: URL? {
|
|
defaults.string(forKey: Self.serverURLDefaultsKey).flatMap(URL.init(string:))
|
|
}
|
|
|
|
init(tokenStore: TokenStoring = KeychainTokenStore(), defaults: UserDefaults = .standard) {
|
|
self.tokenStore = tokenStore
|
|
self.defaults = defaults
|
|
self.isSignedIn = (try? tokenStore.token()) != nil
|
|
self.cacheStore = (try? OfflineCacheStore.makeContainer()).map(OfflineCacheStore.init(modelContainer:))
|
|
|
|
if isSignedIn, let serverURL {
|
|
(apiClient, cachingClient) = Self.makeAPIClient(serverURL: serverURL, tokenStore: tokenStore, cache: cacheStore)
|
|
}
|
|
}
|
|
|
|
func signIn(serverURL: URL, user: OutlineUser, team: OutlineTeam) {
|
|
defaults.set(serverURL.absoluteString, forKey: Self.serverURLDefaultsKey)
|
|
(apiClient, cachingClient) = Self.makeAPIClient(serverURL: serverURL, tokenStore: tokenStore, cache: cacheStore)
|
|
apply(user: user, team: team, serverURL: serverURL)
|
|
isSignedIn = true
|
|
}
|
|
|
|
private static func makeAPIClient(
|
|
serverURL: URL,
|
|
tokenStore: TokenStoring,
|
|
cache: OfflineCacheStore?
|
|
) -> (OutlineAPIClient, CachingOutlineAPIClient?) {
|
|
let live = LiveOutlineAPIClient(
|
|
configuration: OutlineConfiguration(baseURL: serverURL),
|
|
tokenStore: tokenStore
|
|
)
|
|
guard let cache else { return (live, nil) }
|
|
let caching = CachingOutlineAPIClient(live: live, cache: cache)
|
|
return (caching, caching)
|
|
}
|
|
|
|
func signOut() {
|
|
try? tokenStore.clear()
|
|
defaults.removeObject(forKey: Self.serverURLDefaultsKey)
|
|
isSignedIn = false
|
|
userId = nil
|
|
userName = nil
|
|
userEmail = nil
|
|
userAvatarURL = nil
|
|
userLanguage = nil
|
|
userPreferences = nil
|
|
teamName = nil
|
|
teamAvatarURL = nil
|
|
apiClient = nil
|
|
cachingClient = nil
|
|
}
|
|
|
|
/// Re-fetches user/workspace name/logo on relaunch, when the token survived but this
|
|
/// in-memory state didn't.
|
|
func refreshTeamInfoIfNeeded() async {
|
|
guard isSignedIn, teamName == nil, let apiClient, let serverURL else { return }
|
|
guard let auth = try? await apiClient.authInfo() else { return }
|
|
apply(user: auth.user, team: auth.team, serverURL: serverURL)
|
|
}
|
|
|
|
/// Settings calls this after a successful name/avatar change so the
|
|
/// sidebar's account footer and everywhere else reading these reflect
|
|
/// it immediately, without waiting for the next `auth.info` refresh.
|
|
func applyUpdatedProfile(_ user: OutlineUser) {
|
|
guard let serverURL else { return }
|
|
userId = user.id
|
|
userName = user.name
|
|
userAvatarURL = user.avatarUrl.flatMap { URL(string: $0, relativeTo: serverURL)?.absoluteURL }
|
|
userLanguage = user.language
|
|
userPreferences = user.preferences
|
|
}
|
|
|
|
private func apply(user: OutlineUser, team: OutlineTeam, serverURL: URL) {
|
|
userId = user.id
|
|
userName = user.name
|
|
userEmail = user.email
|
|
// Outline can return either an absolute URL or a server-relative path
|
|
// (e.g. `/api/files.get?key=...`) for avatarUrl — resolve against the
|
|
// configured server so relative paths don't fail as "unsupported URL".
|
|
userAvatarURL = user.avatarUrl.flatMap { URL(string: $0, relativeTo: serverURL)?.absoluteURL }
|
|
userLanguage = user.language
|
|
userPreferences = user.preferences
|
|
teamName = team.name
|
|
teamAvatarURL = team.avatarUrl.flatMap { URL(string: $0, relativeTo: serverURL)?.absoluteURL }
|
|
}
|
|
}
|