mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
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
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user