mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-10-31 08:15:24 +00:00
feat(healthchat): advertise the account status in settings
Pending approval and an incomplete profile were only visible in the members tab, which a new user has no reason to open.
This commit is contained in:
@@ -191,6 +191,10 @@ extension AccessibilityTests {
|
||||
try await performAccessibilityAudit(named: "GalleryRoomTimelineView_Previews")
|
||||
}
|
||||
|
||||
func testHealthChatAccountStatusSection() async throws {
|
||||
try await performAccessibilityAudit(named: "HealthChatAccountStatusSection_Previews")
|
||||
}
|
||||
|
||||
func testHealthChatExtendedProfileSection() async throws {
|
||||
try await performAccessibilityAudit(named: "HealthChatExtendedProfileSection_Previews")
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
03BD83E8BDD23AE059802E0D /* UITestsScreenIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CEBE5EA91E8691EDF364EC2 /* UITestsScreenIdentifier.swift */; };
|
||||
03CDCA6243F89B194E3FAD17 /* EncryptionAuthenticity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 955336CBD5ED73C792D1F580 /* EncryptionAuthenticity.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 */; };
|
||||
044DD8F80231BC30570F7965 /* UserDiscoveryService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65AAD845E53B0C8B5E0812C2 /* UserDiscoveryService.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 */; };
|
||||
066A1E9B94723EE9F3038044 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47EBB5D698CE9A25BB553A2D /* Strings.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 */; };
|
||||
06D3942496E9E0E655F14D21 /* NotificationManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A057F2FDC14866C3026A89A4 /* NotificationManagerProtocol.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 */; };
|
||||
0EA6537A07E2DC882AEA5962 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 187853A7E643995EE49FAD43 /* Localizable.stringsdict */; };
|
||||
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 */; };
|
||||
0F6C8033FA60CFD36F7CA205 /* AppLockSetupPINScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A019A12C866D64CF072024B9 /* AppLockSetupPINScreenViewModel.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 */; };
|
||||
31CCF3159E5CE1C05AE5781B /* HTMLFixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A5FBB74981BF8EFFDAE90D /* HTMLFixtures.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 */; };
|
||||
32B7891D937377A59606EDFC /* UserFlowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21DD8599815136EFF5B73F38 /* UserFlowTests.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 */; };
|
||||
A23B8B27A1436A1049EEF68E /* InfoPlistReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A580295A56B55A856CC4084 /* InfoPlistReader.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 */; };
|
||||
A33784831AD880A670CAA9F9 /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DF593C3F7AF4B2FBAEB05D /* FileManager.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 */; };
|
||||
EA4848108A6FE53A4085C9B1 /* VolatileUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2F56C9FFE8D55375CED9741 /* VolatileUserDefaults.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 */; };
|
||||
EA8D941771E762A5D3D7FA0D /* FileMediaEventsTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 430C73079A84654BF46A7FF5 /* FileMediaEventsTimelineView.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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -2232,6 +2239,7 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -2321,6 +2329,7 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -3127,6 +3136,7 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -3211,6 +3221,7 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -3250,6 +3261,7 @@
|
||||
ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; path = message.caf; 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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -4076,6 +4088,7 @@
|
||||
3674212CAB6C5143DD437AC4 /* RoomDetailsScreenHook.swift */,
|
||||
99EB08B2C8B01F3BFE4B0CEC /* RoomMemberDetailsScreenHook.swift */,
|
||||
B343C5255FB408DDE853CFDF /* RoomScreenHook.swift */,
|
||||
56630FCC9F78E5E8F1A75770 /* SettingsScreenHook.swift */,
|
||||
2A95C9B8299A36A6495DECA6 /* TracingHook.swift */,
|
||||
DA5CC7FB1CF8C69041B62DD7 /* UserDetailsEditScreenHook.swift */,
|
||||
8B35734F3743563566413C9A /* UserProfileScreenHook.swift */,
|
||||
@@ -5705,6 +5718,8 @@
|
||||
801BA878EE879E01E82F3A5F /* HealthChat */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
46DE3887AAA265406107ABE6 /* HealthChatAccountStatusLoader.swift */,
|
||||
056FCFA596B562977CA89D58 /* HealthChatAccountStatusSection.swift */,
|
||||
E0B439012B010EC4C3DD24D0 /* HealthChatAppHooks.swift */,
|
||||
E1ECC44D40D05D3A6F0DE0F7 /* HealthChatAppSettingsHook.swift */,
|
||||
7A60C2712F5607406E8B1C80 /* HealthChatBrandColors.swift */,
|
||||
@@ -5714,6 +5729,7 @@
|
||||
AD3C983BFE38789B9C5EA7A5 /* HealthChatExtendedProfileSectionHook.swift */,
|
||||
9EDD488380158986A75D6640 /* HealthChatHomeTabsHook.swift */,
|
||||
715B60D68D20808E328DA1DA /* HealthChatSettings.swift */,
|
||||
EDAE8AB9A3CBD957EDD666ED /* HealthChatSettingsScreenHook.swift */,
|
||||
CA14BE0D369AD8098C93F264 /* HealthChatUserDetailsEditScreenHook.swift */,
|
||||
);
|
||||
path = HealthChat;
|
||||
@@ -5864,6 +5880,7 @@
|
||||
children = (
|
||||
79AFBE494F9CCAAE8046203B /* ExtendedProfileServiceTests.swift */,
|
||||
8AB4E616E6405AA5DD5A04EA /* ExtendedProfileTests.swift */,
|
||||
E851BFC1E8A0E1925D26CD14 /* HealthChatAccountStatusLoaderTests.swift */,
|
||||
CB71F86BB7A3AC9B32845B5B /* HealthChatAPIModelsTests.swift */,
|
||||
087194751572DF7CA65D1CCB /* HealthChatAPIServiceTests.swift */,
|
||||
BB7ABDCA2059497899F226DF /* HealthChatExtendedProfileLoaderTests.swift */,
|
||||
@@ -6693,6 +6710,7 @@
|
||||
5EF73F8BBCB34CC51C31995D /* ExtendedProfile.swift */,
|
||||
E5A1F965895E27CAEA93B58A /* ExtendedProfileService.swift */,
|
||||
180F8DC392F9D8DA27AAF3E5 /* ExtendedProfileServiceProtocol.swift */,
|
||||
DA3BFFFEF695209F27535BED /* HealthChatAccountStatus.swift */,
|
||||
CE02F1DAF9054BD6F960D893 /* HealthChatAPIModels.swift */,
|
||||
6004777518436121012B76CF /* HealthChatAPIService.swift */,
|
||||
994DECEF3C67C4A715B09C82 /* HealthChatAPIServiceProtocol.swift */,
|
||||
@@ -8527,6 +8545,7 @@
|
||||
07756D532EFE33DD1FA258E5 /* GeoURITests.swift in Sources */,
|
||||
C40957497CAFADB846784A2A /* HealthChatAPIModelsTests.swift in Sources */,
|
||||
F995036BBA39691CF7AD1F08 /* HealthChatAPIServiceTests.swift in Sources */,
|
||||
323D76E640CDB21BD33AD9B9 /* HealthChatAccountStatusLoaderTests.swift in Sources */,
|
||||
99DE2B618703A58BC3303B92 /* HealthChatExtendedProfileLoaderTests.swift in Sources */,
|
||||
72031674925CD5CC448FA060 /* HealthChatInviteMemberScreenViewModelTests.swift in Sources */,
|
||||
60317E05BFDD46BD99C2871B /* HealthChatMemberDirectoryScreenViewModelTests.swift in Sources */,
|
||||
@@ -9072,6 +9091,9 @@
|
||||
8DB9BEBB357A9F27208A36EF /* HealthChatAPIModels.swift in Sources */,
|
||||
A87651F9995DE56EF727E24A /* HealthChatAPIService.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 */,
|
||||
665314B4CB31904F107D2D3B /* HealthChatAppSettingsHook.swift in Sources */,
|
||||
6DDA932A28F531791BDBFB08 /* HealthChatBrandColors.swift in Sources */,
|
||||
@@ -9099,6 +9121,7 @@
|
||||
1610E4C02793FFC187434A68 /* HealthChatProfileEditScreenViewModel.swift in Sources */,
|
||||
48CD74299B25EF6034EFF68E /* HealthChatProfileEditScreenViewModelProtocol.swift in Sources */,
|
||||
1E822B072359DD37EEA8CE78 /* HealthChatSettings.swift in Sources */,
|
||||
EA66A9B88287B2E56EB4A95F /* HealthChatSettingsScreenHook.swift in Sources */,
|
||||
D1A0D9EBE3B9BF4E76DE309A /* HealthChatUserDetailsEditScreenHook.swift in Sources */,
|
||||
0C1E537A49ABB386F7554D4A /* HighlightedTimelineItemModifier.swift in Sources */,
|
||||
964B9D2EC38C488C360CE0C9 /* HomeScreen.swift in Sources */,
|
||||
@@ -9654,6 +9677,7 @@
|
||||
755395927DDD6EBDDA5E217A /* SettingsFlowCoordinator.swift in Sources */,
|
||||
34F1261CEF6D6A00D559B520 /* SettingsScreen.swift in Sources */,
|
||||
AF8BFA37791E1756EE243E08 /* SettingsScreenCoordinator.swift in Sources */,
|
||||
A2937A97959A0AFB064BE1DE /* SettingsScreenHook.swift in Sources */,
|
||||
B93D7CE520088AD53FA6D53C /* SettingsScreenModels.swift in Sources */,
|
||||
5E67990F98AF11563EA50080 /* SettingsScreenUserStatusPickerView.swift in Sources */,
|
||||
B74CCA58911A0C9C3E277A6B /* SettingsScreenUserStatusRow.swift in Sources */,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// 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_address" = "Adresse";
|
||||
@@ -11,6 +12,7 @@
|
||||
"common_last_name" = "Nachname";
|
||||
"common_postal_code" = "PLZ";
|
||||
"common_practice_name" = "Praxisname / Firmenname";
|
||||
"common_profile_incomplete" = "Profil unvollständig";
|
||||
"common_specialty" = "Fachgebiet";
|
||||
"common_street" = "Strasse";
|
||||
"common_website" = "Webseite";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// 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_address" = "Address";
|
||||
@@ -11,6 +12,7 @@
|
||||
"common_last_name" = "Last name";
|
||||
"common_postal_code" = "Postal code";
|
||||
"common_practice_name" = "Practice name";
|
||||
"common_profile_incomplete" = "Profile incomplete";
|
||||
"common_specialty" = "Speciality";
|
||||
"common_street" = "Street";
|
||||
"common_website" = "Website";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// 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_address" = "Adresse";
|
||||
@@ -11,6 +12,7 @@
|
||||
"common_last_name" = "Nom";
|
||||
"common_postal_code" = "Code postal";
|
||||
"common_practice_name" = "Nom de l’entreprise";
|
||||
"common_profile_incomplete" = "Profil incomplet";
|
||||
"common_specialty" = "Spécialisation";
|
||||
"common_street" = "Rue";
|
||||
"common_website" = "Site web";
|
||||
|
||||
@@ -15,6 +15,7 @@ final nonisolated class AppHooks: AppHooksProtocol {
|
||||
func configure(with userSession: UserSessionProtocol?) async {
|
||||
await userSessionHook.configure(with: userSession)
|
||||
await extendedProfileSectionHook.configure(with: userSession)
|
||||
await settingsScreenHook.configure(with: userSession)
|
||||
}
|
||||
|
||||
@AppHook(default: DefaultAppSettingsHook())
|
||||
@@ -50,6 +51,9 @@ final nonisolated class AppHooks: AppHooksProtocol {
|
||||
@AppHook(default: DefaultHomeTabsHook())
|
||||
var homeTabsHook: HomeTabsHookProtocol
|
||||
|
||||
@AppHook(default: DefaultSettingsScreenHook())
|
||||
var settingsScreenHook: SettingsScreenHookProtocol
|
||||
|
||||
@AppHook(default: DefaultUserDetailsEditScreenHook())
|
||||
var userDetailsEditScreenHook: UserDetailsEditScreenHookProtocol
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// 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.
|
||||
@Observable
|
||||
final class HealthChatAccountStatusLoader {
|
||||
private(set) var status: HealthChatAccountStatus = .unknown
|
||||
|
||||
/// The load kicked off on creation — await this in tests before asserting.
|
||||
@ObservationIgnored private(set) var loadingTask: Task<Void, Never>?
|
||||
|
||||
init(apiService: HealthChatAPIServiceProtocol) {
|
||||
loadingTask = Task { await load(from: apiService) }
|
||||
}
|
||||
|
||||
/// Creates a loader with a fixed status, for previews.
|
||||
init(status: HealthChatAccountStatus) {
|
||||
self.status = status
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func load(from apiService: HealthChatAPIServiceProtocol) async {
|
||||
// Every failure below leaves the status unknown: a banner must not report an error.
|
||||
guard case let .success(profile) = await apiService.ownProfile() else {
|
||||
return
|
||||
}
|
||||
|
||||
guard profile.isApproved else {
|
||||
status = .pendingApproval
|
||||
return
|
||||
}
|
||||
|
||||
guard case let .success(completeness) = await apiService.profileCompleteness() 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,67 @@
|
||||
//
|
||||
// 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 {
|
||||
// Owned by the hook, not the view: the settings screen rebuilds this section on
|
||||
// every body pass, and a view-owned loader would fire a request each time.
|
||||
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())
|
||||
registerExtendedProfileSectionHook(HealthChatExtendedProfileSectionHook())
|
||||
registerHomeTabsHook(HealthChatHomeTabsHook())
|
||||
registerSettingsScreenHook(HealthChatSettingsScreenHook())
|
||||
registerUserDetailsEditScreenHook(HealthChatUserDetailsEditScreenHook())
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// 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 {
|
||||
/// One load per session — the settings screen asks for this section on every body pass.
|
||||
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))
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// 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?
|
||||
}
|
||||
|
||||
struct DefaultSettingsScreenHook: SettingsScreenHookProtocol {
|
||||
func configure(with userSession: UserSessionProtocol?) async { }
|
||||
|
||||
func accountStatusSection(completeProfileAction: @escaping () -> Void) -> AnyView? {
|
||||
nil
|
||||
}
|
||||
}
|
||||
@@ -74,6 +74,7 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
private func presentSettingsScreen(animated: Bool) {
|
||||
let settingsScreenCoordinator = SettingsScreenCoordinator(parameters: .init(userSession: flowParameters.userSession,
|
||||
appSettings: flowParameters.appSettings,
|
||||
appHooks: flowParameters.appHooks,
|
||||
isBugReportServiceEnabled: flowParameters.bugReportService.isEnabled,
|
||||
isInSecondaryWindow: isInSecondaryWindow,
|
||||
userIndicatorController: flowParameters.userIndicatorController))
|
||||
|
||||
@@ -10,6 +10,8 @@ import Foundation
|
||||
// swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length
|
||||
// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
|
||||
internal nonisolated enum HealthChatL10n {
|
||||
/// 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
|
||||
@@ -34,6 +36,8 @@ internal nonisolated enum HealthChatL10n {
|
||||
internal static var commonPostalCode: String { return HealthChatL10n.tr("HealthChat", "common_postal_code") }
|
||||
/// 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
|
||||
internal static var commonSpecialty: String { return HealthChatL10n.tr("HealthChat", "common_specialty") }
|
||||
/// Street
|
||||
|
||||
@@ -55,6 +55,7 @@ enum TestablePreviewsDictionary {
|
||||
"FormattingToolbar_Previews" : FormattingToolbar_Previews.self,
|
||||
"FullscreenDialog_Previews" : FullscreenDialog_Previews.self,
|
||||
"GalleryRoomTimelineView_Previews" : GalleryRoomTimelineView_Previews.self,
|
||||
"HealthChatAccountStatusSection_Previews" : HealthChatAccountStatusSection_Previews.self,
|
||||
"HealthChatExtendedProfileSection_Previews" : HealthChatExtendedProfileSection_Previews.self,
|
||||
"HealthChatInviteMemberScreen_Previews" : HealthChatInviteMemberScreen_Previews.self,
|
||||
"HealthChatMemberDirectoryScreen_Previews" : HealthChatMemberDirectoryScreen_Previews.self,
|
||||
|
||||
@@ -11,6 +11,8 @@ import SwiftUI
|
||||
|
||||
struct SettingsScreenCoordinatorParameters {
|
||||
let userSession: UserSessionProtocol
|
||||
let appSettings: AppSettings
|
||||
let appHooks: AppHooks
|
||||
let isBugReportServiceEnabled: Bool
|
||||
let isInSecondaryWindow: Bool
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
@@ -18,30 +20,22 @@ struct SettingsScreenCoordinatorParameters {
|
||||
|
||||
enum SettingsScreenCoordinatorAction {
|
||||
case dismiss
|
||||
|
||||
case logout
|
||||
case secureBackup
|
||||
case userDetails
|
||||
case userStatusEmojiPicker(EmojiPickerScreenContinuation)
|
||||
|
||||
case addAccount
|
||||
|
||||
case manageAccount(url: URL)
|
||||
case linkNewDevice
|
||||
case notifications
|
||||
case secureBackup
|
||||
case moderationAndSafety
|
||||
case bugReport
|
||||
|
||||
case logout
|
||||
case deactivateAccount
|
||||
|
||||
case mediaUploadQuality
|
||||
case appLock
|
||||
case locationSharing
|
||||
case analytics
|
||||
case labs
|
||||
case appLock
|
||||
case bugReport
|
||||
case about
|
||||
|
||||
case blockedUsers
|
||||
case linkNewDevice
|
||||
case manageAccount(url: URL)
|
||||
case notifications
|
||||
case advancedSettings
|
||||
case labs
|
||||
case developerOptions
|
||||
case deactivateAccount
|
||||
}
|
||||
|
||||
final class SettingsScreenCoordinator: CoordinatorProtocol {
|
||||
@@ -58,6 +52,8 @@ final class SettingsScreenCoordinator: CoordinatorProtocol {
|
||||
|
||||
init(parameters: SettingsScreenCoordinatorParameters) {
|
||||
viewModel = SettingsScreenViewModel(userSession: parameters.userSession,
|
||||
appSettings: parameters.appSettings,
|
||||
appHooks: parameters.appHooks,
|
||||
isBugReportServiceEnabled: parameters.isBugReportServiceEnabled,
|
||||
isInSecondaryWindow: parameters.isInSecondaryWindow,
|
||||
userIndicatorController: parameters.userIndicatorController)
|
||||
@@ -69,48 +65,38 @@ final class SettingsScreenCoordinator: CoordinatorProtocol {
|
||||
switch action {
|
||||
case .close:
|
||||
actionsSubject.send(.dismiss)
|
||||
|
||||
case .userDetails:
|
||||
actionsSubject.send(.userDetails)
|
||||
case let .userStatusEmojiPicker(continuation):
|
||||
actionsSubject.send(.userStatusEmojiPicker(continuation))
|
||||
|
||||
case .addAccount:
|
||||
actionsSubject.send(.addAccount)
|
||||
|
||||
case let .manageAccount(url):
|
||||
actionsSubject.send(.manageAccount(url: url))
|
||||
case .linkNewDevice:
|
||||
actionsSubject.send(.linkNewDevice)
|
||||
case .notifications:
|
||||
actionsSubject.send(.notifications)
|
||||
case .secureBackup:
|
||||
actionsSubject.send(.secureBackup)
|
||||
case .moderationAndSafety:
|
||||
actionsSubject.send(.moderationAndSafety)
|
||||
case let .manageAccount(url):
|
||||
actionsSubject.send(.manageAccount(url: url))
|
||||
case .analytics:
|
||||
actionsSubject.send(.analytics)
|
||||
case .appLock:
|
||||
actionsSubject.send(.appLock)
|
||||
case .reportBug:
|
||||
actionsSubject.send(.bugReport)
|
||||
|
||||
case .about:
|
||||
actionsSubject.send(.about)
|
||||
case .blockedUsers:
|
||||
actionsSubject.send(.blockedUsers)
|
||||
case .secureBackup:
|
||||
actionsSubject.send(.secureBackup)
|
||||
case .notifications:
|
||||
actionsSubject.send(.notifications)
|
||||
case .advancedSettings:
|
||||
actionsSubject.send(.advancedSettings)
|
||||
case .labs:
|
||||
actionsSubject.send(.labs)
|
||||
case .developerOptions:
|
||||
actionsSubject.send(.developerOptions)
|
||||
case .logout:
|
||||
actionsSubject.send(.logout)
|
||||
case .deactivateAccount:
|
||||
actionsSubject.send(.deactivateAccount)
|
||||
|
||||
case .mediaUploadQuality:
|
||||
actionsSubject.send(.mediaUploadQuality)
|
||||
case .appLock:
|
||||
actionsSubject.send(.appLock)
|
||||
case .locationSharing:
|
||||
actionsSubject.send(.locationSharing)
|
||||
case .analytics:
|
||||
actionsSubject.send(.analytics)
|
||||
case .labs:
|
||||
actionsSubject.send(.labs)
|
||||
case .about:
|
||||
actionsSubject.send(.about)
|
||||
|
||||
case .developerOptions:
|
||||
actionsSubject.send(.developerOptions)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
@@ -10,30 +10,22 @@ import SwiftUI
|
||||
|
||||
enum SettingsScreenViewModelAction {
|
||||
case close
|
||||
|
||||
case userDetails
|
||||
case userStatusEmojiPicker(EmojiPickerScreenContinuation)
|
||||
|
||||
case addAccount
|
||||
|
||||
case manageAccount(url: URL)
|
||||
case linkNewDevice
|
||||
case notifications
|
||||
case secureBackup
|
||||
case moderationAndSafety
|
||||
case manageAccount(url: URL)
|
||||
case analytics
|
||||
case appLock
|
||||
case reportBug
|
||||
|
||||
case about
|
||||
case blockedUsers
|
||||
case secureBackup
|
||||
case notifications
|
||||
case advancedSettings
|
||||
case labs
|
||||
case developerOptions
|
||||
case logout
|
||||
case deactivateAccount
|
||||
|
||||
case mediaUploadQuality
|
||||
case appLock
|
||||
case locationSharing
|
||||
case analytics
|
||||
case labs
|
||||
case about
|
||||
|
||||
case developerOptions
|
||||
}
|
||||
|
||||
enum SettingsScreenSecuritySectionMode {
|
||||
@@ -46,7 +38,6 @@ struct SettingsScreenViewState: BindableState {
|
||||
var userProfile: UserProfile
|
||||
var showUserStatusInput = false
|
||||
var showLinkNewDeviceButton: Bool
|
||||
var showAddAccountButton: Bool
|
||||
var accountProfileURL: URL?
|
||||
var showAccountDeactivation: Bool
|
||||
var showDeveloperOptions: Bool
|
||||
@@ -54,13 +45,16 @@ struct SettingsScreenViewState: BindableState {
|
||||
var securitySectionMode = SettingsScreenSecuritySectionMode.none
|
||||
var showSecuritySectionBadge = false
|
||||
|
||||
var showBlockedUsers = false
|
||||
let showAnalyticsSettings: Bool
|
||||
|
||||
let isBugReportServiceEnabled: Bool
|
||||
|
||||
let navigationBarVisibility: Visibility
|
||||
|
||||
var bindings: SettingsScreenViewStateBindings
|
||||
let appHooks: AppHooks
|
||||
|
||||
var bindings = SettingsScreenViewStateBindings()
|
||||
|
||||
var userStatusRowMode: SettingsScreenUserStatusRow.Mode {
|
||||
if bindings.isShowingCustomStatusField {
|
||||
@@ -74,8 +68,6 @@ struct SettingsScreenViewState: BindableState {
|
||||
}
|
||||
|
||||
struct SettingsScreenViewStateBindings {
|
||||
private let userSettings: UserSettings
|
||||
|
||||
var isPresentingStatusPicker = false
|
||||
var customStatusEmoji: Character = "😄"
|
||||
var isShowingCustomStatusField = false {
|
||||
@@ -87,44 +79,27 @@ struct SettingsScreenViewStateBindings {
|
||||
}
|
||||
|
||||
var isPresentingAccountDeactivationConfirmation = false
|
||||
|
||||
var appAppearance: AppAppearance {
|
||||
get { userSettings.app.appAppearance }
|
||||
set { userSettings.app.appAppearance = newValue }
|
||||
}
|
||||
|
||||
init(userSettings: UserSettings) {
|
||||
self.userSettings = userSettings
|
||||
}
|
||||
}
|
||||
|
||||
enum SettingsScreenViewAction {
|
||||
case close
|
||||
|
||||
case userDetails
|
||||
case userStatus(UserStatusAction)
|
||||
|
||||
case addAccount
|
||||
|
||||
case manageAccount(url: URL)
|
||||
case linkNewDevice
|
||||
case notifications
|
||||
case secureBackup
|
||||
case moderationAndSafety
|
||||
case reportBug
|
||||
|
||||
case logout
|
||||
case deactivateAccount
|
||||
|
||||
case mediaUploadQuality
|
||||
case appLock
|
||||
case locationSharing
|
||||
case analytics
|
||||
case labs
|
||||
case appLock
|
||||
case reportBug
|
||||
case about
|
||||
|
||||
case blockedUsers
|
||||
case secureBackup
|
||||
case linkNewDevice
|
||||
case manageAccount(url: URL)
|
||||
case notifications
|
||||
case enableDeveloperOptions
|
||||
case developerOptions
|
||||
case advancedSettings
|
||||
case labs
|
||||
case logout
|
||||
case deactivateAccount
|
||||
|
||||
enum UserStatusAction {
|
||||
/// Show status picker sheet to select a preset status.
|
||||
|
||||
@@ -12,7 +12,7 @@ import SwiftUI
|
||||
typealias SettingsScreenViewModelType = StateStoreViewModelV2<SettingsScreenViewState, SettingsScreenViewAction>
|
||||
|
||||
class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewModelProtocol {
|
||||
private let userSettings: UserSettings
|
||||
private let appSettings: AppSettings
|
||||
private let clientProxy: ClientProxyProtocol
|
||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||
|
||||
@@ -22,35 +22,30 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(userSession: UserSessionProtocol, isBugReportServiceEnabled: Bool, isInSecondaryWindow: Bool, userIndicatorController: UserIndicatorControllerProtocol) {
|
||||
userSettings = userSession.userSettings
|
||||
init(userSession: UserSessionProtocol, appSettings: AppSettings, appHooks: AppHooks, isBugReportServiceEnabled: Bool, isInSecondaryWindow: Bool, userIndicatorController: UserIndicatorControllerProtocol) {
|
||||
self.appSettings = appSettings
|
||||
clientProxy = userSession.clientProxy
|
||||
self.userIndicatorController = userIndicatorController
|
||||
|
||||
super.init(initialViewState: .init(deviceID: userSession.clientProxy.deviceID,
|
||||
userProfile: userSession.clientProxy.userProfilePublisher.value,
|
||||
showLinkNewDeviceButton: userSettings.app.linkNewDeviceEnabled,
|
||||
showAddAccountButton: userSettings.app.multiAccountEnabled,
|
||||
showLinkNewDeviceButton: appSettings.linkNewDeviceEnabled,
|
||||
showAccountDeactivation: userSession.clientProxy.canDeactivateAccount,
|
||||
showDeveloperOptions: userSettings.app.developerOptionsEnabled,
|
||||
showAnalyticsSettings: userSettings.app.canPromptForAnalytics,
|
||||
showDeveloperOptions: appSettings.developerOptionsEnabled,
|
||||
showAnalyticsSettings: appSettings.canPromptForAnalytics,
|
||||
isBugReportServiceEnabled: isBugReportServiceEnabled,
|
||||
navigationBarVisibility: isInSecondaryWindow ? .hidden : .automatic,
|
||||
bindings: .init(userSettings: userSettings)),
|
||||
appHooks: appHooks),
|
||||
mediaProvider: userSession.mediaProvider)
|
||||
|
||||
userSettings.app.developerOptionsEnabledPublisher
|
||||
appSettings.developerOptionsEnabledPublisher
|
||||
.weakAssign(to: \.state.showDeveloperOptions, on: self)
|
||||
.store(in: &cancellables)
|
||||
|
||||
userSettings.app.linkNewDeviceEnabledPublisher
|
||||
appSettings.linkNewDeviceEnabledPublisher
|
||||
.weakAssign(to: \.state.showLinkNewDeviceButton, on: self)
|
||||
.store(in: &cancellables)
|
||||
|
||||
userSettings.app.multiAccountEnabledPublisher
|
||||
.weakAssign(to: \.state.showAddAccountButton, on: self)
|
||||
.store(in: &cancellables)
|
||||
|
||||
userSession.clientProxy.userProfilePublisher
|
||||
.receive(on: DispatchQueue.main)
|
||||
.weakAssign(to: \.state.userProfile, on: self)
|
||||
@@ -78,6 +73,18 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
userSession.clientProxy.ignoredUsersPublisher
|
||||
.receive(on: DispatchQueue.main)
|
||||
.map {
|
||||
guard let blockedUsers = $0 else {
|
||||
return false
|
||||
}
|
||||
|
||||
return !blockedUsers.isEmpty
|
||||
}
|
||||
.weakAssign(to: \.state.showBlockedUsers, on: self)
|
||||
.store(in: &cancellables)
|
||||
|
||||
Task {
|
||||
if case .success(true) = await userSession.clientProxy.isUserStatusSupported() {
|
||||
state.showUserStatusInput = true
|
||||
@@ -91,7 +98,6 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo
|
||||
switch viewAction {
|
||||
case .close:
|
||||
actionsSubject.send(.close)
|
||||
|
||||
case .userDetails:
|
||||
actionsSubject.send(.userDetails)
|
||||
case .userStatus(.pickStatus):
|
||||
@@ -108,45 +114,36 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo
|
||||
case .userStatus(.cancel):
|
||||
state.bindings.isPresentingStatusPicker = false
|
||||
state.bindings.isShowingCustomStatusField = false
|
||||
|
||||
case .addAccount:
|
||||
actionsSubject.send(.addAccount)
|
||||
|
||||
case let .manageAccount(url):
|
||||
actionsSubject.send(.manageAccount(url: url))
|
||||
case .linkNewDevice:
|
||||
actionsSubject.send(.linkNewDevice)
|
||||
case .notifications:
|
||||
actionsSubject.send(.notifications)
|
||||
case .secureBackup:
|
||||
actionsSubject.send(.secureBackup)
|
||||
case .moderationAndSafety:
|
||||
actionsSubject.send(.moderationAndSafety)
|
||||
case .reportBug:
|
||||
actionsSubject.send(.reportBug)
|
||||
|
||||
case .logout:
|
||||
actionsSubject.send(.logout)
|
||||
case .deactivateAccount:
|
||||
actionsSubject.send(.deactivateAccount)
|
||||
|
||||
case .mediaUploadQuality:
|
||||
actionsSubject.send(.mediaUploadQuality)
|
||||
case .appLock:
|
||||
actionsSubject.send(.appLock)
|
||||
case .locationSharing:
|
||||
actionsSubject.send(.locationSharing)
|
||||
case let .manageAccount(url):
|
||||
actionsSubject.send(.manageAccount(url: url))
|
||||
case .analytics:
|
||||
actionsSubject.send(.analytics)
|
||||
case .labs:
|
||||
actionsSubject.send(.labs)
|
||||
case .appLock:
|
||||
actionsSubject.send(.appLock)
|
||||
case .reportBug:
|
||||
actionsSubject.send(.reportBug)
|
||||
case .about:
|
||||
actionsSubject.send(.about)
|
||||
|
||||
case .blockedUsers:
|
||||
actionsSubject.send(.blockedUsers)
|
||||
case .logout:
|
||||
actionsSubject.send(.logout)
|
||||
case .secureBackup:
|
||||
actionsSubject.send(.secureBackup)
|
||||
case .notifications:
|
||||
actionsSubject.send(.notifications)
|
||||
case .advancedSettings:
|
||||
actionsSubject.send(.advancedSettings)
|
||||
case .labs:
|
||||
actionsSubject.send(.labs)
|
||||
case .enableDeveloperOptions:
|
||||
userSettings.app.developerOptionsEnabled.toggle()
|
||||
appSettings.developerOptionsEnabled.toggle()
|
||||
case .developerOptions:
|
||||
actionsSubject.send(.developerOptions)
|
||||
case .deactivateAccount:
|
||||
actionsSubject.send(.deactivateAccount)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,19 +13,37 @@ import SwiftUI
|
||||
struct SettingsScreen: View {
|
||||
@Bindable var context: SettingsScreenViewModel.Context
|
||||
|
||||
private var shouldHideManageAccountSection: Bool {
|
||||
context.viewState.accountProfileURL == nil &&
|
||||
!context.viewState.showBlockedUsers &&
|
||||
!context.viewState.showLinkNewDeviceButton
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
userSection
|
||||
|
||||
if context.viewState.showAddAccountButton {
|
||||
addAccountSection
|
||||
if context.viewState.showUserStatusInput {
|
||||
userStatusSection
|
||||
}
|
||||
|
||||
accountSettingsSection
|
||||
context.viewState.appHooks.settingsScreenHook.accountStatusSection {
|
||||
context.send(viewAction: .userDetails)
|
||||
}
|
||||
|
||||
if !shouldHideManageAccountSection {
|
||||
manageAccountSection
|
||||
}
|
||||
|
||||
manageMyAppSection
|
||||
|
||||
generalSection
|
||||
|
||||
signOutSection
|
||||
|
||||
appSettingsSection
|
||||
if context.viewState.showDeveloperOptions {
|
||||
developerOptionsSection
|
||||
}
|
||||
}
|
||||
.compoundList()
|
||||
.navigationTitle(L10n.commonSettings)
|
||||
@@ -56,7 +74,13 @@ struct SettingsScreen: View {
|
||||
.accessibilityHidden(true)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
HStack(spacing: 6) {
|
||||
Text(context.viewState.userProfile.displayName ?? "")
|
||||
|
||||
if let statusEmoji = context.viewState.userProfile.status.displayed?.emoji {
|
||||
Text(String(statusEmoji))
|
||||
}
|
||||
}
|
||||
.font(.compound.headingMD)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
|
||||
@@ -73,27 +97,47 @@ struct SettingsScreen: View {
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if context.viewState.showUserStatusInput {
|
||||
private var userStatusSection: some View {
|
||||
Section {
|
||||
SettingsScreenUserStatusRow(mode: context.viewState.userStatusRowMode) { action in
|
||||
context.send(viewAction: .userStatus(action))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var addAccountSection: some View {
|
||||
private var manageMyAppSection: some View {
|
||||
Section {
|
||||
ListRow(label: .default(title: L10n.screenSettingsAddAccount,
|
||||
icon: \.plus),
|
||||
ListRow(label: .default(title: L10n.screenNotificationSettingsTitle,
|
||||
icon: \.notifications),
|
||||
kind: .navigationLink {
|
||||
context.send(viewAction: .addAccount)
|
||||
context.send(viewAction: .notifications)
|
||||
})
|
||||
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.addAccount)
|
||||
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.notifications)
|
||||
|
||||
ListRow(label: .default(title: L10n.commonScreenLock,
|
||||
icon: \.lock),
|
||||
kind: .navigationLink {
|
||||
context.send(viewAction: .appLock)
|
||||
})
|
||||
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.screenLock)
|
||||
|
||||
switch context.viewState.securitySectionMode {
|
||||
case .secureBackup:
|
||||
ListRow(label: .default(title: L10n.commonEncryption,
|
||||
icon: \.key),
|
||||
details: context.viewState.showSecuritySectionBadge ? .icon(securitySectionBadge) : nil,
|
||||
kind: .navigationLink { context.send(viewAction: .secureBackup) })
|
||||
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.secureBackup)
|
||||
default:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var accountSettingsSection: some View {
|
||||
private var manageAccountSection: some View {
|
||||
Section {
|
||||
if let url = context.viewState.accountProfileURL {
|
||||
ListRow(label: .default(title: L10n.actionManageAccountAndDevices,
|
||||
@@ -112,29 +156,38 @@ struct SettingsScreen: View {
|
||||
})
|
||||
}
|
||||
|
||||
ListRow(label: .default(title: L10n.screenNotificationSettingsTitle,
|
||||
icon: \.notifications),
|
||||
if context.viewState.showBlockedUsers {
|
||||
ListRow(label: .default(title: L10n.commonBlockedUsers,
|
||||
icon: \.block),
|
||||
kind: .navigationLink {
|
||||
context.send(viewAction: .notifications)
|
||||
context.send(viewAction: .blockedUsers)
|
||||
})
|
||||
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.notifications)
|
||||
|
||||
switch context.viewState.securitySectionMode {
|
||||
case .secureBackup:
|
||||
ListRow(label: .default(title: L10n.commonEncryption,
|
||||
icon: \.key),
|
||||
details: context.viewState.showSecuritySectionBadge ? .icon(securitySectionBadge) : nil,
|
||||
kind: .navigationLink { context.send(viewAction: .secureBackup) })
|
||||
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.secureBackup)
|
||||
default:
|
||||
EmptyView()
|
||||
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.blockedUsers)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListRow(label: .default(title: L10n.commonModerationAndSafety,
|
||||
icon: \.admin),
|
||||
private var generalSection: some View {
|
||||
Section {
|
||||
ListRow(label: .default(title: L10n.commonAdvancedSettings,
|
||||
icon: \.settings),
|
||||
kind: .navigationLink {
|
||||
context.send(viewAction: .moderationAndSafety)
|
||||
context.send(viewAction: .advancedSettings)
|
||||
})
|
||||
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.advancedSettings)
|
||||
|
||||
ListRow(label: .default(title: L10n.screenAdvancedSettingsLabs,
|
||||
icon: \.labs),
|
||||
kind: .navigationLink {
|
||||
context.send(viewAction: .labs)
|
||||
})
|
||||
|
||||
ListRow(label: .default(title: L10n.commonAbout,
|
||||
icon: \.info),
|
||||
kind: .navigationLink {
|
||||
context.send(viewAction: .about)
|
||||
})
|
||||
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.about)
|
||||
|
||||
if context.viewState.isBugReportServiceEnabled {
|
||||
ListRow(label: .default(title: L10n.commonReportAProblem,
|
||||
@@ -144,9 +197,15 @@ struct SettingsScreen: View {
|
||||
})
|
||||
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.reportBug)
|
||||
}
|
||||
} header: {
|
||||
Text(L10n.commonAccountSettings)
|
||||
.compoundListSectionHeader()
|
||||
|
||||
if context.viewState.showAnalyticsSettings {
|
||||
ListRow(label: .default(title: L10n.commonAnalytics,
|
||||
icon: \.chart),
|
||||
kind: .navigationLink {
|
||||
context.send(viewAction: .analytics)
|
||||
})
|
||||
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.analytics)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,68 +227,21 @@ struct SettingsScreen: View {
|
||||
context.send(viewAction: .deactivateAccount)
|
||||
})
|
||||
}
|
||||
} footer: {
|
||||
if !context.viewState.showDeveloperOptions {
|
||||
versionSection
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var appSettingsSection: some View {
|
||||
private var developerOptionsSection: some View {
|
||||
Section {
|
||||
ListRow(label: .default(title: L10n.commonAppearance,
|
||||
icon: \.darkMode),
|
||||
kind: .picker(selection: $context.appAppearance,
|
||||
items: AppAppearance.allCases.map { (title: $0.name, tag: $0) }))
|
||||
|
||||
ListRow(label: .default(title: L10n.commonMediaUploadQuality,
|
||||
icon: \.image),
|
||||
kind: .navigationLink {
|
||||
context.send(viewAction: .mediaUploadQuality)
|
||||
})
|
||||
|
||||
ListRow(label: .default(title: L10n.commonScreenLock,
|
||||
icon: \.lock),
|
||||
kind: .navigationLink {
|
||||
context.send(viewAction: .appLock)
|
||||
})
|
||||
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.screenLock)
|
||||
|
||||
ListRow(label: .default(title: L10n.commonLocationSharing,
|
||||
icon: \.locationPin),
|
||||
kind: .navigationLink {
|
||||
context.send(viewAction: .locationSharing)
|
||||
})
|
||||
|
||||
if context.viewState.showAnalyticsSettings {
|
||||
ListRow(label: .default(title: L10n.commonAnalytics,
|
||||
icon: \.chart),
|
||||
kind: .navigationLink {
|
||||
context.send(viewAction: .analytics)
|
||||
})
|
||||
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.analytics)
|
||||
}
|
||||
|
||||
ListRow(label: .default(title: L10n.screenAdvancedSettingsLabs,
|
||||
icon: \.labs),
|
||||
kind: .navigationLink {
|
||||
context.send(viewAction: .labs)
|
||||
})
|
||||
|
||||
ListRow(label: .default(title: L10n.commonAbout,
|
||||
icon: \.info),
|
||||
kind: .navigationLink {
|
||||
context.send(viewAction: .about)
|
||||
})
|
||||
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.about)
|
||||
|
||||
if context.viewState.showDeveloperOptions {
|
||||
ListRow(label: .default(title: L10n.commonDeveloperOptions,
|
||||
icon: \.code),
|
||||
kind: .navigationLink {
|
||||
context.send(viewAction: .developerOptions)
|
||||
})
|
||||
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.developerOptions)
|
||||
}
|
||||
} header: {
|
||||
Text(L10n.commonAppSettings)
|
||||
.compoundListSectionHeader()
|
||||
} footer: {
|
||||
versionSection
|
||||
}
|
||||
@@ -274,19 +286,6 @@ struct SettingsScreen: View {
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppAppearance {
|
||||
var name: String {
|
||||
switch self {
|
||||
case .system:
|
||||
L10n.themeSystem
|
||||
case .light:
|
||||
L10n.themeLight
|
||||
case .dark:
|
||||
L10n.themeDark
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
@available(iOS 26.0, *)
|
||||
@@ -299,7 +298,7 @@ struct SettingsScreen_Previews: PreviewProvider, TestablePreview {
|
||||
SettingsScreen(context: viewModel.context)
|
||||
}
|
||||
.snapshotPreferences(expect: viewModel.context.observe(\.viewState.accountProfileURL).map { $0 != nil })
|
||||
.frame(height: 1300)
|
||||
.frame(height: 1100)
|
||||
.previewLayout(.sizeThatFits)
|
||||
.previewDisplayName("Default")
|
||||
|
||||
@@ -307,21 +306,19 @@ struct SettingsScreen_Previews: PreviewProvider, TestablePreview {
|
||||
SettingsScreen(context: bugReportDisabledViewModel.context)
|
||||
}
|
||||
.snapshotPreferences(expect: bugReportDisabledViewModel.context.observe(\.viewState.accountProfileURL).map { $0 != nil })
|
||||
.frame(height: 1250)
|
||||
.frame(height: 1050)
|
||||
.previewLayout(.sizeThatFits)
|
||||
.previewDisplayName("Bug report disabled")
|
||||
}
|
||||
|
||||
static func makeViewModel(isBugReportServiceEnabled: Bool = true) -> SettingsScreenViewModel {
|
||||
let userSettings = UserSettings.mock()
|
||||
userSettings.app.multiAccountEnabled = true
|
||||
userSettings.app.linkNewDeviceEnabled = true
|
||||
let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: "@alice:example.com",
|
||||
deviceID: "AAAAAAAAAAA",
|
||||
displayName: "Alice Liddell",
|
||||
status: .mockFocussing)),
|
||||
userSettings: userSettings))
|
||||
status: .mockFocussing))))
|
||||
return SettingsScreenViewModel(userSession: userSession,
|
||||
appSettings: .volatile(),
|
||||
appHooks: AppHooks(),
|
||||
isBugReportServiceEnabled: isBugReportServiceEnabled,
|
||||
isInSecondaryWindow: false,
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -333,6 +333,13 @@ extension PreviewTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func healthChatAccountStatusSection() async throws {
|
||||
for (index, preview) in HealthChatAccountStatusSection_Previews._allPreviews.enumerated() {
|
||||
try await assertSnapshots(matching: preview, step: index)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func healthChatExtendedProfileSection() async throws {
|
||||
for (index, preview) in HealthChatExtendedProfileSection_Previews._allPreviews.enumerated() {
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func status() async -> HealthChatAccountStatus {
|
||||
let loader = HealthChatAccountStatusLoader(apiService: apiService)
|
||||
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))
|
||||
}
|
||||
}
|
||||
@@ -172,37 +172,13 @@ struct SettingsScreenViewModelTests {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
// MARK: - Add account
|
||||
|
||||
@Test
|
||||
mutating func addAccountButtonFollowsTheFeatureFlag() async throws {
|
||||
let userSettings = UserSettings.mock()
|
||||
setupViewModel(userSettings: userSettings)
|
||||
#expect(!context.viewState.showAddAccountButton)
|
||||
|
||||
var deferred = deferFulfillment(context.observe(\.viewState.showAddAccountButton)) { $0 }
|
||||
userSettings.app.multiAccountEnabled = true
|
||||
try await deferred.fulfill()
|
||||
|
||||
deferred = deferFulfillment(context.observe(\.viewState.showAddAccountButton)) { !$0 }
|
||||
userSettings.app.multiAccountEnabled = false
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
@Test
|
||||
mutating func addAccount() async throws {
|
||||
setupViewModel()
|
||||
|
||||
let deferred = deferFulfillment(viewModel.actions) { $0.isAddAccount }
|
||||
context.send(viewAction: .addAccount)
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private mutating func setupViewModel(status: UserStatus = .init(), userSettings: UserSettings = .mock()) {
|
||||
private mutating func setupViewModel(status: UserStatus = .init()) {
|
||||
clientProxy = ClientProxyMock(.init(userID: "", status: status))
|
||||
viewModel = SettingsScreenViewModel(userSession: UserSessionMock(.init(clientProxy: clientProxy, userSettings: userSettings)),
|
||||
viewModel = SettingsScreenViewModel(userSession: UserSessionMock(.init(clientProxy: clientProxy)),
|
||||
appSettings: AppSettings.volatile(),
|
||||
appHooks: AppHooks(),
|
||||
isBugReportServiceEnabled: true,
|
||||
isInSecondaryWindow: false,
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
@@ -251,11 +227,4 @@ private extension SettingsScreenViewModelAction {
|
||||
default: false
|
||||
}
|
||||
}
|
||||
|
||||
var isAddAccount: Bool {
|
||||
switch self {
|
||||
case .addAccount: true
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user