Re-organise SettingsScreen to have distinct Account/App settings. (#6288)

* Re-organise SettingsScreen to have distinct Account/App settings.

* Remove the AdvancedSettingsScreen.
This commit is contained in:
Doug
2026-10-07 16:51:28 +01:00
committed by GitHub
parent dafda65cbe
commit 3b40aadc98
27 changed files with 321 additions and 687 deletions
@@ -11,10 +11,6 @@ extension AccessibilityTests {
try await performAccessibilityAudit(named: "ActiveCallTimelineItemView_Previews")
}
func testAdvancedSettingsScreen() async throws {
try await performAccessibilityAudit(named: "AdvancedSettingsScreen_Previews")
}
func testAnalyticsPromptScreen() async throws {
try await performAccessibilityAudit(named: "AnalyticsPromptScreen_Previews")
}
-36
View File
@@ -81,7 +81,6 @@
09693596AA3CA4E598C0D2F1 /* ThreadTimelineScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73FEE625AB52042049DB9268 /* ThreadTimelineScreenCoordinator.swift */; };
09713669577CDA8D012EE380 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 6647C55D93508C7CE9D954A5 /* MatrixRustSDK */; };
09D3D7D115318CAD131B4FE7 /* ResolveVerifiedUserSendFailureScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57084488B03BDB33C7B7CA0E /* ResolveVerifiedUserSendFailureScreenViewModelTests.swift */; };
09E1A358EA4205480AEF4698 /* AdvancedSettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = F229480685F30BCB96C439EC /* AdvancedSettingsScreen.swift */; };
0AB4FF7645175FF1F6E942CC /* DeferredFulfillment.swift in Sources */ = {isa = PBXBuildFile; fileRef = B91AD590B0B40718A0AA0C61 /* DeferredFulfillment.swift */; };
0ACAA31FD0399CEEBA3ECC21 /* UserDetailsEditScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85149F56BA333619900E2410 /* UserDetailsEditScreenViewModelProtocol.swift */; };
0AD8EF040A60D62F488C18B5 /* KnockRequestProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F957320D0EB7D7B4E30C79D /* KnockRequestProxyMock.swift */; };
@@ -286,7 +285,6 @@
2BBC0EB1E07963810A5D7423 /* ReadMarkerRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012A284622B32052015F1F89 /* ReadMarkerRoomTimelineView.swift */; };
2BBE320EE426A347AAE5C7DA /* IdentityConfirmationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00AFC5F08734C2EA4EE79C59 /* IdentityConfirmationScreen.swift */; };
2BC579CB5CE90CFE07CA0955 /* EditRoomAddressScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41656BC6267D55C56A2AAC08 /* EditRoomAddressScreenCoordinator.swift */; };
2BEDEA4851E1DF901779362C /* AdvancedSettingsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 922E498EB74CF6F5CC236F81 /* AdvancedSettingsScreenModels.swift */; };
2BFA4C6D5B3D327B02C66AB0 /* TimelineController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4216C12C0369A8AB059EDE9 /* TimelineController.swift */; };
2C289BECCCBEDBA48DC9AC67 /* ClassicAppMXAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74240BE69DACAD01AA670730 /* ClassicAppMXAccount.swift */; };
2C4C750D0039AFABDF24236C /* TemplateScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 342BEBC3C5FC3F9943C41C4C /* TemplateScreenViewModelProtocol.swift */; };
@@ -337,7 +335,6 @@
34320DB3C585FD34F4DEC5C8 /* HomeserverHistoryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D2BBDB7E80E119D5C906285 /* HomeserverHistoryManager.swift */; };
34357B287357BC0B9715DD51 /* UserAgentBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F2529D434C750ED78ADF1ED /* UserAgentBuilder.swift */; };
34390DAE0C574DAD30CCA7D9 /* DeclineAndBlockScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2ADF12A50186B75C68017B61 /* DeclineAndBlockScreenViewModelTests.swift */; };
34ADC506C929D10B138EBA51 /* AdvancedSettingsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42C58159916ABD7F454CE689 /* AdvancedSettingsScreenCoordinator.swift */; };
34C752A73717C691582DC6C7 /* UnsupportedRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1B8500C152BC59445647DA8 /* UnsupportedRoomTimelineItem.swift */; };
34F1261CEF6D6A00D559B520 /* SettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFD5EB0B0EEA4549FB49784 /* SettingsScreen.swift */; };
352C439BE0F75E101EF11FB1 /* RoomScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2886615BEBAE33A0AA4D5F8 /* RoomScreenModels.swift */; };
@@ -1439,7 +1436,6 @@
E1428612B08ED3030EC1FEC3 /* SpacesScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 104B7747499487538483FEAF /* SpacesScreen.swift */; };
E14E469CD97550D0FC58F3CA /* CancellableTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE52983FAFB4E0998C00EE8A /* CancellableTask.swift */; };
E184FFAD32342D3D6E2F89AA /* PinnedEventsTimelineScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D53754227CEBD06358956D7 /* PinnedEventsTimelineScreenCoordinator.swift */; };
E1A0752244567A25E376A2F4 /* AdvancedSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7A1B80FE6E3BA72F9C748AD /* AdvancedSettingsScreenViewModel.swift */; };
E1C67E5D9E22135A8FEBBD60 /* StackedAvatarsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8558D41DD4B553A752C868A /* StackedAvatarsView.swift */; };
E1CC93B1842137A8AB25B815 /* SearchBreadcrumb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 095B8ED0CA87F4A0A4858719 /* SearchBreadcrumb.swift */; };
E1DE8A5422643F759385290F /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD5523BDEDB247E29228476 /* AppSettings.swift */; };
@@ -1629,7 +1625,6 @@
FC10228E73323BDC09526F97 /* LRUCache in Frameworks */ = {isa = PBXBuildFile; productRef = 1081D3630AAD3ACEDDEC3A98 /* LRUCache */; };
FC31493979ED1FDF7D5EA3F9 /* KeychainController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E36CB905A2B9EC2C92A2DA7C /* KeychainController.swift */; };
FCD3F2B82CAB29A07887A127 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = DE8DC9B3FBA402117DC4C49F /* Kingfisher */; };
FCF95603F1D056B1B106A415 /* AdvancedSettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83E2B20431F890ED64255CA1 /* AdvancedSettingsScreenViewModelProtocol.swift */; };
FD19654C18E7C6D8AC79408D /* libMatrixRustSDKMocks.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 73F3153AB9D628110878E24F /* libMatrixRustSDKMocks.a */; };
FD29471C72872F8B7580E3E1 /* KeychainControllerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39C0D861FC397AC34BCF089E /* KeychainControllerMock.swift */; };
FD573B5D665824EB79EABF06 /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5327E3B3C58BEB0E65F4CF98 /* Observable.swift */; };
@@ -2196,7 +2191,6 @@
421E716C521F96D24ECE69B3 /* NoticeRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineItem.swift; sourceTree = "<group>"; };
421FA93BCC2840E66E4F306F /* NotificationSettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsScreenViewModelProtocol.swift; sourceTree = "<group>"; };
42236480CF0431535EBE8387 /* CustomLayoutLabelStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomLayoutLabelStyle.swift; sourceTree = "<group>"; };
42C58159916ABD7F454CE689 /* AdvancedSettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsScreenCoordinator.swift; sourceTree = "<group>"; };
42C64A14EE89928207E3B42B /* AnalyticsSettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenModels.swift; sourceTree = "<group>"; };
42C8C368A611B9CB79C7F5FA /* RoomPollsHistoryScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreen.swift; sourceTree = "<group>"; };
430C73079A84654BF46A7FF5 /* FileMediaEventsTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileMediaEventsTimelineView.swift; sourceTree = "<group>"; };
@@ -2580,7 +2574,6 @@
839E2C35DF3F9C7B54C3CE49 /* RoundedCornerShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedCornerShape.swift; sourceTree = "<group>"; };
83B4E3F1265581683E4997B8 /* RoomSelectionScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSelectionScreenViewModel.swift; sourceTree = "<group>"; };
83B574805B9812C111D6215D /* Tracing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tracing.swift; sourceTree = "<group>"; };
83E2B20431F890ED64255CA1 /* AdvancedSettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsScreenViewModelProtocol.swift; sourceTree = "<group>"; };
840182D7A61402D5947DE094 /* NotificationItemProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationItemProxyMock.swift; sourceTree = "<group>"; };
842F63AA68E8EB410B1BA29F /* MapLibreInterface.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MapLibreInterface.framework; sourceTree = BUILT_PRODUCTS_DIR; };
84311D707B09854D67F78BBF /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = "<group>"; };
@@ -2671,7 +2664,6 @@
91C8BD78F7B9247AC57FA1A3 /* RedactedRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedactedRoomTimelineView.swift; sourceTree = "<group>"; };
91CF6F7D08228D16BA69B63B /* zh-Hant-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant-TW"; path = "zh-Hant-TW.lproj/Localizable.strings"; sourceTree = "<group>"; };
91FFE1F410969ECB23FE9BB2 /* TimelineItemMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMenu.swift; sourceTree = "<group>"; };
922E498EB74CF6F5CC236F81 /* AdvancedSettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsScreenModels.swift; sourceTree = "<group>"; };
92DB574F954CC2B40F7BE892 /* QRCodeScannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeScannerView.swift; sourceTree = "<group>"; };
9332DFE9642F0A46ECA0497B /* BlurHashEncode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashEncode.swift; sourceTree = "<group>"; };
933B074F006F8E930DB98B4E /* TimelineMediaFrame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMediaFrame.swift; sourceTree = "<group>"; };
@@ -2795,7 +2787,6 @@
A768CA51A59B8A5D8C8FD599 /* AuthenticationStartScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationStartScreen.swift; sourceTree = "<group>"; };
A7978C9EFBDD7DE39BD86726 /* RestorationTokenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestorationTokenTests.swift; sourceTree = "<group>"; };
A79A78330B9329E3A99E4105 /* SearchScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchScreen.swift; sourceTree = "<group>"; };
A7A1B80FE6E3BA72F9C748AD /* AdvancedSettingsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsScreenViewModel.swift; sourceTree = "<group>"; };
A7C4EA55DA62F9D0F984A2AE /* CollapsibleTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleTimelineItem.swift; sourceTree = "<group>"; };
A7D452AF7B5F7E3A0A7DB54C /* SessionVerificationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenViewModelProtocol.swift; sourceTree = "<group>"; };
A7E37072597F67C4DD8CC2DB /* ComposerDraftServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerDraftServiceProtocol.swift; sourceTree = "<group>"; };
@@ -3259,7 +3250,6 @@
F17EFA1D3D09FC2F9C5E1CB2 /* MediaProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProvider.swift; sourceTree = "<group>"; };
F1B0FC6A77FDFDE2BFE49E13 /* MediaUploadQualitySettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadQualitySettingsScreenCoordinator.swift; sourceTree = "<group>"; };
F1B8500C152BC59445647DA8 /* UnsupportedRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsupportedRoomTimelineItem.swift; sourceTree = "<group>"; };
F229480685F30BCB96C439EC /* AdvancedSettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsScreen.swift; sourceTree = "<group>"; };
F276F31C1AEC19E52B951B62 /* SendInviteConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendInviteConfirmationView.swift; sourceTree = "<group>"; };
F288457CD4C821DF48C1B6EE /* NativeCallRoomContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeCallRoomContext.swift; sourceTree = "<group>"; };
F2B94F1B0B5D9D42B15AA6E8 /* ChatsTabFlowCoordinatorStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatsTabFlowCoordinatorStateMachine.swift; sourceTree = "<group>"; };
@@ -3489,14 +3479,6 @@
path = VoiceMessage;
sourceTree = "<group>";
};
018074F385C9417ECBEA793E /* View */ = {
isa = PBXGroup;
children = (
F229480685F30BCB96C439EC /* AdvancedSettingsScreen.swift */,
);
path = View;
sourceTree = "<group>";
};
01C939DDDF02F155F115DEF5 /* Sources */ = {
isa = PBXGroup;
children = (
@@ -5273,7 +5255,6 @@
isa = PBXGroup;
children = (
EB5B1119B5AD79297F1D49EB /* AccountSettings */,
FF2C35C46D5B8879A71F0BAC /* AdvancedSettingsScreen */,
09C599CB430ABF160C1EE55C /* AnalyticsSettingsScreen */,
CBB8EC37B802F3DD74A0205E /* DeactivateAccountScreen */,
1CA6CD0DE6F0445156361B6D /* DeveloperOptionsScreen */,
@@ -7605,18 +7586,6 @@
path = View;
sourceTree = "<group>";
};
FF2C35C46D5B8879A71F0BAC /* AdvancedSettingsScreen */ = {
isa = PBXGroup;
children = (
42C58159916ABD7F454CE689 /* AdvancedSettingsScreenCoordinator.swift */,
922E498EB74CF6F5CC236F81 /* AdvancedSettingsScreenModels.swift */,
A7A1B80FE6E3BA72F9C748AD /* AdvancedSettingsScreenViewModel.swift */,
83E2B20431F890ED64255CA1 /* AdvancedSettingsScreenViewModelProtocol.swift */,
018074F385C9417ECBEA793E /* View */,
);
path = AdvancedSettingsScreen;
sourceTree = "<group>";
};
FF654D7FD6693839E3185FAD /* View */ = {
isa = PBXGroup;
children = (
@@ -8739,11 +8708,6 @@
4A7957D034F5D96909127881 /* AccountSettings.swift in Sources */,
EFB3033FE782773D4F105681 /* AccountUserDefaults.swift in Sources */,
21215E17C9908F1EBF621BE0 /* ActiveCallTimelineItemView.swift in Sources */,
09E1A358EA4205480AEF4698 /* AdvancedSettingsScreen.swift in Sources */,
34ADC506C929D10B138EBA51 /* AdvancedSettingsScreenCoordinator.swift in Sources */,
2BEDEA4851E1DF901779362C /* AdvancedSettingsScreenModels.swift in Sources */,
E1A0752244567A25E376A2F4 /* AdvancedSettingsScreenViewModel.swift in Sources */,
FCF95603F1D056B1B106A415 /* AdvancedSettingsScreenViewModelProtocol.swift in Sources */,
53C1E7F6A7D6409D89F36ED7 /* AggregatedReactionMock.swift in Sources */,
4219391CD2351E410554B3E8 /* AggregratedReaction.swift in Sources */,
64D05250CEDE8B604119F6E6 /* Alert.swift in Sources */,
@@ -85,44 +85,52 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
switch action {
case .dismiss:
actionsSubject.send(.dismiss)
case .logout:
actionsSubject.send(.runLogoutFlow)
case .secureBackup:
startEncryptionSettingsFlow()
case .userDetails:
presentUserDetailsEditScreen()
case let .userStatusEmojiPicker(continuation):
presentEmojiPicker(emojiPickerContinuation: continuation)
case .addAccount:
break // Nothing to present until the add account flow exists.
case .linkNewDevice:
startLinkNewDeviceFlow()
case let .manageAccount(url):
presentAccountManagementURL(url)
case .analytics:
presentAnalyticsScreen()
case .appLock:
presentAppLockSetupFlow()
case .linkNewDevice:
startLinkNewDeviceFlow()
case .notifications:
presentNotificationSettings()
case .secureBackup:
startEncryptionSettingsFlow()
case .moderationAndSafety:
presentModerationAndSafetySettings()
case .bugReport:
bugReportFlowCoordinator = BugReportFlowCoordinator(parameters: .init(presentationMode: .push(navigationStackCoordinator),
userIndicatorController: flowParameters.userIndicatorController,
bugReportService: flowParameters.bugReportService,
userSession: flowParameters.userSession))
bugReportFlowCoordinator?.start()
case .about:
presentLegalInformationScreen()
case .blockedUsers:
presentBlockedUsersScreen()
case .notifications:
presentNotificationSettings()
case .advancedSettings:
presentAdvancedSettings()
case .labs:
presentLabs()
case .developerOptions:
presentDeveloperOptions()
case .logout:
actionsSubject.send(.runLogoutFlow)
case .deactivateAccount:
presentDeactivateAccount()
case .mediaUploadQuality:
presentMediaUploadQualitySettings()
case .appLock:
presentAppLockSetupFlow()
case .locationSharing:
presentLocationSharingSettings()
case .analytics:
presentAnalyticsScreen()
case .labs:
presentLabs()
case .about:
presentLegalInformationScreen()
case .developerOptions:
presentDeveloperOptions()
}
}
.store(in: &cancellables)
@@ -273,11 +281,34 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
navigationStackCoordinator.push(coordinator)
}
private func presentAdvancedSettings() {
let coordinator = AdvancedSettingsScreenCoordinator(parameters: .init(userSettings: flowParameters.userSettings,
analytics: flowParameters.analytics,
clientProxy: flowParameters.userSession.clientProxy,
userIndicatorController: flowParameters.userIndicatorController))
private func presentModerationAndSafetySettings() {
let parameters = ModerationAndSafetySettingsScreenCoordinatorParameters(userSession: flowParameters.userSession,
userIndicatorController: flowParameters.userIndicatorController)
let coordinator = ModerationAndSafetySettingsScreenCoordinator(parameters: parameters)
coordinator.actionsPublisher
.sink { [weak self] action in
guard let self else { return }
switch action {
case .blockedUsers:
presentBlockedUsersScreen()
}
}
.store(in: &cancellables)
navigationStackCoordinator.push(coordinator)
}
private func presentMediaUploadQualitySettings() {
let parameters = MediaUploadQualitySettingsScreenCoordinatorParameters(userSettings: flowParameters.userSettings,
analytics: flowParameters.analytics)
let coordinator = MediaUploadQualitySettingsScreenCoordinator(parameters: parameters)
navigationStackCoordinator.push(coordinator)
}
private func presentLocationSharingSettings() {
let parameters = LocationSharingSettingsScreenCoordinatorParameters(userSettings: flowParameters.userSettings)
let coordinator = LocationSharingSettingsScreenCoordinator(parameters: parameters)
navigationStackCoordinator.push(coordinator)
}
@@ -11,7 +11,6 @@ enum TestablePreviewsDictionary {
static let dictionary: [String: any PreviewProvider.Type] = {
var dictionary: [String: any PreviewProvider.Type] = [
"ActiveCallTimelineItemView_Previews" : ActiveCallTimelineItemView_Previews.self,
"AdvancedSettingsScreen_Previews" : AdvancedSettingsScreen_Previews.self,
"AnalyticsPromptScreen_Previews" : AnalyticsPromptScreen_Previews.self,
"AnalyticsSettingsScreen_Previews" : AnalyticsSettingsScreen_Previews.self,
"AppLockScreen_Previews" : AppLockScreen_Previews.self,
@@ -1,32 +0,0 @@
//
// Copyright 2025 Element Creations Ltd.
// Copyright 2022-2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
//
import Combine
import SwiftUI
struct AdvancedSettingsScreenCoordinatorParameters {
let userSettings: UserSettings
let analytics: AnalyticsServiceProtocol
let clientProxy: ClientProxyProtocol
let userIndicatorController: UserIndicatorControllerProtocol
}
final class AdvancedSettingsScreenCoordinator: CoordinatorProtocol {
private var viewModel: AdvancedSettingsScreenViewModelProtocol
init(parameters: AdvancedSettingsScreenCoordinatorParameters) {
viewModel = AdvancedSettingsScreenViewModel(advancedSettings: parameters.userSettings.app,
analytics: parameters.analytics,
clientProxy: parameters.clientProxy,
userIndicatorController: parameters.userIndicatorController)
}
func toPresentable() -> AnyView {
AnyView(AdvancedSettingsScreen(context: viewModel.context))
}
}
@@ -1,65 +0,0 @@
//
// Copyright 2025 Element Creations Ltd.
// Copyright 2022-2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
//
import Foundation
import UIKit
struct AdvancedSettingsScreenViewState: BindableState {
init(timelineMediaVisibility: TimelineMediaVisibility, hideInviteAvatars: Bool, isWaitingTimelineMediaVisibility: Bool = false, isWaitingHideInviteAvatars: Bool = false, bindings: AdvancedSettingsScreenViewStateBindings) {
self.timelineMediaVisibility = timelineMediaVisibility
self.hideInviteAvatars = hideInviteAvatars
self.isWaitingTimelineMediaVisibility = isWaitingTimelineMediaVisibility
self.isWaitingHideInviteAvatars = isWaitingHideInviteAvatars
self.bindings = bindings
let linkPlaceholder = "{link}"
var footerString = AttributedString(L10n.screenAdvancedSettingsLiveLocationSectionFooter(linkPlaceholder))
var linkString = AttributedString(L10n.screenAdvancedSettingsLiveLocationSectionFooterLink)
linkString.link = URL(string: UIApplication.openSettingsURLString)
linkString.bold()
footerString.replace(linkPlaceholder, with: linkString)
liveLocationUpdateFooterAttributedString = footerString
}
let liveLocationUpdateFooterAttributedString: AttributedString
var timelineMediaVisibility: TimelineMediaVisibility
var hideInviteAvatars: Bool
var isWaitingTimelineMediaVisibility: Bool
var isWaitingHideInviteAvatars: Bool
var bindings: AdvancedSettingsScreenViewStateBindings
}
@dynamicMemberLookup
struct AdvancedSettingsScreenViewStateBindings {
private let advancedSettings: AdvancedSettingsProtocol
init(advancedSettings: AdvancedSettingsProtocol) {
self.advancedSettings = advancedSettings
}
subscript<Setting>(dynamicMember keyPath: ReferenceWritableKeyPath<AdvancedSettingsProtocol, Setting>) -> Setting {
get { advancedSettings[keyPath: keyPath] }
set { advancedSettings[keyPath: keyPath] = newValue }
}
}
enum AdvancedSettingsScreenViewAction {
case optimizeMediaUploadsChanged
case updateTimelineMediaVisibility(TimelineMediaVisibility)
case updateHideInviteAvatars(Bool)
}
protocol AdvancedSettingsProtocol: AnyObject {
var viewSourceEnabled: Bool { get set }
var appAppearance: AppAppearance { get set }
var sharePresence: Bool { get set }
var optimizeMediaUploads: Bool { get set }
var liveLocationMinimumDistanceUpdate: Int { get set }
}
extension AppSettings: AdvancedSettingsProtocol { }
@@ -1,101 +0,0 @@
//
// Copyright 2025 Element Creations Ltd.
// Copyright 2022-2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
//
import Combine
import SwiftUI
typealias AdvancedSettingsScreenViewModelType = StateStoreViewModelV2<AdvancedSettingsScreenViewState, AdvancedSettingsScreenViewAction>
class AdvancedSettingsScreenViewModel: AdvancedSettingsScreenViewModelType, AdvancedSettingsScreenViewModelProtocol {
private let analytics: AnalyticsServiceProtocol
private let clientProxy: ClientProxyProtocol
private let userIndicatorController: UserIndicatorControllerProtocol
private var timelineMediaVisibilityTask: Task<Void, Never>?
private var hideInviteAvatarsTask: Task<Void, Never>?
init(advancedSettings: AdvancedSettingsProtocol,
analytics: AnalyticsServiceProtocol,
clientProxy: ClientProxyProtocol,
userIndicatorController: UserIndicatorControllerProtocol) {
self.analytics = analytics
self.clientProxy = clientProxy
self.userIndicatorController = userIndicatorController
let state = AdvancedSettingsScreenViewState(timelineMediaVisibility: clientProxy.timelineMediaVisibilityPublisher.value,
hideInviteAvatars: clientProxy.hideInviteAvatarsPublisher.value,
bindings: .init(advancedSettings: advancedSettings))
super.init(initialViewState: state)
clientProxy.timelineMediaVisibilityPublisher
.removeDuplicates()
.receive(on: DispatchQueue.main)
.weakAssign(to: \.state.timelineMediaVisibility, on: self)
.store(in: &cancellables)
clientProxy.hideInviteAvatarsPublisher
.removeDuplicates()
.receive(on: DispatchQueue.main)
.weakAssign(to: \.state.hideInviteAvatars, on: self)
.store(in: &cancellables)
}
override func process(viewAction: AdvancedSettingsScreenViewAction) {
switch viewAction {
case .optimizeMediaUploadsChanged:
// Note: Using a view action here as sinking the UserSettings publisher tracks the initial value.
analytics.trackInteraction(name: state.bindings.optimizeMediaUploads ? .MobileSettingsOptimizeMediaUploadsEnabled : .MobileSettingsOptimizeMediaUploadsDisabled)
case let .updateHideInviteAvatars(value):
hideInviteAvatarsTask = Task { [weak self] in await self?.updateHideInviteAvatars(value) }
case let .updateTimelineMediaVisibility(value):
timelineMediaVisibilityTask = Task { [weak self] in await self?.updateTimelineMediaVisibility(value) }
}
}
private func updateTimelineMediaVisibility(_ value: TimelineMediaVisibility) async {
defer {
timelineMediaVisibilityTask = nil
state.isWaitingTimelineMediaVisibility = false
}
let previousState = state.timelineMediaVisibility
state.isWaitingTimelineMediaVisibility = true
state.timelineMediaVisibility = value
// If the other value is updating wait also for it to finish
await hideInviteAvatarsTask?.value
switch await clientProxy.setTimelineMediaVisibility(value) {
case .success:
break
case .failure:
state.timelineMediaVisibility = previousState
userIndicatorController.submitIndicator(.init(title: L10n.errorUnknown))
}
}
private func updateHideInviteAvatars(_ value: Bool) async {
defer {
hideInviteAvatarsTask = nil
state.isWaitingHideInviteAvatars = false
}
let previousState = state.hideInviteAvatars
state.isWaitingHideInviteAvatars = true
state.hideInviteAvatars = value
// If the other value is updating wait also for it to finish
await timelineMediaVisibilityTask?.value
switch await clientProxy.setHideInviteAvatars(value) {
case .success:
break
case .failure:
state.hideInviteAvatars = previousState
userIndicatorController.submitIndicator(.init(title: L10n.errorUnknown))
}
}
}
@@ -1,13 +0,0 @@
//
// Copyright 2025 Element Creations Ltd.
// Copyright 2022-2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
//
import Combine
protocol AdvancedSettingsScreenViewModelProtocol {
var context: AdvancedSettingsScreenViewModelType.Context { get }
}
@@ -1,181 +0,0 @@
//
// Copyright 2025 Element Creations Ltd.
// Copyright 2022-2025 New Vector Ltd.
//
// 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
struct AdvancedSettingsScreen: View {
static let measurementFormatter = {
let formatter = MeasurementFormatter()
formatter.unitOptions = .providedUnit
formatter.unitStyle = .short
return formatter
}()
@Bindable var context: AdvancedSettingsScreenViewModel.Context
var body: some View {
Form {
Section {
ListRow(label: .plain(title: L10n.commonAppearance),
kind: .picker(selection: $context.appAppearance,
items: AppAppearance.allCases.map { (title: $0.name, tag: $0) }))
ListRow(label: .plain(title: L10n.actionViewSource,
description: L10n.screenAdvancedSettingsViewSourceDescription),
kind: .toggle($context.viewSourceEnabled))
ListRow(label: .plain(title: L10n.screenAdvancedSettingsSharePresence,
description: L10n.screenAdvancedSettingsSharePresenceDescription),
kind: .toggle($context.sharePresence))
ListRow(label: .plain(title: L10n.screenAdvancedSettingsMediaCompressionTitle,
description: L10n.screenAdvancedSettingsMediaCompressionDescription),
kind: .toggle($context.optimizeMediaUploads))
.onChange(of: context.optimizeMediaUploads) {
context.send(viewAction: .optimizeMediaUploadsChanged)
}
}
moderationAndSafetySection
timelineMediaSection
liveLocationSection
}
.compoundList()
.navigationTitle(L10n.commonAdvancedSettings)
.navigationBarTitleDisplayMode(.inline)
}
@ViewBuilder
private var moderationAndSafetySection: some View {
let binding = Binding(get: {
context.viewState.hideInviteAvatars
}, set: { newValue in
context.send(viewAction: .updateHideInviteAvatars(newValue))
})
Section {
ListRow(label: .plain(title: L10n.screenAdvancedSettingsHideInviteAvatarsToggleTitle),
details: context.viewState.isWaitingHideInviteAvatars ? .isWaiting(true) : nil,
kind: .toggle(binding))
.disabled(context.viewState.isWaitingHideInviteAvatars)
} header: {
Text(L10n.screenAdvancedSettingsModerationAndSafetySectionTitle)
.compoundListSectionHeader()
}
}
@ViewBuilder
private var timelineMediaSection: some View {
let binding = Binding(get: {
context.viewState.timelineMediaVisibility
}, set: { newValue in
context.send(viewAction: .updateTimelineMediaVisibility(newValue))
})
Section {
ListRow(label: .plain(title: L10n.screenAdvancedSettingsShowMediaTimelineTitle),
details: .isWaiting(context.viewState.isWaitingTimelineMediaVisibility),
kind: .inlinePicker(selection: binding,
items: TimelineMediaVisibility.items))
.disabled(context.viewState.isWaitingTimelineMediaVisibility)
} header: {
Text(L10n.screenAdvancedSettingsShowMediaTimelineTitle)
.compoundListSectionHeader()
} footer: {
Text(L10n.screenAdvancedSettingsShowMediaTimelineSubtitle)
.compoundListSectionFooter()
}
}
@ViewBuilder
private var liveLocationSection: some View {
let binding = Binding(get: {
Double(context.liveLocationMinimumDistanceUpdate)
}, set: { newValue in
context.liveLocationMinimumDistanceUpdate = Int(newValue)
})
Section {
ListRow(kind: .custom {
VStack(alignment: .leading, spacing: 0) {
Text(L10n.screenAdvancedSettingsLiveLocationUpdateDistance(context.liveLocationMinimumDistanceUpdate))
.font(.compound.bodyLG)
.foregroundStyle(.compound.textPrimary)
// The internal hidden label of the slider will read voice over
.accessibilityHidden(true)
Slider(value: binding, in: 1...100) {
Text(L10n.screenAdvancedSettingsLiveLocationUpdateDistance(context.liveLocationMinimumDistanceUpdate))
} minimumValueLabel: {
Text(Self.measurementFormatter.string(from: .init(value: 1,
unit: UnitLength.meters)))
.font(.compound.bodyLG)
.foregroundStyle(.compound.textSecondary)
.padding(.trailing, 15)
} maximumValueLabel: {
Text(Self.measurementFormatter.string(from: .init(value: 100,
unit: UnitLength.meters)))
.font(.compound.bodyLG)
.foregroundStyle(.compound.textSecondary)
.padding(.leading, 15)
}
.tint(.compound.iconAccentPrimary)
}
.padding(.horizontal, ListRowPadding.horizontal)
.padding(.vertical, ListRowPadding.vertical)
})
} header: {
VStack(alignment: .leading, spacing: 4) {
Text(L10n.screenAdvancedSettingsLiveLocationSectionTitle)
.compoundListSectionHeader()
Text(L10n.screenAdvancedSettingsLiveLocationSectionDescription)
.font(.compound.bodyMD)
.foregroundStyle(.compound.textSecondary)
}
} footer: {
Text(context.viewState.liveLocationUpdateFooterAttributedString)
.compoundListSectionFooter()
}
}
}
private extension AppAppearance {
var name: String {
switch self {
case .system:
L10n.themeSystem
case .light:
L10n.themeLight
case .dark:
L10n.themeDark
}
}
}
// MARK: - Previews
struct AdvancedSettingsScreen_Previews: PreviewProvider, TestablePreview {
static let viewModel = AdvancedSettingsScreenViewModel(advancedSettings: AppSettings.volatile(),
analytics: AnalyticsServiceMock(.init()),
clientProxy: ClientProxyMock(.init()),
userIndicatorController: UserIndicatorControllerMock())
static var previews: some View {
ElementNavigationStack {
AdvancedSettingsScreen(context: viewModel.context)
}
}
}
private extension TimelineMediaVisibility {
static var items: [(title: String, tag: TimelineMediaVisibility)] {
[(title: L10n.screenAdvancedSettingsShowMediaTimelineAlwaysHide, tag: .never),
(title: L10n.screenAdvancedSettingsShowMediaTimelinePrivateRooms, tag: .privateOnly),
(title: L10n.screenAdvancedSettingsShowMediaTimelineAlwaysShow, tag: .always)]
}
}
@@ -13,12 +13,34 @@ struct ModerationAndSafetySettingsScreenCoordinatorParameters {
let userIndicatorController: UserIndicatorControllerProtocol
}
enum ModerationAndSafetySettingsScreenCoordinatorAction {
case blockedUsers
}
final class ModerationAndSafetySettingsScreenCoordinator: CoordinatorProtocol {
private var viewModel: ModerationAndSafetySettingsScreenViewModelProtocol
private let actionsSubject: PassthroughSubject<ModerationAndSafetySettingsScreenCoordinatorAction, Never> = .init()
var actionsPublisher: AnyPublisher<ModerationAndSafetySettingsScreenCoordinatorAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
private var cancellables = Set<AnyCancellable>()
init(parameters: ModerationAndSafetySettingsScreenCoordinatorParameters) {
viewModel = ModerationAndSafetySettingsScreenViewModel(userSession: parameters.userSession,
userIndicatorController: parameters.userIndicatorController)
viewModel.actionsPublisher
.sink { [weak self] action in
guard let self else { return }
switch action {
case .blockedUsers:
actionsSubject.send(.blockedUsers)
}
}
.store(in: &cancellables)
}
func toPresentable() -> AnyView {
@@ -19,23 +19,30 @@ struct SettingsScreenCoordinatorParameters {
enum SettingsScreenCoordinatorAction {
case dismiss
case logout
case secureBackup
case userDetails
case userStatusEmojiPicker(EmojiPickerScreenContinuation)
case addAccount
case analytics
case appLock
case bugReport
case about
case blockedUsers
case linkNewDevice
case manageAccount(url: URL)
case linkNewDevice
case notifications
case advancedSettings
case labs
case developerOptions
case secureBackup
case moderationAndSafety
case bugReport
case logout
case deactivateAccount
case mediaUploadQuality
case appLock
case locationSharing
case analytics
case labs
case about
case developerOptions
}
final class SettingsScreenCoordinator: CoordinatorProtocol {
@@ -64,40 +71,48 @@ 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 .linkNewDevice:
actionsSubject.send(.linkNewDevice)
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 .linkNewDevice:
actionsSubject.send(.linkNewDevice)
case .notifications:
actionsSubject.send(.notifications)
case .advancedSettings:
actionsSubject.send(.advancedSettings)
case .labs:
actionsSubject.send(.labs)
case .developerOptions:
actionsSubject.send(.developerOptions)
case .secureBackup:
actionsSubject.send(.secureBackup)
case .moderationAndSafety:
actionsSubject.send(.moderationAndSafety)
case .reportBug:
actionsSubject.send(.bugReport)
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,23 +10,30 @@ import SwiftUI
enum SettingsScreenViewModelAction {
case close
case userDetails
case userStatusEmojiPicker(EmojiPickerScreenContinuation)
case addAccount
case linkNewDevice
case manageAccount(url: URL)
case analytics
case appLock
case reportBug
case about
case blockedUsers
case secureBackup
case linkNewDevice
case notifications
case advancedSettings
case labs
case developerOptions
case secureBackup
case moderationAndSafety
case reportBug
case logout
case deactivateAccount
case mediaUploadQuality
case appLock
case locationSharing
case analytics
case labs
case about
case developerOptions
}
enum SettingsScreenSecuritySectionMode {
@@ -47,14 +54,13 @@ 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()
var bindings: SettingsScreenViewStateBindings
var userStatusRowMode: SettingsScreenUserStatusRow.Mode {
if bindings.isShowingCustomStatusField {
@@ -68,6 +74,8 @@ struct SettingsScreenViewState: BindableState {
}
struct SettingsScreenViewStateBindings {
private let userSettings: UserSettings
var isPresentingStatusPicker = false
var customStatusEmoji: Character = "😄"
var isShowingCustomStatusField = false {
@@ -79,29 +87,45 @@ struct SettingsScreenViewStateBindings {
}
var isPresentingAccountDeactivationConfirmation = false
var appAppearance: AppAppearance {
get { userSettings.appAppearance }
set { userSettings.appAppearance = newValue }
}
init(userSettings: UserSettings) {
self.userSettings = userSettings
}
}
enum SettingsScreenViewAction {
case close
case userDetails
case userStatus(UserStatusAction)
case addAccount
case analytics
case appLock
case reportBug
case about
case blockedUsers
case secureBackup
case linkNewDevice
case manageAccount(url: URL)
case linkNewDevice
case notifications
case enableDeveloperOptions
case developerOptions
case advancedSettings
case labs
case secureBackup
case moderationAndSafety
case reportBug
case logout
case deactivateAccount
case mediaUploadQuality
case appLock
case locationSharing
case analytics
case labs
case about
case enableDeveloperOptions
case developerOptions
enum UserStatusAction {
/// Show status picker sheet to select a preset status.
case pickStatus
@@ -35,7 +35,8 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo
showDeveloperOptions: userSettings.developerOptionsEnabled,
showAnalyticsSettings: userSettings.canPromptForAnalytics,
isBugReportServiceEnabled: isBugReportServiceEnabled,
navigationBarVisibility: isInSecondaryWindow ? .hidden : .automatic),
navigationBarVisibility: isInSecondaryWindow ? .hidden : .automatic,
bindings: .init(userSettings: userSettings)),
mediaProvider: userSession.mediaProvider)
userSettings.developerOptionsEnabledPublisher
@@ -77,18 +78,6 @@ 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
@@ -102,6 +91,7 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo
switch viewAction {
case .close:
actionsSubject.send(.close)
case .userDetails:
actionsSubject.send(.userDetails)
case .userStatus(.pickStatus):
@@ -118,38 +108,45 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo
case .userStatus(.cancel):
state.bindings.isPresentingStatusPicker = false
state.bindings.isShowingCustomStatusField = false
case .addAccount:
actionsSubject.send(.addAccount)
case .linkNewDevice:
actionsSubject.send(.linkNewDevice)
case let .manageAccount(url):
actionsSubject.send(.manageAccount(url: url))
case .analytics:
actionsSubject.send(.analytics)
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 .linkNewDevice:
actionsSubject.send(.linkNewDevice)
case .notifications:
actionsSubject.send(.notifications)
case .advancedSettings:
actionsSubject.send(.advancedSettings)
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 .analytics:
actionsSubject.send(.analytics)
case .labs:
actionsSubject.send(.labs)
case .about:
actionsSubject.send(.about)
case .enableDeveloperOptions:
userSettings.developerOptionsEnabled.toggle()
case .developerOptions:
actionsSubject.send(.developerOptions)
case .deactivateAccount:
actionsSubject.send(.deactivateAccount)
}
}
@@ -13,37 +13,19 @@ 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.showUserStatusInput {
userStatusSection
}
if context.viewState.showAddAccountButton {
addAccountSection
}
if !shouldHideManageAccountSection {
manageAccountSection
}
manageMyAppSection
generalSection
accountSettingsSection
signOutSection
if context.viewState.showDeveloperOptions {
developerOptionsSection
}
appSettingsSection
}
.compoundList()
.navigationTitle(L10n.commonSettings)
@@ -91,16 +73,14 @@ struct SettingsScreen: View {
.padding(.vertical, 8)
}
})
}
}
private var userStatusSection: some View {
Section {
if context.viewState.showUserStatusInput {
SettingsScreenUserStatusRow(mode: context.viewState.userStatusRowMode) { action in
context.send(viewAction: .userStatus(action))
}
}
}
}
private var addAccountSection: some View {
Section {
@@ -113,36 +93,7 @@ struct SettingsScreen: View {
}
}
private var manageMyAppSection: some View {
Section {
ListRow(label: .default(title: L10n.screenNotificationSettingsTitle,
icon: \.notifications),
kind: .navigationLink {
context.send(viewAction: .notifications)
})
.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 manageAccountSection: some View {
private var accountSettingsSection: some View {
Section {
if let url = context.viewState.accountProfileURL {
ListRow(label: .default(title: L10n.actionManageAccountAndDevices,
@@ -161,38 +112,29 @@ struct SettingsScreen: View {
})
}
if context.viewState.showBlockedUsers {
ListRow(label: .default(title: L10n.commonBlockedUsers,
icon: \.block),
ListRow(label: .default(title: L10n.screenNotificationSettingsTitle,
icon: \.notifications),
kind: .navigationLink {
context.send(viewAction: .blockedUsers)
context.send(viewAction: .notifications)
})
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.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()
}
private var generalSection: some View {
Section {
ListRow(label: .default(title: L10n.commonAdvancedSettings,
icon: \.settings),
ListRow(label: .default(title: L10n.commonModerationAndSafety,
icon: \.admin),
kind: .navigationLink {
context.send(viewAction: .advancedSettings)
context.send(viewAction: .moderationAndSafety)
})
.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,
@@ -202,15 +144,9 @@ struct SettingsScreen: View {
})
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.reportBug)
}
if context.viewState.showAnalyticsSettings {
ListRow(label: .default(title: L10n.commonAnalytics,
icon: \.chart),
kind: .navigationLink {
context.send(viewAction: .analytics)
})
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.analytics)
}
} header: {
Text(L10n.commonAccountSettings)
.compoundListSectionHeader()
}
}
@@ -232,21 +168,68 @@ struct SettingsScreen: View {
context.send(viewAction: .deactivateAccount)
})
}
} footer: {
if !context.viewState.showDeveloperOptions {
versionSection
}
}
}
private var developerOptionsSection: some View {
private var appSettingsSection: 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
}
@@ -291,6 +274,19 @@ 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, *)
@@ -303,7 +299,7 @@ struct SettingsScreen_Previews: PreviewProvider, TestablePreview {
SettingsScreen(context: viewModel.context)
}
.snapshotPreferences(expect: viewModel.context.observe(\.viewState.accountProfileURL).map { $0 != nil })
.frame(height: 1200)
.frame(height: 1300)
.previewLayout(.sizeThatFits)
.previewDisplayName("Default")
@@ -311,7 +307,7 @@ struct SettingsScreen_Previews: PreviewProvider, TestablePreview {
SettingsScreen(context: bugReportDisabledViewModel.context)
}
.snapshotPreferences(expect: bugReportDisabledViewModel.context.observe(\.viewState.accountProfileURL).map { $0 != nil })
.frame(height: 1150)
.frame(height: 1250)
.previewLayout(.sizeThatFits)
.previewDisplayName("Bug report disabled")
}
@@ -323,6 +319,7 @@ struct SettingsScreen_Previews: PreviewProvider, TestablePreview {
status: .mockFocussing))))
let userSettings = UserSettings.mock()
userSettings.multiAccountEnabled = true
userSettings.linkNewDeviceEnabled = true
return SettingsScreenViewModel(userSession: userSession,
userSettings: userSettings,
isBugReportServiceEnabled: isBugReportServiceEnabled,
@@ -18,13 +18,6 @@ extension PreviewTests {
}
}
@Test
func advancedSettingsScreen() async throws {
for (index, preview) in AdvancedSettingsScreen_Previews._allPreviews.enumerated() {
try await assertSnapshots(matching: preview, step: index)
}
}
@Test
func analyticsPromptScreen() async throws {
for (index, preview) in AnalyticsPromptScreen_Previews._allPreviews.enumerated() {
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e8ebdf9faecc3f6a907749856b1626392b83fb4bfd4ea05965fa2e36c53bc21e
size 203603
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:694ab48bbf3b7d016ee5e6923bbcf984cfc54db145cf9e7bf60360671894ca40
size 241401
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f12e47794c0e762f3169a2f3f79d72932e4c26aa16f4c6932570adb506b5105e
size 139589
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fd0a047fe7214cfd947d44e59f30af73cea884eeb30199d3c782bc2f7430d32b
size 165304
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9eeed90de894bbf66e72b23a9cfe8b8aa455e8a9ff3f509a227203b2734bd411
size 208003
oid sha256:6bac7010123a1a0362bd97ce148379de1be158efff8a4173cbf24bae6db91f8e
size 239301
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:daa1d62daa786fa52e35f9f9924b969a627649037cf64e6949b692de837f3f89
size 218666
oid sha256:15740810913e01029cc6aa4b2529601d34a3469e834c45d6070122b0eeef7a67
size 255708
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:02ddd70542f4e8963f1745ee9f65e6f42e864bad5a28dffe052de35ac2ead3aa
size 132702
oid sha256:487b3d2c1c331d620ba5b25392f5e634fddd3066739a8f49781c018851f3721a
size 158210
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ef1a98de9b2ddfb490882c5a937d2d7159a0a0ab513f41a8e1a1b1b3c95d5323
size 148131
oid sha256:06720e67dd18586e9fad302dc8bb2f130785ed17867d98cfbe16b428186be812
size 179850
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:56417caadb44c989b9ca9032d8eb6b13e5d02cead1537eb04a2ea262c8a3ea00
size 218308
oid sha256:483cb6201103f76f2f102442e16be41c3a6cea8fe66d1b80bb1c111609a6c9fd
size 249597
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:450405a9646962d1889af363b6da096a651d3d40dcb9a0212cafb7c1a2695f7d
size 230377
oid sha256:b39b2a748e16f9da207f2b732afccd521baa926e452c90ec2291d8ede6ad1679
size 267272
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a3dcc827b87f5b49344477648239fa7bb46c1101324fab00ac8690943369fcf6
size 140339
oid sha256:7a06ade1aacefd2c027fd317857ce1a55ce96e7c0a264b87c0cb4352fc6b21d2
size 165653
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:91c5820e827e06ff4c2ab55c5ff86425fe0634172e3cfc2134574a6c955fb89e
size 158619
oid sha256:926584ed061ee942d65d348016ee15775dbf01b91ec8a8408333e48ae16a26c4
size 189795