mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-10-31 08:15:24 +00:00
e5bfc3a26d
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
93 lines
3.6 KiB
Swift
93 lines
3.6 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 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")
|
|
}
|
|
}
|