mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
iOS: fix node notify and identity
This commit is contained in:
committed by
Mariano Belinky
parent
d9cadf9737
commit
761188cd1d
@@ -5,6 +5,36 @@ import SwiftUI
|
||||
import UIKit
|
||||
import UserNotifications
|
||||
|
||||
private struct NotificationCallError: Error, Sendable {
|
||||
let message: String
|
||||
}
|
||||
|
||||
private final class NotificationInvokeLatch<T: Sendable>: @unchecked Sendable {
|
||||
private let lock = NSLock()
|
||||
private var continuation: CheckedContinuation<Result<T, NotificationCallError>, Never>?
|
||||
private var resumed = false
|
||||
|
||||
func setContinuation(_ continuation: CheckedContinuation<Result<T, NotificationCallError>, Never>) {
|
||||
self.lock.lock()
|
||||
defer { self.lock.unlock() }
|
||||
self.continuation = continuation
|
||||
}
|
||||
|
||||
func resume(_ response: Result<T, NotificationCallError>) {
|
||||
let cont: CheckedContinuation<Result<T, NotificationCallError>, Never>?
|
||||
self.lock.lock()
|
||||
if self.resumed {
|
||||
self.lock.unlock()
|
||||
return
|
||||
}
|
||||
self.resumed = true
|
||||
cont = self.continuation
|
||||
self.continuation = nil
|
||||
self.lock.unlock()
|
||||
cont?.resume(returning: response)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
final class NodeAppModel {
|
||||
@@ -139,7 +169,10 @@ final class NodeAppModel {
|
||||
return raw.isEmpty ? "-" : raw
|
||||
}()
|
||||
|
||||
let host = UserDefaults.standard.string(forKey: "node.displayName") ?? UIDevice.current.name
|
||||
let host = NodeDisplayName.resolve(
|
||||
existing: UserDefaults.standard.string(forKey: "node.displayName"),
|
||||
deviceName: UIDevice.current.name,
|
||||
interfaceIdiom: UIDevice.current.userInterfaceIdiom)
|
||||
let instanceId = (UserDefaults.standard.string(forKey: "node.instanceId") ?? "ios-node").lowercased()
|
||||
let contextJSON = OpenClawCanvasA2UIAction.compactJSON(userAction["context"])
|
||||
let sessionKey = self.mainSessionKey
|
||||
@@ -920,11 +953,7 @@ final class NodeAppModel {
|
||||
error: OpenClawNodeError(code: .invalidRequest, message: "INVALID_REQUEST: empty notification"))
|
||||
}
|
||||
|
||||
let status = await self.notificationCenter.authorizationStatus()
|
||||
if status == .notDetermined {
|
||||
_ = try await self.notificationCenter.requestAuthorization(options: [.alert, .sound, .badge])
|
||||
}
|
||||
let finalStatus = await self.notificationCenter.authorizationStatus()
|
||||
let finalStatus = await self.requestNotificationAuthorizationIfNeeded()
|
||||
guard finalStatus == .authorized || finalStatus == .provisional || finalStatus == .ephemeral else {
|
||||
return BridgeInvokeResponse(
|
||||
id: req.id,
|
||||
@@ -932,17 +961,79 @@ final class NodeAppModel {
|
||||
error: OpenClawNodeError(code: .unavailable, message: "NOT_AUTHORIZED: notifications"))
|
||||
}
|
||||
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = title
|
||||
content.body = body
|
||||
let request = UNNotificationRequest(
|
||||
identifier: UUID().uuidString,
|
||||
content: content,
|
||||
trigger: nil)
|
||||
try await self.notificationCenter.add(request)
|
||||
let addResult = await self.runNotificationCall(timeoutSeconds: 2.0) { [notificationCenter] in
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = title
|
||||
content.body = body
|
||||
let request = UNNotificationRequest(
|
||||
identifier: UUID().uuidString,
|
||||
content: content,
|
||||
trigger: nil)
|
||||
try await notificationCenter.add(request)
|
||||
}
|
||||
if case let .failure(error) = addResult {
|
||||
return BridgeInvokeResponse(
|
||||
id: req.id,
|
||||
ok: false,
|
||||
error: OpenClawNodeError(code: .unavailable, message: "NOTIFICATION_FAILED: \(error.message)"))
|
||||
}
|
||||
return BridgeInvokeResponse(id: req.id, ok: true)
|
||||
}
|
||||
|
||||
private func requestNotificationAuthorizationIfNeeded() async -> NotificationAuthorizationStatus {
|
||||
let status = await self.notificationAuthorizationStatus()
|
||||
guard status == .notDetermined else { return status }
|
||||
|
||||
// Avoid hanging invoke requests if the permission prompt is never answered.
|
||||
_ = await self.runNotificationCall(timeoutSeconds: 2.0) { [notificationCenter] in
|
||||
_ = try await notificationCenter.requestAuthorization(options: [.alert, .sound, .badge])
|
||||
}
|
||||
|
||||
return await self.notificationAuthorizationStatus()
|
||||
}
|
||||
|
||||
private func notificationAuthorizationStatus() async -> NotificationAuthorizationStatus {
|
||||
let result = await self.runNotificationCall(timeoutSeconds: 1.5) { [notificationCenter] in
|
||||
await notificationCenter.authorizationStatus()
|
||||
}
|
||||
switch result {
|
||||
case let .success(status):
|
||||
return status
|
||||
case .failure:
|
||||
return .denied
|
||||
}
|
||||
}
|
||||
|
||||
private func runNotificationCall<T: Sendable>(
|
||||
timeoutSeconds: Double,
|
||||
operation: @escaping @Sendable () async throws -> T
|
||||
) async -> Result<T, NotificationCallError> {
|
||||
let latch = NotificationInvokeLatch<T>()
|
||||
var opTask: Task<Void, Never>?
|
||||
var timeoutTask: Task<Void, Never>?
|
||||
let result = await withCheckedContinuation { (cont: CheckedContinuation<Result<T, NotificationCallError>, Never>) in
|
||||
latch.setContinuation(cont)
|
||||
opTask = Task { @MainActor in
|
||||
do {
|
||||
let value = try await operation()
|
||||
latch.resume(.success(value))
|
||||
} catch {
|
||||
latch.resume(.failure(NotificationCallError(message: error.localizedDescription)))
|
||||
}
|
||||
}
|
||||
timeoutTask = Task.detached {
|
||||
let clamped = max(0.0, timeoutSeconds)
|
||||
if clamped > 0 {
|
||||
try? await Task.sleep(nanoseconds: UInt64(clamped * 1_000_000_000))
|
||||
}
|
||||
latch.resume(.failure(NotificationCallError(message: "notification request timed out")))
|
||||
}
|
||||
}
|
||||
opTask?.cancel()
|
||||
timeoutTask?.cancel()
|
||||
return result
|
||||
}
|
||||
|
||||
private func handleDeviceInvoke(_ req: BridgeInvokeRequest) async throws -> BridgeInvokeResponse {
|
||||
switch req.command {
|
||||
case OpenClawDeviceCommand.status.rawValue:
|
||||
|
||||
Reference in New Issue
Block a user