Files
letro-ios/ElementX/Sources/Screens/EditRoomAddressScreen/EditRoomAddressScreenViewModel.swift
Mauro f39206561a Better handling for editing alias in case of different HS (#3695)
* better handling for aliases from different HS

* insert the alias at the top

* removing the old homeserver alias

* code improvement

* always remove the old canonical alias found on the server if exists

* added extensive testing for all the

possible cases on how the save is handled given the various context of the existing room alias
2025-01-22 17:33:43 +00:00

211 lines
9.0 KiB
Swift

//
// Copyright 2022-2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import Combine
import MatrixRustSDK
import SwiftUI
typealias EditRoomAddressScreenViewModelType = StateStoreViewModel<EditRoomAddressScreenViewState, EditRoomAddressScreenViewAction>
class EditRoomAddressScreenViewModel: EditRoomAddressScreenViewModelType, EditRoomAddressScreenViewModelProtocol {
let roomProxy: JoinedRoomProxyProtocol
let clientProxy: ClientProxyProtocol
let userIndicatorController: UserIndicatorControllerProtocol
private let actionsSubject: PassthroughSubject<EditRoomAddressScreenViewModelAction, Never> = .init()
var actionsPublisher: AnyPublisher<EditRoomAddressScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
@CancellableTask private var checkAliasAvailabilityTask: Task<Void, Never>?
init(initialViewState: EditRoomAddressScreenViewState? = nil,
roomProxy: JoinedRoomProxyProtocol,
clientProxy: ClientProxyProtocol,
userIndicatorController: UserIndicatorControllerProtocol) {
self.roomProxy = roomProxy
self.clientProxy = clientProxy
self.userIndicatorController = userIndicatorController
if let initialViewState {
super.init(initialViewState: initialViewState)
} else {
super.init(initialViewState: EditRoomAddressScreenViewState(serverName: clientProxy.userIDServerName ?? ""))
state.currentAliasLocalPart = localPartForMatchingAlias(computeFromDisplayName: false)
state.bindings.desiredAliasLocalPart = localPartForMatchingAlias(computeFromDisplayName: true) ?? ""
}
setupSubscriptions()
}
/// Give priority to aliases from the current user's homeserver as remote ones
/// cannot be edited. If none match then don't fallback and show an empty alias
/// instead so that the user can add one sepecific to this homeserver.
private func localPartForMatchingAlias(computeFromDisplayName: Bool) -> String? {
if let matchingAlias = roomProxy.infoPublisher.value.firstAliasMatching(serverName: clientProxy.userIDServerName, useFallback: false) {
return matchingAlias.aliasLocalPart
}
guard computeFromDisplayName, let displayName = roomProxy.infoPublisher.value.displayName else {
return nil
}
return roomAliasNameFromRoomDisplayName(roomName: displayName)
}
// MARK: - Public
override func process(viewAction: EditRoomAddressScreenViewAction) {
MXLog.info("View model: received view action: \(viewAction)")
switch viewAction {
case .save:
Task { await save() }
case .cancel:
actionsSubject.send(.dismiss)
}
}
private func setupSubscriptions() {
context.$viewState
.map(\.bindings.desiredAliasLocalPart)
.removeDuplicates()
.debounce(for: 0.5, scheduler: DispatchQueue.main)
.sink { [weak self] aliasLocalPart in
guard let self else {
return
}
guard let canonicalAlias = String.makeCanonicalAlias(aliasLocalPart: aliasLocalPart,
serverName: state.serverName) else {
// While is empty don't display the errors, since the save button is already disabled
state.aliasErrors.removeAll()
return
}
if !isRoomAliasFormatValid(alias: canonicalAlias) {
state.aliasErrors.insert(.invalidSymbols)
// If the alias is invalid we don't need to check for availability
state.aliasErrors.remove(.alreadyExists)
checkAliasAvailabilityTask = nil
return
}
state.aliasErrors.remove(.invalidSymbols)
guard aliasLocalPart != state.currentAliasLocalPart else {
// Doesn't make sense to check the availability and display an error if the alias didn't change, the save button should also be disabled
state.aliasErrors.remove(.alreadyExists)
checkAliasAvailabilityTask = nil
return
}
checkAliasAvailabilityTask = Task { [weak self] in
guard let self else {
return
}
if case .success(false) = await self.clientProxy.isAliasAvailable(canonicalAlias) {
guard !Task.isCancelled else { return }
state.aliasErrors.insert(.alreadyExists)
} else {
guard !Task.isCancelled else { return }
state.aliasErrors.remove(.alreadyExists)
}
}
}
.store(in: &cancellables)
}
private func save() async {
showLoadingIndicator()
defer {
hideLoadingIndicator()
}
guard let desiredCanonicalAlias = String.makeCanonicalAlias(aliasLocalPart: state.bindings.desiredAliasLocalPart, serverName: state.serverName),
isRoomAliasFormatValid(alias: desiredCanonicalAlias) else {
state.aliasErrors = [.invalidSymbols]
return
}
switch await clientProxy.isAliasAvailable(desiredCanonicalAlias) {
case .success(true):
break
case .success(false):
state.aliasErrors = [.alreadyExists]
return
case .failure:
userIndicatorController.submitIndicator(.init(title: L10n.errorUnknown))
return
}
let savedAliasFromHomeserver = roomProxy.infoPublisher.value.firstAliasMatching(serverName: state.serverName, useFallback: false)
let savedCanonicalAlias = roomProxy.infoPublisher.value.canonicalAlias
// First publish the desired new alias in the room directory
if case .failure = await roomProxy.publishRoomAliasInRoomDirectory(desiredCanonicalAlias) {
userIndicatorController.submitIndicator(.init(title: L10n.errorUnknown))
return
}
// Then try remove the old alias from the room directory on our current HS
if let savedAliasFromHomeserver {
if case .failure = await roomProxy.removeRoomAliasFromRoomDirectory(savedAliasFromHomeserver) {
userIndicatorController.submitIndicator(.init(title: L10n.errorUnknown))
return
}
}
// Finally update the canonical alias state..
// Allow to update the canonical alias only if the saved canonical alias matches the homeserver or if there is no canonical alias
if savedCanonicalAlias == nil || savedCanonicalAlias?.hasSuffix(state.serverName) == true {
var newAlternativeAliases = roomProxy.infoPublisher.value.alternativeAliases
newAlternativeAliases.removeAll { $0 == savedAliasFromHomeserver }
if case .failure = await roomProxy.updateCanonicalAlias(desiredCanonicalAlias, altAliases: newAlternativeAliases) {
userIndicatorController.submitIndicator(.init(title: L10n.errorUnknown))
return
}
// Otherwise, update the alternative aliases and keep the current canonical alias
} else {
var newAlternativeAliases = roomProxy.infoPublisher.value.alternativeAliases
// We also remove the existing saved alias from our homeserver if exists
newAlternativeAliases.removeAll { $0 == savedAliasFromHomeserver }
newAlternativeAliases.insert(desiredCanonicalAlias, at: 0)
if case .failure = await roomProxy.updateCanonicalAlias(savedCanonicalAlias, altAliases: newAlternativeAliases) {
userIndicatorController.submitIndicator(.init(title: L10n.errorUnknown))
return
}
}
actionsSubject.send(.dismiss)
}
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 String {
var aliasLocalPart: String {
dropFirst().split(separator: ":").first.flatMap(String.init) ?? ""
}
}