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 }