// // 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 SFSafeSymbols 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 } context.viewState.appHooks.settingsScreenHook.accountStatusSection { context.send(viewAction: .userDetails) } if !shouldHideManageAccountSection { manageAccountSection } manageMyAppSection generalSection signOutSection if context.viewState.showDeveloperOptions { developerOptionsSection } } .compoundList() .navigationTitle(L10n.commonSettings) .navigationBarTitleDisplayMode(.inline) .toolbarVisibility(context.viewState.navigationBarVisibility, for: .navigationBar) .toolbar { toolbar } .sheet(isPresented: $context.isPresentingStatusPicker) { SettingsScreenUserStatusPickerView { action in context.send(viewAction: .userStatus(action)) } .presentationDetents([.medium]) .presentationBackground(.compound.bgCanvasDefault) } } private var userSection: some View { Section { ListRow(kind: .custom { Button { context.send(viewAction: .userDetails) } label: { HStack(spacing: 12) { LoadableAvatarImage(url: context.viewState.userProfile.avatarURL, name: context.viewState.userProfile.displayName, contentID: context.viewState.userProfile.id, avatarSize: .user(on: .settings), mediaProvider: context.mediaProvider) .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) Text(context.viewState.userProfile.id) .font(.compound.bodySM) .foregroundColor(.compound.textSecondary) } Spacer() ListRowAccessory.navigationLink } .padding(.horizontal, ListRowPadding.horizontal) .padding(.vertical, 8) } }) } } private var userStatusSection: some View { Section { SettingsScreenUserStatusRow(mode: context.viewState.userStatusRowMode) { action in context.send(viewAction: .userStatus(action)) } } } 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 { Section { if let url = context.viewState.accountProfileURL { ListRow(label: .default(title: L10n.actionManageAccountAndDevices, icon: \.userProfile), kind: .button { context.send(viewAction: .manageAccount(url: url)) }) .accessibilityIdentifier(A11yIdentifiers.settingsScreen.account) } if context.viewState.showLinkNewDeviceButton { ListRow(label: .default(title: L10n.commonLinkNewDevice, icon: \.devices), kind: .navigationLink { context.send(viewAction: .linkNewDevice) }) } if context.viewState.showBlockedUsers { ListRow(label: .default(title: L10n.commonBlockedUsers, icon: \.block), kind: .navigationLink { context.send(viewAction: .blockedUsers) }) .accessibilityIdentifier(A11yIdentifiers.settingsScreen.blockedUsers) } } } 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), 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, icon: \.chatProblem), kind: .navigationLink { context.send(viewAction: .reportBug) }) .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) } } } private var signOutSection: some View { Section { ListRow(label: .action(title: L10n.screenSignoutPreferenceItem, icon: \.close, role: .destructive), kind: .button { context.send(viewAction: .logout) }) .accessibilityIdentifier(A11yIdentifiers.settingsScreen.logout) if context.viewState.showAccountDeactivation { ListRow(label: .action(title: L10n.actionDeleteAccount, icon: \.delete, role: .destructive), kind: .navigationLink { context.send(viewAction: .deactivateAccount) }) } } footer: { if !context.viewState.showDeveloperOptions { versionSection } } } private var developerOptionsSection: some View { Section { ListRow(label: .default(title: L10n.commonDeveloperOptions, icon: \.code), kind: .navigationLink { context.send(viewAction: .developerOptions) }) .accessibilityIdentifier(A11yIdentifiers.settingsScreen.developerOptions) } footer: { versionSection } } private var versionSection: some View { VStack(spacing: 0) { versionText .frame(maxWidth: .infinity) if let deviceID = context.viewState.deviceID { Text(deviceID) } } .compoundListSectionFooter() .textSelection(.enabled) .padding(.top, 24) .onTapGesture(count: 7) { context.send(viewAction: .enableDeveloperOptions) } } private var versionText: Text { // Let's not snapshot a changing version string. let shortVersion = ProcessInfo.isRunningTests ? "0.0.0" : InfoPlistReader.main.bundleShortVersionString let version = ProcessInfo.isRunningTests ? "1" : InfoPlistReader.main.bundleVersion return Text(L10n.settingsVersionNumber(shortVersion, version)) } private var toolbar: some ToolbarContent { ToolbarItem(placement: .primaryAction) { ToolbarButton(role: .close) { context.send(viewAction: .close) } .accessibilityIdentifier(A11yIdentifiers.settingsScreen.done) } } @ViewBuilder private var securitySectionBadge: some View { if context.viewState.showSecuritySectionBadge { BadgeView(size: 10) } } } // MARK: - Previews @available(iOS 26.0, *) struct SettingsScreen_Previews: PreviewProvider, TestablePreview { static let viewModel = makeViewModel() static let bugReportDisabledViewModel = makeViewModel(isBugReportServiceEnabled: false) static var previews: some View { ElementNavigationStack { SettingsScreen(context: viewModel.context) } .snapshotPreferences(expect: viewModel.context.observe(\.viewState.accountProfileURL).map { $0 != nil }) .frame(height: 1100) .previewLayout(.sizeThatFits) .previewDisplayName("Default") ElementNavigationStack { SettingsScreen(context: bugReportDisabledViewModel.context) } .snapshotPreferences(expect: bugReportDisabledViewModel.context.observe(\.viewState.accountProfileURL).map { $0 != nil }) .frame(height: 1050) .previewLayout(.sizeThatFits) .previewDisplayName("Bug report disabled") } static func makeViewModel(isBugReportServiceEnabled: Bool = true) -> SettingsScreenViewModel { let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: "@alice:example.com", deviceID: "AAAAAAAAAAA", displayName: "Alice Liddell", status: .mockFocussing)))) return SettingsScreenViewModel(userSession: userSession, appSettings: .volatile(), appHooks: AppHooks(), isBugReportServiceEnabled: isBugReportServiceEnabled, isInSecondaryWindow: false, userIndicatorController: UserIndicatorControllerMock()) } }