✨ 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import Foundation
|
|||||||
|
|
||||||
class NetworkService {
|
class NetworkService {
|
||||||
private let session = URLSession.shared
|
private let session = URLSession.shared
|
||||||
|
|
||||||
func fetchActivities(filter: String, cursor: String? = nil, token: String, serverUrl: String) async throws -> ActivityResponse {
|
func fetchActivities(filter: String, cursor: String? = nil, token: String, serverUrl: String) async throws -> ActivityResponse {
|
||||||
guard let baseURL = URL(string: serverUrl) else {
|
guard let baseURL = URL(string: serverUrl) else {
|
||||||
throw URLError(.badURL)
|
throw URLError(.badURL)
|
||||||
@@ -25,53 +25,53 @@ class NetworkService {
|
|||||||
queryItems.append(URLQueryItem(name: "cursor", value: cursor))
|
queryItems.append(URLQueryItem(name: "cursor", value: cursor))
|
||||||
}
|
}
|
||||||
components.queryItems = queryItems
|
components.queryItems = queryItems
|
||||||
|
|
||||||
var request = URLRequest(url: components.url!)
|
var request = URLRequest(url: components.url!)
|
||||||
request.httpMethod = "GET"
|
request.httpMethod = "GET"
|
||||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
|
|
||||||
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||||
|
|
||||||
let (data, _) = try await session.data(for: request)
|
let (data, _) = try await session.data(for: request)
|
||||||
|
|
||||||
let decoder = JSONDecoder()
|
let decoder = JSONDecoder()
|
||||||
decoder.dateDecodingStrategy = .iso8601
|
decoder.dateDecodingStrategy = .iso8601
|
||||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
|
|
||||||
let activities = try decoder.decode([SnActivity].self, from: data)
|
let activities = try decoder.decode([SnActivity].self, from: data)
|
||||||
|
|
||||||
let hasMore = (activities.first?.type ?? "empty") != "empty"
|
let hasMore = (activities.first?.type ?? "empty") != "empty"
|
||||||
let nextCursor = activities.isEmpty ? nil : activities.map { $0.createdAt }.min()?.ISO8601Format()
|
let nextCursor = activities.isEmpty ? nil : activities.map { $0.createdAt }.min()?.ISO8601Format()
|
||||||
|
|
||||||
return ActivityResponse(activities: activities, hasMore: hasMore, nextCursor: nextCursor)
|
return ActivityResponse(activities: activities, hasMore: hasMore, nextCursor: nextCursor)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createPost(title: String, content: String, token: String, serverUrl: String) async throws {
|
func createPost(title: String, content: String, token: String, serverUrl: String) async throws {
|
||||||
guard let baseURL = URL(string: serverUrl) else {
|
guard let baseURL = URL(string: serverUrl) else {
|
||||||
throw URLError(.badURL)
|
throw URLError(.badURL)
|
||||||
}
|
}
|
||||||
let url = baseURL.appendingPathComponent("/sphere/posts")
|
let url = baseURL.appendingPathComponent("/sphere/posts")
|
||||||
|
|
||||||
var request = URLRequest(url: url)
|
var request = URLRequest(url: url)
|
||||||
request.httpMethod = "POST"
|
request.httpMethod = "POST"
|
||||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||||
|
|
||||||
let body: [String: Any] = ["title": title, "content": content]
|
let body: [String: Any] = ["title": title, "content": content]
|
||||||
request.httpBody = try JSONSerialization.data(withJSONObject: body)
|
request.httpBody = try JSONSerialization.data(withJSONObject: body)
|
||||||
|
|
||||||
let (data, response) = try await session.data(for: request)
|
let (data, response) = try await session.data(for: request)
|
||||||
|
|
||||||
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 201 {
|
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 201 {
|
||||||
let responseBody = String(data: data, encoding: .utf8) ?? ""
|
let responseBody = String(data: data, encoding: .utf8) ?? ""
|
||||||
print("[watchOS] createPost failed with status code: \(httpResponse.statusCode), body: \(responseBody)")
|
print("[watchOS] createPost failed with status code: \(httpResponse.statusCode), body: \(responseBody)")
|
||||||
throw URLError(URLError.Code(rawValue: httpResponse.statusCode))
|
throw URLError(URLError.Code(rawValue: httpResponse.statusCode))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchNotifications(offset: Int = 0, take: Int = 20, token: String, serverUrl: String) async throws -> NotificationResponse {
|
func fetchNotifications(offset: Int = 0, take: Int = 20, token: String, serverUrl: String) async throws -> NotificationResponse {
|
||||||
guard let baseURL = URL(string: serverUrl) else {
|
guard let baseURL = URL(string: serverUrl) else {
|
||||||
throw URLError(.badURL)
|
throw URLError(.badURL)
|
||||||
@@ -79,249 +79,321 @@ class NetworkService {
|
|||||||
var components = URLComponents(url: baseURL.appendingPathComponent("/ring/notifications"), resolvingAgainstBaseURL: false)!
|
var components = URLComponents(url: baseURL.appendingPathComponent("/ring/notifications"), resolvingAgainstBaseURL: false)!
|
||||||
var queryItems = [URLQueryItem(name: "offset", value: String(offset)), URLQueryItem(name: "take", value: String(take))]
|
var queryItems = [URLQueryItem(name: "offset", value: String(offset)), URLQueryItem(name: "take", value: String(take))]
|
||||||
components.queryItems = queryItems
|
components.queryItems = queryItems
|
||||||
|
|
||||||
var request = URLRequest(url: components.url!)
|
var request = URLRequest(url: components.url!)
|
||||||
request.httpMethod = "GET"
|
request.httpMethod = "GET"
|
||||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||||
|
|
||||||
let (data, response) = try await session.data(for: request)
|
let (data, response) = try await session.data(for: request)
|
||||||
|
|
||||||
let decoder = JSONDecoder()
|
let decoder = JSONDecoder()
|
||||||
decoder.dateDecodingStrategy = .iso8601
|
decoder.dateDecodingStrategy = .iso8601
|
||||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
|
|
||||||
let notifications = try decoder.decode([SnNotification].self, from: data)
|
let notifications = try decoder.decode([SnNotification].self, from: data)
|
||||||
|
|
||||||
let httpResponse = response as? HTTPURLResponse
|
let httpResponse = response as? HTTPURLResponse
|
||||||
let total = Int(httpResponse?.value(forHTTPHeaderField: "X-Total") ?? "0") ?? 0
|
let total = Int(httpResponse?.value(forHTTPHeaderField: "X-Total") ?? "0") ?? 0
|
||||||
let hasMore = offset + notifications.count < total
|
let hasMore = offset + notifications.count < total
|
||||||
|
|
||||||
return NotificationResponse(notifications: notifications, total: total, hasMore: hasMore)
|
return NotificationResponse(notifications: notifications, total: total, hasMore: hasMore)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchUserProfile(token: String, serverUrl: String) async throws -> SnAccount {
|
func fetchUserProfile(token: String, serverUrl: String) async throws -> SnAccount {
|
||||||
guard let baseURL = URL(string: serverUrl) else {
|
guard let baseURL = URL(string: serverUrl) else {
|
||||||
throw URLError(.badURL)
|
throw URLError(.badURL)
|
||||||
}
|
}
|
||||||
let url = baseURL.appendingPathComponent("/pass/accounts/me")
|
let url = baseURL.appendingPathComponent("/pass/accounts/me")
|
||||||
|
|
||||||
var request = URLRequest(url: url)
|
var request = URLRequest(url: url)
|
||||||
request.httpMethod = "GET"
|
request.httpMethod = "GET"
|
||||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||||
|
|
||||||
let (data, _) = try await session.data(for: request)
|
let (data, _) = try await session.data(for: request)
|
||||||
|
|
||||||
let decoder = JSONDecoder()
|
let decoder = JSONDecoder()
|
||||||
decoder.dateDecodingStrategy = .iso8601
|
decoder.dateDecodingStrategy = .iso8601
|
||||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
|
|
||||||
return try decoder.decode(SnAccount.self, from: data)
|
return try decoder.decode(SnAccount.self, from: data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchAccountStatus(token: String, serverUrl: String) async throws -> SnAccountStatus? {
|
func fetchAccountStatus(token: String, serverUrl: String) async throws -> SnAccountStatus? {
|
||||||
guard let baseURL = URL(string: serverUrl) else {
|
guard let baseURL = URL(string: serverUrl) else {
|
||||||
throw URLError(.badURL)
|
throw URLError(.badURL)
|
||||||
}
|
}
|
||||||
let url = baseURL.appendingPathComponent("/pass/accounts/me/statuses")
|
let url = baseURL.appendingPathComponent("/pass/accounts/me/statuses")
|
||||||
|
|
||||||
var request = URLRequest(url: url)
|
var request = URLRequest(url: url)
|
||||||
request.httpMethod = "GET"
|
request.httpMethod = "GET"
|
||||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||||
|
|
||||||
let (data, response) = try await session.data(for: request)
|
let (data, response) = try await session.data(for: request)
|
||||||
|
|
||||||
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 404 {
|
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 404 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
let decoder = JSONDecoder()
|
let decoder = JSONDecoder()
|
||||||
decoder.dateDecodingStrategy = .iso8601
|
decoder.dateDecodingStrategy = .iso8601
|
||||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
|
|
||||||
return try decoder.decode(SnAccountStatus.self, from: data)
|
return try decoder.decode(SnAccountStatus.self, from: data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createOrUpdateStatus(attitude: Int, isInvisible: Bool, isNotDisturb: Bool, label: String?, token: String, serverUrl: String) async throws -> SnAccountStatus {
|
func createOrUpdateStatus(attitude: Int, isInvisible: Bool, isNotDisturb: Bool, label: String?, token: String, serverUrl: String) async throws -> SnAccountStatus {
|
||||||
// Check if there's already a customized status
|
// Check if there's already a customized status
|
||||||
let existingStatus = try? await fetchAccountStatus(token: token, serverUrl: serverUrl)
|
let existingStatus = try? await fetchAccountStatus(token: token, serverUrl: serverUrl)
|
||||||
let method = (existingStatus?.isCustomized == true) ? "PATCH" : "POST"
|
let method = (existingStatus?.isCustomized == true) ? "PATCH" : "POST"
|
||||||
|
|
||||||
guard let baseURL = URL(string: serverUrl) else {
|
guard let baseURL = URL(string: serverUrl) else {
|
||||||
throw URLError(.badURL)
|
throw URLError(.badURL)
|
||||||
}
|
}
|
||||||
let url = baseURL.appendingPathComponent("/pass/accounts/me/statuses")
|
let url = baseURL.appendingPathComponent("/pass/accounts/me/statuses")
|
||||||
|
|
||||||
var request = URLRequest(url: url)
|
var request = URLRequest(url: url)
|
||||||
request.httpMethod = method
|
request.httpMethod = method
|
||||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||||
|
|
||||||
var body: [String: Any] = [
|
var body: [String: Any] = [
|
||||||
"attitude": attitude,
|
"attitude": attitude,
|
||||||
"is_invisible": isInvisible,
|
"is_invisible": isInvisible,
|
||||||
"is_not_disturb": isNotDisturb
|
"is_not_disturb": isNotDisturb
|
||||||
]
|
]
|
||||||
|
|
||||||
if let label = label, !label.isEmpty {
|
if let label = label, !label.isEmpty {
|
||||||
body["label"] = label
|
body["label"] = label
|
||||||
}
|
}
|
||||||
|
|
||||||
request.httpBody = try JSONSerialization.data(withJSONObject: body)
|
request.httpBody = try JSONSerialization.data(withJSONObject: body)
|
||||||
|
|
||||||
let (data, response) = try await session.data(for: request)
|
let (data, response) = try await session.data(for: request)
|
||||||
|
|
||||||
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 201 && httpResponse.statusCode != 200 {
|
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 201 && httpResponse.statusCode != 200 {
|
||||||
let responseBody = String(data: data, encoding: .utf8) ?? ""
|
let responseBody = String(data: data, encoding: .utf8) ?? ""
|
||||||
print("[watchOS] createOrUpdateStatus failed with status code: \(httpResponse.statusCode), body: \(responseBody)")
|
print("[watchOS] createOrUpdateStatus failed with status code: \(httpResponse.statusCode), body: \(responseBody)")
|
||||||
throw URLError(URLError.Code(rawValue: httpResponse.statusCode))
|
throw URLError(URLError.Code(rawValue: httpResponse.statusCode))
|
||||||
}
|
}
|
||||||
|
|
||||||
let decoder = JSONDecoder()
|
let decoder = JSONDecoder()
|
||||||
decoder.dateDecodingStrategy = .iso8601
|
decoder.dateDecodingStrategy = .iso8601
|
||||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
|
|
||||||
return try decoder.decode(SnAccountStatus.self, from: data)
|
return try decoder.decode(SnAccountStatus.self, from: data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func clearStatus(token: String, serverUrl: String) async throws {
|
func clearStatus(token: String, serverUrl: String) async throws {
|
||||||
guard let baseURL = URL(string: serverUrl) else {
|
guard let baseURL = URL(string: serverUrl) else {
|
||||||
throw URLError(.badURL)
|
throw URLError(.badURL)
|
||||||
}
|
}
|
||||||
let url = baseURL.appendingPathComponent("/pass/accounts/me/statuses")
|
let url = baseURL.appendingPathComponent("/pass/accounts/me/statuses")
|
||||||
|
|
||||||
var request = URLRequest(url: url)
|
var request = URLRequest(url: url)
|
||||||
request.httpMethod = "DELETE"
|
request.httpMethod = "DELETE"
|
||||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||||
|
|
||||||
let (data, response) = try await session.data(for: request)
|
let (data, response) = try await session.data(for: request)
|
||||||
|
|
||||||
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 204 {
|
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 204 {
|
||||||
let responseBody = String(data: data, encoding: .utf8) ?? ""
|
let responseBody = String(data: data, encoding: .utf8) ?? ""
|
||||||
print("[watchOS] clearStatus failed with status code: \(httpResponse.statusCode), body: \(responseBody)")
|
print("[watchOS] clearStatus failed with status code: \(httpResponse.statusCode), body: \(responseBody)")
|
||||||
throw URLError(URLError.Code(rawValue: httpResponse.statusCode))
|
throw URLError(URLError.Code(rawValue: httpResponse.statusCode))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Chat API Methods
|
// MARK: - Chat API Methods
|
||||||
|
|
||||||
func fetchChatRooms(token: String, serverUrl: String) async throws -> ChatRoomsResponse {
|
func fetchChatRooms(token: String, serverUrl: String) async throws -> ChatRoomsResponse {
|
||||||
guard let baseURL = URL(string: serverUrl) else {
|
guard let baseURL = URL(string: serverUrl) else {
|
||||||
throw URLError(.badURL)
|
throw URLError(.badURL)
|
||||||
}
|
}
|
||||||
let url = baseURL.appendingPathComponent("/sphere/chat")
|
let url = baseURL.appendingPathComponent("/sphere/chat")
|
||||||
|
|
||||||
var request = URLRequest(url: url)
|
var request = URLRequest(url: url)
|
||||||
request.httpMethod = "GET"
|
request.httpMethod = "GET"
|
||||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||||
|
|
||||||
let (data, _) = try await session.data(for: request)
|
let (data, _) = try await session.data(for: request)
|
||||||
|
|
||||||
let decoder = JSONDecoder()
|
let decoder = JSONDecoder()
|
||||||
decoder.dateDecodingStrategy = .iso8601
|
decoder.dateDecodingStrategy = .iso8601
|
||||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
|
|
||||||
let rooms = try decoder.decode([SnChatRoom].self, from: data)
|
let rooms = try decoder.decode([SnChatRoom].self, from: data)
|
||||||
return ChatRoomsResponse(rooms: rooms)
|
return ChatRoomsResponse(rooms: rooms)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchChatRoom(identifier: String, token: String, serverUrl: String) async throws -> SnChatRoom {
|
func fetchChatRoom(identifier: String, token: String, serverUrl: String) async throws -> SnChatRoom {
|
||||||
guard let baseURL = URL(string: serverUrl) else {
|
guard let baseURL = URL(string: serverUrl) else {
|
||||||
throw URLError(.badURL)
|
throw URLError(.badURL)
|
||||||
}
|
}
|
||||||
let url = baseURL.appendingPathComponent("/sphere/chat/\(identifier)")
|
let url = baseURL.appendingPathComponent("/sphere/chat/\(identifier)")
|
||||||
|
|
||||||
var request = URLRequest(url: url)
|
var request = URLRequest(url: url)
|
||||||
request.httpMethod = "GET"
|
request.httpMethod = "GET"
|
||||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||||
|
|
||||||
let (data, response) = try await session.data(for: request)
|
let (data, response) = try await session.data(for: request)
|
||||||
|
|
||||||
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 404 {
|
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 404 {
|
||||||
throw URLError(.resourceUnavailable)
|
throw URLError(.resourceUnavailable)
|
||||||
}
|
}
|
||||||
|
|
||||||
let decoder = JSONDecoder()
|
let decoder = JSONDecoder()
|
||||||
decoder.dateDecodingStrategy = .iso8601
|
decoder.dateDecodingStrategy = .iso8601
|
||||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
|
|
||||||
return try decoder.decode(SnChatRoom.self, from: data)
|
return try decoder.decode(SnChatRoom.self, from: data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchChatInvites(token: String, serverUrl: String) async throws -> ChatInvitesResponse {
|
func fetchChatInvites(token: String, serverUrl: String) async throws -> ChatInvitesResponse {
|
||||||
guard let baseURL = URL(string: serverUrl) else {
|
guard let baseURL = URL(string: serverUrl) else {
|
||||||
throw URLError(.badURL)
|
throw URLError(.badURL)
|
||||||
}
|
}
|
||||||
let url = baseURL.appendingPathComponent("/sphere/chat/invites")
|
let url = baseURL.appendingPathComponent("/sphere/chat/invites")
|
||||||
|
|
||||||
var request = URLRequest(url: url)
|
var request = URLRequest(url: url)
|
||||||
request.httpMethod = "GET"
|
request.httpMethod = "GET"
|
||||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||||
|
|
||||||
let (data, _) = try await session.data(for: request)
|
let (data, _) = try await session.data(for: request)
|
||||||
|
|
||||||
let decoder = JSONDecoder()
|
let decoder = JSONDecoder()
|
||||||
decoder.dateDecodingStrategy = .iso8601
|
decoder.dateDecodingStrategy = .iso8601
|
||||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
|
|
||||||
let invites = try decoder.decode([SnChatMember].self, from: data)
|
let invites = try decoder.decode([SnChatMember].self, from: data)
|
||||||
return ChatInvitesResponse(invites: invites)
|
return ChatInvitesResponse(invites: invites)
|
||||||
}
|
}
|
||||||
|
|
||||||
func acceptChatInvite(chatRoomId: String, token: String, serverUrl: String) async throws {
|
func acceptChatInvite(chatRoomId: String, token: String, serverUrl: String) async throws {
|
||||||
guard let baseURL = URL(string: serverUrl) else {
|
guard let baseURL = URL(string: serverUrl) else {
|
||||||
throw URLError(.badURL)
|
throw URLError(.badURL)
|
||||||
}
|
}
|
||||||
let url = baseURL.appendingPathComponent("/sphere/chat/invites/\(chatRoomId)/accept")
|
let url = baseURL.appendingPathComponent("/sphere/chat/invites/\(chatRoomId)/accept")
|
||||||
|
|
||||||
var request = URLRequest(url: url)
|
var request = URLRequest(url: url)
|
||||||
request.httpMethod = "POST"
|
request.httpMethod = "POST"
|
||||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||||
|
|
||||||
let (data, response) = try await session.data(for: request)
|
let (data, response) = try await session.data(for: request)
|
||||||
|
|
||||||
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 {
|
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 {
|
||||||
let responseBody = String(data: data, encoding: .utf8) ?? ""
|
let responseBody = String(data: data, encoding: .utf8) ?? ""
|
||||||
print("[watchOS] acceptChatInvite failed with status code: \(httpResponse.statusCode), body: \(responseBody)")
|
print("[watchOS] acceptChatInvite failed with status code: \(httpResponse.statusCode), body: \(responseBody)")
|
||||||
throw URLError(URLError.Code(rawValue: httpResponse.statusCode))
|
throw URLError(URLError.Code(rawValue: httpResponse.statusCode))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func declineChatInvite(chatRoomId: String, token: String, serverUrl: String) async throws {
|
func declineChatInvite(chatRoomId: String, token: String, serverUrl: String) async throws {
|
||||||
guard let baseURL = URL(string: serverUrl) else {
|
guard let baseURL = URL(string: serverUrl) else {
|
||||||
throw URLError(.badURL)
|
throw URLError(.badURL)
|
||||||
}
|
}
|
||||||
let url = baseURL.appendingPathComponent("/sphere/chat/invites/\(chatRoomId)/decline")
|
let url = baseURL.appendingPathComponent("/sphere/chat/invites/\(chatRoomId)/decline")
|
||||||
|
|
||||||
var request = URLRequest(url: url)
|
var request = URLRequest(url: url)
|
||||||
request.httpMethod = "POST"
|
request.httpMethod = "POST"
|
||||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||||
|
|
||||||
let (data, response) = try await session.data(for: request)
|
let (data, response) = try await session.data(for: request)
|
||||||
|
|
||||||
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 {
|
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 {
|
||||||
let responseBody = String(data: data, encoding: .utf8) ?? ""
|
let responseBody = String(data: data, encoding: .utf8) ?? ""
|
||||||
print("[watchOS] declineChatInvite failed with status code: \(httpResponse.statusCode), body: \(responseBody)")
|
print("[watchOS] declineChatInvite failed with status code: \(httpResponse.statusCode), body: \(responseBody)")
|
||||||
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