PuzzleNotificationText.swift (3251B)
1 import Foundation 2 3 enum PuzzleNotificationText { 4 static func title(_ title: String, publisher: String?, date: Date?) -> String { 5 let subtitle = subtitle(publisher: publisher, date: date) 6 guard let subtitle else { return title } 7 return "\(title) – \(subtitle)" 8 } 9 10 static func title(for entity: GameEntity?) -> String { 11 guard let entity else { return "" } 12 return title( 13 entity.title ?? "", 14 publisher: entity.cachedPublisher, 15 date: entity.cachedPuzzleDate 16 ) 17 } 18 19 /// Body for a session-end push, addressed to a single recipient, 20 /// describing what the peer did since that recipient last looked (entries 21 /// in the peer's journal newer than the recipient's last-known 22 /// `Player.readAt`): net letter `fills` / `clears`, and the number of 23 /// `checks` / `reveals` *gestures* run. When every count is zero the 24 /// recipient still gets the push as a presence signal ("stopped solving") 25 /// — the session end is worth surfacing even with nothing unseen — but the 26 /// payload's zero counts keep it from bumping the badge. 27 static func pauseBody( 28 playerName: String, 29 puzzleTitle: String, 30 fills: Int, 31 clears: Int, 32 checks: Int, 33 reveals: Int 34 ) -> String { 35 let resolvedName = playerName.isEmpty ? "A player" : playerName 36 let puzzleSuffix = puzzleTitle.isEmpty 37 ? "the puzzle" 38 : "the puzzle '\(puzzleTitle)'" 39 40 func letters(_ n: Int) -> String { "\(n) \(n == 1 ? "letter" : "letters")" } 41 42 var clauses: [String] = [] 43 if fills > 0 { clauses.append("filled \(letters(fills))") } 44 if clears > 0 { clauses.append("cleared \(letters(clears))") } 45 // Checks and reveals are help gestures; fold them into one "ran …" 46 // clause so the sentence doesn't repeat the verb. 47 var help: [String] = [] 48 if checks > 0 { help.append("\(checks) \(checks == 1 ? "check" : "checks")") } 49 if reveals > 0 { help.append("\(reveals) \(reveals == 1 ? "reveal" : "reveals")") } 50 if !help.isEmpty { clauses.append("ran \(joinList(help))") } 51 52 if clauses.isEmpty { 53 return "\(resolvedName) stopped solving \(puzzleSuffix)." 54 } 55 return "\(resolvedName) \(joinList(clauses)) in \(puzzleSuffix)" 56 } 57 58 /// Joins clauses into prose: "a", "a and b", or "a, b and c". 59 private static func joinList(_ parts: [String]) -> String { 60 switch parts.count { 61 case 0: return "" 62 case 1: return parts[0] 63 case 2: return "\(parts[0]) and \(parts[1])" 64 default: return "\(parts.dropLast().joined(separator: ", ")) and \(parts[parts.count - 1])" 65 } 66 } 67 68 private static func subtitle(publisher: String?, date: Date?) -> String? { 69 let formattedDate = date?.formatted(date: .long, time: .omitted) 70 if let publisher, !publisher.isEmpty, let formattedDate { 71 return "\(publisher) · \(formattedDate)" 72 } 73 if let formattedDate { 74 return formattedDate 75 } 76 if let publisher, !publisher.isEmpty { 77 return publisher 78 } 79 return nil 80 } 81 }