// // Copyright 2026 Element Creations Ltd. // // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. // Please see LICENSE files in the repository root for full details. // import Foundation /// A member record as returned by the HealthChat API (`MatrixUser` in the backend). /// Every field except the user ID is optional so additive backend changes never break decoding. nonisolated struct HealthChatMember: Identifiable, Equatable { let userID: String let displayName: String? let avatarURL: URL? let title: String? let firstName: String? let lastName: String? let specialty: String? let practiceName: String? let businessCity: String? let businessEmail: String? let businessTel: String? let furtherInfo: String? let street: String? let houseNumber: String? let postalCode: String? let website: String? let isApproved: Bool var id: String { userID } } extension HealthChatMember: Decodable { private enum CodingKeys: String, CodingKey { case userID = "userId" case avatarURL = "avatarMxcUrl" case displayName, title, firstName, lastName, specialty, practiceName case businessCity, businessEmail, businessTel, furtherInfo case street, houseNumber, postalCode, website, isApproved } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) func value(_ key: CodingKeys) -> String? { let raw = try? container.decodeIfPresent(String.self, forKey: key) return (raw ?? nil).flatMap { $0.isEmpty ? nil : $0 } } userID = try container.decode(String.self, forKey: .userID) displayName = value(.displayName) avatarURL = value(.avatarURL).flatMap(URL.init(string:)) title = value(.title) firstName = value(.firstName) lastName = value(.lastName) specialty = value(.specialty) practiceName = value(.practiceName) businessCity = value(.businessCity) businessEmail = value(.businessEmail) businessTel = value(.businessTel) furtherInfo = value(.furtherInfo) street = value(.street) houseNumber = value(.houseNumber) postalCode = value(.postalCode) website = value(.website) isApproved = try container.decodeIfPresent(Bool.self, forKey: .isApproved) ?? true } } /// The response of `GET /api/usermatrix/users` (`MatrixUserSearchResult` in the backend). nonisolated struct HealthChatMembersResponse: Equatable { let users: [HealthChatMember] let isApproved: Bool /// The hint shown to accounts that haven't been approved yet. let pendingMessage: String? } extension HealthChatMembersResponse: Decodable { private enum CodingKeys: String, CodingKey { case users, isApproved, pendingMessage } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) users = try container.decodeIfPresent([HealthChatMember].self, forKey: .users) ?? [] isApproved = try container.decodeIfPresent(Bool.self, forKey: .isApproved) ?? true let message = try container.decodeIfPresent(String.self, forKey: .pendingMessage) pendingMessage = message.flatMap { $0.isEmpty ? nil : $0 } } } /// A partial profile update for `PUT /api/usermatrix/profile` (`UpdateProfileRequest` in the backend). /// `nil` leaves a field unchanged, an empty string clears it. nonisolated struct HealthChatProfileUpdate: Encodable, Equatable { var firstName: String? var lastName: String? var title: String? var specialty: String? var practiceName: String? var businessCity: String? var businessEmail: String? var businessTel: String? var furtherInfo: String? var street: String? var houseNumber: String? var postalCode: String? var website: String? } /// The response of `GET /api/usermatrix/profile/completeness`. nonisolated struct HealthChatProfileCompleteness: Equatable { let isComplete: Bool let missingFields: [String] let message: String? let hasProfilePhoto: Bool } extension HealthChatProfileCompleteness: Decodable { private enum CodingKeys: String, CodingKey { case isComplete, missingFields, message, hasProfilePhoto } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) isComplete = try container.decodeIfPresent(Bool.self, forKey: .isComplete) ?? false missingFields = try container.decodeIfPresent([String].self, forKey: .missingFields) ?? [] let rawMessage = try container.decodeIfPresent(String.self, forKey: .message) message = rawMessage.flatMap { $0.isEmpty ? nil : $0 } hasProfilePhoto = try container.decodeIfPresent(Bool.self, forKey: .hasProfilePhoto) ?? false } }