Files
element-x-ios/ElementX/Sources/Services/HealthChat/HealthChatAPIModels.swift
joel.poyet 00f08ab2dc Add HealthChat API and extended profile services (#9)
Expose OpenID tokens and homeserver credentials from the client proxy, and
add fork-only services for the HealthChat member directory backend
(api.healthchat.ch) plus a service that reads MSC4133 extended profile
fields directly from the homeserver. Groundwork for the members tab and
the profile screens.

Claude-Session: https://claude.ai/code/session_013qdTbAWsZj2quQzEkagXaS
2026-09-22 18:21:58 +02:00

135 lines
4.9 KiB
Swift

//
// 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
}
}