✨ Message loading on watchOS
This commit is contained in:
		| @@ -271,7 +271,7 @@ struct SnChatMessage: Codable, Identifiable { | |||||||
|     let content: String? |     let content: String? | ||||||
|     let nonce: String? |     let nonce: String? | ||||||
|     let meta: [String: AnyCodable] |     let meta: [String: AnyCodable] | ||||||
|     let membersMentioned: [String] |     let membersMentioned: [String]? | ||||||
|     let editedAt: Date? |     let editedAt: Date? | ||||||
|     let attachments: [SnCloudFile] |     let attachments: [SnCloudFile] | ||||||
|     let reactions: [SnChatReaction] |     let reactions: [SnChatReaction] | ||||||
| @@ -283,6 +283,31 @@ struct SnChatMessage: Codable, Identifiable { | |||||||
|     let createdAt: Date |     let createdAt: Date | ||||||
|     let updatedAt: Date |     let updatedAt: Date | ||||||
|     let deletedAt: Date? |     let deletedAt: Date? | ||||||
|  |  | ||||||
|  |     enum CodingKeys: String, CodingKey { | ||||||
|  |         case id, type, content, nonce, meta, membersMentioned, editedAt, attachments, reactions, repliedMessageId, forwardedMessageId, senderId, sender, chatRoomId, createdAt, updatedAt, deletedAt | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     init(from decoder: Decoder) throws { | ||||||
|  |         let container = try decoder.container(keyedBy: CodingKeys.self) | ||||||
|  |         id = try container.decode(String.self, forKey: .id) | ||||||
|  |         type = try container.decode(String.self, forKey: .type) | ||||||
|  |         content = try container.decodeIfPresent(String.self, forKey: .content) | ||||||
|  |         nonce = try container.decodeIfPresent(String.self, forKey: .nonce) | ||||||
|  |         meta = try container.decode([String: AnyCodable].self, forKey: .meta) | ||||||
|  |         membersMentioned = try container.decodeIfPresent([String].self, forKey: .membersMentioned) ?? [] | ||||||
|  |         editedAt = try container.decodeIfPresent(Date.self, forKey: .editedAt) | ||||||
|  |         attachments = try container.decode([SnCloudFile].self, forKey: .attachments) | ||||||
|  |         reactions = try container.decode([SnChatReaction].self, forKey: .reactions) | ||||||
|  |         repliedMessageId = try container.decodeIfPresent(String.self, forKey: .repliedMessageId) | ||||||
|  |         forwardedMessageId = try container.decodeIfPresent(String.self, forKey: .forwardedMessageId) | ||||||
|  |         senderId = try container.decode(String.self, forKey: .senderId) | ||||||
|  |         sender = try container.decode(SnChatMember.self, forKey: .sender) | ||||||
|  |         chatRoomId = try container.decode(String.self, forKey: .chatRoomId) | ||||||
|  |         createdAt = try container.decode(Date.self, forKey: .createdAt) | ||||||
|  |         updatedAt = try container.decode(Date.self, forKey: .updatedAt) | ||||||
|  |         deletedAt = try container.decodeIfPresent(Date.self, forKey: .deletedAt) | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| struct SnChatReaction: Codable, Identifiable { | struct SnChatReaction: Codable, Identifiable { | ||||||
| @@ -328,3 +353,13 @@ struct ChatRoomsResponse { | |||||||
| struct ChatInvitesResponse { | struct ChatInvitesResponse { | ||||||
|     let invites: [SnChatMember] |     let invites: [SnChatMember] | ||||||
| } | } | ||||||
|  |  | ||||||
|  | struct MessageSyncResponse: Codable { | ||||||
|  |     let messages: [SnChatMessage] | ||||||
|  |     let currentTimestamp: Date | ||||||
|  |  | ||||||
|  |     enum CodingKeys: String, CodingKey { | ||||||
|  |         case messages | ||||||
|  |         case currentTimestamp = "current_timestamp" | ||||||
|  |     } | ||||||
|  | } | ||||||
|   | |||||||
| @@ -324,4 +324,76 @@ class NetworkService { | |||||||
|             throw URLError(URLError.Code(rawValue: httpResponse.statusCode)) |             throw URLError(URLError.Code(rawValue: httpResponse.statusCode)) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |      | ||||||
|  |     // MARK: - Message API Methods | ||||||
|  |      | ||||||
|  |     func fetchChatMessages(chatRoomId: String, token: String, serverUrl: String, before: Date? = nil, take: Int = 50) async throws -> [SnChatMessage] { | ||||||
|  |         guard let baseURL = URL(string: serverUrl) else { | ||||||
|  |             throw URLError(.badURL) | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // Try a different pattern: /sphere/chat/messages with roomId as query param | ||||||
|  |         var components = URLComponents( | ||||||
|  |             url: baseURL.appendingPathComponent("/sphere/chat/\(chatRoomId)/messages"), | ||||||
|  |             resolvingAgainstBaseURL: false | ||||||
|  |         )! | ||||||
|  |         var queryItems = [ | ||||||
|  |             URLQueryItem(name: "take", value: String(take)) | ||||||
|  |         ] | ||||||
|  |         if let before = before { | ||||||
|  |             queryItems.append(URLQueryItem(name: "before", value: ISO8601DateFormatter().string(from: before))) | ||||||
|  |         } | ||||||
|  |         components.queryItems = queryItems | ||||||
|  |          | ||||||
|  |         var request = URLRequest(url: components.url!) | ||||||
|  |         request.httpMethod = "GET" | ||||||
|  |         request.setValue("application/json", forHTTPHeaderField: "Accept") | ||||||
|  |         request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization") | ||||||
|  |         request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent") | ||||||
|  |          | ||||||
|  |         let (data, response) = try await session.data(for: request) | ||||||
|  |  | ||||||
|  |         if let httpResponse = response as? HTTPURLResponse { | ||||||
|  |             _ = String(data: data, encoding: .utf8) ?? "Unable to decode response body" | ||||||
|  |  | ||||||
|  |             if httpResponse.statusCode != 200 { | ||||||
|  |                 print("[watchOS] fetchChatMessages failed with status \(httpResponse.statusCode)") | ||||||
|  |                 throw URLError(URLError.Code(rawValue: httpResponse.statusCode)) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Check if data is empty | ||||||
|  |         if data.isEmpty { | ||||||
|  |             print("[watchOS] fetchChatMessages received empty response data") | ||||||
|  |             return [] | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         let decoder = JSONDecoder() | ||||||
|  |         decoder.dateDecodingStrategy = .iso8601 | ||||||
|  |         decoder.keyDecodingStrategy = .convertFromSnakeCase | ||||||
|  |  | ||||||
|  |         do { | ||||||
|  |             let messages = try decoder.decode([SnChatMessage].self, from: data) | ||||||
|  |             print("[watchOS] fetchChatMessages successfully decoded \(messages.count) messages") | ||||||
|  |             return messages | ||||||
|  |         } catch DecodingError.dataCorrupted(let context) { | ||||||
|  |             print(context) | ||||||
|  |             return [] | ||||||
|  |         } catch DecodingError.keyNotFound(let key, let context) { | ||||||
|  |             print("Key '\(key)' not found:", context.debugDescription) | ||||||
|  |             print("codingPath:", context.codingPath) | ||||||
|  |             return [] | ||||||
|  |         } catch DecodingError.valueNotFound(let value, let context) { | ||||||
|  |             print("Value '\(value)' not found:", context.debugDescription) | ||||||
|  |             print("codingPath:", context.codingPath) | ||||||
|  |             return [] | ||||||
|  |         } catch DecodingError.typeMismatch(let type, let context) { | ||||||
|  |             print("Type '\(type)' mismatch:", context.debugDescription) | ||||||
|  |             print("codingPath:", context.codingPath) | ||||||
|  |             return [] | ||||||
|  |         } catch { | ||||||
|  |             print("error: ", error) | ||||||
|  |             throw error | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -181,7 +181,10 @@ struct ChatRoomListItem: View { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     var body: some View { |     var body: some View { | ||||||
|         NavigationLink(destination: ChatRoomView(room: room)) { |         NavigationLink( | ||||||
|  |             destination: ChatRoomView(room: room) | ||||||
|  |                 .environmentObject(appState) | ||||||
|  |         ) { | ||||||
|             HStack { |             HStack { | ||||||
|                 // Avatar using ImageLoader pattern |                 // Avatar using ImageLoader pattern | ||||||
|                 Group { |                 Group { | ||||||
| @@ -292,9 +295,23 @@ struct ChatRoomView: View { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     private func loadMessages() async { |     private func loadMessages() async { | ||||||
|         // Placeholder for message loading |         guard let token = appState.token, let serverUrl = appState.serverUrl else { | ||||||
|         // In a full implementation, this would fetch messages from the API |             isLoading = false | ||||||
|         // For now, just show empty state |             return | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         do { | ||||||
|  |             let messages = try await appState.networkService.fetchChatMessages( | ||||||
|  |                 chatRoomId: room.id, | ||||||
|  |                 token: token, | ||||||
|  |                 serverUrl: serverUrl | ||||||
|  |             ) | ||||||
|  |             self.messages = messages.sorted { $0.createdAt < $1.createdAt } | ||||||
|  |         } catch { | ||||||
|  |             print("[watchOS] Error loading messages: \(error.localizedDescription)") | ||||||
|  |             self.error = error | ||||||
|  |         } | ||||||
|  |  | ||||||
|         isLoading = false |         isLoading = false | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user