61 lines
1.7 KiB
Swift
61 lines
1.7 KiB
Swift
|
|
import Foundation
|
||
|
|
import Combine
|
||
|
|
|
||
|
|
/// View model for the presence experience.
|
||
|
|
///
|
||
|
|
/// Exposes the state the UI needs to render the "am I present?" moment, the
|
||
|
|
/// radius tier picker, and the live feed of encounters (silhouettes).
|
||
|
|
@MainActor
|
||
|
|
final class PresenceViewModel: ObservableObject {
|
||
|
|
|
||
|
|
@Published var isPresent: Bool = false
|
||
|
|
@Published var radiusTier: DistanceTier = .rightHere
|
||
|
|
@Published private(set) var activeEngines: Set<ProximityEngine> = []
|
||
|
|
@Published private(set) var encounters: [Encounter] = []
|
||
|
|
@Published private(set) var lastScan: Date?
|
||
|
|
|
||
|
|
private let engine: PresenceEngine
|
||
|
|
private var cancellables = Set<AnyCancellable>()
|
||
|
|
|
||
|
|
init(engine: PresenceEngine) {
|
||
|
|
self.engine = engine
|
||
|
|
|
||
|
|
engine.$isPresent
|
||
|
|
.assign(to: &$isPresent)
|
||
|
|
engine.$radiusTier
|
||
|
|
.assign(to: &$radiusTier)
|
||
|
|
engine.$activeEngines
|
||
|
|
.assign(to: &$activeEngines)
|
||
|
|
engine.$recentEncounters
|
||
|
|
.assign(to: &$encounters)
|
||
|
|
engine.$lastScan
|
||
|
|
.assign(to: &$lastScan)
|
||
|
|
}
|
||
|
|
|
||
|
|
// MARK: - User actions
|
||
|
|
|
||
|
|
func togglePresence() {
|
||
|
|
isPresent.toggle()
|
||
|
|
Task { await engine.setPresent(isPresent) }
|
||
|
|
}
|
||
|
|
|
||
|
|
func setRadius(_ tier: DistanceTier) {
|
||
|
|
radiusTier = tier
|
||
|
|
Task { await engine.setRadiusTier(tier) }
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Send a "wave" to a silhouette — the first step toward mutual reveal.
|
||
|
|
func wave(to encounter: Encounter) {
|
||
|
|
// In production: POST /v1/wave with the encounter token.
|
||
|
|
Task {
|
||
|
|
await engine.wave(to: encounter)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func block(_ encounter: Encounter) {
|
||
|
|
Task {
|
||
|
|
await engine.block(encounter)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|