field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

TeraTodayPresentation.swift (3821B)


      1 import Foundation
      2 
      3 enum TeraTodayContentAvailability: Sendable, Equatable {
      4   case notLoaded
      5   case empty
      6   case available
      7 }
      8 
      9 enum TeraTodayRefreshState: Sendable, Equatable {
     10   case idle
     11   case refreshing
     12   case completed
     13   case failed(TeraTodayFailure)
     14 }
     15 
     16 enum TeraTodayFreshness: Sendable, Equatable {
     17   case unconfirmed
     18   /// A local projection receipt does not prove complete relay history.
     19   case refreshed(contentGeneration: UInt64)
     20 }
     21 
     22 enum TeraTodayFailure: Sendable, Equatable {
     23   case offline(message: String)
     24   case failed(message: String)
     25   case staleCursor(message: String)
     26 
     27   init(_ error: Error) {
     28     let message = TeraUserMessages.text(for: error, fallback: .todayUnavailable)
     29     switch TeraRuntimeFailure.from(error)?.recovery.disposition {
     30     case .staleCursor:
     31       self = .staleCursor(message: message)
     32     case .networkUnavailable:
     33       self = .offline(message: message)
     34     default:
     35       self = .failed(message: message)
     36     }
     37   }
     38 
     39   var message: String {
     40     switch self {
     41     case let .offline(message), let .failed(message), let .staleCursor(message): message
     42     }
     43   }
     44 
     45   var systemImage: String {
     46     switch self {
     47     case .offline: "wifi.slash"
     48     case .failed: "exclamationmark.triangle"
     49     case .staleCursor: "arrow.clockwise"
     50     }
     51   }
     52 
     53   var requiresRefresh: Bool {
     54     if case .staleCursor = self {
     55       return true
     56     }
     57     return false
     58   }
     59 
     60   var readStatus: String {
     61     requiresRefresh ? message : "Saved posts could not be read. \(message)"
     62   }
     63 }
     64 
     65 struct TeraTodayPresentation: Sendable, Equatable {
     66   private(set) var content: TeraTodayContentAvailability = .notLoaded
     67   private(set) var refresh: TeraTodayRefreshState = .idle
     68   private(set) var freshness: TeraTodayFreshness = .unconfirmed
     69   private(set) var relayReceipt: TeraTodaySyncReceipt?
     70   private(set) var readFailure: TeraTodayFailure?
     71   private(set) var isReading = false
     72 
     73   mutating func beginReload() {
     74     relayReceipt = nil
     75     readFailure = nil
     76     isReading = false
     77     freshness = .unconfirmed
     78     if refresh == .refreshing {
     79       refresh = .idle
     80     }
     81   }
     82 
     83   mutating func beginRefresh() {
     84     refresh = .refreshing
     85   }
     86 
     87   mutating func refreshCompleted(_ receipt: TeraTodaySyncReceipt) {
     88     relayReceipt = receipt
     89     refresh = .completed
     90   }
     91 
     92   mutating func refreshFailed(_ error: Error) {
     93     refresh = .failed(TeraTodayFailure(error))
     94   }
     95 
     96   mutating func beginRead() {
     97     isReading = true
     98   }
     99 
    100   mutating func finishReading() {
    101     isReading = false
    102   }
    103 
    104   mutating func acceptPage(count: Int, receipt: TeraTodayRefreshReceipt? = nil) {
    105     content = count == 0 ? .empty : .available
    106     readFailure = nil
    107     isReading = false
    108     if let receipt {
    109       freshness = .refreshed(contentGeneration: receipt.contentGeneration)
    110     }
    111   }
    112 
    113   mutating func failRead(_ failure: TeraTodayFailure) {
    114     readFailure = failure
    115     isReading = false
    116     freshness = .unconfirmed
    117   }
    118 
    119   mutating func stop() {
    120     isReading = false
    121     if refresh == .refreshing {
    122       refresh = .idle
    123     }
    124   }
    125 
    126   var freshnessMessage: String? {
    127     guard content != .notLoaded else { return nil }
    128     switch freshness {
    129     case .unconfirmed: return "Showing saved posts."
    130     case .refreshed: return "Saved posts checked."
    131     }
    132   }
    133 
    134   var accessibilityStatus: String {
    135     var messages: [String] = []
    136     if refresh == .refreshing {
    137       messages.append("Checking for updates.")
    138     }
    139     if isReading {
    140       messages.append("Reading saved posts.")
    141     }
    142     if case let .failed(failure) = refresh {
    143       messages.append("Refresh failed. \(failure.message)")
    144     }
    145     messages.append(contentsOf: relayMessages)
    146     if let readFailure {
    147       messages.append(readFailure.readStatus)
    148     }
    149     if let freshnessMessage {
    150       messages.append(freshnessMessage)
    151     }
    152     return messages.joined(separator: " ")
    153   }
    154 }