crossmate

A collaborative crossword app for iOS
Log | Files | Refs | LICENSE

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 }