From 0ca801d96314ffbdb6309b6c4dbe049e01793c30 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Fri, 31 Oct 2025 00:11:24 +0800 Subject: [PATCH] :sparkles: Live updates of chat messages with websocket on watchOS --- .../Views/AppInfoHeaderView.swift | 2 +- .../Views/{ChatView.swift => ChatViews.swift} | 56 ++++++++++++++----- 2 files changed, 42 insertions(+), 16 deletions(-) rename ios/WatchRunner Watch App/Views/{ChatView.swift => ChatViews.swift} (90%) diff --git a/ios/WatchRunner Watch App/Views/AppInfoHeaderView.swift b/ios/WatchRunner Watch App/Views/AppInfoHeaderView.swift index 9384a5dd..94b94fc9 100644 --- a/ios/WatchRunner Watch App/Views/AppInfoHeaderView.swift +++ b/ios/WatchRunner Watch App/Views/AppInfoHeaderView.swift @@ -26,7 +26,7 @@ struct AppInfoHeaderView : View { // Display WebSocket connection status Text(webSocketStatusMessage) - .font(.caption2) + .font(.system(size: 10)) .foregroundColor(.secondary) } } diff --git a/ios/WatchRunner Watch App/Views/ChatView.swift b/ios/WatchRunner Watch App/Views/ChatViews.swift similarity index 90% rename from ios/WatchRunner Watch App/Views/ChatView.swift rename to ios/WatchRunner Watch App/Views/ChatViews.swift index d5fc71d7..040a420c 100644 --- a/ios/WatchRunner Watch App/Views/ChatView.swift +++ b/ios/WatchRunner Watch App/Views/ChatViews.swift @@ -259,18 +259,22 @@ struct ChatRoomView: View { @State private var messages: [SnChatMessage] = [] @State private var isLoading = false @State private var error: Error? - @State private var webSocketConnectionState: WebSocketState = .disconnected // New state for WebSocket status + @State private var wsState: WebSocketState = .disconnected // New state for WebSocket status @State private var cancellables = Set() // For managing subscriptions var body: some View { VStack { // Display WebSocket connection status - Text(webSocketStatusMessage) - .font(.caption2) - .foregroundColor(.secondary) - .padding(.vertical, 2) - .animation(.easeInOut, value: webSocketConnectionState) // Animate status changes + if (wsState != .connected) + { + Text(webSocketStatusMessage) + .font(.caption2) + .foregroundColor(.secondary) + .padding(.vertical, 2) + .animation(.easeInOut, value: wsState) // Animate status changes + .transition(.opacity) + } if isLoading { ProgressView() @@ -336,7 +340,7 @@ struct ChatRoomView: View { } private var webSocketStatusMessage: String { - switch webSocketConnectionState { + switch wsState { case .connected: return "Connected" case .connecting: return "Connecting..." case .disconnected: return "Disconnected" @@ -368,6 +372,15 @@ struct ChatRoomView: View { isLoading = false } + private func sendReadReceipt() { + let data: [String: Any] = ["chat_room_id": room.id] + let packet: [String: Any] = ["type": "messages.read", "data": data, "endpoint": "sphere"] + if let jsonData = try? JSONSerialization.data(withJSONObject: packet, options: []), + let jsonString = String(data: jsonData, encoding: .utf8) { + appState.networkService.sendWebSocketMessage(message: jsonString) + } + } + private func setupWebSocketListeners() { // Listen for WebSocket packets (new messages) appState.networkService.packetStream @@ -377,20 +390,33 @@ struct ChatRoomView: View { print("[ChatRoomView] WebSocket packet stream error: \(err.localizedDescription)") } }, receiveValue: { packet in - // Assuming 'message.created' is the type for new messages - if packet.type == "message.created", + if ["messages.new", "messages.update", "messages.delete"].contains(packet.type), let messageData = packet.data { do { let jsonData = try JSONSerialization.data(withJSONObject: messageData, options: []) let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 decoder.keyDecodingStrategy = .convertFromSnakeCase - let newMessage = try decoder.decode(SnChatMessage.self, from: jsonData) + let message = try decoder.decode(SnChatMessage.self, from: jsonData) - if newMessage.chatRoomId == room.id { - // Avoid adding duplicates - if !messages.contains(where: { $0.id == newMessage.id }) { - messages.append(newMessage) + if message.chatRoomId == room.id { + switch packet.type { + case "messages.new": + if message.type.hasPrefix("call") { + // TODO: Handle ongoing call + } + if !messages.contains(where: { $0.id == message.id }) { + messages.append(message) + } + sendReadReceipt() + case "messages.update": + if let index = messages.firstIndex(where: { $0.id == message.id }) { + messages[index] = message + } + case "messages.delete": + messages.removeAll(where: { $0.id == message.id }) + default: + break } } } catch { @@ -404,7 +430,7 @@ struct ChatRoomView: View { appState.networkService.stateStream .receive(on: DispatchQueue.main) // Ensure UI updates on main thread .sink { state in - webSocketConnectionState = state + wsState = state } .store(in: &cancellables) }