172 lines
5.5 KiB
Swift
172 lines
5.5 KiB
Swift
|
|
import Foundation
|
||
|
|
import Combine
|
||
|
|
|
||
|
|
/// Root application state. Owns the long-lived services and wires them together.
|
||
|
|
///
|
||
|
|
/// The central philosophy: **Presence is foreground-first.** The app is not a
|
||
|
|
/// passive background listener — it is an active instrument you hold while you
|
||
|
|
/// are out in the world. This is a feature, not a limitation.
|
||
|
|
@MainActor
|
||
|
|
final class AppState: ObservableObject {
|
||
|
|
|
||
|
|
// MARK: - Published state
|
||
|
|
|
||
|
|
/// Whether the user is currently "open to connection" and the app is
|
||
|
|
/// actively broadcasting + listening. This is the master switch.
|
||
|
|
@Published var isPresent: Bool = false
|
||
|
|
|
||
|
|
/// The user's chosen radius tier. Defaults to "right here" (UWB).
|
||
|
|
@Published var radiusTier: DistanceTier = .rightHere
|
||
|
|
|
||
|
|
/// The current proximity engine (UWB / BLE / beacon) in use.
|
||
|
|
@Published private(set) var activeEngine: ProximityEngine = .uwb
|
||
|
|
|
||
|
|
// MARK: - Debug / simulation state
|
||
|
|
#if DEBUG
|
||
|
|
/// Whether we're using the simulated adapter (no hardware).
|
||
|
|
@Published var isSimulated = true
|
||
|
|
/// When true, the simulated adapter emits nearby users on a timer.
|
||
|
|
@Published var autoBroadcast = false {
|
||
|
|
didSet { configureSimulation() }
|
||
|
|
}
|
||
|
|
let simulatedAdapter = SimulatedAdapter()
|
||
|
|
#endif
|
||
|
|
|
||
|
|
// MARK: - Services
|
||
|
|
|
||
|
|
let presenceEngine: PresenceEngine
|
||
|
|
let tokenManager: TokenManager
|
||
|
|
let cryptoManager: CryptoManager
|
||
|
|
let apiClient: APIClient
|
||
|
|
|
||
|
|
let authService: AuthService
|
||
|
|
let profileImportService: ProfileImportService
|
||
|
|
|
||
|
|
let graphStore: GraphStore
|
||
|
|
let realtime: RealtimeClient
|
||
|
|
let revealFlow: RevealFlow
|
||
|
|
let chatViewModel: ChatViewModel
|
||
|
|
|
||
|
|
let presenceViewModel: PresenceViewModel
|
||
|
|
|
||
|
|
private var cancellables = Set<AnyCancellable>()
|
||
|
|
|
||
|
|
init() {
|
||
|
|
let tokenManager = TokenManager()
|
||
|
|
let cryptoManager = CryptoManager()
|
||
|
|
|
||
|
|
#if DEBUG
|
||
|
|
// In the simulator, point at the local reference server.
|
||
|
|
let apiClient = APIClient.local()
|
||
|
|
#else
|
||
|
|
let apiClient = APIClient()
|
||
|
|
#endif
|
||
|
|
|
||
|
|
self.tokenManager = tokenManager
|
||
|
|
self.cryptoManager = cryptoManager
|
||
|
|
self.apiClient = apiClient
|
||
|
|
|
||
|
|
let authService = AuthService(apiClient: apiClient)
|
||
|
|
self.authService = authService
|
||
|
|
let profileImportService = ProfileImportService()
|
||
|
|
self.profileImportService = profileImportService
|
||
|
|
|
||
|
|
let graphStore = GraphStore()
|
||
|
|
self.graphStore = graphStore
|
||
|
|
|
||
|
|
let realtime = RealtimeClient()
|
||
|
|
self.realtime = realtime
|
||
|
|
|
||
|
|
let revealFlow = RevealFlow(
|
||
|
|
apiClient: apiClient,
|
||
|
|
cryptoManager: cryptoManager,
|
||
|
|
profileService: profileImportService,
|
||
|
|
graphStore: graphStore,
|
||
|
|
realtime: realtime
|
||
|
|
)
|
||
|
|
self.revealFlow = revealFlow
|
||
|
|
|
||
|
|
let chatViewModel = ChatViewModel(
|
||
|
|
apiClient: apiClient,
|
||
|
|
cryptoManager: cryptoManager,
|
||
|
|
graphStore: graphStore,
|
||
|
|
realtime: realtime
|
||
|
|
)
|
||
|
|
self.chatViewModel = chatViewModel
|
||
|
|
|
||
|
|
let presenceEngine: PresenceEngine
|
||
|
|
#if DEBUG
|
||
|
|
// In DEBUG (simulator), drive the engine with the simulated adapter
|
||
|
|
// so the whole interaction loop can be tested without hardware.
|
||
|
|
presenceEngine = PresenceEngine(
|
||
|
|
tokenManager: tokenManager,
|
||
|
|
cryptoManager: cryptoManager,
|
||
|
|
apiClient: apiClient,
|
||
|
|
uwbAdapter: simulatedAdapter,
|
||
|
|
bleAdapter: simulatedAdapter,
|
||
|
|
beaconAdapter: simulatedAdapter
|
||
|
|
)
|
||
|
|
#else
|
||
|
|
presenceEngine = PresenceEngine(
|
||
|
|
tokenManager: tokenManager,
|
||
|
|
cryptoManager: cryptoManager,
|
||
|
|
apiClient: apiClient
|
||
|
|
)
|
||
|
|
#endif
|
||
|
|
self.presenceEngine = presenceEngine
|
||
|
|
|
||
|
|
self.presenceViewModel = PresenceViewModel(presenceEngine: presenceEngine)
|
||
|
|
|
||
|
|
// When the user toggles presence, drive the engine and open the
|
||
|
|
// live socket so mutual waves and messages arrive in real time.
|
||
|
|
$isPresent
|
||
|
|
.sink { [weak presenceEngine, weak realtime] present in
|
||
|
|
Task {
|
||
|
|
await presenceEngine?.setPresent(present)
|
||
|
|
if present {
|
||
|
|
realtime?.connect(token: tokenManager.currentToken())
|
||
|
|
} else {
|
||
|
|
realtime?.disconnect()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
.store(in: &cancellables)
|
||
|
|
|
||
|
|
$radiusTier
|
||
|
|
.sink { [weak presenceEngine] tier in
|
||
|
|
Task { await presenceEngine?.setRadiusTier(tier) }
|
||
|
|
}
|
||
|
|
.store(in: &cancellables)
|
||
|
|
|
||
|
|
#if DEBUG
|
||
|
|
configureSimulation()
|
||
|
|
#endif
|
||
|
|
}
|
||
|
|
|
||
|
|
// MARK: - Debug / simulation
|
||
|
|
#if DEBUG
|
||
|
|
/// Forward the simulated adapter's events into the engine and set up
|
||
|
|
/// auto-broadcast when enabled.
|
||
|
|
private func configureSimulation() {
|
||
|
|
simulatedAdapter.autoBroadcast = autoBroadcast
|
||
|
|
// The engine already wired onEncounter/onBroadcast/onRegion to the
|
||
|
|
// simulated adapter at init. Auto-broadcast is handled internally.
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Used by the debug menu to simulate a single nearby user.
|
||
|
|
func simulateEncounter() {
|
||
|
|
simulatedAdapter.simulateEncounter(remoteToken: "sim-\(Int.random(in: 100_000...999_999))")
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Used by the debug menu to simulate a crowd.
|
||
|
|
func simulateCrowd() {
|
||
|
|
simulatedAdapter.simulateCrowd(count: 20)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Reconnect the realtime socket (e.g. after starting the local server).
|
||
|
|
func restartRealtime() {
|
||
|
|
realtime.connect(token: tokenManager.currentToken())
|
||
|
|
}
|
||
|
|
#endif
|
||
|
|
}
|