mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-10-31 16:24:11 +00:00
49457f7ae8
* Add a multi-account feature flag The flag lives in Developer options. Switching it on arms the upcoming one-time multi-account announcement, whose seen state defaults to true so fresh installs never see it. * Add an Add account row to Settings behind the multi-account flag The row sits in its own section under the user status, as in the designs. Tapping it is routed up to the settings flow, which ignores it until the add account flow exists. * Remove the Developer options view model tests The Developer options screen is excluded from coverage, so these tests don't add value. * Apply review suggestions to the multi-account flag - Order the multi-account requirements in DeveloperOptionsProtocol to match the screen's sections. - Drop the redundant "Experimental" from the toggle's subtitle.
224 lines
9.1 KiB
Swift
224 lines
9.1 KiB
Swift
//
|
|
// 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 SettingsScreenViewModelType = StateStoreViewModelV2<SettingsScreenViewState, SettingsScreenViewAction>
|
|
|
|
class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewModelProtocol {
|
|
private let userSettings: UserSettings
|
|
private let clientProxy: ClientProxyProtocol
|
|
private let userIndicatorController: UserIndicatorControllerProtocol
|
|
|
|
private var actionsSubject: PassthroughSubject<SettingsScreenViewModelAction, Never> = .init()
|
|
|
|
var actions: AnyPublisher<SettingsScreenViewModelAction, Never> {
|
|
actionsSubject.eraseToAnyPublisher()
|
|
}
|
|
|
|
init(userSession: UserSessionProtocol, userSettings: UserSettings, isBugReportServiceEnabled: Bool, isInSecondaryWindow: Bool, userIndicatorController: UserIndicatorControllerProtocol) {
|
|
self.userSettings = userSettings
|
|
clientProxy = userSession.clientProxy
|
|
self.userIndicatorController = userIndicatorController
|
|
|
|
super.init(initialViewState: .init(deviceID: userSession.clientProxy.deviceID,
|
|
userProfile: userSession.clientProxy.userProfilePublisher.value,
|
|
showLinkNewDeviceButton: userSettings.linkNewDeviceEnabled,
|
|
showAddAccountButton: userSettings.multiAccountEnabled,
|
|
showAccountDeactivation: userSession.clientProxy.canDeactivateAccount,
|
|
showDeveloperOptions: userSettings.developerOptionsEnabled,
|
|
showAnalyticsSettings: userSettings.canPromptForAnalytics,
|
|
isBugReportServiceEnabled: isBugReportServiceEnabled,
|
|
navigationBarVisibility: isInSecondaryWindow ? .hidden : .automatic),
|
|
mediaProvider: userSession.mediaProvider)
|
|
|
|
userSettings.developerOptionsEnabledPublisher
|
|
.weakAssign(to: \.state.showDeveloperOptions, on: self)
|
|
.store(in: &cancellables)
|
|
|
|
userSettings.linkNewDeviceEnabledPublisher
|
|
.weakAssign(to: \.state.showLinkNewDeviceButton, on: self)
|
|
.store(in: &cancellables)
|
|
|
|
userSettings.multiAccountEnabledPublisher
|
|
.weakAssign(to: \.state.showAddAccountButton, on: self)
|
|
.store(in: &cancellables)
|
|
|
|
userSession.clientProxy.userProfilePublisher
|
|
.receive(on: DispatchQueue.main)
|
|
.weakAssign(to: \.state.userProfile, on: self)
|
|
.store(in: &cancellables)
|
|
|
|
userSession.sessionSecurityStatePublisher
|
|
.receive(on: DispatchQueue.main)
|
|
.sink { [weak self] securityState in
|
|
guard let self else { return }
|
|
|
|
switch (securityState.verificationState, securityState.recoveryState) {
|
|
case (.verified, .disabled):
|
|
state.showSecuritySectionBadge = true
|
|
state.securitySectionMode = .secureBackup
|
|
case (.verified, .incomplete):
|
|
state.showSecuritySectionBadge = true
|
|
state.securitySectionMode = .secureBackup
|
|
case (.unknown, _):
|
|
state.showSecuritySectionBadge = false
|
|
state.securitySectionMode = .none
|
|
default:
|
|
state.showSecuritySectionBadge = false
|
|
state.securitySectionMode = .secureBackup
|
|
}
|
|
}
|
|
.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
|
|
}
|
|
await userSession.clientProxy.loadUserProfileIfNeeded()
|
|
}
|
|
Task { await state.accountProfileURL = userSession.clientProxy.accountURL(action: .profile) }
|
|
}
|
|
|
|
override func process(viewAction: SettingsScreenViewAction) {
|
|
switch viewAction {
|
|
case .close:
|
|
actionsSubject.send(.close)
|
|
case .userDetails:
|
|
actionsSubject.send(.userDetails)
|
|
case .userStatus(.pickStatus):
|
|
state.bindings.isPresentingStatusPicker = true
|
|
case .userStatus(.customStatus):
|
|
state.bindings.isPresentingStatusPicker = false
|
|
state.bindings.isShowingCustomStatusField = true
|
|
case .userStatus(.pickCustomEmoji):
|
|
pickCustomEmoji()
|
|
case .userStatus(.set(let status)):
|
|
Task { await setUserStatus(status) }
|
|
case .userStatus(.clear):
|
|
Task { await clearUserStatus() }
|
|
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 .notifications:
|
|
actionsSubject.send(.notifications)
|
|
case .advancedSettings:
|
|
actionsSubject.send(.advancedSettings)
|
|
case .labs:
|
|
actionsSubject.send(.labs)
|
|
case .enableDeveloperOptions:
|
|
userSettings.developerOptionsEnabled.toggle()
|
|
case .developerOptions:
|
|
actionsSubject.send(.developerOptions)
|
|
case .deactivateAccount:
|
|
actionsSubject.send(.deactivateAccount)
|
|
}
|
|
}
|
|
|
|
// MARK: - Private
|
|
|
|
private var pickCustomEmojiCancellable: AnyCancellable?
|
|
private func pickCustomEmoji() {
|
|
let (stream, continuation) = AsyncStream<String>.makeStream()
|
|
actionsSubject.send(.userStatusEmojiPicker(continuation))
|
|
|
|
pickCustomEmojiCancellable = Task { [weak self] in
|
|
for await emoji in stream {
|
|
self?.state.bindings.customStatusEmoji = Character(emoji)
|
|
}
|
|
}
|
|
.asCancellable()
|
|
}
|
|
|
|
private func setUserStatus(_ status: UserStatus.Raw) async {
|
|
showSavingIndicator()
|
|
defer { hideSavingIndicator() }
|
|
|
|
state.bindings.isPresentingStatusPicker = false
|
|
state.bindings.isShowingCustomStatusField = false
|
|
|
|
if case .failure = await clientProxy.setUserStatus(status) {
|
|
showFailureIndicator()
|
|
}
|
|
}
|
|
|
|
/// Clears both the `UserStatus.Raw` and `UserStatus.Call` values simultaneously.
|
|
private func clearUserStatus() async {
|
|
showSavingIndicator()
|
|
defer { hideSavingIndicator() }
|
|
|
|
state.bindings.isPresentingStatusPicker = false
|
|
state.bindings.isShowingCustomStatusField = false
|
|
|
|
if case .failure = await clientProxy.clearUserStatus() {
|
|
showFailureIndicator()
|
|
}
|
|
}
|
|
|
|
// MARK: - Indicators
|
|
|
|
private static var savingIndicatorID: String {
|
|
"\(Self.self)-Saving"
|
|
}
|
|
|
|
private static var failureIndicatorID: String {
|
|
"\(Self.self)-Failure"
|
|
}
|
|
|
|
private func showSavingIndicator() {
|
|
userIndicatorController.submitIndicator(UserIndicator(id: Self.savingIndicatorID,
|
|
type: .toast(progress: .indeterminate),
|
|
title: L10n.commonSaving,
|
|
persistent: true))
|
|
}
|
|
|
|
private func hideSavingIndicator() {
|
|
userIndicatorController.retractIndicatorWithId(Self.savingIndicatorID)
|
|
}
|
|
|
|
private func showFailureIndicator() {
|
|
userIndicatorController.submitIndicator(UserIndicator(id: Self.failureIndicatorID,
|
|
type: .toast,
|
|
title: L10n.commonFailed,
|
|
icon: \.close))
|
|
}
|
|
}
|