Files
letro-ios/ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenViewModel.swift
Stefan Ceriu 1ae6fc67c4 SDK update (#3891)
* Bump the RustSDK to v25.03.11

* Replace oidc login prompt with nil following the changes from https://github.com/matrix-org/matrix-rust-sdk/pull/4761

```
/// * `prompt` - The desired user experience in the web UI. No value means
///   that the user wishes to login into an existing account, and a value of
///   `Create` means that the user wishes to register a new account.
```

* Fix trailing closure warnings

* Update the client proxy after making `getNotificationSettings()` and  `cachedAvatarUrl()` async (they used to be blocking on the rust side).

* Move `Room.isEncrypted` to the info publisher and manually update the encryption state when creating the room.

* Bump the SDK again to v25.03.12 - This introduces a new way to configure the tokio runtime that we can use to have extensions use less memory
- introduce a new Target struct that takes care of setting up rust services (tracing and tokio) for our various targets
- cleanup MXLog and friends

* Address PR comments

* Bump the SDK again, switch back to using `.consent` as the OIDC login prompt (which was reintroduced in matrix-org/matrix-rust-sdk/pull/4791)
2025-03-13 11:17:37 +02:00

237 lines
9.9 KiB
Swift

//
// Copyright 2022-2024 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 MatrixRustSDK
import SwiftUI
typealias SecurityAndPrivacyScreenViewModelType = StateStoreViewModel<SecurityAndPrivacyScreenViewState, SecurityAndPrivacyScreenViewAction>
class SecurityAndPrivacyScreenViewModel: SecurityAndPrivacyScreenViewModelType, SecurityAndPrivacyScreenViewModelProtocol {
private let roomProxy: JoinedRoomProxyProtocol
private let clientProxy: ClientProxyProtocol
private let userIndicatorController: UserIndicatorControllerProtocol
private let actionsSubject: PassthroughSubject<SecurityAndPrivacyScreenViewModelAction, Never> = .init()
var actionsPublisher: AnyPublisher<SecurityAndPrivacyScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(roomProxy: JoinedRoomProxyProtocol,
clientProxy: ClientProxyProtocol,
userIndicatorController: UserIndicatorControllerProtocol) {
self.roomProxy = roomProxy
self.clientProxy = clientProxy
self.userIndicatorController = userIndicatorController
super.init(initialViewState: SecurityAndPrivacyScreenViewState(serverName: clientProxy.userIDServerName ?? "",
accessType: roomProxy.infoPublisher.value.joinRule.toSecurityAndPrivacyRoomAccessType,
isEncryptionEnabled: roomProxy.infoPublisher.value.isEncrypted,
historyVisibility: roomProxy.infoPublisher.value.historyVisibility.toSecurityAndPrivacyHistoryVisibility))
setupRoomDirectoryVisibility()
setupSubscriptions()
}
// MARK: - Public
override func process(viewAction: SecurityAndPrivacyScreenViewAction) {
MXLog.info("View model: received view action: \(viewAction)")
switch viewAction {
case .save:
Task {
await saveDesiredSettings()
}
case .tryUpdatingEncryption(let updatedValue):
if updatedValue {
state.bindings.alertInfo = .init(id: .enableEncryption,
title: L10n.screenSecurityAndPrivacyEnableEncryptionAlertTitle,
message: L10n.screenSecurityAndPrivacyEnableEncryptionAlertDescription,
primaryButton: .init(title: L10n.screenSecurityAndPrivacyEnableEncryptionAlertConfirmButtonTitle) { [weak self] in self?.state.bindings.desiredSettings.isEncryptionEnabled = true },
secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil))
} else {
state.bindings.desiredSettings.isEncryptionEnabled = false
}
case .editAddress:
actionsSubject.send(.displayEditAddressScreen)
}
}
// MARK: - Private
private func setupSubscriptions() {
context.$viewState
.map(\.availableVisibilityOptions)
.removeDuplicates()
// To allow the view to update properly
.receive(on: DispatchQueue.main)
// When the available options changes always default to `sinceSelection` if the currently selected option is not available
.sink { [weak self] availableVisibilityOptions in
guard let self else { return }
let desiredHistoryVisibility = state.bindings.desiredSettings.historyVisibility
if !availableVisibilityOptions.contains(desiredHistoryVisibility) {
state.bindings.desiredSettings.historyVisibility = desiredHistoryVisibility.fallbackOption
}
}
.store(in: &cancellables)
let userIDServerName = clientProxy.userIDServerName
roomProxy.infoPublisher
.compactMap { roomInfo in
guard let userIDServerName else {
return nil
}
// Give priority to aliases from the current user's homeserver as remote ones
// cannot be edited.
return roomInfo.firstAliasMatching(serverName: userIDServerName, useFallback: true)
}
.removeDuplicates()
.receive(on: DispatchQueue.main)
.weakAssign(to: \.state.canonicalAlias, on: self)
.store(in: &cancellables)
}
private func setupRoomDirectoryVisibility() {
Task {
switch await roomProxy.isVisibleInRoomDirectory() {
case .success(let value):
state.bindings.desiredSettings.isVisibileInRoomDirectory = value
state.currentSettings.isVisibileInRoomDirectory = value
case .failure:
userIndicatorController.submitIndicator(.init(title: L10n.errorUnknown))
state.bindings.desiredSettings.isVisibileInRoomDirectory = false
state.currentSettings.isVisibileInRoomDirectory = false
}
}
}
private func saveDesiredSettings() async {
showLoadingIndicator()
defer {
hideLoadingIndicator()
}
if state.currentSettings.isEncryptionEnabled != state.bindings.desiredSettings.isEncryptionEnabled {
switch await roomProxy.enableEncryption() {
case .success:
state.currentSettings.isEncryptionEnabled = state.bindings.desiredSettings.isEncryptionEnabled
case .failure:
userIndicatorController.submitIndicator(.init(title: L10n.errorUnknown))
}
}
if state.currentSettings.historyVisibility != state.bindings.desiredSettings.historyVisibility {
switch await roomProxy.updateHistoryVisibility(state.bindings.desiredSettings.historyVisibility.toRoomHistoryVisibility) {
case .success:
state.currentSettings.historyVisibility = state.bindings.desiredSettings.historyVisibility
case .failure:
userIndicatorController.submitIndicator(.init(title: L10n.errorUnknown))
}
}
if state.currentSettings.accessType != state.bindings.desiredSettings.accessType {
// When a user changes join rules to something other than knock or public,
// the room should be automatically made invisible (private) in the room directory.
if state.currentSettings.accessType != .askToJoin, state.currentSettings.accessType != .anyone {
state.bindings.desiredSettings.isVisibileInRoomDirectory = false
}
switch await roomProxy.updateJoinRule(state.bindings.desiredSettings.accessType.toJoinRule) {
case .success:
state.currentSettings.accessType = state.bindings.desiredSettings.accessType
case .failure:
userIndicatorController.submitIndicator(.init(title: L10n.errorUnknown))
}
}
if state.currentSettings.isVisibileInRoomDirectory != state.bindings.desiredSettings.isVisibileInRoomDirectory {
let visibility: RoomVisibility = state.bindings.desiredSettings.isVisibileInRoomDirectory == true ? .public : .private
switch await roomProxy.updateRoomDirectoryVisibility(visibility) {
case .success:
state.currentSettings.isVisibileInRoomDirectory = state.bindings.desiredSettings.isVisibileInRoomDirectory
case .failure:
userIndicatorController.submitIndicator(.init(title: L10n.errorUnknown))
}
}
}
private static let loadingIndicatorIdentifier = "\(EditRoomAddressScreenViewModel.self)-Loading"
private func showLoadingIndicator() {
userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier,
type: .modal,
title: L10n.commonLoading,
persistent: true))
}
private func hideLoadingIndicator() {
userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier)
}
}
private extension SecurityAndPrivacyRoomAccessType {
var toJoinRule: JoinRule {
switch self {
case .inviteOnly:
.invite
case .askToJoin:
.knock
case .anyone:
.public
case .spaceUsers:
fatalError("The user shouldn't be able to select this rule")
}
}
}
private extension Optional where Wrapped == JoinRule {
var toSecurityAndPrivacyRoomAccessType: SecurityAndPrivacyRoomAccessType {
switch self {
case .none, .public:
return .anyone
case .invite:
return .inviteOnly
case .knock, .knockRestricted:
return .askToJoin
case .restricted:
return .spaceUsers
default:
return .inviteOnly
}
}
}
private extension RoomHistoryVisibility {
var toSecurityAndPrivacyHistoryVisibility: SecurityAndPrivacyHistoryVisibility {
switch self {
case .joined, .invited:
return .sinceInvite
case .shared, .custom:
return .sinceSelection
case .worldReadable:
return .anyone
}
}
}
private extension SecurityAndPrivacyHistoryVisibility {
var toRoomHistoryVisibility: RoomHistoryVisibility {
switch self {
case .sinceSelection:
return .shared
case .sinceInvite:
return .invited
case .anyone:
return .worldReadable
}
}
}