mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-10-31 08:15:24 +00:00
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:
@@ -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,
|
||||
|
||||
-32
@@ -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))
|
||||
}
|
||||
}
|
||||
-65
@@ -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 { }
|
||||
-101
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
-13
@@ -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 }
|
||||
}
|
||||
-181
@@ -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)]
|
||||
}
|
||||
}
|
||||
+22
@@ -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,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,
|
||||
|
||||
Reference in New Issue
Block a user