mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Add alias to public room creation (#3450)
* added the address section * updated code and strings * syncing name and address * improved code * added a way to reset the state * better documentation * update strings * handling the alias * alias error state * update strings * error handling * improved the error handling * new preview tests, even if they do not work well * improved tests * unit tests * pr comments and using the correct value * fix * pr comments * to improve safety and control of the FF * fixed a test * updated tests * update SDK
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
//
|
||||
|
||||
import Combine
|
||||
import MatrixRustSDK
|
||||
import SwiftUI
|
||||
|
||||
typealias CreateRoomViewModelType = StateStoreViewModel<CreateRoomViewState, CreateRoomViewAction>
|
||||
@@ -15,6 +16,8 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
|
||||
private var createRoomParameters: CreateRoomFlowParameters
|
||||
private let analytics: AnalyticsService
|
||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||
private var syncNameAndAlias = true
|
||||
@CancellableTask private var checkAliasAvailabilityTask: Task<Void, Never>?
|
||||
|
||||
private var actionsSubject: PassthroughSubject<CreateRoomViewModelAction, Never> = .init()
|
||||
|
||||
@@ -35,9 +38,17 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
|
||||
self.analytics = analytics
|
||||
self.userIndicatorController = userIndicatorController
|
||||
|
||||
let bindings = CreateRoomViewStateBindings(roomName: parameters.name, roomTopic: parameters.topic, isRoomPrivate: parameters.isRoomPrivate)
|
||||
let bindings = CreateRoomViewStateBindings(roomTopic: parameters.topic,
|
||||
isRoomPrivate: parameters.isRoomPrivate,
|
||||
isKnockingOnly: appSettings.knockingEnabled ? parameters.isKnockingOnly : false)
|
||||
|
||||
super.init(initialViewState: CreateRoomViewState(isKnockingFeatureEnabled: appSettings.knockingEnabled, selectedUsers: selectedUsers.value, bindings: bindings), mediaProvider: userSession.mediaProvider)
|
||||
super.init(initialViewState: CreateRoomViewState(roomName: parameters.name,
|
||||
serverName: userSession.clientProxy.userIDServerName ?? "",
|
||||
isKnockingFeatureEnabled: appSettings.knockingEnabled,
|
||||
selectedUsers: selectedUsers.value,
|
||||
aliasLocalPart: parameters.aliasLocalPart ?? roomAliasNameFromRoomDisplayName(roomName: parameters.name),
|
||||
bindings: bindings),
|
||||
mediaProvider: userSession.mediaProvider)
|
||||
|
||||
createRoomParameters
|
||||
.map(\.avatarImageMedia)
|
||||
@@ -80,31 +91,111 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
|
||||
actionsSubject.send(.displayMediaPicker)
|
||||
case .removeImage:
|
||||
actionsSubject.send(.removeImage)
|
||||
case .updateAliasLocalPart(let aliasLocalPart):
|
||||
state.aliasLocalPart = aliasLocalPart
|
||||
// If this has been called this means that the user wants a custom address not necessarily reflecting the name
|
||||
// So we disable the two from syncing.
|
||||
syncNameAndAlias = false
|
||||
case .updateRoomName(let name):
|
||||
// Reset the syncing if the name is fully cancelled
|
||||
if name.isEmpty {
|
||||
syncNameAndAlias = true
|
||||
}
|
||||
state.roomName = name
|
||||
if syncNameAndAlias {
|
||||
state.aliasLocalPart = roomAliasNameFromRoomDisplayName(roomName: name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func setupBindings() {
|
||||
// Reset the state related to public rooms if the user choses the room to be empty
|
||||
context.$viewState
|
||||
.dropFirst()
|
||||
.map(\.bindings.isRoomPrivate)
|
||||
.removeDuplicates()
|
||||
.filter { $0 }
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] _ in
|
||||
guard let self else { return }
|
||||
state.bindings.isKnockingOnly = false
|
||||
state.aliasErrors = []
|
||||
state.aliasLocalPart = roomAliasNameFromRoomDisplayName(roomName: state.roomName)
|
||||
syncNameAndAlias = true
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
context.$viewState
|
||||
.map(\.bindings)
|
||||
.throttle(for: 0.5, scheduler: DispatchQueue.main, latest: true)
|
||||
.removeDuplicates { old, new in
|
||||
old.roomName == new.roomName && old.roomTopic == new.roomTopic && old.isRoomPrivate == new.isRoomPrivate
|
||||
old.roomName == new.roomName &&
|
||||
old.bindings.roomTopic == new.bindings.roomTopic &&
|
||||
old.bindings.isRoomPrivate == new.bindings.isRoomPrivate &&
|
||||
old.bindings.isKnockingOnly == new.bindings.isKnockingOnly &&
|
||||
old.aliasLocalPart == new.aliasLocalPart
|
||||
}
|
||||
.sink { [weak self] bindings in
|
||||
.sink { [weak self] state in
|
||||
guard let self else { return }
|
||||
updateParameters(bindings: bindings)
|
||||
updateParameters(state: state)
|
||||
actionsSubject.send(.updateDetails(createRoomParameters))
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
context.$viewState
|
||||
.map(\.aliasLocalPart)
|
||||
.removeDuplicates()
|
||||
.debounce(for: 1, scheduler: DispatchQueue.main)
|
||||
.sink { [weak self] aliasLocalPart in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
guard state.isKnockingFeatureEnabled,
|
||||
!state.bindings.isRoomPrivate,
|
||||
let canonicalAlias = canonicalAlias(aliasLocalPart: aliasLocalPart) else {
|
||||
// While is empty or private room we don't change or display the error
|
||||
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)
|
||||
|
||||
checkAliasAvailabilityTask = Task { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
if case .success(false) = await self.userSession.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 updateParameters(bindings: CreateRoomViewStateBindings) {
|
||||
createRoomParameters.name = bindings.roomName
|
||||
createRoomParameters.topic = bindings.roomTopic
|
||||
createRoomParameters.isRoomPrivate = bindings.isRoomPrivate
|
||||
createRoomParameters.isKnockingOnly = bindings.isKnockingOnly
|
||||
private func updateParameters(state: CreateRoomViewState) {
|
||||
createRoomParameters.name = state.roomName
|
||||
createRoomParameters.topic = state.bindings.roomTopic
|
||||
createRoomParameters.isRoomPrivate = state.bindings.isRoomPrivate
|
||||
createRoomParameters.isKnockingOnly = state.bindings.isKnockingOnly
|
||||
if state.isKnockingFeatureEnabled, !state.aliasLocalPart.isEmpty {
|
||||
createRoomParameters.aliasLocalPart = state.aliasLocalPart
|
||||
} else {
|
||||
createRoomParameters.aliasLocalPart = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func createRoom() async {
|
||||
@@ -114,7 +205,28 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
|
||||
showLoadingIndicator()
|
||||
|
||||
// Since the parameters are throttled, we need to make sure that the latest values are used
|
||||
updateParameters(bindings: state.bindings)
|
||||
updateParameters(state: state)
|
||||
|
||||
// Better to double check the errors also when trying to create the room
|
||||
if state.isKnockingFeatureEnabled, !createRoomParameters.isRoomPrivate {
|
||||
guard let canonicalAlias = canonicalAlias(aliasLocalPart: createRoomParameters.aliasLocalPart),
|
||||
isRoomAliasFormatValid(alias: canonicalAlias) else {
|
||||
state.aliasErrors = [.invalidSymbols]
|
||||
return
|
||||
}
|
||||
|
||||
switch await userSession.clientProxy.isAliasAvailable(canonicalAlias) {
|
||||
case .success(true):
|
||||
break
|
||||
case .success(false):
|
||||
state.aliasErrors = [.alreadyExists]
|
||||
return
|
||||
case .failure:
|
||||
state.bindings.alertInfo = AlertInfo(id: .unknown)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let avatarURL: URL?
|
||||
if let media = createRoomParameters.avatarImageMedia {
|
||||
switch await userSession.clientProxy.uploadMedia(media) {
|
||||
@@ -147,7 +259,8 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
|
||||
// As of right now we don't want to make private rooms with the knock rule
|
||||
isKnockingOnly: createRoomParameters.isRoomPrivate ? false : createRoomParameters.isKnockingOnly,
|
||||
userIDs: state.selectedUsers.map(\.userID),
|
||||
avatarURL: avatarURL) {
|
||||
avatarURL: avatarURL,
|
||||
aliasLocalPart: createRoomParameters.isRoomPrivate ? nil : createRoomParameters.aliasLocalPart) {
|
||||
case .success(let roomId):
|
||||
analytics.trackCreatedRoom(isDM: false)
|
||||
actionsSubject.send(.openRoom(withIdentifier: roomId))
|
||||
@@ -158,6 +271,14 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
|
||||
}
|
||||
}
|
||||
|
||||
func canonicalAlias(aliasLocalPart: String?) -> String? {
|
||||
guard let aliasLocalPart,
|
||||
!aliasLocalPart.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
return "#\(aliasLocalPart):\(state.serverName)"
|
||||
}
|
||||
|
||||
// MARK: Loading indicator
|
||||
|
||||
private static let loadingIndicatorIdentifier = "\(CreateRoomViewModel.self)-Loading"
|
||||
|
||||
Reference in New Issue
Block a user