diff --git a/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift b/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift index 7aca4abe4..2e518daba 100644 --- a/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift +++ b/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift @@ -191,6 +191,10 @@ extension AccessibilityTests { try await performAccessibilityAudit(named: "GalleryRoomTimelineView_Previews") } + func testHealthChatExtendedProfileSection() async throws { + try await performAccessibilityAudit(named: "HealthChatExtendedProfileSection_Previews") + } + func testHighlightedTimelineItemModifier() async throws { try await performAccessibilityAudit(named: "HighlightedTimelineItemModifier_Previews") } diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 90b550022..686ebb631 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -291,6 +291,7 @@ 2CC3F27CD76DB00A747BEA6C /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD5523BDEDB247E29228476 /* AppSettings.swift */; }; 2D086938A67869C2C75B6E28 /* MapLibreShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AB4CF512176C4115052BA0E /* MapLibreShim.swift */; }; 2D0E3983288E2D35613AD681 /* SecureBackupControllerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AB29A2D95D3469B5F016655 /* SecureBackupControllerMock.swift */; }; + 2D23BAA9221F9A44EB28147C /* HealthChat.strings in Resources */ = {isa = PBXBuildFile; fileRef = 641A03A914D002082B17DAA2 /* HealthChat.strings */; }; 2D2D8A53B35BE8D8A01449C6 /* PinnedEventsBannerStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FA38E813BE14149F173F461 /* PinnedEventsBannerStateTests.swift */; }; 2D38D39B1789B91AE69F477F /* PhotoLibraryManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD955A0380C287C418F1A74D /* PhotoLibraryManagerMock.swift */; }; 2D45A04699BB6BA3B3A0CB9A /* TracingHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A95C9B8299A36A6495DECA6 /* TracingHook.swift */; }; @@ -403,6 +404,7 @@ 407DCE030E0F9B7C9861D38A /* KZFileWatchers in Frameworks */ = {isa = PBXBuildFile; productRef = 81DB3AB6CE996AB3954F4F03 /* KZFileWatchers */; }; 40839665086F5DAFA55D0EED /* AnalyticsConsentState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 353024006CB726E9F9187B3A /* AnalyticsConsentState.swift */; }; 414F50CFCFEEE2611127DCFB /* RestorationToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3558A15CFB934F9229301527 /* RestorationToken.swift */; }; + 4174B9D073774F9BBA1801FA /* Strings+HealthChat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AA3E050B96A797707BACD2 /* Strings+HealthChat.swift */; }; 41B72420FE80DCD1FD115D47 /* ExtendedProfileService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5A1F965895E27CAEA93B58A /* ExtendedProfileService.swift */; }; 41B84DF733BE6CB0180D1CC1 /* NotificationContentBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F0AF1A13D358CA0BDA6065F /* NotificationContentBuilderTests.swift */; }; 41C5DA0C06F30311A221E85B /* ClientSDKMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EAF4A49F3ACD8BB8B0D2371 /* ClientSDKMock.swift */; }; @@ -610,6 +612,7 @@ 6298AB0906DDD3525CD78C6B /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 0DD568A494247444A4B56031 /* Kingfisher */; }; 62A7FC3A0191BC7181AA432B /* AudioRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 907FA4DE17DEA1A3738EFB83 /* AudioRecorder.swift */; }; 62C5876C4254C58C2086F0DE /* HomeScreenContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3B4B58B79A6FA250B24A1EC /* HomeScreenContent.swift */; }; + 630CC29D639086CB1F88C74E /* HealthChatExtendedProfileSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 687D3C6697703342512AF6AF /* HealthChatExtendedProfileSection.swift */; }; 633400018E07D2DC7175B16E /* LiveLocationShareProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA12A7F5EF5C6D0B992869ED /* LiveLocationShareProxy.swift */; }; 633501761094E09DFBEBFFAD /* CopyTextButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B682FE2C44C5E163E7023B05 /* CopyTextButton.swift */; }; 6386EA3C898AD1A4BC1DC8A5 /* TimelineMediaPreviewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FD40B92FCF20165658296AD /* TimelineMediaPreviewModifier.swift */; }; @@ -698,6 +701,7 @@ 6E47D126DD7585E8F8237CE7 /* LoadableAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B590BD4507D4F0A377FDE01A /* LoadableAvatarImage.swift */; }; 6E4E401BE97AC241DA7C7716 /* AppLockSetupSettingsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 502F986D57158674172C58E3 /* AppLockSetupSettingsScreenModels.swift */; }; 6E63704717F17593A475D152 /* RoomNotificationSettingsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA14564EE143F73F7E4D1F79 /* RoomNotificationSettingsScreenModels.swift */; }; + 6E6F688AF2A9BBAB2E61E18C /* ExtendedProfileSectionHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC2AAA62D6EE2520CE2036F0 /* ExtendedProfileSectionHook.swift */; }; 6EB46C92ECFEAE71959D91D2 /* TimelineReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C25B6EBEB414431187D73B7 /* TimelineReplyView.swift */; }; 6EC7A40A537CFB3D526A111C /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47EBB5D698CE9A25BB553A2D /* Strings.swift */; }; 6F26CBC84AE87EB4068D398B /* LRUCache in Frameworks */ = {isa = PBXBuildFile; productRef = 78B28D75FF7AF8E6146DEE2A /* LRUCache */; }; @@ -990,6 +994,7 @@ 9912F9EB2D6589141A2957B4 /* AppLockScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C44BBC892499BE45B074F89 /* AppLockScreenCoordinator.swift */; }; 992F5E750F5030C4BA2D0D03 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 01C4C7DB37597D7D8379511A /* Assets.xcassets */; }; 99C463F12F89C99C77D02077 /* DeactivateAccountScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03D7ECAC68C2FFB8CF01BCB /* DeactivateAccountScreen.swift */; }; + 99DE2B618703A58BC3303B92 /* HealthChatExtendedProfileLoaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB7ABDCA2059497899F226DF /* HealthChatExtendedProfileLoaderTests.swift */; }; 99ED42B8F8D6BFB1DBCF4C45 /* AnalyticsEvents in Frameworks */ = {isa = PBXBuildFile; productRef = D661CAB418C075A94306A792 /* AnalyticsEvents */; }; 9A0326D2375075871D2AB537 /* ResolveVerifiedUserSendFailureScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 574CB70E82D7EAEA538E4135 /* ResolveVerifiedUserSendFailureScreenViewModel.swift */; }; 9A3B0CDF097E3838FB1B9595 /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E89E530A8E92EC44301CA1 /* Bundle.swift */; }; @@ -1119,6 +1124,7 @@ AE1160076F663BF14E0E893A /* EffectsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4548A9BDE5CB3AB864BCA9F /* EffectsView.swift */; }; AE1A73B24D63DA3D63DC4EE3 /* SessionVerificationControllerProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 248649EBA5BC33DB93698734 /* SessionVerificationControllerProxyMock.swift */; }; AE5AAD9E32511544FDFA5560 /* WindowManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06F27F588F9059128E17C669 /* WindowManagerProtocol.swift */; }; + AEDBB75C62E53A9DAA32DFE1 /* HealthChatExtendedProfileLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89310978E1570B02FF4E4A3 /* HealthChatExtendedProfileLoader.swift */; }; AF19D65A9C60C6B2646F3210 /* RedactedRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6E6BDF9D26DB05C88901416 /* RedactedRoomTimelineItem.swift */; }; AF2ABA2794E376B64104C964 /* MockSoftLogoutScreenState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5644919DB2022397D9D5825A /* MockSoftLogoutScreenState.swift */; }; AF33B9044498211C3D82F1E1 /* UNTextInputNotificationResponse+Creator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 130ED565A078F7E0B59D9D25 /* UNTextInputNotificationResponse+Creator.swift */; }; @@ -1595,6 +1601,7 @@ FA5A7E32B1920FCB4EEDC1BA /* RoomDetailsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6493AC9979CEB1410302BFE3 /* RoomDetailsScreenCoordinator.swift */; }; FA71CD334F2D2289BEF0D749 /* SecureBackupRecoveryKeyScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A2FCA3D0F239B9E911B966B /* SecureBackupRecoveryKeyScreen.swift */; }; FA9C427FFB11B1AA2DCC5602 /* RoomProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */; }; + FAA41D6957FC5BA357092D6D /* HealthChatExtendedProfileSectionHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD3C983BFE38789B9C5EA7A5 /* HealthChatExtendedProfileSectionHook.swift */; }; FB595EC9C00AB32F39034055 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A37E2FACFD041CE466223CD /* SceneDelegate.swift */; }; FBD402E3170EB1ED0D1AA672 /* EncryptionKeyProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2355398E4A55DA5A89128AD1 /* EncryptionKeyProvider.swift */; }; FBF09B6C900415800DDF2A21 /* EmojiProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C113E0CB7E15E9765B1817A /* EmojiProvider.swift */; }; @@ -2383,6 +2390,7 @@ 68010886142843705E342645 /* ProgressMaskModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressMaskModifier.swift; sourceTree = ""; }; 682BC7BAF0EFEF512A8C5140 /* AuthenticationStartScreenBackgroundImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationStartScreenBackgroundImage.swift; sourceTree = ""; }; 6861FE915C7B5466E6962BBA /* StartChatScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreen.swift; sourceTree = ""; }; + 687D3C6697703342512AF6AF /* HealthChatExtendedProfileSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthChatExtendedProfileSection.swift; sourceTree = ""; }; 6885D1340AA9C76063E26868 /* LinkMetadataProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkMetadataProviderProtocol.swift; sourceTree = ""; }; 68F8744C32ACA0821A771EBB /* test_audio.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = test_audio.mp3; sourceTree = ""; }; 690915157A874D1F21ADE70C /* preview_avatar_room.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = preview_avatar_room.jpg; sourceTree = ""; }; @@ -2670,6 +2678,7 @@ 98784280D98C852727BE0111 /* AuthenticationStartLogo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationStartLogo.swift; sourceTree = ""; }; 989D7380D9C86B3A10D30B13 /* AppLockSetupPINScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupPINScreenViewModelTests.swift; sourceTree = ""; }; 989FC684408B31A677F5538B /* CompletionSuggestionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletionSuggestionView.swift; sourceTree = ""; }; + 98AA3E050B96A797707BACD2 /* Strings+HealthChat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Strings+HealthChat.swift"; sourceTree = ""; }; 98ABC939BC8F08CA3E967D6C /* JoinCallButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinCallButton.swift; sourceTree = ""; }; 98C6A082F2B2A15E1B9BE280 /* TimelineItemThreadSummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemThreadSummary.swift; sourceTree = ""; }; 98F4DFFFC62187D9A4D2030D /* ClassicAppAccountManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassicAppAccountManager.swift; sourceTree = ""; }; @@ -2683,6 +2692,7 @@ 9A34D9BCA1A7D9A56E1EAF1D /* ManageRoomMemberSheetViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageRoomMemberSheetViewModel.swift; sourceTree = ""; }; 9A68BCE6438873D2661D93D0 /* BugReportServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportServiceProtocol.swift; sourceTree = ""; }; 9AA3AF94A06D319BB37E52DA /* TimelineItemFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemFactoryTests.swift; sourceTree = ""; }; + 9B3711C137F33EC1F8BEAB10 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/HealthChat.strings; sourceTree = ""; }; 9B6572E6EF5D5F4B0C338A40 /* PinnedEventsTimelineScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedEventsTimelineScreenModels.swift; sourceTree = ""; }; 9B663BE498BB39EADC24025D /* SettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenModels.swift; sourceTree = ""; }; 9B67DF223EEB8DCAF178A1D4 /* AnalyticsPromptScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptScreenCoordinator.swift; sourceTree = ""; }; @@ -2784,6 +2794,7 @@ ABF84AA68B2B7584D9275769 /* VoiceMessageTrashButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageTrashButton.swift; sourceTree = ""; }; AC0275CEE9CA078B34028BDF /* AppLockScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenViewModelTests.swift; sourceTree = ""; }; AC1DA29A5A041CC0BACA7CB0 /* MockImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockImageCache.swift; sourceTree = ""; }; + AC2AAA62D6EE2520CE2036F0 /* ExtendedProfileSectionHook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtendedProfileSectionHook.swift; sourceTree = ""; }; AC43313F21511C853D34544E /* SoftLogoutScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutScreenViewModelTests.swift; sourceTree = ""; }; AC4F10BDD56FA77FEC742333 /* VoiceMessageMediaManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageMediaManagerTests.swift; sourceTree = ""; }; AC5F5209279A752D98AAC4B2 /* CollapsibleFlowLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleFlowLayoutTests.swift; sourceTree = ""; }; @@ -2794,6 +2805,7 @@ AD0FF64B0E6470F66F42E182 /* EstimatedWaveformView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EstimatedWaveformView.swift; sourceTree = ""; }; AD3521DF92391551E02004D8 /* VoiceMessageMediaEventsTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageMediaEventsTimelineView.swift; sourceTree = ""; }; AD378D580A41E42560C60E9C /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/Localizable.strings; sourceTree = ""; }; + AD3C983BFE38789B9C5EA7A5 /* HealthChatExtendedProfileSectionHook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthChatExtendedProfileSectionHook.swift; sourceTree = ""; }; AD529C89924EE32CE307F36F /* VisualListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualListItem.swift; sourceTree = ""; }; AD558A898847C179E4B7A237 /* SecureBackupKeyBackupScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupKeyBackupScreen.swift; sourceTree = ""; }; AD6B522BD637845AB9570B10 /* RoomNotificationSettingsProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsProxy.swift; sourceTree = ""; }; @@ -2889,6 +2901,7 @@ BB284643AF7AB131E307DCE0 /* AudioSessionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSessionProtocol.swift; sourceTree = ""; }; BB5B00A014307CE37B2812CD /* TimelineViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineViewModelProtocol.swift; sourceTree = ""; }; BB6ED50FE104992419310EEB /* NotificationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationHandler.swift; sourceTree = ""; }; + BB7ABDCA2059497899F226DF /* HealthChatExtendedProfileLoaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthChatExtendedProfileLoaderTests.swift; sourceTree = ""; }; BB8BC4C791D0E88CFCF4E5DF /* ServerSelectionScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenCoordinator.swift; sourceTree = ""; }; BBEC57C204D77908E355EF42 /* AudioRecorderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecorderProtocol.swift; sourceTree = ""; }; BC2E7CEE6E9314FD69FE0ED9 /* ThreadTimelineScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadTimelineScreen.swift; sourceTree = ""; }; @@ -2922,6 +2935,7 @@ C11397904D19CFF0E3689F0E /* SpaceScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceScreenModels.swift; sourceTree = ""; }; C142248014E08E885E323E56 /* Avatars.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Avatars.swift; sourceTree = ""; }; C1511766C534367700C8DD75 /* RoomNotificationModeProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationModeProxy.swift; sourceTree = ""; }; + C1675D38277D7DDF3C3BFAA6 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/HealthChat.strings; sourceTree = ""; }; C1FA515B3B0D61EF1E907D2D /* BadgeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeView.swift; sourceTree = ""; }; C258C9C815272911A5B132C3 /* FormattedBodyText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormattedBodyText.swift; sourceTree = ""; }; C2886615BEBAE33A0AA4D5F8 /* RoomScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenModels.swift; sourceTree = ""; }; @@ -3249,8 +3263,10 @@ F70373EF23A73181D2568D79 /* SyncServiceBuilderSDKMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncServiceBuilderSDKMock.swift; sourceTree = ""; }; F72EFC8C634469F9262659C7 /* NSItemProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSItemProvider.swift; sourceTree = ""; }; F78B4E56DBFFD4A7A39D10F5 /* AudioPlaybackSpeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlaybackSpeed.swift; sourceTree = ""; }; + F7B3715209CBD8D7CF4FDA1D /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/HealthChat.strings; sourceTree = ""; }; F7E247E29C7A0E421DB839D7 /* RoomPowerLevel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPowerLevel.swift; sourceTree = ""; }; F875D71347DC81EAE7687446 /* NavigationRootCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRootCoordinatorTests.swift; sourceTree = ""; }; + F89310978E1570B02FF4E4A3 /* HealthChatExtendedProfileLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthChatExtendedProfileLoader.swift; sourceTree = ""; }; F899D02CF26EA7675EEBE74C /* UserSessionScreenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionScreenTests.swift; sourceTree = ""; }; F8CCF9A924521DECA44778C4 /* AppLockSetupBiometricsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupBiometricsScreen.swift; sourceTree = ""; }; F91AC45CDDF85E946391CA5C /* RoomThreadListScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomThreadListScreenViewModel.swift; sourceTree = ""; }; @@ -3996,6 +4012,7 @@ B3561843E139EA7CDAED0933 /* ClientFactoryHook.swift */, 8B89D6C760E8CAE29CA28FB1 /* CompoundHook.swift */, 0F60FDB3AEFC60830BD289FE /* DeveloperOptionsScreenHook.swift */, + AC2AAA62D6EE2520CE2036F0 /* ExtendedProfileSectionHook.swift */, 04CAC77224E949D8E758E2E3 /* OAuthPresenterHook.swift */, 7AE97AD18893E2D8120F559D /* RecoveryKeyScreenHook.swift */, D5D186A6DB8FAC5C9D0E4D61 /* RemoteSettingsHook.swift */, @@ -4211,6 +4228,7 @@ children = ( 71D52BAA5BADB06E5E8C295D /* Assets.swift */, 47EBB5D698CE9A25BB553A2D /* Strings.swift */, + 98AA3E050B96A797707BACD2 /* Strings+HealthChat.swift */, B172057567E049007A5C4D92 /* Strings+SAS.swift */, 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */, ); @@ -5611,6 +5629,9 @@ E1ECC44D40D05D3A6F0DE0F7 /* HealthChatAppSettingsHook.swift */, 7A60C2712F5607406E8B1C80 /* HealthChatBrandColors.swift */, E09E2B1AB937F2D01815495F /* HealthChatCompoundHook.swift */, + F89310978E1570B02FF4E4A3 /* HealthChatExtendedProfileLoader.swift */, + 687D3C6697703342512AF6AF /* HealthChatExtendedProfileSection.swift */, + AD3C983BFE38789B9C5EA7A5 /* HealthChatExtendedProfileSectionHook.swift */, 715B60D68D20808E328DA1DA /* HealthChatSettings.swift */, ); path = HealthChat; @@ -5755,6 +5776,7 @@ 8AB4E616E6405AA5DD5A04EA /* ExtendedProfileTests.swift */, CB71F86BB7A3AC9B32845B5B /* HealthChatAPIModelsTests.swift */, 087194751572DF7CA65D1CCB /* HealthChatAPIServiceTests.swift */, + BB7ABDCA2059497899F226DF /* HealthChatExtendedProfileLoaderTests.swift */, 11E3983406311A8BBE37A19A /* HealthChatOpenIDTokenProviderTests.swift */, ); path = HealthChat; @@ -6155,6 +6177,7 @@ A0C06C0F6A8621B22BFAEB56 /* Localizations */ = { isa = PBXGroup; children = ( + 641A03A914D002082B17DAA2 /* HealthChat.strings */, 91DE43B8815918E590912DDA /* InfoPlist.strings */, 7109E709A7738E6BCC4553E6 /* Localizable.strings */, 187853A7E643995EE49FAD43 /* Localizable.stringsdict */, @@ -7992,6 +8015,7 @@ 6CBC544A7CC4DDDBDED4D0A6 /* AppIcon.icon in Resources */, 992F5E750F5030C4BA2D0D03 /* Assets.xcassets in Resources */, 2CA6ABBC9A88EB89EA52FCCB /* ConfettiScene.scn in Resources */, + 2D23BAA9221F9A44EB28147C /* HealthChat.strings in Resources */, B6DA66EFC13A90846B625836 /* InfoPlist.strings in Resources */, 5F5488FBC9CFEB6F433D74A4 /* Localizable.strings in Resources */, 0EA6537A07E2DC882AEA5962 /* Localizable.stringsdict in Resources */, @@ -8383,6 +8407,7 @@ 07756D532EFE33DD1FA258E5 /* GeoURITests.swift in Sources */, C40957497CAFADB846784A2A /* HealthChatAPIModelsTests.swift in Sources */, F995036BBA39691CF7AD1F08 /* HealthChatAPIServiceTests.swift in Sources */, + 99DE2B618703A58BC3303B92 /* HealthChatExtendedProfileLoaderTests.swift in Sources */, BC38C35A60F164A7AE3743E4 /* HealthChatOpenIDTokenProviderTests.swift in Sources */, 6817EAD73DC1FFD8B943B5B9 /* HomeScreenRoomTests.swift in Sources */, F6F49E37272AD7397CD29A01 /* HomeScreenViewModelTests.swift in Sources */, @@ -8895,6 +8920,7 @@ BED59052E5C5163D2B065CA6 /* EventTimelineItem.swift in Sources */, 63E46D18B91D08E15FC04125 /* ExpiringTaskRunner.swift in Sources */, 61A4401FF2B46AA7EF8B0CD8 /* ExtendedProfile.swift in Sources */, + 6E6F688AF2A9BBAB2E61E18C /* ExtendedProfileSectionHook.swift in Sources */, 41B72420FE80DCD1FD115D47 /* ExtendedProfileService.swift in Sources */, 8DE0906C2D4B3880D28CC8DA /* ExtendedProfileServiceProtocol.swift in Sources */, 36206F74DDEBF9BEAF6A6A1F /* ExtensionLogger.swift in Sources */, @@ -8927,6 +8953,9 @@ 665314B4CB31904F107D2D3B /* HealthChatAppSettingsHook.swift in Sources */, 6DDA932A28F531791BDBFB08 /* HealthChatBrandColors.swift in Sources */, 95C338B0F2101D8A8CA16A98 /* HealthChatCompoundHook.swift in Sources */, + AEDBB75C62E53A9DAA32DFE1 /* HealthChatExtendedProfileLoader.swift in Sources */, + 630CC29D639086CB1F88C74E /* HealthChatExtendedProfileSection.swift in Sources */, + FAA41D6957FC5BA357092D6D /* HealthChatExtendedProfileSectionHook.swift in Sources */, 4B5E2E8FBE54B73F79E607BC /* HealthChatOpenIDTokenProvider.swift in Sources */, 1E822B072359DD37EEA8CE78 /* HealthChatSettings.swift in Sources */, 0C1E537A49ABB386F7554D4A /* HighlightedTimelineItemModifier.swift in Sources */, @@ -9551,6 +9580,7 @@ 197441F1EF23A5DABACCA79F /* StickerRoomTimelineView.swift in Sources */, 715FB2D768E8919979EACD71 /* StopButton.swift in Sources */, 2F94054F50E312AF30BE07F3 /* String.swift in Sources */, + 4174B9D073774F9BBA1801FA /* Strings+HealthChat.swift in Sources */, 7640A4B412CACF15D143CCD4 /* Strings+SAS.swift in Sources */, A7D48E44D485B143AADDB77D /* Strings+Untranslated.swift in Sources */, 066A1E9B94723EE9F3038044 /* Strings.swift in Sources */, @@ -9930,6 +9960,16 @@ name = Localizable.stringsdict; sourceTree = ""; }; + 641A03A914D002082B17DAA2 /* HealthChat.strings */ = { + isa = PBXVariantGroup; + children = ( + C1675D38277D7DDF3C3BFAA6 /* de */, + 9B3711C137F33EC1F8BEAB10 /* en */, + F7B3715209CBD8D7CF4FDA1D /* fr */, + ); + name = HealthChat.strings; + sourceTree = ""; + }; 7109E709A7738E6BCC4553E6 /* Localizable.strings */ = { isa = PBXVariantGroup; children = ( diff --git a/ElementX/Resources/Localizations/de.lproj/HealthChat.strings b/ElementX/Resources/Localizations/de.lproj/HealthChat.strings new file mode 100644 index 000000000..ef7fe10bc --- /dev/null +++ b/ElementX/Resources/Localizations/de.lproj/HealthChat.strings @@ -0,0 +1,9 @@ +// HealthChat fork strings. Not managed by Localazy — edit all three languages by hand. +"common_academic_title" = "Titel"; +"common_address" = "Adresse"; +"common_business_email" = "E-Mail (geschäftlich)"; +"common_business_tel" = "Telefon (geschäftlich)"; +"common_further_info" = "Weitere Infos / Qualifikationen"; +"common_practice_name" = "Praxisname / Firmenname"; +"common_specialty" = "Fachgebiet"; +"common_website" = "Webseite"; diff --git a/ElementX/Resources/Localizations/en.lproj/HealthChat.strings b/ElementX/Resources/Localizations/en.lproj/HealthChat.strings new file mode 100644 index 000000000..d7d5e23c9 --- /dev/null +++ b/ElementX/Resources/Localizations/en.lproj/HealthChat.strings @@ -0,0 +1,9 @@ +// HealthChat fork strings. Not managed by Localazy — edit all three languages by hand. +"common_academic_title" = "Title"; +"common_address" = "Address"; +"common_business_email" = "Business email"; +"common_business_tel" = "Business phone"; +"common_further_info" = "Further infos / qualifications"; +"common_practice_name" = "Practice name"; +"common_specialty" = "Speciality"; +"common_website" = "Website"; diff --git a/ElementX/Resources/Localizations/fr.lproj/HealthChat.strings b/ElementX/Resources/Localizations/fr.lproj/HealthChat.strings new file mode 100644 index 000000000..385bc26a1 --- /dev/null +++ b/ElementX/Resources/Localizations/fr.lproj/HealthChat.strings @@ -0,0 +1,9 @@ +// HealthChat fork strings. Not managed by Localazy — edit all three languages by hand. +"common_academic_title" = "Titre"; +"common_address" = "Adresse"; +"common_business_email" = "Email (professionnel)"; +"common_business_tel" = "Téléphone (professionnel)"; +"common_further_info" = "Informations supplémentaires / qualifications"; +"common_practice_name" = "Nom de l’entreprise"; +"common_specialty" = "Spécialisation"; +"common_website" = "Site web"; diff --git a/ElementX/Sources/AppHooks/AppHooks.swift b/ElementX/Sources/AppHooks/AppHooks.swift index f10400fd6..4bcbdf9af 100644 --- a/ElementX/Sources/AppHooks/AppHooks.swift +++ b/ElementX/Sources/AppHooks/AppHooks.swift @@ -12,6 +12,11 @@ import Synchronization final nonisolated class AppHooks: AppHooksProtocol { #if IS_MAIN_APP + func configure(with userSession: UserSessionProtocol?) async { + await userSessionHook.configure(with: userSession) + await extendedProfileSectionHook.configure(with: userSession) + } + @AppHook(default: DefaultAppSettingsHook()) var appSettingsHook: AppSettingsHookProtocol @@ -24,8 +29,29 @@ final nonisolated class AppHooks: AppHooksProtocol { @AppHook(default: DefaultOAuthPresenterHook()) var oAuthPresenterHook: OAuthPresenterHookProtocol + @AppHook(default: DefaultUserSessionHook()) + var userSessionHook: UserSessionHookProtocol + + @AppHook(default: DefaultRoomScreenHook()) + var roomScreenHook: RoomScreenHookProtocol + + @AppHook(default: DefaultRoomDetailsScreenHook()) + var roomDetailsScreenHook: RoomDetailsScreenHookProtocol + + @AppHook(default: DefaultRoomMemberDetailsScreenHook()) + var roomMemberDetailsScreenHook: RoomMemberDetailsScreenHookProtocol + + @AppHook(default: DefaultUserProfileScreenHook()) + var userProfileScreenHook: UserProfileScreenHookProtocol + + @AppHook(default: DefaultExtendedProfileSectionHook()) + var extendedProfileSectionHook: ExtendedProfileSectionHookProtocol + @AppHook(default: DefaultDeveloperOptionsScreenHook()) var developerOptionsScreenHook: DeveloperOptionsScreenHookProtocol + + @AppHook(default: DefaultRecoveryKeyScreenHook()) + var recoveryKeyScreenHook: RecoveryKeyScreenHookProtocol #endif @AppHook(default: DefaultTracingHook()) @@ -36,9 +62,6 @@ final nonisolated class AppHooks: AppHooksProtocol { @AppHook(default: DefaultRemoteSettingsHook()) var remoteSettingsHook: RemoteSettingsHookProtocol - - @AppHook(default: DefaultUserSessionHook()) - var userSessionHook: UserSessionHookProtocol } nonisolated protocol AppHooksProtocol: Sendable { diff --git a/ElementX/Sources/AppHooks/HealthChat/HealthChatAppHooks.swift b/ElementX/Sources/AppHooks/HealthChat/HealthChatAppHooks.swift index 1c71859e2..e697db398 100644 --- a/ElementX/Sources/AppHooks/HealthChat/HealthChatAppHooks.swift +++ b/ElementX/Sources/AppHooks/HealthChat/HealthChatAppHooks.swift @@ -16,6 +16,7 @@ extension AppHooks { #if IS_MAIN_APP registerAppSettingsHook(HealthChatAppSettingsHook()) registerCompoundHook(HealthChatCompoundHook()) + registerExtendedProfileSectionHook(HealthChatExtendedProfileSectionHook()) #endif } } diff --git a/ElementX/Sources/AppHooks/HealthChat/HealthChatExtendedProfileLoader.swift b/ElementX/Sources/AppHooks/HealthChat/HealthChatExtendedProfileLoader.swift new file mode 100644 index 000000000..a1c08a550 --- /dev/null +++ b/ElementX/Sources/AppHooks/HealthChat/HealthChatExtendedProfileLoader.swift @@ -0,0 +1,32 @@ +// +// Copyright 2026 Novaloop AG +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. +// Please see LICENSE files in the repository root for full details. +// + +import Foundation +import Observation + +/// Loads a user's extended profile for display, hiding profiles without any HealthChat fields. +@Observable +final class HealthChatExtendedProfileLoader { + private(set) var profile: ExtendedProfile? + + /// The load kicked off on creation — await this in tests before asserting. + @ObservationIgnored private(set) var loadingTask: Task? + + init(userID: String, service: ExtendedProfileServiceProtocol) { + loadingTask = Task { + guard case let .success(profile) = await service.profile(for: userID), profile.hasContent else { + return + } + self.profile = profile + } + } + + /// Creates a loader with a fixed result, for previews. + init(profile: ExtendedProfile?) { + self.profile = profile + } +} diff --git a/ElementX/Sources/AppHooks/HealthChat/HealthChatExtendedProfileSection.swift b/ElementX/Sources/AppHooks/HealthChat/HealthChatExtendedProfileSection.swift new file mode 100644 index 000000000..8687bfd9c --- /dev/null +++ b/ElementX/Sources/AppHooks/HealthChat/HealthChatExtendedProfileSection.swift @@ -0,0 +1,139 @@ +// +// Copyright 2026 Novaloop AG +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. +// Please see LICENSE files in the repository root for full details. +// + +import Compound +import SwiftUI + +/// The HealthChat profile fields shown below the header on the profile screens. +/// Renders nothing while loading and for profiles without any HealthChat fields. +struct HealthChatExtendedProfileSection: View { + @State private var loader: HealthChatExtendedProfileLoader + + init(userID: String, service: ExtendedProfileServiceProtocol) { + _loader = State(initialValue: HealthChatExtendedProfileLoader(userID: userID, service: service)) + } + + init(loader: HealthChatExtendedProfileLoader) { + _loader = State(initialValue: loader) + } + + var body: some View { + if let profile = loader.profile { + Section { + if let title = profile.title { + row(label: HealthChatL10n.commonAcademicTitle, value: title) + } + if let specialty = profile.specialty { + row(label: HealthChatL10n.commonSpecialty, value: specialty) + } + if let practiceName = profile.practiceName { + row(label: HealthChatL10n.commonPracticeName, value: practiceName) + } + if let address = profile.formattedAddress { + row(label: HealthChatL10n.commonAddress, value: address) + } + if let businessEmail = profile.businessEmail { + linkRow(label: HealthChatL10n.commonBusinessEmail, + value: businessEmail, + url: URL(string: "mailto:\(businessEmail)")) + } + if let businessTel = profile.businessTel { + linkRow(label: HealthChatL10n.commonBusinessTel, + value: businessTel, + url: URL(string: "tel:\(businessTel.filter { !$0.isWhitespace })")) + } + if let website = profile.website { + linkRow(label: HealthChatL10n.commonWebsite, + value: website, + url: websiteURL(for: website)) + } + if let furtherInfo = profile.furtherInfo { + row(label: HealthChatL10n.commonFurtherInfo, value: furtherInfo) + } + } + } + } + + // MARK: - Private + + private func row(label: String, value: String) -> some View { + ListRow(kind: .custom { + rowContent(label: label, value: value, valueColor: .compound.textPrimary) + }) + } + + private func linkRow(label: String, value: String, url: URL?) -> some View { + ListRow(kind: .custom { + Button { + guard let url else { return } + UIApplication.shared.open(url) + } label: { + rowContent(label: label, value: value, valueColor: .compound.textLinkExternal) + } + }) + } + + private func rowContent(label: String, value: String, valueColor: Color) -> some View { + VStack(alignment: .leading, spacing: 2) { + Text(label) + .font(.compound.bodySM) + .foregroundColor(.compound.textSecondary) + Text(value) + .font(.compound.bodyLG) + .foregroundColor(valueColor) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, ListRowPadding.horizontal) + .padding(.vertical, 8) + } + + private func websiteURL(for website: String) -> URL? { + guard let url = URL(string: website) else { + return nil + } + return url.scheme == nil ? URL(string: "https://\(website)") : url + } +} + +// MARK: - Previews + +struct HealthChatExtendedProfileSection_Previews: PreviewProvider, TestablePreview { + static let fullProfile = try? JSONDecoder().decode(ExtendedProfile.self, from: Data(""" + { + "displayname": "Dr. Alice Alpha", + "io.element.profile.title": "Dr. med.", + "io.element.profile.specialization": "Physiotherapie", + "io.element.profile.practice_name": "Praxis Alpha", + "io.element.profile.business_street": "Friedaustrasse", + "io.element.profile.business_street_nr": "17", + "io.element.profile.business_plz": "8003", + "io.element.profile.business_city": "Zürich", + "io.element.profile.business_email": "praxis@example.ch", + "io.element.profile.business_tel": "+41 44 123 45 67", + "io.element.profile.website": "https://example.ch", + "io.element.profile.further_info": "Krankenkassen-anerkannt\\nHausbesuche möglich" + } + """.utf8)) + + static let contactOnlyProfile = try? JSONDecoder().decode(ExtendedProfile.self, from: Data(""" + { "io.element.profile.business_tel": "+41 44 123 45 67" } + """.utf8)) + + static var previews: some View { + Form { + HealthChatExtendedProfileSection(loader: .init(profile: fullProfile)) + } + .compoundList() + .previewDisplayName("Full profile") + + Form { + HealthChatExtendedProfileSection(loader: .init(profile: contactOnlyProfile)) + } + .compoundList() + .previewDisplayName("Contact only") + } +} diff --git a/ElementX/Sources/AppHooks/HealthChat/HealthChatExtendedProfileSectionHook.swift b/ElementX/Sources/AppHooks/HealthChat/HealthChatExtendedProfileSectionHook.swift new file mode 100644 index 000000000..b61385e82 --- /dev/null +++ b/ElementX/Sources/AppHooks/HealthChat/HealthChatExtendedProfileSectionHook.swift @@ -0,0 +1,31 @@ +// +// Copyright 2026 Novaloop AG +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. +// Please see LICENSE files in the repository root for full details. +// + +import SwiftUI + +#if IS_MAIN_APP +// Conforming to the nonisolated hook protocol would infer nonisolated, but the +// service is main-actor state — the hook's requirements are all @MainActor anyway. +@MainActor +final class HealthChatExtendedProfileSectionHook: ExtendedProfileSectionHookProtocol { + private var extendedProfileService: ExtendedProfileServiceProtocol? + + // Constructed during hook registration, before the main actor is reachable. + nonisolated init() { } + + func configure(with userSession: UserSessionProtocol?) async { + extendedProfileService = userSession.map { ExtendedProfileService(clientProxy: $0.clientProxy) } + } + + func extendedProfileSection(userID: String) -> AnyView? { + guard let extendedProfileService else { + return nil + } + return AnyView(HealthChatExtendedProfileSection(userID: userID, service: extendedProfileService)) + } +} +#endif diff --git a/ElementX/Sources/AppHooks/Hooks/ExtendedProfileSectionHook.swift b/ElementX/Sources/AppHooks/Hooks/ExtendedProfileSectionHook.swift new file mode 100644 index 000000000..0de1acb81 --- /dev/null +++ b/ElementX/Sources/AppHooks/Hooks/ExtendedProfileSectionHook.swift @@ -0,0 +1,24 @@ +// +// 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 SwiftUI + +nonisolated protocol ExtendedProfileSectionHookProtocol: Sendable { + @MainActor func configure(with userSession: UserSessionProtocol?) async + + /// An extra Form section rendered below the header on the user profile + /// and room member details screens. + @MainActor func extendedProfileSection(userID: String) -> AnyView? +} + +struct DefaultExtendedProfileSectionHook: ExtendedProfileSectionHookProtocol { + func configure(with userSession: UserSessionProtocol?) async { } + + func extendedProfileSection(userID: String) -> AnyView? { + nil + } +} diff --git a/ElementX/Sources/Generated/Strings+HealthChat.swift b/ElementX/Sources/Generated/Strings+HealthChat.swift new file mode 100644 index 000000000..1f944c41f --- /dev/null +++ b/ElementX/Sources/Generated/Strings+HealthChat.swift @@ -0,0 +1,61 @@ +// swiftlint:disable all +// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen + +import Foundation + +// swiftlint:disable superfluous_disable_command file_length implicit_return + +// MARK: - Strings + +// swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length +// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces +internal nonisolated enum HealthChatL10n { + /// Title + internal static var commonAcademicTitle: String { return HealthChatL10n.tr("HealthChat", "common_academic_title") } + /// Address + internal static var commonAddress: String { return HealthChatL10n.tr("HealthChat", "common_address") } + /// Business email + internal static var commonBusinessEmail: String { return HealthChatL10n.tr("HealthChat", "common_business_email") } + /// Business phone + internal static var commonBusinessTel: String { return HealthChatL10n.tr("HealthChat", "common_business_tel") } + /// Further infos / qualifications + internal static var commonFurtherInfo: String { return HealthChatL10n.tr("HealthChat", "common_further_info") } + /// Practice name + internal static var commonPracticeName: String { return HealthChatL10n.tr("HealthChat", "common_practice_name") } + /// Speciality + internal static var commonSpecialty: String { return HealthChatL10n.tr("HealthChat", "common_specialty") } + /// Website + internal static var commonWebsite: String { return HealthChatL10n.tr("HealthChat", "common_website") } +} +// swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length +// swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces + +// MARK: - Implementation Details + +nonisolated extension HealthChatL10n { + static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String { + // Use preferredLocalizations to get a language that is in the bundle and the user's preferred list of languages. + let languages = Bundle.overrideLocalizations ?? Bundle.app.preferredLocalizations + + for language in languages { + if let translation = trIn(language, table, key, args) { + return translation + } + } + return Bundle.app.developmentLocalization.flatMap { trIn($0, table, key, args) } ?? key + } + + private static func trIn(_ language: String, _ table: String, _ key: String, _ args: CVarArg...) -> String? { + guard let bundle = Bundle.lprojBundle(for: language) else { return nil } + let format = NSLocalizedString(key, tableName: table, bundle: bundle, comment: "") + let translation = String(format: format, locale: Locale(identifier: language), arguments: args) + guard translation != key, + translation != "\(key) \(key)" // Handle double pseudo for tests + else { + return nil + } + return translation + } +} + +// swiftlint:enable all diff --git a/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift b/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift index c1a2d035a..c9a419d88 100644 --- a/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift +++ b/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift @@ -55,6 +55,7 @@ enum TestablePreviewsDictionary { "FormattingToolbar_Previews" : FormattingToolbar_Previews.self, "FullscreenDialog_Previews" : FullscreenDialog_Previews.self, "GalleryRoomTimelineView_Previews" : GalleryRoomTimelineView_Previews.self, + "HealthChatExtendedProfileSection_Previews" : HealthChatExtendedProfileSection_Previews.self, "HighlightedTimelineItemModifier_Previews" : HighlightedTimelineItemModifier_Previews.self, "HomeScreenEmptyStateView_Previews" : HomeScreenEmptyStateView_Previews.self, "HomeScreenInviteCell_Previews" : HomeScreenInviteCell_Previews.self, diff --git a/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenModels.swift b/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenModels.swift index 496a18f04..2e031afd4 100644 --- a/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenModels.swift +++ b/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenModels.swift @@ -17,6 +17,7 @@ enum RoomMemberDetailsScreenViewModelAction { struct RoomMemberDetailsScreenViewState: BindableState { let userID: String + let appHooks: AppHooks var memberDetails: RoomMemberDetails? var verificationState: UserIdentityVerificationState? var isOwnMemberDetails = false diff --git a/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModel.swift b/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModel.swift index b943d15e0..01f043e3d 100644 --- a/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModel.swift @@ -35,7 +35,7 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro self.userIndicatorController = userIndicatorController self.analytics = analytics - let initialViewState = RoomMemberDetailsScreenViewState(userID: userID, bindings: .init()) + let initialViewState = RoomMemberDetailsScreenViewState(userID: userID, appHooks: appHooks, bindings: .init()) super.init(initialViewState: userSession.userHooks.roomMemberDetailsScreenHook.update(initialViewState), mediaProvider: userSession.mediaProvider) diff --git a/ElementX/Sources/Screens/RoomMemberDetailsScreen/View/RoomMemberDetailsScreen.swift b/ElementX/Sources/Screens/RoomMemberDetailsScreen/View/RoomMemberDetailsScreen.swift index 3eccd3f4a..ad3df9b78 100644 --- a/ElementX/Sources/Screens/RoomMemberDetailsScreen/View/RoomMemberDetailsScreen.swift +++ b/ElementX/Sources/Screens/RoomMemberDetailsScreen/View/RoomMemberDetailsScreen.swift @@ -16,6 +16,8 @@ struct RoomMemberDetailsScreen: View { Form { headerSection + context.viewState.appHooks.extendedProfileSectionHook.extendedProfileSection(userID: context.viewState.userID) + if context.viewState.showVerifyIdentitySection { verificationSection } diff --git a/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenModels.swift b/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenModels.swift index 7dfbe45ee..8e50a44f9 100644 --- a/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenModels.swift +++ b/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenModels.swift @@ -18,6 +18,7 @@ struct UserProfileScreenViewState: BindableState { let userID: String let isOwnUser: Bool let isPresentedModally: Bool + let appHooks: AppHooks var userProfile: UserProfile? var isIdentityKnown = false diff --git a/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenViewModel.swift b/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenViewModel.swift index 03d9767cb..4a39e4958 100644 --- a/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenViewModel.swift +++ b/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenViewModel.swift @@ -34,6 +34,7 @@ class UserProfileScreenViewModel: UserProfileScreenViewModelType, UserProfileScr let initialViewState = UserProfileScreenViewState(userID: userID, isOwnUser: userID == userSession.clientProxy.userID, isPresentedModally: isPresentedModally, + appHooks: appHooks, bindings: .init()) super.init(initialViewState: userSession.userHooks.userProfileScreenHook.update(initialViewState), diff --git a/ElementX/Sources/Screens/UserProfileScreen/View/UserProfileScreen.swift b/ElementX/Sources/Screens/UserProfileScreen/View/UserProfileScreen.swift index 14dbb13f5..2452eec54 100644 --- a/ElementX/Sources/Screens/UserProfileScreen/View/UserProfileScreen.swift +++ b/ElementX/Sources/Screens/UserProfileScreen/View/UserProfileScreen.swift @@ -15,6 +15,8 @@ struct UserProfileScreen: View { var body: some View { Form { headerSection + + context.viewState.appHooks.extendedProfileSectionHook.extendedProfileSection(userID: context.viewState.userID) } .compoundList() .navigationTitle(L10n.screenRoomMemberDetailsTitle) diff --git a/PreviewTests/Sources/GeneratedPreviewTests.swift b/PreviewTests/Sources/GeneratedPreviewTests.swift index f35458d24..33dcddf0b 100644 --- a/PreviewTests/Sources/GeneratedPreviewTests.swift +++ b/PreviewTests/Sources/GeneratedPreviewTests.swift @@ -333,6 +333,13 @@ extension PreviewTests { } } + @Test + func healthChatExtendedProfileSection() async throws { + for (index, preview) in HealthChatExtendedProfileSection_Previews._allPreviews.enumerated() { + try await assertSnapshots(matching: preview, step: index) + } + } + @Test func highlightedTimelineItemModifier() async throws { for (index, preview) in HighlightedTimelineItemModifier_Previews._allPreviews.enumerated() { diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/healthChatExtendedProfileSection.Contact-only-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/healthChatExtendedProfileSection.Contact-only-iPad-en-GB.png new file mode 100644 index 000000000..613bbb780 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/healthChatExtendedProfileSection.Contact-only-iPad-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d3588ff03cab4ae26b509697c581527eb9fcac8c576975f9159dc30cf2f3e56 +size 83868 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/healthChatExtendedProfileSection.Contact-only-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/healthChatExtendedProfileSection.Contact-only-iPad-pseudo.png new file mode 100644 index 000000000..f45814159 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/healthChatExtendedProfileSection.Contact-only-iPad-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f8ca33d1e3f081db2d1d781e22882fbf782bfbe4d9ad9b96cd381a7077a54ac +size 84983 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/healthChatExtendedProfileSection.Contact-only-iPhone-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/healthChatExtendedProfileSection.Contact-only-iPhone-en-GB.png new file mode 100644 index 000000000..eee7877a6 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/healthChatExtendedProfileSection.Contact-only-iPhone-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b4a8af0283cffdb0512442a856f77c7e26e2018c578dd042675011f7d3946131 +size 37865 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/healthChatExtendedProfileSection.Contact-only-iPhone-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/healthChatExtendedProfileSection.Contact-only-iPhone-pseudo.png new file mode 100644 index 000000000..613f82dcc --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/healthChatExtendedProfileSection.Contact-only-iPhone-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ded4f7151b2a76ec08b682f7301fac1fbe604db78e61dbb350a6d966a0079bed +size 38216 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/healthChatExtendedProfileSection.Full-profile-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/healthChatExtendedProfileSection.Full-profile-iPad-en-GB.png new file mode 100644 index 000000000..37709f662 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/healthChatExtendedProfileSection.Full-profile-iPad-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9529bf7156b2a534ceef90635f2cf0a9e384ca528ad18822d40ca9a380b29f90 +size 141126 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/healthChatExtendedProfileSection.Full-profile-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/healthChatExtendedProfileSection.Full-profile-iPad-pseudo.png new file mode 100644 index 000000000..85b610926 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/healthChatExtendedProfileSection.Full-profile-iPad-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:47f4621c563dcec8d761110404d0804cb4e20bf3d634b9d0e9610c486afe0bfe +size 151865 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/healthChatExtendedProfileSection.Full-profile-iPhone-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/healthChatExtendedProfileSection.Full-profile-iPhone-en-GB.png new file mode 100644 index 000000000..c731d8737 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/healthChatExtendedProfileSection.Full-profile-iPhone-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:21e64679e06f1f0fb2e48bea125a1bbd5e5825208a4e9f844a0bcce25e500ab6 +size 91057 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/healthChatExtendedProfileSection.Full-profile-iPhone-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/healthChatExtendedProfileSection.Full-profile-iPhone-pseudo.png new file mode 100644 index 000000000..4fb414df5 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/healthChatExtendedProfileSection.Full-profile-iPhone-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bae2b1fdabfb0404bfc1c355ae626c9fe5214eb4e79ae01bcfedc286e5088606 +size 99109 diff --git a/Tools/SwiftGen/swiftgen-config.yml b/Tools/SwiftGen/swiftgen-config.yml index 39f640320..047389590 100755 --- a/Tools/SwiftGen/swiftgen-config.yml +++ b/Tools/SwiftGen/swiftgen-config.yml @@ -25,3 +25,12 @@ strings: enumName: UntranslatedL10n lookupFunction: tr # used for the loc to be computed variables publicAccess: false + - inputs: Resources/Localizations/en.lproj + filter: HealthChat* + outputs: + templatePath: Templates/Strings/structured-swift5-element.stencil + output: Strings+HealthChat.swift + params: + enumName: HealthChatL10n + lookupFunction: tr # used for the loc to be computed variables + publicAccess: false diff --git a/UnitTests/Sources/HealthChat/HealthChatExtendedProfileLoaderTests.swift b/UnitTests/Sources/HealthChat/HealthChatExtendedProfileLoaderTests.swift new file mode 100644 index 000000000..7dcb70d3e --- /dev/null +++ b/UnitTests/Sources/HealthChat/HealthChatExtendedProfileLoaderTests.swift @@ -0,0 +1,53 @@ +// +// 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. +// + +@testable import ElementX +import Foundation +import Testing + +final class HealthChatExtendedProfileLoaderTests { + private let service = ExtendedProfileServiceMock() + + @Test + func showsProfileWithContent() async throws { + service.profileForReturnValue = try .success(makeProfile(""" + { "displayname": "Alice", "io.element.profile.business_tel": "+41 44 123 45 67" } + """)) + + let loader = HealthChatExtendedProfileLoader(userID: "@alice:healthchat.ch", service: service) + await loader.loadingTask?.value + + #expect(loader.profile?.businessTel == "+41 44 123 45 67") + #expect(service.profileForReceivedUserID == "@alice:healthchat.ch") + } + + @Test + func hidesProfileWithoutHealthChatFields() async throws { + service.profileForReturnValue = try .success(makeProfile(""" + { "displayname": "Alice" } + """)) + + let loader = HealthChatExtendedProfileLoader(userID: "@alice:healthchat.ch", service: service) + await loader.loadingTask?.value + + #expect(loader.profile == nil) + } + + @Test + func hidesProfileOnFailure() async { + service.profileForReturnValue = .failure(.networkFailure) + + let loader = HealthChatExtendedProfileLoader(userID: "@alice:healthchat.ch", service: service) + await loader.loadingTask?.value + + #expect(loader.profile == nil) + } + + private func makeProfile(_ json: String) throws -> ExtendedProfile { + try JSONDecoder().decode(ExtendedProfile.self, from: Data(json.utf8)) + } +}