mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-09 07:20:07 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6089808d9c | |||
| 570c7534c3 | |||
| c93af478a7 | |||
| 6a08c26cd7 | |||
| be803d0542 | |||
| d01d04846a | |||
| b70e77f7c5 |
@@ -56,3 +56,7 @@ provisioning_profiles/
|
|||||||
# Regenerated by every fastlane invocation, including on the runner. Ignored rather than
|
# Regenerated by every fastlane invocation, including on the runner. Ignored rather than
|
||||||
# committed, matching element-ios.
|
# committed, matching element-ios.
|
||||||
fastlane/README.md
|
fastlane/README.md
|
||||||
|
|
||||||
|
# Review working data (per-machine)
|
||||||
|
.claude/review-context/
|
||||||
|
.claude/review-log.jsonl
|
||||||
|
|||||||
@@ -195,6 +195,10 @@ extension AccessibilityTests {
|
|||||||
try await performAccessibilityAudit(named: "GalleryRoomTimelineView_Previews")
|
try await performAccessibilityAudit(named: "GalleryRoomTimelineView_Previews")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testHealthChatAccountStatusSection() async throws {
|
||||||
|
try await performAccessibilityAudit(named: "HealthChatAccountStatusSection_Previews")
|
||||||
|
}
|
||||||
|
|
||||||
func testHealthChatExtendedProfileSection() async throws {
|
func testHealthChatExtendedProfileSection() async throws {
|
||||||
try await performAccessibilityAudit(named: "HealthChatExtendedProfileSection_Previews")
|
try await performAccessibilityAudit(named: "HealthChatExtendedProfileSection_Previews")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
03BD83E8BDD23AE059802E0D /* UITestsScreenIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CEBE5EA91E8691EDF364EC2 /* UITestsScreenIdentifier.swift */; };
|
03BD83E8BDD23AE059802E0D /* UITestsScreenIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CEBE5EA91E8691EDF364EC2 /* UITestsScreenIdentifier.swift */; };
|
||||||
03CDCA6243F89B194E3FAD17 /* EncryptionAuthenticity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 955336CBD5ED73C792D1F580 /* EncryptionAuthenticity.swift */; };
|
03CDCA6243F89B194E3FAD17 /* EncryptionAuthenticity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 955336CBD5ED73C792D1F580 /* EncryptionAuthenticity.swift */; };
|
||||||
03F192C5C011AFA6068C9EAA /* SpaceServiceSDKMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90B1E4D1F0A6FAAE64B54D76 /* SpaceServiceSDKMock.swift */; };
|
03F192C5C011AFA6068C9EAA /* SpaceServiceSDKMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90B1E4D1F0A6FAAE64B54D76 /* SpaceServiceSDKMock.swift */; };
|
||||||
|
04154A8A1D1A7A819D7CE8E4 /* HealthChatAccountStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA3BFFFEF695209F27535BED /* HealthChatAccountStatus.swift */; };
|
||||||
0437765FF480249486893CC7 /* ScreenTrackerViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 196004E7695FBA292A7944AF /* ScreenTrackerViewModifier.swift */; };
|
0437765FF480249486893CC7 /* ScreenTrackerViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 196004E7695FBA292A7944AF /* ScreenTrackerViewModifier.swift */; };
|
||||||
044DD8F80231BC30570F7965 /* UserDiscoveryService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65AAD845E53B0C8B5E0812C2 /* UserDiscoveryService.swift */; };
|
044DD8F80231BC30570F7965 /* UserDiscoveryService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65AAD845E53B0C8B5E0812C2 /* UserDiscoveryService.swift */; };
|
||||||
04F17DE71A50206336749BAC /* UserPreferenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA241DEEF7C8A7181C0AEDC9 /* UserPreferenceTests.swift */; };
|
04F17DE71A50206336749BAC /* UserPreferenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA241DEEF7C8A7181C0AEDC9 /* UserPreferenceTests.swift */; };
|
||||||
@@ -60,6 +61,7 @@
|
|||||||
0668BECCF11E8EE6936CDF16 /* TimelineItemMenuActionProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C98C243A90369929D669873D /* TimelineItemMenuActionProviderTests.swift */; };
|
0668BECCF11E8EE6936CDF16 /* TimelineItemMenuActionProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C98C243A90369929D669873D /* TimelineItemMenuActionProviderTests.swift */; };
|
||||||
066A1E9B94723EE9F3038044 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47EBB5D698CE9A25BB553A2D /* Strings.swift */; };
|
066A1E9B94723EE9F3038044 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47EBB5D698CE9A25BB553A2D /* Strings.swift */; };
|
||||||
06B55882911B4BF5B14E9851 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227AC5D71A4CE43512062243 /* URL.swift */; };
|
06B55882911B4BF5B14E9851 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227AC5D71A4CE43512062243 /* URL.swift */; };
|
||||||
|
06BF5F111812981DC65CC34F /* HealthChatAccountStatusLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46DE3887AAA265406107ABE6 /* HealthChatAccountStatusLoader.swift */; };
|
||||||
06D17F7813AA931FF18FD5D0 /* SDKListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE5CD2993048222B64C45006 /* SDKListener.swift */; };
|
06D17F7813AA931FF18FD5D0 /* SDKListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE5CD2993048222B64C45006 /* SDKListener.swift */; };
|
||||||
06D3942496E9E0E655F14D21 /* NotificationManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A057F2FDC14866C3026A89A4 /* NotificationManagerProtocol.swift */; };
|
06D3942496E9E0E655F14D21 /* NotificationManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A057F2FDC14866C3026A89A4 /* NotificationManagerProtocol.swift */; };
|
||||||
06F8EDF52E33A2D36BCC1161 /* AppLockScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D6F88FE35A0979D2821E06 /* AppLockScreen.swift */; };
|
06F8EDF52E33A2D36BCC1161 /* AppLockScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D6F88FE35A0979D2821E06 /* AppLockScreen.swift */; };
|
||||||
@@ -107,6 +109,7 @@
|
|||||||
0E8C480700870BB34A2A360F /* DeviceKit in Frameworks */ = {isa = PBXBuildFile; productRef = 4003BC24B24C9E63D3304177 /* DeviceKit */; };
|
0E8C480700870BB34A2A360F /* DeviceKit in Frameworks */ = {isa = PBXBuildFile; productRef = 4003BC24B24C9E63D3304177 /* DeviceKit */; };
|
||||||
0EA6537A07E2DC882AEA5962 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 187853A7E643995EE49FAD43 /* Localizable.stringsdict */; };
|
0EA6537A07E2DC882AEA5962 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 187853A7E643995EE49FAD43 /* Localizable.stringsdict */; };
|
||||||
0EBF0A411E1C46D4BFA2B087 /* ManageAuthorizedSpacesScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB9CD3A0A39BC063438E12D4 /* ManageAuthorizedSpacesScreenCoordinator.swift */; };
|
0EBF0A411E1C46D4BFA2B087 /* ManageAuthorizedSpacesScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB9CD3A0A39BC063438E12D4 /* ManageAuthorizedSpacesScreenCoordinator.swift */; };
|
||||||
|
0EDE5E11785CA18FC8BFF1CC /* HealthChatAccountStatusSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 056FCFA596B562977CA89D58 /* HealthChatAccountStatusSection.swift */; };
|
||||||
0EEC614342F823E5BF966C2C /* AppLockTimerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5B4CD611DE7E94F5BA87B2 /* AppLockTimerTests.swift */; };
|
0EEC614342F823E5BF966C2C /* AppLockTimerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5B4CD611DE7E94F5BA87B2 /* AppLockTimerTests.swift */; };
|
||||||
0F6C8033FA60CFD36F7CA205 /* AppLockSetupPINScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A019A12C866D64CF072024B9 /* AppLockSetupPINScreenViewModel.swift */; };
|
0F6C8033FA60CFD36F7CA205 /* AppLockSetupPINScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A019A12C866D64CF072024B9 /* AppLockSetupPINScreenViewModel.swift */; };
|
||||||
0F81DEE3E02A6549B20DF09A /* DeactivateAccountScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 633924B26ACCD29C18BEF4E8 /* DeactivateAccountScreenViewModelProtocol.swift */; };
|
0F81DEE3E02A6549B20DF09A /* DeactivateAccountScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 633924B26ACCD29C18BEF4E8 /* DeactivateAccountScreenViewModelProtocol.swift */; };
|
||||||
@@ -322,6 +325,7 @@
|
|||||||
3118D9ABFD4BE5A3492FF88A /* ElementCallConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC437C491EA6996513B1CEAB /* ElementCallConfiguration.swift */; };
|
3118D9ABFD4BE5A3492FF88A /* ElementCallConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC437C491EA6996513B1CEAB /* ElementCallConfiguration.swift */; };
|
||||||
31CCF3159E5CE1C05AE5781B /* HTMLFixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A5FBB74981BF8EFFDAE90D /* HTMLFixtures.swift */; };
|
31CCF3159E5CE1C05AE5781B /* HTMLFixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A5FBB74981BF8EFFDAE90D /* HTMLFixtures.swift */; };
|
||||||
31DE6B7FD15E1C6E43885717 /* RoomRole.swift in Sources */ = {isa = PBXBuildFile; fileRef = 578AF9CE60816069536C0953 /* RoomRole.swift */; };
|
31DE6B7FD15E1C6E43885717 /* RoomRole.swift in Sources */ = {isa = PBXBuildFile; fileRef = 578AF9CE60816069536C0953 /* RoomRole.swift */; };
|
||||||
|
323D76E640CDB21BD33AD9B9 /* HealthChatAccountStatusLoaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E851BFC1E8A0E1925D26CD14 /* HealthChatAccountStatusLoaderTests.swift */; };
|
||||||
329D65DD7CC0E42CAD282EF7 /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2C6AE2DB4FE7FAEA6402663 /* Secrets.swift */; };
|
329D65DD7CC0E42CAD282EF7 /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2C6AE2DB4FE7FAEA6402663 /* Secrets.swift */; };
|
||||||
32B7891D937377A59606EDFC /* UserFlowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21DD8599815136EFF5B73F38 /* UserFlowTests.swift */; };
|
32B7891D937377A59606EDFC /* UserFlowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21DD8599815136EFF5B73F38 /* UserFlowTests.swift */; };
|
||||||
32BFB9B4D2EC46196537B72F /* ContentScannerProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E6EB3C22BACF0A17FEDA4C4 /* ContentScannerProxyProtocol.swift */; };
|
32BFB9B4D2EC46196537B72F /* ContentScannerProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E6EB3C22BACF0A17FEDA4C4 /* ContentScannerProxyProtocol.swift */; };
|
||||||
@@ -1072,6 +1076,7 @@
|
|||||||
A2357AA4A188BC37085BC6F0 /* EditRoomAddressScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5FDFAA04174CC99FB66391C /* EditRoomAddressScreenViewModel.swift */; };
|
A2357AA4A188BC37085BC6F0 /* EditRoomAddressScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5FDFAA04174CC99FB66391C /* EditRoomAddressScreenViewModel.swift */; };
|
||||||
A23B8B27A1436A1049EEF68E /* InfoPlistReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A580295A56B55A856CC4084 /* InfoPlistReader.swift */; };
|
A23B8B27A1436A1049EEF68E /* InfoPlistReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A580295A56B55A856CC4084 /* InfoPlistReader.swift */; };
|
||||||
A2434D4DFB49A68E5CD0F53C /* MediaLoaderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A02406480C351B8C6E0682C /* MediaLoaderProtocol.swift */; };
|
A2434D4DFB49A68E5CD0F53C /* MediaLoaderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A02406480C351B8C6E0682C /* MediaLoaderProtocol.swift */; };
|
||||||
|
A2937A97959A0AFB064BE1DE /* SettingsScreenHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56630FCC9F78E5E8F1A75770 /* SettingsScreenHook.swift */; };
|
||||||
A32384E3D85CA65342D3A908 /* TimelineMediaPreviewRedactConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C75FE3F524B575D53787868C /* TimelineMediaPreviewRedactConfirmationView.swift */; };
|
A32384E3D85CA65342D3A908 /* TimelineMediaPreviewRedactConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C75FE3F524B575D53787868C /* TimelineMediaPreviewRedactConfirmationView.swift */; };
|
||||||
A33784831AD880A670CAA9F9 /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */; };
|
A33784831AD880A670CAA9F9 /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */; };
|
||||||
A36AD251013402EDBD666C75 /* AppMediatorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BAC027034248429A438886B /* AppMediatorMock.swift */; };
|
A36AD251013402EDBD666C75 /* AppMediatorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BAC027034248429A438886B /* AppMediatorMock.swift */; };
|
||||||
@@ -1508,6 +1513,7 @@
|
|||||||
EA2FECCD9E00D9784AC6017D /* PhishingDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB07F03461023BC39C730922 /* PhishingDetector.swift */; };
|
EA2FECCD9E00D9784AC6017D /* PhishingDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB07F03461023BC39C730922 /* PhishingDetector.swift */; };
|
||||||
EA4848108A6FE53A4085C9B1 /* VolatileUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2F56C9FFE8D55375CED9741 /* VolatileUserDefaults.swift */; };
|
EA4848108A6FE53A4085C9B1 /* VolatileUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2F56C9FFE8D55375CED9741 /* VolatileUserDefaults.swift */; };
|
||||||
EA6613B29BA671F39CE1B1D2 /* ConfirmationDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = B383DCD3DCB19E00FD478A5F /* ConfirmationDialog.swift */; };
|
EA6613B29BA671F39CE1B1D2 /* ConfirmationDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = B383DCD3DCB19E00FD478A5F /* ConfirmationDialog.swift */; };
|
||||||
|
EA66A9B88287B2E56EB4A95F /* HealthChatSettingsScreenHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDAE8AB9A3CBD957EDD666ED /* HealthChatSettingsScreenHook.swift */; };
|
||||||
EA78A7512AFB1E5451744EB1 /* AppRouteURLParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E461B3C8BBBFCA400B268D14 /* AppRouteURLParserTests.swift */; };
|
EA78A7512AFB1E5451744EB1 /* AppRouteURLParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E461B3C8BBBFCA400B268D14 /* AppRouteURLParserTests.swift */; };
|
||||||
EA8D941771E762A5D3D7FA0D /* FileMediaEventsTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 430C73079A84654BF46A7FF5 /* FileMediaEventsTimelineView.swift */; };
|
EA8D941771E762A5D3D7FA0D /* FileMediaEventsTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 430C73079A84654BF46A7FF5 /* FileMediaEventsTimelineView.swift */; };
|
||||||
EAB3C1F0BC7F671ED8BDF82D /* CompletionSuggestionServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ECF11669EF253E98AA2977A /* CompletionSuggestionServiceProtocol.swift */; };
|
EAB3C1F0BC7F671ED8BDF82D /* CompletionSuggestionServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ECF11669EF253E98AA2977A /* CompletionSuggestionServiceProtocol.swift */; };
|
||||||
@@ -1822,6 +1828,7 @@
|
|||||||
0545AC444BEEA89FF8C509FD /* zh-Hant-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant-TW"; path = "zh-Hant-TW.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
0545AC444BEEA89FF8C509FD /* zh-Hant-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant-TW"; path = "zh-Hant-TW.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||||
05512FB13987D221B7205DE0 /* HomeScreenRecoveryKeyConfirmationBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRecoveryKeyConfirmationBanner.swift; sourceTree = "<group>"; };
|
05512FB13987D221B7205DE0 /* HomeScreenRecoveryKeyConfirmationBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRecoveryKeyConfirmationBanner.swift; sourceTree = "<group>"; };
|
||||||
05596E4A11A8C9346E9E54AE /* SoftLogoutScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutScreenCoordinator.swift; sourceTree = "<group>"; };
|
05596E4A11A8C9346E9E54AE /* SoftLogoutScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||||
|
056FCFA596B562977CA89D58 /* HealthChatAccountStatusSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthChatAccountStatusSection.swift; sourceTree = "<group>"; };
|
||||||
057B747CF045D3C6C30EAB2C /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fi; path = fi.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
057B747CF045D3C6C30EAB2C /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fi; path = fi.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||||
05A3E8741D199CD1A37F4CBF /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = "<group>"; };
|
05A3E8741D199CD1A37F4CBF /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = "<group>"; };
|
||||||
05AF58372CA884A789EB9C5A /* AppMediatorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppMediatorProtocol.swift; sourceTree = "<group>"; };
|
05AF58372CA884A789EB9C5A /* AppMediatorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppMediatorProtocol.swift; sourceTree = "<group>"; };
|
||||||
@@ -2232,6 +2239,7 @@
|
|||||||
46C208DA43CE25D13E670F40 /* UITestsAppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsAppCoordinator.swift; sourceTree = "<group>"; };
|
46C208DA43CE25D13E670F40 /* UITestsAppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsAppCoordinator.swift; sourceTree = "<group>"; };
|
||||||
46D0BA44B1838E65B507B277 /* NotificationPermissionsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPermissionsScreen.swift; sourceTree = "<group>"; };
|
46D0BA44B1838E65B507B277 /* NotificationPermissionsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPermissionsScreen.swift; sourceTree = "<group>"; };
|
||||||
46D560DDA3B20C82766ACFAD /* NotificationSettingsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsScreenViewModel.swift; sourceTree = "<group>"; };
|
46D560DDA3B20C82766ACFAD /* NotificationSettingsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsScreenViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
46DE3887AAA265406107ABE6 /* HealthChatAccountStatusLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthChatAccountStatusLoader.swift; sourceTree = "<group>"; };
|
||||||
47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxyProtocol.swift; sourceTree = "<group>"; };
|
47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxyProtocol.swift; sourceTree = "<group>"; };
|
||||||
471BB7276C97AF60B3A5463B /* RoomDirectorySearchProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchProxy.swift; sourceTree = "<group>"; };
|
471BB7276C97AF60B3A5463B /* RoomDirectorySearchProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchProxy.swift; sourceTree = "<group>"; };
|
||||||
475D47D0BFE961B02BAC5D49 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = id; path = id.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
475D47D0BFE961B02BAC5D49 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = id; path = id.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||||
@@ -2321,6 +2329,7 @@
|
|||||||
55BA4D3DFEA16CDD3FE99064 /* InverseMask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InverseMask.swift; sourceTree = "<group>"; };
|
55BA4D3DFEA16CDD3FE99064 /* InverseMask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InverseMask.swift; sourceTree = "<group>"; };
|
||||||
5644919DB2022397D9D5825A /* MockSoftLogoutScreenState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSoftLogoutScreenState.swift; sourceTree = "<group>"; };
|
5644919DB2022397D9D5825A /* MockSoftLogoutScreenState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSoftLogoutScreenState.swift; sourceTree = "<group>"; };
|
||||||
565F1B2B300597C616B37888 /* FullscreenDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullscreenDialog.swift; sourceTree = "<group>"; };
|
565F1B2B300597C616B37888 /* FullscreenDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullscreenDialog.swift; sourceTree = "<group>"; };
|
||||||
|
56630FCC9F78E5E8F1A75770 /* SettingsScreenHook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenHook.swift; sourceTree = "<group>"; };
|
||||||
56852036214ABA9D7D305768 /* ResolveVerifiedUserSendFailureScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResolveVerifiedUserSendFailureScreenViewModelProtocol.swift; sourceTree = "<group>"; };
|
56852036214ABA9D7D305768 /* ResolveVerifiedUserSendFailureScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResolveVerifiedUserSendFailureScreenViewModelProtocol.swift; sourceTree = "<group>"; };
|
||||||
56D6F88FE35A0979D2821E06 /* AppLockScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreen.swift; sourceTree = "<group>"; };
|
56D6F88FE35A0979D2821E06 /* AppLockScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreen.swift; sourceTree = "<group>"; };
|
||||||
57084488B03BDB33C7B7CA0E /* ResolveVerifiedUserSendFailureScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResolveVerifiedUserSendFailureScreenViewModelTests.swift; sourceTree = "<group>"; };
|
57084488B03BDB33C7B7CA0E /* ResolveVerifiedUserSendFailureScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResolveVerifiedUserSendFailureScreenViewModelTests.swift; sourceTree = "<group>"; };
|
||||||
@@ -3127,6 +3136,7 @@
|
|||||||
D9C5AA3EF7EC67C01C75CEDD /* LabsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabsScreen.swift; sourceTree = "<group>"; };
|
D9C5AA3EF7EC67C01C75CEDD /* LabsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabsScreen.swift; sourceTree = "<group>"; };
|
||||||
DA14564EE143F73F7E4D1F79 /* RoomNotificationSettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsScreenModels.swift; sourceTree = "<group>"; };
|
DA14564EE143F73F7E4D1F79 /* RoomNotificationSettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsScreenModels.swift; sourceTree = "<group>"; };
|
||||||
DA2FEFA393FC7D2870263012 /* LiveLocationSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveLocationSession.swift; sourceTree = "<group>"; };
|
DA2FEFA393FC7D2870263012 /* LiveLocationSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveLocationSession.swift; sourceTree = "<group>"; };
|
||||||
|
DA3BFFFEF695209F27535BED /* HealthChatAccountStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthChatAccountStatus.swift; sourceTree = "<group>"; };
|
||||||
DA3D82522494E78746B2214E /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/SAS.strings; sourceTree = "<group>"; };
|
DA3D82522494E78746B2214E /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/SAS.strings; sourceTree = "<group>"; };
|
||||||
DA46D6DD4B4AB17E1D45092E /* CLLocationManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLLocationManagerProtocol.swift; sourceTree = "<group>"; };
|
DA46D6DD4B4AB17E1D45092E /* CLLocationManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLLocationManagerProtocol.swift; sourceTree = "<group>"; };
|
||||||
DA5CC7FB1CF8C69041B62DD7 /* UserDetailsEditScreenHook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDetailsEditScreenHook.swift; sourceTree = "<group>"; };
|
DA5CC7FB1CF8C69041B62DD7 /* UserDetailsEditScreenHook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDetailsEditScreenHook.swift; sourceTree = "<group>"; };
|
||||||
@@ -3211,6 +3221,7 @@
|
|||||||
E7C3B926E1BBC7569AC6C682 /* TimelineSelectionToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineSelectionToolbar.swift; sourceTree = "<group>"; };
|
E7C3B926E1BBC7569AC6C682 /* TimelineSelectionToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineSelectionToolbar.swift; sourceTree = "<group>"; };
|
||||||
E8294DB9E95C0C0630418466 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; };
|
E8294DB9E95C0C0630418466 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
E8495F37D6245AD0CFA1F60B /* AppLockTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockTests.swift; sourceTree = "<group>"; };
|
E8495F37D6245AD0CFA1F60B /* AppLockTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockTests.swift; sourceTree = "<group>"; };
|
||||||
|
E851BFC1E8A0E1925D26CD14 /* HealthChatAccountStatusLoaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthChatAccountStatusLoaderTests.swift; sourceTree = "<group>"; };
|
||||||
E8A1F98AE670377B20679FF5 /* MediaPlayerProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayerProvider.swift; sourceTree = "<group>"; };
|
E8A1F98AE670377B20679FF5 /* MediaPlayerProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayerProvider.swift; sourceTree = "<group>"; };
|
||||||
E8AE4B3273BA189FDCD4055C /* UserIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicator.swift; sourceTree = "<group>"; };
|
E8AE4B3273BA189FDCD4055C /* UserIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicator.swift; sourceTree = "<group>"; };
|
||||||
E944F717FC10A428D027074D /* RoomPowerLevelsProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPowerLevelsProxyMock.swift; sourceTree = "<group>"; };
|
E944F717FC10A428D027074D /* RoomPowerLevelsProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPowerLevelsProxyMock.swift; sourceTree = "<group>"; };
|
||||||
@@ -3250,6 +3261,7 @@
|
|||||||
ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; path = message.caf; sourceTree = "<group>"; };
|
ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; path = message.caf; sourceTree = "<group>"; };
|
||||||
ED49073BB1C1FC649DAC2CCD /* LocationRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRoomTimelineView.swift; sourceTree = "<group>"; };
|
ED49073BB1C1FC649DAC2CCD /* LocationRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRoomTimelineView.swift; sourceTree = "<group>"; };
|
||||||
ED60E4D2CD678E1EBF16F77A /* BlockedUsersScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreen.swift; sourceTree = "<group>"; };
|
ED60E4D2CD678E1EBF16F77A /* BlockedUsersScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreen.swift; sourceTree = "<group>"; };
|
||||||
|
EDAE8AB9A3CBD957EDD666ED /* HealthChatSettingsScreenHook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthChatSettingsScreenHook.swift; sourceTree = "<group>"; };
|
||||||
EDDE826EAB1BAB80C1104980 /* SpaceFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceFlowCoordinator.swift; sourceTree = "<group>"; };
|
EDDE826EAB1BAB80C1104980 /* SpaceFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceFlowCoordinator.swift; sourceTree = "<group>"; };
|
||||||
EDF6E8208542BE7A02A94939 /* PresentationColorSchemeModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentationColorSchemeModifier.swift; sourceTree = "<group>"; };
|
EDF6E8208542BE7A02A94939 /* PresentationColorSchemeModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentationColorSchemeModifier.swift; sourceTree = "<group>"; };
|
||||||
EE378083653EF0C9B5E9D580 /* EmoteRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItemContent.swift; sourceTree = "<group>"; };
|
EE378083653EF0C9B5E9D580 /* EmoteRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItemContent.swift; sourceTree = "<group>"; };
|
||||||
@@ -4076,6 +4088,7 @@
|
|||||||
3674212CAB6C5143DD437AC4 /* RoomDetailsScreenHook.swift */,
|
3674212CAB6C5143DD437AC4 /* RoomDetailsScreenHook.swift */,
|
||||||
99EB08B2C8B01F3BFE4B0CEC /* RoomMemberDetailsScreenHook.swift */,
|
99EB08B2C8B01F3BFE4B0CEC /* RoomMemberDetailsScreenHook.swift */,
|
||||||
B343C5255FB408DDE853CFDF /* RoomScreenHook.swift */,
|
B343C5255FB408DDE853CFDF /* RoomScreenHook.swift */,
|
||||||
|
56630FCC9F78E5E8F1A75770 /* SettingsScreenHook.swift */,
|
||||||
2A95C9B8299A36A6495DECA6 /* TracingHook.swift */,
|
2A95C9B8299A36A6495DECA6 /* TracingHook.swift */,
|
||||||
DA5CC7FB1CF8C69041B62DD7 /* UserDetailsEditScreenHook.swift */,
|
DA5CC7FB1CF8C69041B62DD7 /* UserDetailsEditScreenHook.swift */,
|
||||||
8B35734F3743563566413C9A /* UserProfileScreenHook.swift */,
|
8B35734F3743563566413C9A /* UserProfileScreenHook.swift */,
|
||||||
@@ -5705,6 +5718,8 @@
|
|||||||
801BA878EE879E01E82F3A5F /* HealthChat */ = {
|
801BA878EE879E01E82F3A5F /* HealthChat */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
46DE3887AAA265406107ABE6 /* HealthChatAccountStatusLoader.swift */,
|
||||||
|
056FCFA596B562977CA89D58 /* HealthChatAccountStatusSection.swift */,
|
||||||
E0B439012B010EC4C3DD24D0 /* HealthChatAppHooks.swift */,
|
E0B439012B010EC4C3DD24D0 /* HealthChatAppHooks.swift */,
|
||||||
E1ECC44D40D05D3A6F0DE0F7 /* HealthChatAppSettingsHook.swift */,
|
E1ECC44D40D05D3A6F0DE0F7 /* HealthChatAppSettingsHook.swift */,
|
||||||
7A60C2712F5607406E8B1C80 /* HealthChatBrandColors.swift */,
|
7A60C2712F5607406E8B1C80 /* HealthChatBrandColors.swift */,
|
||||||
@@ -5714,6 +5729,7 @@
|
|||||||
AD3C983BFE38789B9C5EA7A5 /* HealthChatExtendedProfileSectionHook.swift */,
|
AD3C983BFE38789B9C5EA7A5 /* HealthChatExtendedProfileSectionHook.swift */,
|
||||||
9EDD488380158986A75D6640 /* HealthChatHomeTabsHook.swift */,
|
9EDD488380158986A75D6640 /* HealthChatHomeTabsHook.swift */,
|
||||||
715B60D68D20808E328DA1DA /* HealthChatSettings.swift */,
|
715B60D68D20808E328DA1DA /* HealthChatSettings.swift */,
|
||||||
|
EDAE8AB9A3CBD957EDD666ED /* HealthChatSettingsScreenHook.swift */,
|
||||||
CA14BE0D369AD8098C93F264 /* HealthChatUserDetailsEditScreenHook.swift */,
|
CA14BE0D369AD8098C93F264 /* HealthChatUserDetailsEditScreenHook.swift */,
|
||||||
);
|
);
|
||||||
path = HealthChat;
|
path = HealthChat;
|
||||||
@@ -5864,6 +5880,7 @@
|
|||||||
children = (
|
children = (
|
||||||
79AFBE494F9CCAAE8046203B /* ExtendedProfileServiceTests.swift */,
|
79AFBE494F9CCAAE8046203B /* ExtendedProfileServiceTests.swift */,
|
||||||
8AB4E616E6405AA5DD5A04EA /* ExtendedProfileTests.swift */,
|
8AB4E616E6405AA5DD5A04EA /* ExtendedProfileTests.swift */,
|
||||||
|
E851BFC1E8A0E1925D26CD14 /* HealthChatAccountStatusLoaderTests.swift */,
|
||||||
CB71F86BB7A3AC9B32845B5B /* HealthChatAPIModelsTests.swift */,
|
CB71F86BB7A3AC9B32845B5B /* HealthChatAPIModelsTests.swift */,
|
||||||
087194751572DF7CA65D1CCB /* HealthChatAPIServiceTests.swift */,
|
087194751572DF7CA65D1CCB /* HealthChatAPIServiceTests.swift */,
|
||||||
BB7ABDCA2059497899F226DF /* HealthChatExtendedProfileLoaderTests.swift */,
|
BB7ABDCA2059497899F226DF /* HealthChatExtendedProfileLoaderTests.swift */,
|
||||||
@@ -6693,6 +6710,7 @@
|
|||||||
5EF73F8BBCB34CC51C31995D /* ExtendedProfile.swift */,
|
5EF73F8BBCB34CC51C31995D /* ExtendedProfile.swift */,
|
||||||
E5A1F965895E27CAEA93B58A /* ExtendedProfileService.swift */,
|
E5A1F965895E27CAEA93B58A /* ExtendedProfileService.swift */,
|
||||||
180F8DC392F9D8DA27AAF3E5 /* ExtendedProfileServiceProtocol.swift */,
|
180F8DC392F9D8DA27AAF3E5 /* ExtendedProfileServiceProtocol.swift */,
|
||||||
|
DA3BFFFEF695209F27535BED /* HealthChatAccountStatus.swift */,
|
||||||
CE02F1DAF9054BD6F960D893 /* HealthChatAPIModels.swift */,
|
CE02F1DAF9054BD6F960D893 /* HealthChatAPIModels.swift */,
|
||||||
6004777518436121012B76CF /* HealthChatAPIService.swift */,
|
6004777518436121012B76CF /* HealthChatAPIService.swift */,
|
||||||
994DECEF3C67C4A715B09C82 /* HealthChatAPIServiceProtocol.swift */,
|
994DECEF3C67C4A715B09C82 /* HealthChatAPIServiceProtocol.swift */,
|
||||||
@@ -8527,6 +8545,7 @@
|
|||||||
07756D532EFE33DD1FA258E5 /* GeoURITests.swift in Sources */,
|
07756D532EFE33DD1FA258E5 /* GeoURITests.swift in Sources */,
|
||||||
C40957497CAFADB846784A2A /* HealthChatAPIModelsTests.swift in Sources */,
|
C40957497CAFADB846784A2A /* HealthChatAPIModelsTests.swift in Sources */,
|
||||||
F995036BBA39691CF7AD1F08 /* HealthChatAPIServiceTests.swift in Sources */,
|
F995036BBA39691CF7AD1F08 /* HealthChatAPIServiceTests.swift in Sources */,
|
||||||
|
323D76E640CDB21BD33AD9B9 /* HealthChatAccountStatusLoaderTests.swift in Sources */,
|
||||||
99DE2B618703A58BC3303B92 /* HealthChatExtendedProfileLoaderTests.swift in Sources */,
|
99DE2B618703A58BC3303B92 /* HealthChatExtendedProfileLoaderTests.swift in Sources */,
|
||||||
72031674925CD5CC448FA060 /* HealthChatInviteMemberScreenViewModelTests.swift in Sources */,
|
72031674925CD5CC448FA060 /* HealthChatInviteMemberScreenViewModelTests.swift in Sources */,
|
||||||
60317E05BFDD46BD99C2871B /* HealthChatMemberDirectoryScreenViewModelTests.swift in Sources */,
|
60317E05BFDD46BD99C2871B /* HealthChatMemberDirectoryScreenViewModelTests.swift in Sources */,
|
||||||
@@ -9072,6 +9091,9 @@
|
|||||||
8DB9BEBB357A9F27208A36EF /* HealthChatAPIModels.swift in Sources */,
|
8DB9BEBB357A9F27208A36EF /* HealthChatAPIModels.swift in Sources */,
|
||||||
A87651F9995DE56EF727E24A /* HealthChatAPIService.swift in Sources */,
|
A87651F9995DE56EF727E24A /* HealthChatAPIService.swift in Sources */,
|
||||||
8D57FE274A1929697ACD8254 /* HealthChatAPIServiceProtocol.swift in Sources */,
|
8D57FE274A1929697ACD8254 /* HealthChatAPIServiceProtocol.swift in Sources */,
|
||||||
|
04154A8A1D1A7A819D7CE8E4 /* HealthChatAccountStatus.swift in Sources */,
|
||||||
|
06BF5F111812981DC65CC34F /* HealthChatAccountStatusLoader.swift in Sources */,
|
||||||
|
0EDE5E11785CA18FC8BFF1CC /* HealthChatAccountStatusSection.swift in Sources */,
|
||||||
E7F27870397639C204402434 /* HealthChatAppHooks.swift in Sources */,
|
E7F27870397639C204402434 /* HealthChatAppHooks.swift in Sources */,
|
||||||
665314B4CB31904F107D2D3B /* HealthChatAppSettingsHook.swift in Sources */,
|
665314B4CB31904F107D2D3B /* HealthChatAppSettingsHook.swift in Sources */,
|
||||||
6DDA932A28F531791BDBFB08 /* HealthChatBrandColors.swift in Sources */,
|
6DDA932A28F531791BDBFB08 /* HealthChatBrandColors.swift in Sources */,
|
||||||
@@ -9099,6 +9121,7 @@
|
|||||||
1610E4C02793FFC187434A68 /* HealthChatProfileEditScreenViewModel.swift in Sources */,
|
1610E4C02793FFC187434A68 /* HealthChatProfileEditScreenViewModel.swift in Sources */,
|
||||||
48CD74299B25EF6034EFF68E /* HealthChatProfileEditScreenViewModelProtocol.swift in Sources */,
|
48CD74299B25EF6034EFF68E /* HealthChatProfileEditScreenViewModelProtocol.swift in Sources */,
|
||||||
1E822B072359DD37EEA8CE78 /* HealthChatSettings.swift in Sources */,
|
1E822B072359DD37EEA8CE78 /* HealthChatSettings.swift in Sources */,
|
||||||
|
EA66A9B88287B2E56EB4A95F /* HealthChatSettingsScreenHook.swift in Sources */,
|
||||||
D1A0D9EBE3B9BF4E76DE309A /* HealthChatUserDetailsEditScreenHook.swift in Sources */,
|
D1A0D9EBE3B9BF4E76DE309A /* HealthChatUserDetailsEditScreenHook.swift in Sources */,
|
||||||
0C1E537A49ABB386F7554D4A /* HighlightedTimelineItemModifier.swift in Sources */,
|
0C1E537A49ABB386F7554D4A /* HighlightedTimelineItemModifier.swift in Sources */,
|
||||||
964B9D2EC38C488C360CE0C9 /* HomeScreen.swift in Sources */,
|
964B9D2EC38C488C360CE0C9 /* HomeScreen.swift in Sources */,
|
||||||
@@ -9654,6 +9677,7 @@
|
|||||||
755395927DDD6EBDDA5E217A /* SettingsFlowCoordinator.swift in Sources */,
|
755395927DDD6EBDDA5E217A /* SettingsFlowCoordinator.swift in Sources */,
|
||||||
34F1261CEF6D6A00D559B520 /* SettingsScreen.swift in Sources */,
|
34F1261CEF6D6A00D559B520 /* SettingsScreen.swift in Sources */,
|
||||||
AF8BFA37791E1756EE243E08 /* SettingsScreenCoordinator.swift in Sources */,
|
AF8BFA37791E1756EE243E08 /* SettingsScreenCoordinator.swift in Sources */,
|
||||||
|
A2937A97959A0AFB064BE1DE /* SettingsScreenHook.swift in Sources */,
|
||||||
B93D7CE520088AD53FA6D53C /* SettingsScreenModels.swift in Sources */,
|
B93D7CE520088AD53FA6D53C /* SettingsScreenModels.swift in Sources */,
|
||||||
5E67990F98AF11563EA50080 /* SettingsScreenUserStatusPickerView.swift in Sources */,
|
5E67990F98AF11563EA50080 /* SettingsScreenUserStatusPickerView.swift in Sources */,
|
||||||
B74CCA58911A0C9C3E277A6B /* SettingsScreenUserStatusRow.swift in Sources */,
|
B74CCA58911A0C9C3E277A6B /* SettingsScreenUserStatusRow.swift in Sources */,
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
// HealthChat fork strings. Not managed by Localazy — edit all three languages by hand.
|
// HealthChat fork strings. Not managed by Localazy — edit all three languages by hand.
|
||||||
|
"action_complete_profile" = "Profil vervollständigen";
|
||||||
|
"action_refresh" = "Aktualisieren";
|
||||||
"common_academic_title" = "Titel";
|
"common_academic_title" = "Titel";
|
||||||
"common_address" = "Adresse";
|
"common_address" = "Adresse";
|
||||||
"common_business_email" = "E-Mail (geschäftlich)";
|
"common_business_email" = "E-Mail (geschäftlich)";
|
||||||
@@ -10,6 +12,7 @@
|
|||||||
"common_last_name" = "Nachname";
|
"common_last_name" = "Nachname";
|
||||||
"common_postal_code" = "PLZ";
|
"common_postal_code" = "PLZ";
|
||||||
"common_practice_name" = "Praxisname / Firmenname";
|
"common_practice_name" = "Praxisname / Firmenname";
|
||||||
|
"common_profile_incomplete" = "Profil unvollständig";
|
||||||
"common_specialty" = "Fachgebiet";
|
"common_specialty" = "Fachgebiet";
|
||||||
"common_street" = "Strasse";
|
"common_street" = "Strasse";
|
||||||
"common_website" = "Webseite";
|
"common_website" = "Webseite";
|
||||||
@@ -19,6 +22,7 @@
|
|||||||
"screen_invite_member_success" = "Einladung wurde versendet";
|
"screen_invite_member_success" = "Einladung wurde versendet";
|
||||||
"screen_invite_member_title" = "Mitglied einladen";
|
"screen_invite_member_title" = "Mitglied einladen";
|
||||||
"screen_member_directory_pending_approval" = "Ihr Konto wird derzeit geprüft. Sie erhalten Zugriff auf das Mitgliederverzeichnis, sobald Ihr Konto freigegeben wurde.";
|
"screen_member_directory_pending_approval" = "Ihr Konto wird derzeit geprüft. Sie erhalten Zugriff auf das Mitgliederverzeichnis, sobald Ihr Konto freigegeben wurde.";
|
||||||
|
"screen_member_directory_pending_title" = "Konto wird geprüft";
|
||||||
"screen_member_directory_search_placeholder" = "Name, Praxis, Ort oder Fachgebiet";
|
"screen_member_directory_search_placeholder" = "Name, Praxis, Ort oder Fachgebiet";
|
||||||
"screen_member_directory_title" = "Mitglieder";
|
"screen_member_directory_title" = "Mitglieder";
|
||||||
"screen_profile_edit_section_address" = "Praxisadresse";
|
"screen_profile_edit_section_address" = "Praxisadresse";
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
// HealthChat fork strings. Not managed by Localazy — edit all three languages by hand.
|
// HealthChat fork strings. Not managed by Localazy — edit all three languages by hand.
|
||||||
|
"action_complete_profile" = "Complete profile";
|
||||||
|
"action_refresh" = "Refresh";
|
||||||
"common_academic_title" = "Title";
|
"common_academic_title" = "Title";
|
||||||
"common_address" = "Address";
|
"common_address" = "Address";
|
||||||
"common_business_email" = "Business email";
|
"common_business_email" = "Business email";
|
||||||
@@ -10,6 +12,7 @@
|
|||||||
"common_last_name" = "Last name";
|
"common_last_name" = "Last name";
|
||||||
"common_postal_code" = "Postal code";
|
"common_postal_code" = "Postal code";
|
||||||
"common_practice_name" = "Practice name";
|
"common_practice_name" = "Practice name";
|
||||||
|
"common_profile_incomplete" = "Profile incomplete";
|
||||||
"common_specialty" = "Speciality";
|
"common_specialty" = "Speciality";
|
||||||
"common_street" = "Street";
|
"common_street" = "Street";
|
||||||
"common_website" = "Website";
|
"common_website" = "Website";
|
||||||
@@ -19,6 +22,7 @@
|
|||||||
"screen_invite_member_success" = "Invitation sent";
|
"screen_invite_member_success" = "Invitation sent";
|
||||||
"screen_invite_member_title" = "Invite a member";
|
"screen_invite_member_title" = "Invite a member";
|
||||||
"screen_member_directory_pending_approval" = "Your account is being reviewed. You’ll get access to the member directory once it has been approved.";
|
"screen_member_directory_pending_approval" = "Your account is being reviewed. You’ll get access to the member directory once it has been approved.";
|
||||||
|
"screen_member_directory_pending_title" = "Account under review";
|
||||||
"screen_member_directory_search_placeholder" = "Name, practice, city or speciality";
|
"screen_member_directory_search_placeholder" = "Name, practice, city or speciality";
|
||||||
"screen_member_directory_title" = "Members";
|
"screen_member_directory_title" = "Members";
|
||||||
"screen_profile_edit_section_address" = "Practice address";
|
"screen_profile_edit_section_address" = "Practice address";
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
// HealthChat fork strings. Not managed by Localazy — edit all three languages by hand.
|
// HealthChat fork strings. Not managed by Localazy — edit all three languages by hand.
|
||||||
|
"action_complete_profile" = "Compléter le profil";
|
||||||
|
"action_refresh" = "Actualiser";
|
||||||
"common_academic_title" = "Titre";
|
"common_academic_title" = "Titre";
|
||||||
"common_address" = "Adresse";
|
"common_address" = "Adresse";
|
||||||
"common_business_email" = "Email (professionnel)";
|
"common_business_email" = "Email (professionnel)";
|
||||||
@@ -10,6 +12,7 @@
|
|||||||
"common_last_name" = "Nom";
|
"common_last_name" = "Nom";
|
||||||
"common_postal_code" = "Code postal";
|
"common_postal_code" = "Code postal";
|
||||||
"common_practice_name" = "Nom de l’entreprise";
|
"common_practice_name" = "Nom de l’entreprise";
|
||||||
|
"common_profile_incomplete" = "Profil incomplet";
|
||||||
"common_specialty" = "Spécialisation";
|
"common_specialty" = "Spécialisation";
|
||||||
"common_street" = "Rue";
|
"common_street" = "Rue";
|
||||||
"common_website" = "Site web";
|
"common_website" = "Site web";
|
||||||
@@ -19,6 +22,7 @@
|
|||||||
"screen_invite_member_success" = "Invitation envoyée";
|
"screen_invite_member_success" = "Invitation envoyée";
|
||||||
"screen_invite_member_title" = "Inviter un membre";
|
"screen_invite_member_title" = "Inviter un membre";
|
||||||
"screen_member_directory_pending_approval" = "Votre compte est en cours de vérification. Vous aurez accès à l’annuaire des membres dès qu’il aura été approuvé.";
|
"screen_member_directory_pending_approval" = "Votre compte est en cours de vérification. Vous aurez accès à l’annuaire des membres dès qu’il aura été approuvé.";
|
||||||
|
"screen_member_directory_pending_title" = "Compte en cours de vérification";
|
||||||
"screen_member_directory_search_placeholder" = "Nom, cabinet, ville ou spécialisation";
|
"screen_member_directory_search_placeholder" = "Nom, cabinet, ville ou spécialisation";
|
||||||
"screen_member_directory_title" = "Membres";
|
"screen_member_directory_title" = "Membres";
|
||||||
"screen_profile_edit_section_address" = "Adresse du cabinet";
|
"screen_profile_edit_section_address" = "Adresse du cabinet";
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ final nonisolated class AppHooks: AppHooksProtocol {
|
|||||||
func configure(with userSession: UserSessionProtocol?) async {
|
func configure(with userSession: UserSessionProtocol?) async {
|
||||||
await userSessionHook.configure(with: userSession)
|
await userSessionHook.configure(with: userSession)
|
||||||
await extendedProfileSectionHook.configure(with: userSession)
|
await extendedProfileSectionHook.configure(with: userSession)
|
||||||
|
await settingsScreenHook.configure(with: userSession)
|
||||||
}
|
}
|
||||||
|
|
||||||
@AppHook(default: DefaultAppSettingsHook())
|
@AppHook(default: DefaultAppSettingsHook())
|
||||||
@@ -50,6 +51,9 @@ final nonisolated class AppHooks: AppHooksProtocol {
|
|||||||
@AppHook(default: DefaultHomeTabsHook())
|
@AppHook(default: DefaultHomeTabsHook())
|
||||||
var homeTabsHook: HomeTabsHookProtocol
|
var homeTabsHook: HomeTabsHookProtocol
|
||||||
|
|
||||||
|
@AppHook(default: DefaultSettingsScreenHook())
|
||||||
|
var settingsScreenHook: SettingsScreenHookProtocol
|
||||||
|
|
||||||
@AppHook(default: DefaultUserDetailsEditScreenHook())
|
@AppHook(default: DefaultUserDetailsEditScreenHook())
|
||||||
var userDetailsEditScreenHook: UserDetailsEditScreenHookProtocol
|
var userDetailsEditScreenHook: UserDetailsEditScreenHookProtocol
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
//
|
||||||
|
// 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 the own account's status for the banners that advertise it.
|
||||||
|
/// Nothing is fetched until ``load()`` is called, so the settings screen decides when.
|
||||||
|
@Observable
|
||||||
|
final class HealthChatAccountStatusLoader {
|
||||||
|
private(set) var status: HealthChatAccountStatus = .unknown
|
||||||
|
|
||||||
|
@ObservationIgnored private let apiService: HealthChatAPIServiceProtocol?
|
||||||
|
|
||||||
|
/// The load in flight — await this in tests before asserting.
|
||||||
|
@ObservationIgnored @CancellableTask private(set) var loadingTask: Task<Void, Never>?
|
||||||
|
|
||||||
|
init(apiService: HealthChatAPIServiceProtocol) {
|
||||||
|
self.apiService = apiService
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a loader with a fixed status, for previews.
|
||||||
|
init(status: HealthChatAccountStatus) {
|
||||||
|
self.status = status
|
||||||
|
apiService = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches the status again, replacing a load still in flight.
|
||||||
|
func load() {
|
||||||
|
guard let apiService else { return }
|
||||||
|
loadingTask = Task { await load(from: apiService) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private
|
||||||
|
|
||||||
|
private func load(from apiService: HealthChatAPIServiceProtocol) async {
|
||||||
|
// Every failure below leaves the status untouched: a banner must not report an error.
|
||||||
|
guard case let .success(profile) = await apiService.ownProfile(), !Task.isCancelled else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard profile.isApproved else {
|
||||||
|
status = .pendingApproval
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard case let .success(completeness) = await apiService.profileCompleteness(), !Task.isCancelled else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// The server words the hint itself; without one there is nothing to show.
|
||||||
|
guard !completeness.isComplete, let message = completeness.message else {
|
||||||
|
status = .ok
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
status = .incompleteProfile(message: message)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
//
|
||||||
|
// 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 account status advertised in the app's settings. Renders nothing while loading,
|
||||||
|
/// for approved accounts with a complete profile, and when the status can't be read.
|
||||||
|
///
|
||||||
|
/// Neither banner is dismissible — both resolve server-side.
|
||||||
|
struct HealthChatAccountStatusSection: View {
|
||||||
|
private let loader: HealthChatAccountStatusLoader
|
||||||
|
private let completeProfileAction: () -> Void
|
||||||
|
|
||||||
|
init(loader: HealthChatAccountStatusLoader, completeProfileAction: @escaping () -> Void = { }) {
|
||||||
|
self.loader = loader
|
||||||
|
self.completeProfileAction = completeProfileAction
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
switch loader.status {
|
||||||
|
case .unknown, .ok:
|
||||||
|
EmptyView()
|
||||||
|
case .pendingApproval:
|
||||||
|
Section {
|
||||||
|
ListRow(label: .default(title: HealthChatL10n.screenMemberDirectoryPendingTitle,
|
||||||
|
description: HealthChatL10n.screenMemberDirectoryPendingApproval,
|
||||||
|
icon: \.time),
|
||||||
|
kind: .label)
|
||||||
|
}
|
||||||
|
case .incompleteProfile(let message):
|
||||||
|
Section {
|
||||||
|
ListRow(label: .default(title: HealthChatL10n.commonProfileIncomplete,
|
||||||
|
description: message,
|
||||||
|
icon: \.info),
|
||||||
|
kind: .label)
|
||||||
|
|
||||||
|
ListRow(label: .action(title: HealthChatL10n.actionCompleteProfile, icon: \.userProfile),
|
||||||
|
kind: .button(action: completeProfileAction))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Previews
|
||||||
|
|
||||||
|
struct HealthChatAccountStatusSection_Previews: PreviewProvider, TestablePreview {
|
||||||
|
static var previews: some View {
|
||||||
|
Form {
|
||||||
|
HealthChatAccountStatusSection(loader: .init(status: .pendingApproval))
|
||||||
|
}
|
||||||
|
.compoundList()
|
||||||
|
.previewDisplayName("Pending approval")
|
||||||
|
|
||||||
|
Form {
|
||||||
|
HealthChatAccountStatusSection(loader: .init(status: .incompleteProfile(message: "Bitte vervollständigen Sie Ihr Profil, bevor Sie die Benutzersuche verwenden")))
|
||||||
|
}
|
||||||
|
.compoundList()
|
||||||
|
.previewDisplayName("Incomplete profile")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ extension AppHooks {
|
|||||||
registerCompoundHook(HealthChatCompoundHook())
|
registerCompoundHook(HealthChatCompoundHook())
|
||||||
registerExtendedProfileSectionHook(HealthChatExtendedProfileSectionHook())
|
registerExtendedProfileSectionHook(HealthChatExtendedProfileSectionHook())
|
||||||
registerHomeTabsHook(HealthChatHomeTabsHook())
|
registerHomeTabsHook(HealthChatHomeTabsHook())
|
||||||
|
registerSettingsScreenHook(HealthChatSettingsScreenHook())
|
||||||
registerUserDetailsEditScreenHook(HealthChatUserDetailsEditScreenHook())
|
registerUserDetailsEditScreenHook(HealthChatUserDetailsEditScreenHook())
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
//
|
||||||
|
// 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 HealthChatSettingsScreenHook: SettingsScreenHookProtocol {
|
||||||
|
// Owned here rather than by the section: the settings screen asks for the section on
|
||||||
|
// every body pass. Nothing is fetched until the screen appears and asks for a refresh.
|
||||||
|
private var statusLoader: HealthChatAccountStatusLoader?
|
||||||
|
|
||||||
|
// Constructed during hook registration, before the main actor is reachable.
|
||||||
|
nonisolated init() { }
|
||||||
|
|
||||||
|
func configure(with userSession: UserSessionProtocol?) async {
|
||||||
|
statusLoader = userSession.map {
|
||||||
|
let apiService = HealthChatAPIService(tokenProvider: HealthChatOpenIDTokenProvider(clientProxy: $0.clientProxy))
|
||||||
|
return HealthChatAccountStatusLoader(apiService: apiService)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func accountStatusSection(completeProfileAction: @escaping () -> Void) -> AnyView? {
|
||||||
|
guard let statusLoader else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return AnyView(HealthChatAccountStatusSection(loader: statusLoader,
|
||||||
|
completeProfileAction: completeProfileAction))
|
||||||
|
}
|
||||||
|
|
||||||
|
func refreshAccountStatus() {
|
||||||
|
statusLoader?.load()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
//
|
||||||
|
// 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 SettingsScreenHookProtocol: Sendable {
|
||||||
|
@MainActor func configure(with userSession: UserSessionProtocol?) async
|
||||||
|
|
||||||
|
/// An extra Form section rendered between the user and the account sections,
|
||||||
|
/// advertising whatever is holding the account back.
|
||||||
|
/// - Parameter completeProfileAction: Opens the own profile editor.
|
||||||
|
@MainActor func accountStatusSection(completeProfileAction: @escaping () -> Void) -> AnyView?
|
||||||
|
|
||||||
|
/// Called whenever the settings screen appears, so the section reflects what the user just did.
|
||||||
|
@MainActor func refreshAccountStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DefaultSettingsScreenHook: SettingsScreenHookProtocol {
|
||||||
|
func configure(with userSession: UserSessionProtocol?) async { }
|
||||||
|
|
||||||
|
func accountStatusSection(completeProfileAction: @escaping () -> Void) -> AnyView? {
|
||||||
|
nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func refreshAccountStatus() { }
|
||||||
|
}
|
||||||
@@ -113,7 +113,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
|||||||
|
|
||||||
userIndicatorController = UserIndicatorController()
|
userIndicatorController = UserIndicatorController()
|
||||||
|
|
||||||
elementCallService = ElementCallService()
|
elementCallService = ElementCallService(appSettings: appSettings)
|
||||||
|
|
||||||
navigationRootCoordinator = NavigationRootCoordinator()
|
navigationRootCoordinator = NavigationRootCoordinator()
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ nonisolated protocol CommonSettingsProtocol: AnyObject, Sendable {
|
|||||||
var enableOnlySignedDeviceIsolationMode: Bool { get }
|
var enableOnlySignedDeviceIsolationMode: Bool { get }
|
||||||
var threadsEnabled: Bool { get }
|
var threadsEnabled: Bool { get }
|
||||||
var hideQuietNotificationAlerts: Bool { get }
|
var hideQuietNotificationAlerts: Bool { get }
|
||||||
|
|
||||||
|
var voipPusherAppID: String { get }
|
||||||
|
var pushKitDeviceToken: String? { get }
|
||||||
|
var voipPushGatewayNotifyURL: String? { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
nonisolated enum AppBuildType {
|
nonisolated enum AppBuildType {
|
||||||
@@ -244,6 +248,28 @@ final nonisolated class AppSettings: @unchecked Sendable {
|
|||||||
pushGatewayBaseURL.appending(path: "_matrix/push/v1/notify")
|
pushGatewayBaseURL.appending(path: "_matrix/push/v1/notify")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HealthChat: the NSE lacks the notification filtering entitlement, so it cannot hand ringing
|
||||||
|
// calls to CallKit directly. Instead it asks the push gateway for a PushKit (VoIP) push, the
|
||||||
|
// way the legacy element-ios app did. The app persists what the NSE needs for that request
|
||||||
|
// here, because the NSE neither receives PushKit credentials nor runs the app settings hook.
|
||||||
|
|
||||||
|
/// The push gateway app ID that delivers PushKit pushes (`push_type: voip` in Sygnal).
|
||||||
|
var voipPusherAppID: String {
|
||||||
|
#if DEBUG
|
||||||
|
InfoPlistReader.main.baseBundleIdentifier + ".ios.voip.dev"
|
||||||
|
#else
|
||||||
|
InfoPlistReader.main.baseBundleIdentifier + ".ios.voip.prod"
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The device's PushKit token, base64 encoded like the APNs pushkey.
|
||||||
|
@UserPreference
|
||||||
|
var pushKitDeviceToken: String?
|
||||||
|
|
||||||
|
/// `pushGatewayNotifyEndpoint` as configured in the app, for the NSE.
|
||||||
|
@UserPreference
|
||||||
|
var voipPushGatewayNotifyURL: String?
|
||||||
|
|
||||||
@UserPreference(defaultValue: true)
|
@UserPreference(defaultValue: true)
|
||||||
var enableNotifications: Bool
|
var enableNotifications: Bool
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
private func presentSettingsScreen(animated: Bool) {
|
private func presentSettingsScreen(animated: Bool) {
|
||||||
let settingsScreenCoordinator = SettingsScreenCoordinator(parameters: .init(userSession: flowParameters.userSession,
|
let settingsScreenCoordinator = SettingsScreenCoordinator(parameters: .init(userSession: flowParameters.userSession,
|
||||||
appSettings: flowParameters.appSettings,
|
appSettings: flowParameters.appSettings,
|
||||||
|
appHooks: flowParameters.appHooks,
|
||||||
isBugReportServiceEnabled: flowParameters.bugReportService.isEnabled,
|
isBugReportServiceEnabled: flowParameters.bugReportService.isEnabled,
|
||||||
isInSecondaryWindow: isInSecondaryWindow,
|
isInSecondaryWindow: isInSecondaryWindow,
|
||||||
userIndicatorController: flowParameters.userIndicatorController))
|
userIndicatorController: flowParameters.userIndicatorController))
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ import Foundation
|
|||||||
// swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length
|
// swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length
|
||||||
// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
|
// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
|
||||||
internal nonisolated enum HealthChatL10n {
|
internal nonisolated enum HealthChatL10n {
|
||||||
|
/// Complete profile
|
||||||
|
internal static var actionCompleteProfile: String { return HealthChatL10n.tr("HealthChat", "action_complete_profile") }
|
||||||
|
/// Refresh
|
||||||
|
internal static var actionRefresh: String { return HealthChatL10n.tr("HealthChat", "action_refresh") }
|
||||||
/// Title
|
/// Title
|
||||||
internal static var commonAcademicTitle: String { return HealthChatL10n.tr("HealthChat", "common_academic_title") }
|
internal static var commonAcademicTitle: String { return HealthChatL10n.tr("HealthChat", "common_academic_title") }
|
||||||
/// Address
|
/// Address
|
||||||
@@ -32,6 +36,8 @@ internal nonisolated enum HealthChatL10n {
|
|||||||
internal static var commonPostalCode: String { return HealthChatL10n.tr("HealthChat", "common_postal_code") }
|
internal static var commonPostalCode: String { return HealthChatL10n.tr("HealthChat", "common_postal_code") }
|
||||||
/// Practice name
|
/// Practice name
|
||||||
internal static var commonPracticeName: String { return HealthChatL10n.tr("HealthChat", "common_practice_name") }
|
internal static var commonPracticeName: String { return HealthChatL10n.tr("HealthChat", "common_practice_name") }
|
||||||
|
/// Profile incomplete
|
||||||
|
internal static var commonProfileIncomplete: String { return HealthChatL10n.tr("HealthChat", "common_profile_incomplete") }
|
||||||
/// Speciality
|
/// Speciality
|
||||||
internal static var commonSpecialty: String { return HealthChatL10n.tr("HealthChat", "common_specialty") }
|
internal static var commonSpecialty: String { return HealthChatL10n.tr("HealthChat", "common_specialty") }
|
||||||
/// Street
|
/// Street
|
||||||
@@ -50,6 +56,8 @@ internal nonisolated enum HealthChatL10n {
|
|||||||
internal static var screenInviteMemberTitle: String { return HealthChatL10n.tr("HealthChat", "screen_invite_member_title") }
|
internal static var screenInviteMemberTitle: String { return HealthChatL10n.tr("HealthChat", "screen_invite_member_title") }
|
||||||
/// Your account is being reviewed. You’ll get access to the member directory once it has been approved.
|
/// Your account is being reviewed. You’ll get access to the member directory once it has been approved.
|
||||||
internal static var screenMemberDirectoryPendingApproval: String { return HealthChatL10n.tr("HealthChat", "screen_member_directory_pending_approval") }
|
internal static var screenMemberDirectoryPendingApproval: String { return HealthChatL10n.tr("HealthChat", "screen_member_directory_pending_approval") }
|
||||||
|
/// Account under review
|
||||||
|
internal static var screenMemberDirectoryPendingTitle: String { return HealthChatL10n.tr("HealthChat", "screen_member_directory_pending_title") }
|
||||||
/// Name, practice, city or speciality
|
/// Name, practice, city or speciality
|
||||||
internal static var screenMemberDirectorySearchPlaceholder: String { return HealthChatL10n.tr("HealthChat", "screen_member_directory_search_placeholder") }
|
internal static var screenMemberDirectorySearchPlaceholder: String { return HealthChatL10n.tr("HealthChat", "screen_member_directory_search_placeholder") }
|
||||||
/// Members
|
/// Members
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ enum TestablePreviewsDictionary {
|
|||||||
"FormattingToolbar_Previews" : FormattingToolbar_Previews.self,
|
"FormattingToolbar_Previews" : FormattingToolbar_Previews.self,
|
||||||
"FullscreenDialog_Previews" : FullscreenDialog_Previews.self,
|
"FullscreenDialog_Previews" : FullscreenDialog_Previews.self,
|
||||||
"GalleryRoomTimelineView_Previews" : GalleryRoomTimelineView_Previews.self,
|
"GalleryRoomTimelineView_Previews" : GalleryRoomTimelineView_Previews.self,
|
||||||
|
"HealthChatAccountStatusSection_Previews" : HealthChatAccountStatusSection_Previews.self,
|
||||||
"HealthChatExtendedProfileSection_Previews" : HealthChatExtendedProfileSection_Previews.self,
|
"HealthChatExtendedProfileSection_Previews" : HealthChatExtendedProfileSection_Previews.self,
|
||||||
"HealthChatInviteMemberScreen_Previews" : HealthChatInviteMemberScreen_Previews.self,
|
"HealthChatInviteMemberScreen_Previews" : HealthChatInviteMemberScreen_Previews.self,
|
||||||
"HealthChatMemberDirectoryScreen_Previews" : HealthChatMemberDirectoryScreen_Previews.self,
|
"HealthChatMemberDirectoryScreen_Previews" : HealthChatMemberDirectoryScreen_Previews.self,
|
||||||
|
|||||||
+4
-2
@@ -17,14 +17,15 @@ enum HealthChatMemberDirectoryScreenViewModelAction {
|
|||||||
nonisolated enum HealthChatMemberDirectoryScreenContent: Equatable {
|
nonisolated enum HealthChatMemberDirectoryScreenContent: Equatable {
|
||||||
case loading
|
case loading
|
||||||
case members([HealthChatMember])
|
case members([HealthChatMember])
|
||||||
case pendingApproval(message: String)
|
/// Not approved yet: the server's explanation alongside the default contacts it still allows.
|
||||||
|
case pendingApproval(message: String, members: [HealthChatMember])
|
||||||
case failure
|
case failure
|
||||||
}
|
}
|
||||||
|
|
||||||
struct HealthChatMemberDirectoryScreenViewState: BindableState {
|
struct HealthChatMemberDirectoryScreenViewState: BindableState {
|
||||||
var content: HealthChatMemberDirectoryScreenContent = .loading
|
var content: HealthChatMemberDirectoryScreenContent = .loading
|
||||||
|
|
||||||
/// Inviting others is only available to approved accounts.
|
/// Searching and inviting others are only available to approved accounts.
|
||||||
var isApproved = false
|
var isApproved = false
|
||||||
|
|
||||||
/// The hint shown while the own profile is missing required fields.
|
/// The hint shown while the own profile is missing required fields.
|
||||||
@@ -44,6 +45,7 @@ enum HealthChatMemberDirectoryScreenAlertType: Hashable {
|
|||||||
|
|
||||||
enum HealthChatMemberDirectoryScreenViewAction {
|
enum HealthChatMemberDirectoryScreenViewAction {
|
||||||
case retry
|
case retry
|
||||||
|
case refresh
|
||||||
case selectMember(HealthChatMember)
|
case selectMember(HealthChatMember)
|
||||||
case startChat(HealthChatMember)
|
case startChat(HealthChatMember)
|
||||||
case editProfile
|
case editProfile
|
||||||
|
|||||||
+4
-1
@@ -60,6 +60,8 @@ class HealthChatMemberDirectoryScreenViewModel: HealthChatMemberDirectoryScreenV
|
|||||||
case .retry:
|
case .retry:
|
||||||
state.content = .loading
|
state.content = .loading
|
||||||
fetchMembers()
|
fetchMembers()
|
||||||
|
case .refresh:
|
||||||
|
fetchMembers()
|
||||||
case .selectMember(let member):
|
case .selectMember(let member):
|
||||||
actionsSubject.send(.selectMember(userID: member.userID))
|
actionsSubject.send(.selectMember(userID: member.userID))
|
||||||
case .startChat(let member):
|
case .startChat(let member):
|
||||||
@@ -86,7 +88,8 @@ class HealthChatMemberDirectoryScreenViewModel: HealthChatMemberDirectoryScreenV
|
|||||||
case .success(let response) where !response.isApproved:
|
case .success(let response) where !response.isApproved:
|
||||||
state.isApproved = false
|
state.isApproved = false
|
||||||
state.completenessMessage = nil
|
state.completenessMessage = nil
|
||||||
state.content = .pendingApproval(message: response.pendingMessage ?? HealthChatL10n.screenMemberDirectoryPendingApproval)
|
state.content = .pendingApproval(message: response.pendingMessage ?? HealthChatL10n.screenMemberDirectoryPendingApproval,
|
||||||
|
members: response.users)
|
||||||
case .success(let response):
|
case .success(let response):
|
||||||
state.isApproved = true
|
state.isApproved = true
|
||||||
await updateCompletenessMessage()
|
await updateCompletenessMessage()
|
||||||
|
|||||||
+63
-12
@@ -12,12 +12,21 @@ struct HealthChatMemberDirectoryScreen: View {
|
|||||||
@Bindable var context: HealthChatMemberDirectoryScreenViewModel.Context
|
@Bindable var context: HealthChatMemberDirectoryScreenViewModel.Context
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
// Searching is approved-only, and the search controller can't be applied conditionally.
|
||||||
|
if context.viewState.isApproved {
|
||||||
|
screen
|
||||||
|
.searchController(query: $context.searchQuery,
|
||||||
|
placeholder: HealthChatL10n.screenMemberDirectorySearchPlaceholder)
|
||||||
|
} else {
|
||||||
|
screen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var screen: some View {
|
||||||
content
|
content
|
||||||
.navigationTitle(HealthChatL10n.screenMemberDirectoryTitle)
|
.navigationTitle(HealthChatL10n.screenMemberDirectoryTitle)
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.background(Color.compound.bgCanvasDefault.ignoresSafeArea())
|
.background(Color.compound.bgCanvasDefault.ignoresSafeArea())
|
||||||
.searchController(query: $context.searchQuery,
|
|
||||||
placeholder: HealthChatL10n.screenMemberDirectorySearchPlaceholder)
|
|
||||||
.toolbar { toolbar }
|
.toolbar { toolbar }
|
||||||
.alert(item: $context.alertInfo)
|
.alert(item: $context.alertInfo)
|
||||||
}
|
}
|
||||||
@@ -53,8 +62,8 @@ struct HealthChatMemberDirectoryScreen: View {
|
|||||||
case .loading:
|
case .loading:
|
||||||
ProgressView()
|
ProgressView()
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
case .pendingApproval(let message):
|
case .pendingApproval(let message, let members):
|
||||||
statusView(icon: \.time, message: message)
|
pendingApprovalList(message: message, members: members)
|
||||||
case .failure:
|
case .failure:
|
||||||
failureView
|
failureView
|
||||||
case .members(let members) where members.isEmpty:
|
case .members(let members) where members.isEmpty:
|
||||||
@@ -75,17 +84,44 @@ struct HealthChatMemberDirectoryScreen: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
memberSection(members)
|
||||||
|
}
|
||||||
|
.compoundList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The server allows an unapproved account the default contacts only, so they are listed
|
||||||
|
/// below the hint rather than withheld — they are the way to reach support.
|
||||||
|
private func pendingApprovalList(message: String, members: [HealthChatMember]) -> some View {
|
||||||
|
Form {
|
||||||
Section {
|
Section {
|
||||||
ForEach(members) { member in
|
ListRow(label: .default(title: HealthChatL10n.screenMemberDirectoryPendingTitle,
|
||||||
HealthChatMemberListRow(member: member, mediaProvider: context.mediaProvider) {
|
description: message,
|
||||||
context.send(viewAction: .selectMember(member))
|
icon: \.time),
|
||||||
} startChatAction: {
|
kind: .label)
|
||||||
context.send(viewAction: .startChat(member))
|
|
||||||
}
|
// Without the search field there is no other way to pick up an approval
|
||||||
|
// that arrives while the app is running.
|
||||||
|
ListRow(label: .default(title: HealthChatL10n.actionRefresh, icon: \.restart),
|
||||||
|
kind: .button {
|
||||||
|
context.send(viewAction: .refresh)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
memberSection(members)
|
||||||
|
}
|
||||||
|
.compoundList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func memberSection(_ members: [HealthChatMember]) -> some View {
|
||||||
|
Section {
|
||||||
|
ForEach(members) { member in
|
||||||
|
HealthChatMemberListRow(member: member, mediaProvider: context.mediaProvider) {
|
||||||
|
context.send(viewAction: .selectMember(member))
|
||||||
|
} startChatAction: {
|
||||||
|
context.send(viewAction: .startChat(member))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.compoundList()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var failureView: some View {
|
private var failureView: some View {
|
||||||
@@ -202,7 +238,22 @@ struct HealthChatMemberDirectoryScreen_Previews: PreviewProvider, TestablePrevie
|
|||||||
apiService.profileCompletenessReturnValue = try? .success(JSONDecoder().decode(HealthChatProfileCompleteness.self,
|
apiService.profileCompletenessReturnValue = try? .success(JSONDecoder().decode(HealthChatProfileCompleteness.self,
|
||||||
from: Data(#"{ "isComplete": false, "message": "Profil unvollständig – Fehlende Angaben: Fachgebiet, Ort" }"#.utf8)))
|
from: Data(#"{ "isComplete": false, "message": "Profil unvollständig – Fehlende Angaben: Fachgebiet, Ort" }"#.utf8)))
|
||||||
case .pendingApproval:
|
case .pendingApproval:
|
||||||
apiService.membersSearchQueryReturnValue = .success(.init(users: [],
|
// The server hands an unapproved account its default contacts, nothing else.
|
||||||
|
let defaultContacts = try? JSONDecoder().decode([HealthChatMember].self, from: Data("""
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"userId": "@natascha.katumba:healthchat.ch",
|
||||||
|
"displayName": "Natascha Katumba",
|
||||||
|
"practiceName": "HealthChat Support"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"userId": "@sophia.cagna:healthchat.ch",
|
||||||
|
"displayName": "Sophia Cagna",
|
||||||
|
"practiceName": "HealthChat Support"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
""".utf8))
|
||||||
|
apiService.membersSearchQueryReturnValue = .success(.init(users: defaultContacts ?? [],
|
||||||
isApproved: false,
|
isApproved: false,
|
||||||
pendingMessage: "Ihr Konto wird derzeit geprüft. Sie erhalten Zugriff auf das Mitgliederverzeichnis, sobald Ihr Konto freigegeben wurde."))
|
pendingMessage: "Ihr Konto wird derzeit geprüft. Sie erhalten Zugriff auf das Mitgliederverzeichnis, sobald Ihr Konto freigegeben wurde."))
|
||||||
case .failure:
|
case .failure:
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import SwiftUI
|
|||||||
struct SettingsScreenCoordinatorParameters {
|
struct SettingsScreenCoordinatorParameters {
|
||||||
let userSession: UserSessionProtocol
|
let userSession: UserSessionProtocol
|
||||||
let appSettings: AppSettings
|
let appSettings: AppSettings
|
||||||
|
let appHooks: AppHooks
|
||||||
let isBugReportServiceEnabled: Bool
|
let isBugReportServiceEnabled: Bool
|
||||||
let isInSecondaryWindow: Bool
|
let isInSecondaryWindow: Bool
|
||||||
let userIndicatorController: UserIndicatorControllerProtocol
|
let userIndicatorController: UserIndicatorControllerProtocol
|
||||||
@@ -52,6 +53,7 @@ final class SettingsScreenCoordinator: CoordinatorProtocol {
|
|||||||
init(parameters: SettingsScreenCoordinatorParameters) {
|
init(parameters: SettingsScreenCoordinatorParameters) {
|
||||||
viewModel = SettingsScreenViewModel(userSession: parameters.userSession,
|
viewModel = SettingsScreenViewModel(userSession: parameters.userSession,
|
||||||
appSettings: parameters.appSettings,
|
appSettings: parameters.appSettings,
|
||||||
|
appHooks: parameters.appHooks,
|
||||||
isBugReportServiceEnabled: parameters.isBugReportServiceEnabled,
|
isBugReportServiceEnabled: parameters.isBugReportServiceEnabled,
|
||||||
isInSecondaryWindow: parameters.isInSecondaryWindow,
|
isInSecondaryWindow: parameters.isInSecondaryWindow,
|
||||||
userIndicatorController: parameters.userIndicatorController)
|
userIndicatorController: parameters.userIndicatorController)
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ struct SettingsScreenViewState: BindableState {
|
|||||||
|
|
||||||
let navigationBarVisibility: Visibility
|
let navigationBarVisibility: Visibility
|
||||||
|
|
||||||
|
let appHooks: AppHooks
|
||||||
|
|
||||||
var bindings = SettingsScreenViewStateBindings()
|
var bindings = SettingsScreenViewStateBindings()
|
||||||
|
|
||||||
var userStatusRowMode: SettingsScreenUserStatusRow.Mode {
|
var userStatusRowMode: SettingsScreenUserStatusRow.Mode {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo
|
|||||||
actionsSubject.eraseToAnyPublisher()
|
actionsSubject.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
init(userSession: UserSessionProtocol, appSettings: AppSettings, isBugReportServiceEnabled: Bool, isInSecondaryWindow: Bool, userIndicatorController: UserIndicatorControllerProtocol) {
|
init(userSession: UserSessionProtocol, appSettings: AppSettings, appHooks: AppHooks, isBugReportServiceEnabled: Bool, isInSecondaryWindow: Bool, userIndicatorController: UserIndicatorControllerProtocol) {
|
||||||
self.appSettings = appSettings
|
self.appSettings = appSettings
|
||||||
clientProxy = userSession.clientProxy
|
clientProxy = userSession.clientProxy
|
||||||
self.userIndicatorController = userIndicatorController
|
self.userIndicatorController = userIndicatorController
|
||||||
@@ -34,7 +34,8 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo
|
|||||||
showDeveloperOptions: appSettings.developerOptionsEnabled,
|
showDeveloperOptions: appSettings.developerOptionsEnabled,
|
||||||
showAnalyticsSettings: appSettings.canPromptForAnalytics,
|
showAnalyticsSettings: appSettings.canPromptForAnalytics,
|
||||||
isBugReportServiceEnabled: isBugReportServiceEnabled,
|
isBugReportServiceEnabled: isBugReportServiceEnabled,
|
||||||
navigationBarVisibility: isInSecondaryWindow ? .hidden : .automatic),
|
navigationBarVisibility: isInSecondaryWindow ? .hidden : .automatic,
|
||||||
|
appHooks: appHooks),
|
||||||
mediaProvider: userSession.mediaProvider)
|
mediaProvider: userSession.mediaProvider)
|
||||||
|
|
||||||
appSettings.developerOptionsEnabledPublisher
|
appSettings.developerOptionsEnabledPublisher
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ struct SettingsScreen: View {
|
|||||||
userStatusSection
|
userStatusSection
|
||||||
}
|
}
|
||||||
|
|
||||||
|
context.viewState.appHooks.settingsScreenHook.accountStatusSection {
|
||||||
|
context.send(viewAction: .userDetails)
|
||||||
|
}
|
||||||
|
|
||||||
if !shouldHideManageAccountSection {
|
if !shouldHideManageAccountSection {
|
||||||
manageAccountSection
|
manageAccountSection
|
||||||
}
|
}
|
||||||
@@ -46,6 +50,7 @@ struct SettingsScreen: View {
|
|||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbarVisibility(context.viewState.navigationBarVisibility, for: .navigationBar)
|
.toolbarVisibility(context.viewState.navigationBarVisibility, for: .navigationBar)
|
||||||
.toolbar { toolbar }
|
.toolbar { toolbar }
|
||||||
|
.onAppear { context.viewState.appHooks.settingsScreenHook.refreshAccountStatus() }
|
||||||
.sheet(isPresented: $context.isPresentingStatusPicker) {
|
.sheet(isPresented: $context.isPresentingStatusPicker) {
|
||||||
SettingsScreenUserStatusPickerView { action in
|
SettingsScreenUserStatusPickerView { action in
|
||||||
context.send(viewAction: .userStatus(action))
|
context.send(viewAction: .userStatus(action))
|
||||||
@@ -314,6 +319,7 @@ struct SettingsScreen_Previews: PreviewProvider, TestablePreview {
|
|||||||
status: .mockFocussing))))
|
status: .mockFocussing))))
|
||||||
return SettingsScreenViewModel(userSession: userSession,
|
return SettingsScreenViewModel(userSession: userSession,
|
||||||
appSettings: .volatile(),
|
appSettings: .volatile(),
|
||||||
|
appHooks: AppHooks(),
|
||||||
isBugReportServiceEnabled: isBugReportServiceEnabled,
|
isBugReportServiceEnabled: isBugReportServiceEnabled,
|
||||||
isInSecondaryWindow: false,
|
isInSecondaryWindow: false,
|
||||||
userIndicatorController: UserIndicatorControllerMock())
|
userIndicatorController: UserIndicatorControllerMock())
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
|
|||||||
private let callController = CXCallController()
|
private let callController = CXCallController()
|
||||||
private let callProvider: CXProviderProtocol
|
private let callProvider: CXProviderProtocol
|
||||||
private let timeProvider: TimeProvider
|
private let timeProvider: TimeProvider
|
||||||
|
private let appSettings: AppSettings
|
||||||
|
|
||||||
private weak var clientProxy: ClientProxyProtocol? {
|
private weak var clientProxy: ClientProxyProtocol? {
|
||||||
didSet {
|
didSet {
|
||||||
@@ -67,10 +68,11 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
|
|||||||
|
|
||||||
private var declineListenerHandle: TaskHandle?
|
private var declineListenerHandle: TaskHandle?
|
||||||
|
|
||||||
init(callProvider: CXProviderProtocol? = nil, timeProvider: TimeProvider? = nil) {
|
init(callProvider: CXProviderProtocol? = nil, timeProvider: TimeProvider? = nil, appSettings: AppSettings) {
|
||||||
pushRegistry = PKPushRegistry(queue: nil)
|
pushRegistry = PKPushRegistry(queue: nil)
|
||||||
|
|
||||||
self.timeProvider = timeProvider ?? TimeProvider(clock: ContinuousClock(), now: Date.init)
|
self.timeProvider = timeProvider ?? TimeProvider(clock: ContinuousClock(), now: Date.init)
|
||||||
|
self.appSettings = appSettings
|
||||||
|
|
||||||
if let callProvider {
|
if let callProvider {
|
||||||
self.callProvider = callProvider
|
self.callProvider = callProvider
|
||||||
@@ -157,7 +159,16 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
|
|||||||
|
|
||||||
// MARK: - PKPushRegistryDelegate
|
// MARK: - PKPushRegistryDelegate
|
||||||
|
|
||||||
func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) { }
|
func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
|
||||||
|
// HealthChat: stored for the NSE, which requests a VoIP push through the push gateway
|
||||||
|
// for ringing calls as long as the notification filtering entitlement is missing.
|
||||||
|
appSettings.pushKitDeviceToken = pushCredentials.token.base64EncodedString()
|
||||||
|
appSettings.voipPushGatewayNotifyURL = appSettings.pushGatewayNotifyEndpoint.absoluteString
|
||||||
|
}
|
||||||
|
|
||||||
|
func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) {
|
||||||
|
appSettings.pushKitDeviceToken = nil
|
||||||
|
}
|
||||||
|
|
||||||
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
|
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
|
||||||
guard let roomID = payload.dictionaryPayload[ElementCallServiceNotificationKey.roomID.rawValue] as? String else {
|
guard let roomID = payload.dictionaryPayload[ElementCallServiceNotificationKey.roomID.rawValue] as? String else {
|
||||||
@@ -189,7 +200,7 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
|
|||||||
let callID = CallID(callKitID: UUID(), roomID: roomID, rtcNotificationID: rtcNotificationID, isVoiceCall: isVoiceCall)
|
let callID = CallID(callKitID: UUID(), roomID: roomID, rtcNotificationID: rtcNotificationID, isVoiceCall: isVoiceCall)
|
||||||
incomingCallID = callID
|
incomingCallID = callID
|
||||||
|
|
||||||
guard let expirationDate = (payload.dictionaryPayload[ElementCallServiceNotificationKey.expirationDate.rawValue] as? Date) else {
|
guard let expirationDate = Self.expirationDate(from: payload.dictionaryPayload) else {
|
||||||
MXLog.error("Something went wrong, missing expiration timestamp for incoming voip call: \(payload)")
|
MXLog.error("Something went wrong, missing expiration timestamp for incoming voip call: \(payload)")
|
||||||
assertionFailure("Incoming voip push is missing the expiration timestamp")
|
assertionFailure("Incoming voip push is missing the expiration timestamp")
|
||||||
clearIncomingCallState()
|
clearIncomingCallState()
|
||||||
@@ -353,6 +364,19 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
|
|||||||
/// a call and immediately end it. `callerInfo` names the call after the room so the brief
|
/// a call and immediately end it. `callerInfo` names the call after the room so the brief
|
||||||
/// system UI and the Recents entry show the room rather than "Unknown", and the entry can be
|
/// system UI and the Recents entry show the room rather than "Unknown", and the entry can be
|
||||||
/// tapped to call back.
|
/// tapped to call back.
|
||||||
|
/// CallKit hands the NSE's payload over as-is, so the expiration arrives as a `Date`. A VoIP push
|
||||||
|
/// requested from the push gateway travels as JSON, where the same key is seconds since 1970.
|
||||||
|
private static func expirationDate(from payload: [AnyHashable: Any]) -> Date? {
|
||||||
|
switch payload[ElementCallServiceNotificationKey.expirationDate.rawValue] {
|
||||||
|
case let date as Date:
|
||||||
|
date
|
||||||
|
case let secondsSince1970 as TimeInterval:
|
||||||
|
Date(timeIntervalSince1970: secondsSince1970)
|
||||||
|
default:
|
||||||
|
nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func reportImmediatelyEndedCall(reason: CXCallEndedReason,
|
private func reportImmediatelyEndedCall(reason: CXCallEndedReason,
|
||||||
callerInfo: (roomID: String, roomDisplayName: String?)? = nil,
|
callerInfo: (roomID: String, roomDisplayName: String?)? = nil,
|
||||||
completion: @escaping () -> Void) {
|
completion: @escaping () -> Void) {
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
//
|
||||||
|
// 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
|
||||||
|
|
||||||
|
/// What is holding an account back from using the whole app.
|
||||||
|
nonisolated enum HealthChatAccountStatus: Equatable {
|
||||||
|
/// Nothing is known, e.g. the API is unreachable. Renders nothing — never an error.
|
||||||
|
case unknown
|
||||||
|
/// Not approved by an admin yet. Takes precedence over ``incompleteProfile``,
|
||||||
|
/// as completing a profile does nothing for an unapproved account.
|
||||||
|
case pendingApproval
|
||||||
|
/// Approved, but the server reports missing profile fields. The message is its wording.
|
||||||
|
case incompleteProfile(message: String)
|
||||||
|
case ok
|
||||||
|
}
|
||||||
@@ -9,6 +9,18 @@ matches `## Changes in <version>` exactly.
|
|||||||
|
|
||||||
Upstream's `CHANGES.md` tracks element-hq's own releases and must not be edited here.
|
Upstream's `CHANGES.md` tracks element-hq's own releases and must not be edited here.
|
||||||
|
|
||||||
|
## Changes in 26.10.1 (2026-10-12)
|
||||||
|
|
||||||
|
🙌 Improvements
|
||||||
|
|
||||||
|
- Incoming calls ring again on iOS, with the system call screen and an entry in the Phone app.
|
||||||
|
- Members see their account status in Settings and default contacts while approval is pending.
|
||||||
|
|
||||||
|
⚠️ Known issues
|
||||||
|
|
||||||
|
- An "Incoming call" notification is shown next to the call screen, and a few notifications
|
||||||
|
may show generic text, while Apple reviews the notification filtering entitlement.
|
||||||
|
|
||||||
## Changes in 26.09.2 (2026-09-24)
|
## Changes in 26.09.2 (2026-09-24)
|
||||||
|
|
||||||
🙌 Improvements
|
🙌 Improvements
|
||||||
|
|||||||
@@ -235,13 +235,62 @@ nonisolated class NotificationHandler {
|
|||||||
try await CXProvider.reportNewIncomingVoIPPushPayload(payload)
|
try await CXProvider.reportNewIncomingVoIPPushPayload(payload)
|
||||||
MXLog.info("Call notification delegated to CallKit")
|
MXLog.info("Call notification delegated to CallKit")
|
||||||
} catch {
|
} catch {
|
||||||
MXLog.error("Failed reporting voip call with error: \(error). Handling as push notification")
|
// HealthChat: without the notification filtering entitlement CallKit rejects the hand-off,
|
||||||
|
// so ask the push gateway for a PushKit push instead and keep the alert visible.
|
||||||
|
MXLog.error("Failed reporting voip call with error: \(error). Requesting a VoIP push instead")
|
||||||
|
await requestVoIPPush(roomID: roomID,
|
||||||
|
roomDisplayName: roomDisplayName,
|
||||||
|
rtcNotifyEventID: rtcNotifyEventID,
|
||||||
|
expirationDate: expirationDate,
|
||||||
|
isVoiceCall: callIntent == RtcCallIntent.audio)
|
||||||
return .shouldDisplay
|
return .shouldDisplay
|
||||||
}
|
}
|
||||||
|
|
||||||
return .processedShouldDiscard
|
return .processedShouldDiscard
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Asks the push gateway to deliver a PushKit push with the call details, the way the legacy
|
||||||
|
/// element-ios app did. The app's `PKPushRegistryDelegate` then reports the call to CallKit.
|
||||||
|
private func requestVoIPPush(roomID: String, roomDisplayName: String, rtcNotifyEventID: String, expirationDate: Date, isVoiceCall: Bool) async {
|
||||||
|
guard let token = settings.pushKitDeviceToken,
|
||||||
|
let notifyURL = settings.voipPushGatewayNotifyURL.flatMap(URL.init(string:)) else {
|
||||||
|
MXLog.warning("No PushKit token or push gateway stored by the app, cannot request a VoIP push")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// The gateway merges `default_payload` into the push verbatim, so the keys match what the app expects.
|
||||||
|
let defaultPayload: [String: Any] = [ElementCallServiceNotificationKey.roomID.rawValue: roomID,
|
||||||
|
ElementCallServiceNotificationKey.roomDisplayName.rawValue: roomDisplayName,
|
||||||
|
ElementCallServiceNotificationKey.rtcNotifyEventID.rawValue: rtcNotifyEventID,
|
||||||
|
ElementCallServiceNotificationKey.expirationDate.rawValue: expirationDate.timeIntervalSince1970,
|
||||||
|
ElementCallServiceNotificationKey.isVoiceCall.rawValue: isVoiceCall]
|
||||||
|
let body: [String: Any] = ["notification": ["event_id": rtcNotifyEventID,
|
||||||
|
"room_id": roomID,
|
||||||
|
"prio": "high",
|
||||||
|
"devices": [["app_id": settings.voipPusherAppID,
|
||||||
|
"pushkey": token,
|
||||||
|
"data": ["format": "event_id_only",
|
||||||
|
"default_payload": defaultPayload]]]]]
|
||||||
|
|
||||||
|
var request = URLRequest(url: notifyURL, timeoutInterval: 5)
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
|
|
||||||
|
do {
|
||||||
|
request.httpBody = try JSONSerialization.data(withJSONObject: body)
|
||||||
|
let (data, response) = try await URLSession.shared.data(for: request)
|
||||||
|
let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0
|
||||||
|
let rejected = (try? JSONSerialization.jsonObject(with: data) as? [String: Any])?["rejected"] as? [String] ?? []
|
||||||
|
guard statusCode == 200, rejected.isEmpty else {
|
||||||
|
MXLog.error("Push gateway refused the VoIP push: status \(statusCode), rejected \(rejected.count) pushkey(s)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
MXLog.info("Requested a VoIP push for the call in \(roomID)")
|
||||||
|
} catch {
|
||||||
|
MXLog.error("Failed requesting a VoIP push: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private enum NotificationProcessingResult {
|
private enum NotificationProcessingResult {
|
||||||
case shouldDisplay
|
case shouldDisplay
|
||||||
case processedShouldDiscard
|
case processedShouldDiscard
|
||||||
|
|||||||
@@ -340,6 +340,13 @@ extension PreviewTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func healthChatAccountStatusSection() async throws {
|
||||||
|
for (index, preview) in HealthChatAccountStatusSection_Previews._allPreviews.enumerated() {
|
||||||
|
try await assertSnapshots(matching: preview, step: index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
func healthChatExtendedProfileSection() async throws {
|
func healthChatExtendedProfileSection() async throws {
|
||||||
for (index, preview) in HealthChatExtendedProfileSection_Previews._allPreviews.enumerated() {
|
for (index, preview) in HealthChatExtendedProfileSection_Previews._allPreviews.enumerated() {
|
||||||
|
|||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:56e4888d0b7b72b89d0a755ba6945ba66eec0409d522a73b0dd41ad2607e4839
|
||||||
|
size 98189
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:94e0a31cd4c89d86725ac7f2f24efdeb5e90a4d539ac8d61399af173fc51c12a
|
||||||
|
size 99990
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:df681ee07447a0313a23cda55910a5b874b93dbc761c9f81c21f38dd95a20599
|
||||||
|
size 51658
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:55d8ceadac6fa5fd407a70368e35733b055f19f465108784b49fb0189c0aa9d8
|
||||||
|
size 55001
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:7fb1832cf11eb4a859921e63c61b74c902c516d9d9e1c0c9135e7c753f73ce94
|
||||||
|
size 94576
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:62572abbd7579066c8b2d552f2304a8088523a326b24d737c6334ee012ffde82
|
||||||
|
size 106619
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:b2b7549e839d4a51ffac1f5b226fe856119570e92d7bbb405a4b42fd946f3022
|
||||||
|
size 51171
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:94275d7f7d28cc6277a226c32ecaf4faed77468355ffe9cb54879fdea9282dbe
|
||||||
|
size 67199
|
||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:1330bdf08030ca860724fd5c2f65033004dd42c066fc8617ff8d00954bad83c8
|
oid sha256:564959b31187328722008fc6482df285b5d67b5722a785bdb5419d7b9f29e73a
|
||||||
size 79809
|
size 80013
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ca1253d8529e3c7ca8541d8e9b4533680496e97fed4d0783a48998cb01137821
|
oid sha256:d07ce58d9c680476c5dd2dbf91e462216d096d522e888856ad2fe02ed8b8f41d
|
||||||
size 81699
|
size 81803
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:91247aee8564f70018e5ef3fe17966fc2111d1c1a1f910e8ea382657320adf43
|
oid sha256:fb57f598fb89e77a1b4aca07ac13c93ba1aa19dac38ef069383bcb00e0d28fb6
|
||||||
size 39454
|
size 39750
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:736ba40f231684e1a886bc5aac2ca14002f0d73329c25bc3786f6733c09e310c
|
oid sha256:282b1c9d62d4479e6c2fb182063efa6823aa833cfe6181cfea23d8c04e110777
|
||||||
size 42377
|
size 42755
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:de8ee6d4ea64a8bbec86e011b8d6ff507ed1f0c8664e5f2b50c2e25f8e089525
|
oid sha256:6aa7df7096f1eff7e7ea53ad04f792c847bd7615699464aabc7b0eebd5d3f1d7
|
||||||
size 85969
|
size 128975
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:de8ee6d4ea64a8bbec86e011b8d6ff507ed1f0c8664e5f2b50c2e25f8e089525
|
oid sha256:ddfed612e6c64a14c574568c4d5fbbf6d7f784dfbdcfd6ad21942e72bad59b26
|
||||||
size 85969
|
size 130543
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:67522686b52a4479ea55eb1a202e516236ce40c7277112ca77e3c86d9ba565a6
|
oid sha256:dea98e8d5fccba2f9f8778453d4f19f09ab5867cfa0262c73379587fa4f9a2ec
|
||||||
size 50652
|
size 79446
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:67522686b52a4479ea55eb1a202e516236ce40c7277112ca77e3c86d9ba565a6
|
oid sha256:c8248a256752f282470c2c080ec933d616322787519d9541a5789cbe0486125e
|
||||||
size 50652
|
size 86402
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ final class ElementCallServiceTests {
|
|||||||
let dateProvider: () -> Date = {
|
let dateProvider: () -> Date = {
|
||||||
self.currentDate
|
self.currentDate
|
||||||
}
|
}
|
||||||
service = ElementCallService(callProvider: callProvider, timeProvider: TimeProvider(clock: clock, now: dateProvider))
|
service = ElementCallService(callProvider: callProvider,
|
||||||
|
timeProvider: TimeProvider(clock: clock, now: dateProvider),
|
||||||
|
appSettings: AppSettings.volatile())
|
||||||
}
|
}
|
||||||
|
|
||||||
isolated deinit {
|
isolated deinit {
|
||||||
@@ -57,6 +59,21 @@ final class ElementCallServiceTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// HealthChat: a VoIP push requested from the push gateway carries the expiration as JSON seconds.
|
||||||
|
@Test
|
||||||
|
func incomingCallFromPushGatewayVoIPPush() async {
|
||||||
|
await waitForConfirmation { confirmation in
|
||||||
|
let pkPushPayloadMock = PKPushPayloadMock().updatingExpiration(secondsSince1970: currentDate.timeIntervalSince1970 + 30)
|
||||||
|
|
||||||
|
service.pushRegistry(pushRegistry, didReceiveIncomingPushWith: pkPushPayloadMock, for: .voIP) {
|
||||||
|
confirmation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#expect(callProvider.reportNewIncomingCallWithUpdateCompletionCalled)
|
||||||
|
#expect(!callProvider.reportCallWithEndedAtReasonCalled)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
func incomingVoiceCall() async {
|
func incomingVoiceCall() async {
|
||||||
#expect(!callProvider.reportNewIncomingCallWithUpdateCompletionCalled)
|
#expect(!callProvider.reportNewIncomingCallWithUpdateCompletionCalled)
|
||||||
@@ -280,6 +297,11 @@ private nonisolated class PKPushPayloadMock: PKPushPayload {
|
|||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updatingExpiration(secondsSince1970: TimeInterval) -> Self {
|
||||||
|
dict[ElementCallServiceNotificationKey.expirationDate.rawValue] = secondsSince1970
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
func updateIsVoice(_ isVoice: Bool) -> Self {
|
func updateIsVoice(_ isVoice: Bool) -> Self {
|
||||||
dict[ElementCallServiceNotificationKey.isVoiceCall.rawValue] = isVoice
|
dict[ElementCallServiceNotificationKey.isVoiceCall.rawValue] = isVoice
|
||||||
return self
|
return self
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
//
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
|
||||||
|
@testable import ElementX
|
||||||
|
import Foundation
|
||||||
|
import Testing
|
||||||
|
|
||||||
|
final class HealthChatAccountStatusLoaderTests {
|
||||||
|
private let apiService = HealthChatAPIServiceMock()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func reportsPendingApprovalForAnUnapprovedAccount() async throws {
|
||||||
|
apiService.ownProfileReturnValue = try .success(makeMember(isApproved: false))
|
||||||
|
|
||||||
|
#expect(await status() == .pendingApproval)
|
||||||
|
#expect(apiService.profileCompletenessCallsCount == 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func reportsTheIncompleteProfileMessage() async throws {
|
||||||
|
apiService.ownProfileReturnValue = try .success(makeMember(isApproved: true))
|
||||||
|
apiService.profileCompletenessReturnValue = try .success(makeCompleteness(isComplete: false,
|
||||||
|
message: "Profil unvollständig"))
|
||||||
|
|
||||||
|
#expect(await status() == .incompleteProfile(message: "Profil unvollständig"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func reportsOKForAnApprovedAccountWithACompleteProfile() async throws {
|
||||||
|
apiService.ownProfileReturnValue = try .success(makeMember(isApproved: true))
|
||||||
|
apiService.profileCompletenessReturnValue = try .success(makeCompleteness(isComplete: true, message: nil))
|
||||||
|
|
||||||
|
#expect(await status() == .ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func reportsOKWhenTheServerWordsNoHint() async throws {
|
||||||
|
apiService.ownProfileReturnValue = try .success(makeMember(isApproved: true))
|
||||||
|
apiService.profileCompletenessReturnValue = try .success(makeCompleteness(isComplete: false, message: nil))
|
||||||
|
|
||||||
|
#expect(await status() == .ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func staysUnknownWhenTheProfileCannotBeRead() async {
|
||||||
|
apiService.ownProfileReturnValue = .failure(.networkFailure)
|
||||||
|
|
||||||
|
#expect(await status() == .unknown)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func staysUnknownWhenCompletenessCannotBeRead() async throws {
|
||||||
|
apiService.ownProfileReturnValue = try .success(makeMember(isApproved: true))
|
||||||
|
apiService.profileCompletenessReturnValue = .failure(.networkFailure)
|
||||||
|
|
||||||
|
#expect(await status() == .unknown)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func fetchesNothingUntilAsked() throws {
|
||||||
|
apiService.ownProfileReturnValue = try .success(makeMember(isApproved: false))
|
||||||
|
|
||||||
|
let loader = HealthChatAccountStatusLoader(apiService: apiService)
|
||||||
|
|
||||||
|
#expect(loader.status == .unknown)
|
||||||
|
#expect(apiService.ownProfileCallsCount == 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func loadingAgainReflectsTheNewServerState() async throws {
|
||||||
|
apiService.ownProfileReturnValue = try .success(makeMember(isApproved: true))
|
||||||
|
apiService.profileCompletenessReturnValue = try .success(makeCompleteness(isComplete: false,
|
||||||
|
message: "Profil unvollständig"))
|
||||||
|
let loader = HealthChatAccountStatusLoader(apiService: apiService)
|
||||||
|
loader.load()
|
||||||
|
await loader.loadingTask?.value
|
||||||
|
#expect(loader.status == .incompleteProfile(message: "Profil unvollständig"))
|
||||||
|
|
||||||
|
apiService.profileCompletenessReturnValue = try .success(makeCompleteness(isComplete: true, message: nil))
|
||||||
|
loader.load()
|
||||||
|
await loader.loadingTask?.value
|
||||||
|
|
||||||
|
#expect(loader.status == .ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func aFailedReloadKeepsTheLastKnownStatus() async throws {
|
||||||
|
apiService.ownProfileReturnValue = try .success(makeMember(isApproved: false))
|
||||||
|
let loader = HealthChatAccountStatusLoader(apiService: apiService)
|
||||||
|
loader.load()
|
||||||
|
await loader.loadingTask?.value
|
||||||
|
#expect(loader.status == .pendingApproval)
|
||||||
|
|
||||||
|
apiService.ownProfileReturnValue = .failure(.networkFailure)
|
||||||
|
loader.load()
|
||||||
|
await loader.loadingTask?.value
|
||||||
|
|
||||||
|
#expect(loader.status == .pendingApproval)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Helpers
|
||||||
|
|
||||||
|
private func status() async -> HealthChatAccountStatus {
|
||||||
|
let loader = HealthChatAccountStatusLoader(apiService: apiService)
|
||||||
|
loader.load()
|
||||||
|
await loader.loadingTask?.value
|
||||||
|
return loader.status
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeMember(isApproved: Bool) throws -> HealthChatMember {
|
||||||
|
try JSONDecoder().decode(HealthChatMember.self, from: Data("""
|
||||||
|
{ "userId": "@alice:healthchat.ch", "isApproved": \(isApproved) }
|
||||||
|
""".utf8))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeCompleteness(isComplete: Bool, message: String?) throws -> HealthChatProfileCompleteness {
|
||||||
|
let messageJSON = message.map { "\"\($0)\"" } ?? "null"
|
||||||
|
return try JSONDecoder().decode(HealthChatProfileCompleteness.self, from: Data("""
|
||||||
|
{ "isComplete": \(isComplete), "message": \(messageJSON) }
|
||||||
|
""".utf8))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,19 +31,46 @@ final class HealthChatMemberDirectoryScreenViewModelTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
func showsPendingApprovalMessage() async throws {
|
func showsPendingApprovalMessageWithTheDefaultContacts() async throws {
|
||||||
apiService.membersSearchQueryReturnValue = .success(.init(users: [],
|
apiService.membersSearchQueryReturnValue = try .success(makeResponse(userIDs: ["@natascha.katumba:healthchat.ch"],
|
||||||
isApproved: false,
|
isApproved: false,
|
||||||
pendingMessage: "Ihr Konto wird geprüft"))
|
pendingMessage: "Ihr Konto wird geprüft"))
|
||||||
|
|
||||||
let viewModel = makeViewModel()
|
let viewModel = makeViewModel()
|
||||||
|
|
||||||
let deferred = deferFulfillment(viewModel.context.observe(\.viewState.content)) { content in
|
let deferred = deferFulfillment(viewModel.context.observe(\.viewState.content)) { content in
|
||||||
content == .pendingApproval(message: "Ihr Konto wird geprüft")
|
guard case .pendingApproval(let message, let members) = content else { return false }
|
||||||
|
return message == "Ihr Konto wird geprüft" && members.map(\.userID) == ["@natascha.katumba:healthchat.ch"]
|
||||||
}
|
}
|
||||||
try await deferred.fulfill()
|
try await deferred.fulfill()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func refreshPicksUpAnApprovalWithoutClearingTheList() async throws {
|
||||||
|
apiService.membersSearchQueryReturnValue = try .success(makeResponse(userIDs: ["@natascha.katumba:healthchat.ch"],
|
||||||
|
isApproved: false,
|
||||||
|
pendingMessage: "Ihr Konto wird geprüft"))
|
||||||
|
|
||||||
|
let viewModel = makeViewModel()
|
||||||
|
|
||||||
|
let deferredPending = deferFulfillment(viewModel.context.observe(\.viewState.content)) { content in
|
||||||
|
guard case .pendingApproval = content else { return false }
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
try await deferredPending.fulfill()
|
||||||
|
|
||||||
|
apiService.membersSearchQueryReturnValue = try .success(makeResponse(userIDs: ["@alice:healthchat.ch"]))
|
||||||
|
|
||||||
|
let deferredMembers = deferFulfillment(viewModel.context.observe(\.viewState.content)) { content in
|
||||||
|
guard case .members(let members) = content else { return false }
|
||||||
|
return members.map(\.userID) == ["@alice:healthchat.ch"]
|
||||||
|
}
|
||||||
|
viewModel.context.send(viewAction: .refresh)
|
||||||
|
try await deferredMembers.fulfill()
|
||||||
|
|
||||||
|
#expect(viewModel.context.viewState.isApproved)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
func showsFailureAndRecoversOnRetry() async throws {
|
func showsFailureAndRecoversOnRetry() async throws {
|
||||||
apiService.membersSearchQueryReturnValue = .failure(.networkFailure)
|
apiService.membersSearchQueryReturnValue = .failure(.networkFailure)
|
||||||
@@ -160,10 +187,10 @@ final class HealthChatMemberDirectoryScreenViewModelTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
func pendingApprovalDisablesInviting() async throws {
|
func pendingApprovalDisablesSearchingAndInviting() async throws {
|
||||||
apiService.membersSearchQueryReturnValue = .success(.init(users: [],
|
apiService.membersSearchQueryReturnValue = try .success(makeResponse(userIDs: [],
|
||||||
isApproved: false,
|
isApproved: false,
|
||||||
pendingMessage: "Ihr Konto wird geprüft"))
|
pendingMessage: "Ihr Konto wird geprüft"))
|
||||||
|
|
||||||
let viewModel = makeViewModel()
|
let viewModel = makeViewModel()
|
||||||
|
|
||||||
@@ -220,8 +247,10 @@ final class HealthChatMemberDirectoryScreenViewModelTests {
|
|||||||
userIndicatorController: UserIndicatorControllerMock())
|
userIndicatorController: UserIndicatorControllerMock())
|
||||||
}
|
}
|
||||||
|
|
||||||
private func makeResponse(userIDs: [String]) throws -> HealthChatMembersResponse {
|
private func makeResponse(userIDs: [String],
|
||||||
try .init(users: userIDs.map(makeMember(userID:)), isApproved: true, pendingMessage: nil)
|
isApproved: Bool = true,
|
||||||
|
pendingMessage: String? = nil) throws -> HealthChatMembersResponse {
|
||||||
|
try .init(users: userIDs.map(makeMember(userID:)), isApproved: isApproved, pendingMessage: pendingMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func makeMember(userID: String) throws -> HealthChatMember {
|
private func makeMember(userID: String) throws -> HealthChatMember {
|
||||||
|
|||||||
@@ -178,6 +178,7 @@ struct SettingsScreenViewModelTests {
|
|||||||
clientProxy = ClientProxyMock(.init(userID: "", status: status))
|
clientProxy = ClientProxyMock(.init(userID: "", status: status))
|
||||||
viewModel = SettingsScreenViewModel(userSession: UserSessionMock(.init(clientProxy: clientProxy)),
|
viewModel = SettingsScreenViewModel(userSession: UserSessionMock(.init(clientProxy: clientProxy)),
|
||||||
appSettings: AppSettings.volatile(),
|
appSettings: AppSettings.volatile(),
|
||||||
|
appHooks: AppHooks(),
|
||||||
isBugReportServiceEnabled: true,
|
isBugReportServiceEnabled: true,
|
||||||
isInSecondaryWindow: false,
|
isInSecondaryWindow: false,
|
||||||
userIndicatorController: UserIndicatorControllerMock())
|
userIndicatorController: UserIndicatorControllerMock())
|
||||||
|
|||||||
Reference in New Issue
Block a user