SessionMonitor.swift (2382B)
1 import Foundation 2 3 /// Receiver-side data for the catch-up banner that fires when the user opens a 4 /// game ("Alice added 12 letters; Bob cleared 5"). A thin wrapper over 5 /// `GameStore.recentChanges`: it reduces the per-author counts that the border 6 /// highlights are also drawn from, and hydrates each peer's display name. Both 7 /// surfaces read the one `viewedAt` cutoff (`GameViewedStore`), so the banner 8 /// and the borders can never describe different change sets. 9 @MainActor 10 final class SessionMonitor { 11 private let store: GameStore 12 private let localAuthorIDProvider: () -> String? 13 14 init(store: GameStore, localAuthorIDProvider: @escaping () -> String?) { 15 self.store = store 16 self.localAuthorIDProvider = localAuthorIDProvider 17 } 18 19 /// One author's worth of unseen activity, surfaced to the puzzle's 20 /// announcement banner when the user opens a game. Hydrated with the 21 /// player name so the caller can format a body string without a second 22 /// Core Data round-trip. 23 struct SessionSummary: Equatable, Sendable { 24 let authorID: String 25 let playerName: String 26 let added: Int 27 let cleared: Int 28 } 29 30 /// Per-peer summaries of what changed since `since` (this device's last-view 31 /// baseline), ordered deterministically by author so the banner text is 32 /// stable. Empty when nothing changed — including the first-ever open, where 33 /// `since` is the caller's baseline and there is simply no prior view to diff 34 /// against. Read-only; the baseline advances on leave, never here. 35 func summaries(for gameID: UUID, since: Date) -> [SessionSummary] { 36 let counts = store.recentChanges(forGame: gameID, since: since).counts 37 guard !counts.isEmpty else { return [] } 38 return counts.keys.sorted().map { authorID in 39 let count = counts[authorID] ?? RecentChanges.Count(added: 0, cleared: 0) 40 return SessionSummary( 41 authorID: authorID, 42 // A nickname the user assigned via Rename wins over the peer's 43 // own published name, matching every other surface. 44 playerName: store.friendNickname(for: authorID) 45 ?? store.playerName(for: gameID, by: authorID), 46 added: count.added, 47 cleared: count.cleared 48 ) 49 } 50 } 51 }