// The watch face complication: how many threads are waiting on you, from `GET /v1/summary`. It // reads the device token from the shared Keychain group and the demo flag from the App Group, and // the app reloads it whenever its own feed or a notification changes the counts. import SwiftUI import WidgetKit @main struct WristCodeWidgets: WidgetBundle { var body: some Widget { WaitingWidget() } } struct WaitingWidget: Widget { var body: some WidgetConfiguration { StaticConfiguration(kind: "WaitingCount", provider: WaitingProvider()) { entry in WaitingView(entry: entry) } .configurationDisplayName("Waiting on you") .description("Threads waiting for your approval or answer.") .supportedFamilies([.accessoryCircular, .accessoryRectangular, .accessoryInline, .accessoryCorner]) } } struct WaitingEntry: TimelineEntry { enum Content: Equatable { /// Counts from the gateway. `stale` when they are the last ones seen because the gateway /// could not be reached. case counts(Summary, stale: Bool) case notPaired /// The gateway could not be reached and there is nothing saved to show. case unavailable } var date: Date var content: Content /// Ranks the complication higher in the Smart Stack while something waits. var relevance: TimelineEntryRelevance? { guard case let .counts(summary, _) = content, summary.needsYou > 0 else { return nil } return TimelineEntryRelevance(score: Float(summary.needsYou)) } static let sample = WaitingEntry(date: .now, content: .counts(Summary( needsYou: 2, working: 3, next: .init(hostId: "mac-mini", threadId: "t-ocr", title: "Receipt OCR", hostLabel: "Mac mini"), hostsOffline: 0), stale: false)) } struct WaitingProvider: TimelineProvider { /// How long until WidgetKit asks again. The app reloads sooner when it learns something. static let refreshInterval: TimeInterval = 15 * 60 func placeholder(in context: Context) -> WaitingEntry { .sample } func getSnapshot(in context: Context, completion: @escaping @Sendable (WaitingEntry) -> Void) { if context.isPreview { completion(.sample) return } Task { completion(await Self.load()) } } func getTimeline(in context: Context, completion: @escaping @Sendable (Timeline) -> Void) { Task { let entry = await Self.load() completion(Timeline(entries: [entry], policy: .after(entry.date.addingTimeInterval(Self.refreshInterval)))) } } static func load() async -> WaitingEntry { let api: any GatewayAPI if SharedState.demo { api = DemoGateway.shared } else if let credentials = DeviceStore.load() { api = GatewayClient(baseURL: credentials.gatewayURL, token: credentials.token) } else { return WaitingEntry(date: .now, content: .notPaired) } do { let summary = try await api.summary() SharedState.lastSummary = summary return WaitingEntry(date: .now, content: .counts(summary, stale: false)) } catch GatewayError.unauthorized { return WaitingEntry(date: .now, content: .notPaired) } catch { guard let last = SharedState.lastSummary else { return WaitingEntry(date: .now, content: .unavailable) } return WaitingEntry(date: .now, content: .counts(last, stale: true)) } } }