mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 19:44:05 +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,92 @@
|
||||
//
|
||||
// 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 user's profile including the HealthChat extended profile fields (MSC4133),
|
||||
/// as returned by `GET /_matrix/client/v3/profile/{userId}`.
|
||||
nonisolated struct ExtendedProfile: Equatable {
|
||||
let displayName: String?
|
||||
let avatarURL: URL?
|
||||
let title: String?
|
||||
let firstName: String?
|
||||
let lastName: String?
|
||||
let specialty: String?
|
||||
let practiceName: String?
|
||||
let furtherInfo: String?
|
||||
let businessEmail: String?
|
||||
let businessTel: String?
|
||||
let street: String?
|
||||
let houseNumber: String?
|
||||
let postalCode: String?
|
||||
let city: String?
|
||||
let website: String?
|
||||
|
||||
/// Whether any of the HealthChat-specific fields carry a value.
|
||||
var hasContent: Bool {
|
||||
[title, firstName, lastName, specialty, practiceName, furtherInfo,
|
||||
businessEmail, businessTel, street, houseNumber, postalCode, city, website]
|
||||
.contains { $0 != nil }
|
||||
}
|
||||
|
||||
/// The postal address in the same "street nr, plz city" format the web client uses.
|
||||
var formattedAddress: String? {
|
||||
let streetLine = [street, houseNumber].compactMap { $0 }.joined(separator: " ")
|
||||
let cityLine = [postalCode, city].compactMap { $0 }.joined(separator: " ")
|
||||
let lines = [streetLine, cityLine].filter { !$0.isEmpty }
|
||||
return lines.isEmpty ? nil : lines.joined(separator: ", ")
|
||||
}
|
||||
}
|
||||
|
||||
extension ExtendedProfile: Decodable {
|
||||
private struct StringKey: CodingKey {
|
||||
let stringValue: String
|
||||
var intValue: Int? {
|
||||
nil
|
||||
}
|
||||
|
||||
init(stringValue: String) {
|
||||
self.stringValue = stringValue
|
||||
}
|
||||
|
||||
init?(intValue: Int) {
|
||||
nil
|
||||
}
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: StringKey.self)
|
||||
|
||||
// Presence of the canonical key suppresses the legacy key even when its value is
|
||||
// empty, so a field the user cleared isn't resurrected by stale legacy data.
|
||||
// Empty and non-string values read as nil.
|
||||
func value(_ canonicalKey: String, legacyKey: String? = nil) -> String? {
|
||||
for key in [canonicalKey, legacyKey].compactMap({ $0 }) {
|
||||
guard container.contains(StringKey(stringValue: key)) else { continue }
|
||||
let raw = try? container.decode(String.self, forKey: .init(stringValue: key))
|
||||
return raw.flatMap { $0.isEmpty ? nil : $0 }
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
displayName = value("displayname")
|
||||
avatarURL = value("avatar_url").flatMap(URL.init(string:))
|
||||
title = value("io.element.profile.title")
|
||||
firstName = value("io.element.profile.first_name")
|
||||
lastName = value("io.element.profile.last_name")
|
||||
specialty = value("io.element.profile.specialization")
|
||||
practiceName = value("io.element.profile.practice_name")
|
||||
furtherInfo = value("io.element.profile.further_info")
|
||||
businessEmail = value("io.element.profile.business_email")
|
||||
businessTel = value("io.element.profile.business_tel")
|
||||
street = value("io.element.profile.business_street", legacyKey: "io.element.profile.street")
|
||||
houseNumber = value("io.element.profile.business_street_nr", legacyKey: "io.element.profile.house_number")
|
||||
postalCode = value("io.element.profile.business_plz", legacyKey: "io.element.profile.postal_code")
|
||||
city = value("io.element.profile.business_city")
|
||||
website = value("io.element.profile.website")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user