RecentChanges.swift (3022B)
1 import Foundation 2 3 /// Reduces the per-cell letter-change ledger (`PeerChange` rows) into the two 4 /// shapes the receiver's "changed while you were away" surfaces need: the 5 /// per-cell author map that draws the border highlights, and the per-author net 6 /// counts that fill the catch-up banner. Both come from one pass over one cutoff 7 /// so the borders and the banner can never describe different change sets. 8 /// 9 /// The ledger already records *when each cell's letter last changed* (see 10 /// `PeerChange`), so a peer's check — which re-stamps `updatedAt` without 11 /// touching the letter — never advanced an entry's `changedAt` and so never 12 /// appears here. We therefore gate purely on `changedAt > since`; no 13 /// reconstruction of the grid at the cutoff is needed. 14 /// 15 /// Each flagged cell is attributed to the author the ledger recorded for its 16 /// current letter (preserved through checks; a clear credits whoever emptied it). 17 enum RecentChanges { 18 /// A peer's changes since a cutoff, in the two shapes the receiver surfaces 19 /// need: the per-cell author map that draws the border highlights, and the 20 /// per-author net counts that fill the catch-up banner. 21 struct Changes: Equatable { 22 /// Changed positions mapped to the author who wrote the current letter. 23 var cells: [GridPosition: String] 24 /// Per-author net counts: `added` for cells whose current letter is 25 /// non-empty, `cleared` for cells emptied since the cutoff. 26 var counts: [String: Count] 27 28 static let empty = Changes(cells: [:], counts: [:]) 29 } 30 31 struct Count: Equatable { 32 var added: Int 33 var cleared: Int 34 } 35 36 /// Ledger rows whose letter last changed strictly after `since`, each mapped 37 /// to the recorded author, plus the per-author add/clear tally over the same 38 /// set (`excluded` — typically the local user — is dropped, as are rows with 39 /// no recorded author). 40 static func changes( 41 in entries: [PeerChange], 42 since: Date, 43 excludingAuthor excluded: String 44 ) -> Changes { 45 var cells: [GridPosition: String] = [:] 46 var counts: [String: Count] = [:] 47 for entry in entries { 48 guard entry.changedAt > since else { continue } 49 guard let writer = entry.authorID, writer != excluded else { continue } 50 cells[entry.position] = writer 51 var count = counts[writer] ?? Count(added: 0, cleared: 0) 52 if entry.letter.isEmpty { 53 count.cleared += 1 54 } else { 55 count.added += 1 56 } 57 counts[writer] = count 58 } 59 return Changes(cells: cells, counts: counts) 60 } 61 62 /// The cell→author map alone, for the border highlights. 63 static func changedCells( 64 in entries: [PeerChange], 65 since: Date, 66 excludingAuthor excluded: String 67 ) -> [GridPosition: String] { 68 changes(in: entries, since: since, excludingAuthor: excluded).cells 69 } 70 }