mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 720791ae6b
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
125 lines
4.0 KiB
Swift
125 lines
4.0 KiB
Swift
import Foundation
|
|
import Observation
|
|
import UserNotifications
|
|
import WatchKit
|
|
|
|
struct WatchNotifyMessage: Sendable {
|
|
var id: String?
|
|
var title: String
|
|
var body: String
|
|
var sentAtMs: Int?
|
|
}
|
|
|
|
@MainActor @Observable final class WatchInboxStore {
|
|
private struct PersistedState: Codable {
|
|
var title: String
|
|
var body: String
|
|
var transport: String
|
|
var updatedAt: Date
|
|
var lastDeliveryKey: String?
|
|
}
|
|
|
|
private static let persistedStateKey = "watch.inbox.state.v1"
|
|
private let defaults: UserDefaults
|
|
|
|
var title = "OpenClaw"
|
|
var body = "Waiting for messages from your iPhone."
|
|
var transport = "none"
|
|
var updatedAt: Date?
|
|
private var lastDeliveryKey: String?
|
|
|
|
init(defaults: UserDefaults = .standard) {
|
|
self.defaults = defaults
|
|
self.restorePersistedState()
|
|
Task {
|
|
await self.ensureNotificationAuthorization()
|
|
}
|
|
}
|
|
|
|
func consume(message: WatchNotifyMessage, transport: String) {
|
|
let messageID = message.id?
|
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
let deliveryKey = self.deliveryKey(
|
|
messageID: messageID,
|
|
title: message.title,
|
|
body: message.body,
|
|
sentAtMs: message.sentAtMs)
|
|
guard deliveryKey != self.lastDeliveryKey else { return }
|
|
|
|
let normalizedTitle = message.title.isEmpty ? "OpenClaw" : message.title
|
|
self.title = normalizedTitle
|
|
self.body = message.body
|
|
self.transport = transport
|
|
self.updatedAt = Date()
|
|
self.lastDeliveryKey = deliveryKey
|
|
self.persistState()
|
|
|
|
Task {
|
|
await self.postLocalNotification(
|
|
identifier: deliveryKey,
|
|
title: normalizedTitle,
|
|
body: message.body)
|
|
}
|
|
}
|
|
|
|
private func restorePersistedState() {
|
|
guard let data = self.defaults.data(forKey: Self.persistedStateKey),
|
|
let state = try? JSONDecoder().decode(PersistedState.self, from: data)
|
|
else {
|
|
return
|
|
}
|
|
|
|
self.title = state.title
|
|
self.body = state.body
|
|
self.transport = state.transport
|
|
self.updatedAt = state.updatedAt
|
|
self.lastDeliveryKey = state.lastDeliveryKey
|
|
}
|
|
|
|
private func persistState() {
|
|
guard let updatedAt = self.updatedAt else { return }
|
|
let state = PersistedState(
|
|
title: self.title,
|
|
body: self.body,
|
|
transport: self.transport,
|
|
updatedAt: updatedAt,
|
|
lastDeliveryKey: self.lastDeliveryKey)
|
|
guard let data = try? JSONEncoder().encode(state) else { return }
|
|
self.defaults.set(data, forKey: Self.persistedStateKey)
|
|
}
|
|
|
|
private func deliveryKey(messageID: String?, title: String, body: String, sentAtMs: Int?) -> String {
|
|
if let messageID, messageID.isEmpty == false {
|
|
return "id:\(messageID)"
|
|
}
|
|
return "content:\(title)|\(body)|\(sentAtMs ?? 0)"
|
|
}
|
|
|
|
private func ensureNotificationAuthorization() async {
|
|
let center = UNUserNotificationCenter.current()
|
|
let settings = await center.notificationSettings()
|
|
switch settings.authorizationStatus {
|
|
case .notDetermined:
|
|
_ = try? await center.requestAuthorization(options: [.alert, .sound])
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
private func postLocalNotification(identifier: String, title: String, body: String) async {
|
|
let content = UNMutableNotificationContent()
|
|
content.title = title
|
|
content.body = body
|
|
content.sound = .default
|
|
content.threadIdentifier = "openclaw-watch"
|
|
|
|
let request = UNNotificationRequest(
|
|
identifier: identifier,
|
|
content: content,
|
|
trigger: UNTimeIntervalNotificationTrigger(timeInterval: 0.2, repeats: false))
|
|
|
|
_ = try? await UNUserNotificationCenter.current().add(request)
|
|
WKInterfaceDevice.current().play(.notification)
|
|
}
|
|
}
|