crossmate

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

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 }