mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
00f08ab2dc
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
135 lines
4.9 KiB
Swift
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
|
|
}
|
|
}
|