|
|
|
@ -17,8 +17,13 @@ public final class RelayClient: NSObject, URLSessionWebSocketDelegate { |
|
|
|
private var monitorTimerSource: DispatchSourceTimer? |
|
|
|
private let monitorQueue = DispatchQueue(label: "com.soniox.macsync.monitor", qos: .userInteractive) |
|
|
|
|
|
|
|
// Callback: (text, cursor, isFullReplace) |
|
|
|
public var onRemoteUpdateReceived: ((String, Int?, Bool) -> Void)? |
|
|
|
|
|
|
|
// Dedup guard: prevent rapid duplicate keystrokes within 200ms |
|
|
|
private var lastInjectedText: String = "" |
|
|
|
private var lastInjectedTime: Double = 0 |
|
|
|
|
|
|
|
override private init() { |
|
|
|
super.init() |
|
|
|
let config = URLSessionConfiguration.default |
|
|
|
@ -72,7 +77,7 @@ public final class RelayClient: NSObject, URLSessionWebSocketDelegate { |
|
|
|
case .success(let message): |
|
|
|
self.isConnected = true |
|
|
|
switch message { |
|
|
|
case .string(let text): |
|
|
|
case .string(let text): |
|
|
|
self.handleMessageString(text) |
|
|
|
case .data(let data): |
|
|
|
if let text = String(data: data, encoding: .utf8) { |
|
|
|
@ -97,7 +102,7 @@ public final class RelayClient: NSObject, URLSessionWebSocketDelegate { |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
let action = json["action"] as? String ?? json["type"] as? String |
|
|
|
let action = json["action"] as? String ?? json["type"] as? String ?? "" |
|
|
|
let source = json["source"] as? String ?? "" |
|
|
|
|
|
|
|
// Ignore echo messages originated by Mac itself |
|
|
|
@ -106,13 +111,43 @@ public final class RelayClient: NSObject, URLSessionWebSocketDelegate { |
|
|
|
let updateText = json["text"] as? String ?? json["insert_text"] as? String ?? "" |
|
|
|
let cursor = json["cursor"] as? Int ?? json["cursor_pos"] as? Int |
|
|
|
|
|
|
|
if action == "paste" || action == "set_text" || action == "update_input" || action == "phone_input_edit" || action == "sync_state" || action == "insert_speech" { |
|
|
|
let isFullReplace = (action == "update_input" || action == "set_text" || action == "force_replace") |
|
|
|
print("RelayClient: ⚡ Received remote update: '\(updateText.prefix(30))...' (replace: \(isFullReplace), cursor: \(cursor ?? -1))") |
|
|
|
let now = Date().timeIntervalSince1970 |
|
|
|
|
|
|
|
// 1. Live Voice Dictation from Phone Mic: Paste ONLY new speech at active cursor (Cmd+V) |
|
|
|
if action == "insert_speech" || action == "speech_insert" { |
|
|
|
guard !updateText.isEmpty else { return } |
|
|
|
|
|
|
|
// Dedup exact duplicate within 250ms |
|
|
|
if updateText == lastInjectedText && (now - lastInjectedTime) < 0.25 { |
|
|
|
return |
|
|
|
} |
|
|
|
lastInjectedText = updateText |
|
|
|
lastInjectedTime = now |
|
|
|
|
|
|
|
print("RelayClient: 🎙️ Voice Dictation Paste: '\(updateText.prefix(30))...' (Cmd+V at cursor)") |
|
|
|
DispatchQueue.main.async { |
|
|
|
self.onRemoteUpdateReceived?(updateText, cursor, isFullReplace) |
|
|
|
self.onRemoteUpdateReceived?(updateText, cursor, false) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 2. Explicit "Send to Mac" Button on Phone: Full Replace (Cmd+A + Cmd+V) |
|
|
|
else if action == "update_input" || action == "force_replace" || action == "paste" { |
|
|
|
guard !updateText.isEmpty else { return } |
|
|
|
|
|
|
|
lastInjectedText = updateText |
|
|
|
lastInjectedTime = now |
|
|
|
|
|
|
|
print("RelayClient: 🚀 Force Replace All: '\(updateText.prefix(30))...' (Cmd+A + Cmd+V)") |
|
|
|
DispatchQueue.main.async { |
|
|
|
self.onRemoteUpdateReceived?(updateText, cursor, true) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 3. Passive State Synchronization (typing on phone keyboard): |
|
|
|
// Updates room state cache only — DOES NOT trigger keystrokes on Mac! |
|
|
|
else if action == "sync_state" || action == "phone_input_edit" { |
|
|
|
// Passive sync only |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private func startInputMonitoring() { |
|
|
|
|