crossmate

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

EngagementStore.swift (2117B)


      1 import Foundation
      2 import Observation
      3 
      4 struct EngagementSelectionUpdate: Codable, Equatable, Sendable {
      5     var gameID: UUID
      6     var authorID: String
      7     var deviceID: String
      8     var row: Int
      9     var col: Int
     10     var directionRaw: Int
     11     var updatedAt: Date
     12 
     13     init(
     14         gameID: UUID,
     15         authorID: String,
     16         deviceID: String,
     17         selection: PlayerSelection,
     18         updatedAt: Date = Date()
     19     ) {
     20         self.gameID = gameID
     21         self.authorID = authorID
     22         self.deviceID = deviceID
     23         self.row = selection.row
     24         self.col = selection.col
     25         self.directionRaw = selection.direction.rawValue
     26         self.updatedAt = updatedAt
     27     }
     28 
     29     var selection: PlayerSelection? {
     30         guard let direction = Puzzle.Direction(rawValue: directionRaw) else { return nil }
     31         return PlayerSelection(row: row, col: col, direction: direction)
     32     }
     33 }
     34 
     35 @MainActor
     36 @Observable
     37 final class EngagementStore {
     38     struct Entry: Equatable, Sendable {
     39         let authorID: String
     40         let row: Int
     41         let col: Int
     42         let direction: Puzzle.Direction
     43         let updatedAt: Date
     44     }
     45 
     46     private var selectionsByGame: [UUID: [String: Entry]] = [:]
     47 
     48     func set(_ update: EngagementSelectionUpdate) {
     49         guard !update.authorID.isEmpty,
     50               !update.deviceID.isEmpty,
     51               update.deviceID != RecordSerializer.localDeviceID,
     52               let selection = update.selection
     53         else { return }
     54 
     55         let entry = Entry(
     56             authorID: update.authorID,
     57             row: selection.row,
     58             col: selection.col,
     59             direction: selection.direction,
     60             updatedAt: update.updatedAt
     61         )
     62         let current = selectionsByGame[update.gameID]?[update.authorID]
     63         guard current.map({ $0.updatedAt <= entry.updatedAt }) ?? true else { return }
     64         selectionsByGame[update.gameID, default: [:]][update.authorID] = entry
     65     }
     66 
     67     func selections(for gameID: UUID) -> [String: Entry] {
     68         selectionsByGame[gameID] ?? [:]
     69     }
     70 
     71     func clear(gameID: UUID) {
     72         selectionsByGame[gameID] = nil
     73     }
     74 }