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