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
@@ -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,13 +73,11 @@ struct SettingsScreen: View {
.padding(.vertical, 8)
}
})
}
}
private var userStatusSection: some View {
Section {
SettingsScreenUserStatusRow(mode: context.viewState.userStatusRowMode) { action in
context.send(viewAction: .userStatus(action))
if context.viewState.showUserStatusInput {
SettingsScreenUserStatusRow(mode: context.viewState.userStatusRowMode) { action in
context.send(viewAction: .userStatus(action))
}
}
}
}
@@ -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,39 +112,30 @@ struct SettingsScreen: View {
})
}
if context.viewState.showBlockedUsers {
ListRow(label: .default(title: L10n.commonBlockedUsers,
icon: \.block),
kind: .navigationLink {
context.send(viewAction: .blockedUsers)
})
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.blockedUsers)
ListRow(label: .default(title: L10n.screenNotificationSettingsTitle,
icon: \.notifications),
kind: .navigationLink {
context.send(viewAction: .notifications)
})
.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),
kind: .navigationLink {
context.send(viewAction: .advancedSettings)
})
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.advancedSettings)
ListRow(label: .default(title: L10n.screenAdvancedSettingsLabs,
icon: \.labs),
ListRow(label: .default(title: L10n.commonModerationAndSafety,
icon: \.admin),
kind: .navigationLink {
context.send(viewAction: .labs)
context.send(viewAction: .moderationAndSafety)
})
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,
icon: \.chatProblem),
@@ -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.commonDeveloperOptions,
icon: \.code),
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: .developerOptions)
context.send(viewAction: .mediaUploadQuality)
})
.accessibilityIdentifier(A11yIdentifiers.settingsScreen.developerOptions)
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,