mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-30 01:06:11 +00:00
iOS: fix watch companion delivery and watchOS target
This commit is contained in:
committed by
mbelinky
parent
8d26530172
commit
5003b7b2dd
@@ -1,7 +1,7 @@
|
||||
import Foundation
|
||||
import WatchConnectivity
|
||||
|
||||
final class WatchConnectivityReceiver: NSObject {
|
||||
final class WatchConnectivityReceiver: NSObject, @unchecked Sendable {
|
||||
private let store: WatchInboxStore
|
||||
private let session: WCSession?
|
||||
|
||||
@@ -20,6 +20,31 @@ final class WatchConnectivityReceiver: NSObject {
|
||||
session.delegate = self
|
||||
session.activate()
|
||||
}
|
||||
|
||||
private static func parseNotificationPayload(_ payload: [String: Any]) -> WatchNotifyMessage? {
|
||||
guard let type = payload["type"] as? String, type == "watch.notify" else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let title = (payload["title"] as? String)?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
let body = (payload["body"] as? String)?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
|
||||
guard title.isEmpty == false || body.isEmpty == false else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let id = (payload["id"] as? String)?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let sentAtMs = (payload["sentAtMs"] as? Int) ?? (payload["sentAtMs"] as? NSNumber)?.intValue
|
||||
|
||||
return WatchNotifyMessage(
|
||||
id: id,
|
||||
title: title,
|
||||
body: body,
|
||||
sentAtMs: sentAtMs)
|
||||
}
|
||||
}
|
||||
|
||||
extension WatchConnectivityReceiver: WCSessionDelegate {
|
||||
@@ -30,20 +55,23 @@ extension WatchConnectivityReceiver: WCSessionDelegate {
|
||||
{}
|
||||
|
||||
func session(_: WCSession, didReceiveMessage message: [String: Any]) {
|
||||
guard let incoming = Self.parseNotificationPayload(message) else { return }
|
||||
Task { @MainActor in
|
||||
self.store.consume(payload: message, transport: "sendMessage")
|
||||
self.store.consume(message: incoming, transport: "sendMessage")
|
||||
}
|
||||
}
|
||||
|
||||
func session(_: WCSession, didReceiveUserInfo userInfo: [String: Any]) {
|
||||
guard let incoming = Self.parseNotificationPayload(userInfo) else { return }
|
||||
Task { @MainActor in
|
||||
self.store.consume(payload: userInfo, transport: "transferUserInfo")
|
||||
self.store.consume(message: incoming, transport: "transferUserInfo")
|
||||
}
|
||||
}
|
||||
|
||||
func session(_: WCSession, didReceiveApplicationContext applicationContext: [String: Any]) {
|
||||
guard let incoming = Self.parseNotificationPayload(applicationContext) else { return }
|
||||
Task { @MainActor in
|
||||
self.store.consume(payload: applicationContext, transport: "applicationContext")
|
||||
self.store.consume(message: incoming, transport: "applicationContext")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,124 @@
|
||||
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?
|
||||
|
||||
func consume(payload: [String: Any], transport: String) {
|
||||
guard let type = payload["type"] as? String, type == "watch.notify" else {
|
||||
return
|
||||
init(defaults: UserDefaults = .standard) {
|
||||
self.defaults = defaults
|
||||
self.restorePersistedState()
|
||||
Task {
|
||||
await self.ensureNotificationAuthorization()
|
||||
}
|
||||
}
|
||||
|
||||
let titleValue = (payload["title"] as? String)?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
let bodyValue = (payload["body"] as? String)?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
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 }
|
||||
|
||||
guard titleValue.isEmpty == false || bodyValue.isEmpty == false else {
|
||||
return
|
||||
}
|
||||
|
||||
self.title = titleValue.isEmpty ? "OpenClaw" : titleValue
|
||||
self.body = bodyValue
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user