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:
@@ -7858,7 +7858,7 @@
|
||||
repositoryURL = "https://github.com/element-hq/matrix-rust-components-swift";
|
||||
requirement = {
|
||||
kind = exactVersion;
|
||||
version = 1.0.65;
|
||||
version = 1.0.66;
|
||||
};
|
||||
};
|
||||
701C7BEF8F70F7A83E852DCC /* XCRemoteSwiftPackageReference "GZIP" */ = {
|
||||
|
||||
@@ -149,8 +149,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/element-hq/matrix-rust-components-swift",
|
||||
"state" : {
|
||||
"revision" : "399cc70987856c73e24b8888ac1ecc0eecf1716b",
|
||||
"version" : "1.0.65"
|
||||
"revision" : "902979581ff4f35e54a83cd7c5c340745d6f0d0e",
|
||||
"version" : "1.0.66"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -348,14 +348,16 @@
|
||||
"screen_advanced_settings_element_call_base_url" = "Custom Element Call base URL";
|
||||
"screen_advanced_settings_element_call_base_url_description" = "Set a custom base URL for Element Call.";
|
||||
"screen_advanced_settings_element_call_base_url_validation_error" = "Invalid URL, please make sure you include the protocol (http/https) and the correct address.";
|
||||
"screen_create_room_room_access_section_anyone_option_description" = "Anyone can join this room";
|
||||
"screen_create_room_room_access_section_anyone_option_title" = "Anyone";
|
||||
"screen_create_room_room_access_section_header" = "Room Access";
|
||||
"screen_create_room_room_access_section_knocking_option_description" = "Anyone can ask to join the room but an administrator or a moderator will have to accept the request";
|
||||
"screen_create_room_room_access_section_knocking_option_title" = "Ask to join";
|
||||
"screen_create_room_room_address_invalid_symbols_error_description" = "Some characters are not allowed. Only letters, digits and the following symbols are supported ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _";
|
||||
"screen_create_room_room_address_not_available_error_description" = "This room address already exists, please try editing the room address field or change the room name";
|
||||
"screen_create_room_room_address_section_footer" = "In order for this room to be visible in the public room directory, you will need a room address.";
|
||||
"screen_create_room_room_address_section_title" = "Room address";
|
||||
"screen_create_room_room_visibility_section_title" = "Room visibility";
|
||||
"screen_create_room_access_section_anyone_option_description" = "Anyone can join this room";
|
||||
"screen_create_room_access_section_anyone_option_title" = "Anyone";
|
||||
"screen_create_room_access_section_header" = "Room Access";
|
||||
"screen_create_room_access_section_knocking_option_description" = "Anyone can ask to join the room but an administrator or a moderator will have to accept the request";
|
||||
"screen_create_room_access_section_knocking_option_title" = "Ask to join";
|
||||
"screen_join_room_cancel_knock_action" = "Cancel request";
|
||||
"screen_join_room_cancel_knock_alert_confirmation" = "Yes, cancel";
|
||||
"screen_join_room_cancel_knock_alert_description" = "Are you sure that you want to cancel your request to join this room?";
|
||||
|
||||
@@ -1093,16 +1093,6 @@ internal enum L10n {
|
||||
internal static var screenCreatePollQuestionHint: String { return L10n.tr("Localizable", "screen_create_poll_question_hint") }
|
||||
/// Create Poll
|
||||
internal static var screenCreatePollTitle: String { return L10n.tr("Localizable", "screen_create_poll_title") }
|
||||
/// Anyone can join this room
|
||||
internal static var screenCreateRoomAccessSectionAnyoneOptionDescription: String { return L10n.tr("Localizable", "screen_create_room_access_section_anyone_option_description") }
|
||||
/// Anyone
|
||||
internal static var screenCreateRoomAccessSectionAnyoneOptionTitle: String { return L10n.tr("Localizable", "screen_create_room_access_section_anyone_option_title") }
|
||||
/// Room Access
|
||||
internal static var screenCreateRoomAccessSectionHeader: String { return L10n.tr("Localizable", "screen_create_room_access_section_header") }
|
||||
/// Anyone can ask to join the room but an administrator or a moderator will have to accept the request
|
||||
internal static var screenCreateRoomAccessSectionKnockingOptionDescription: String { return L10n.tr("Localizable", "screen_create_room_access_section_knocking_option_description") }
|
||||
/// Ask to join
|
||||
internal static var screenCreateRoomAccessSectionKnockingOptionTitle: String { return L10n.tr("Localizable", "screen_create_room_access_section_knocking_option_title") }
|
||||
/// New room
|
||||
internal static var screenCreateRoomActionCreateRoom: String { return L10n.tr("Localizable", "screen_create_room_action_create_room") }
|
||||
/// Invite people
|
||||
@@ -1118,6 +1108,20 @@ internal enum L10n {
|
||||
internal static var screenCreateRoomPublicOptionDescription: String { return L10n.tr("Localizable", "screen_create_room_public_option_description") }
|
||||
/// Public room
|
||||
internal static var screenCreateRoomPublicOptionTitle: String { return L10n.tr("Localizable", "screen_create_room_public_option_title") }
|
||||
/// Anyone can join this room
|
||||
internal static var screenCreateRoomRoomAccessSectionAnyoneOptionDescription: String { return L10n.tr("Localizable", "screen_create_room_room_access_section_anyone_option_description") }
|
||||
/// Anyone
|
||||
internal static var screenCreateRoomRoomAccessSectionAnyoneOptionTitle: String { return L10n.tr("Localizable", "screen_create_room_room_access_section_anyone_option_title") }
|
||||
/// Room Access
|
||||
internal static var screenCreateRoomRoomAccessSectionHeader: String { return L10n.tr("Localizable", "screen_create_room_room_access_section_header") }
|
||||
/// Anyone can ask to join the room but an administrator or a moderator will have to accept the request
|
||||
internal static var screenCreateRoomRoomAccessSectionKnockingOptionDescription: String { return L10n.tr("Localizable", "screen_create_room_room_access_section_knocking_option_description") }
|
||||
/// Ask to join
|
||||
internal static var screenCreateRoomRoomAccessSectionKnockingOptionTitle: String { return L10n.tr("Localizable", "screen_create_room_room_access_section_knocking_option_title") }
|
||||
/// Some characters are not allowed. Only letters, digits and the following symbols are supported ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _
|
||||
internal static var screenCreateRoomRoomAddressInvalidSymbolsErrorDescription: String { return L10n.tr("Localizable", "screen_create_room_room_address_invalid_symbols_error_description") }
|
||||
/// This room address already exists, please try editing the room address field or change the room name
|
||||
internal static var screenCreateRoomRoomAddressNotAvailableErrorDescription: String { return L10n.tr("Localizable", "screen_create_room_room_address_not_available_error_description") }
|
||||
/// In order for this room to be visible in the public room directory, you will need a room address.
|
||||
internal static var screenCreateRoomRoomAddressSectionFooter: String { return L10n.tr("Localizable", "screen_create_room_room_address_section_footer") }
|
||||
/// Room address
|
||||
|
||||
@@ -9,6 +9,8 @@ import Combine
|
||||
import Foundation
|
||||
|
||||
struct ClientProxyMockConfiguration {
|
||||
var homeserver = ""
|
||||
var userIDServerName: String?
|
||||
var userID: String = RoomMemberProxyMock.mockMe.userID
|
||||
var deviceID: String?
|
||||
var roomSummaryProvider: RoomSummaryProviderProtocol? = RoomSummaryProviderMock(.init())
|
||||
@@ -28,7 +30,8 @@ extension ClientProxyMock {
|
||||
userID = configuration.userID
|
||||
deviceID = configuration.deviceID
|
||||
|
||||
homeserver = ""
|
||||
homeserver = configuration.homeserver
|
||||
userIDServerName = configuration.userIDServerName
|
||||
|
||||
roomSummaryProvider = configuration.roomSummaryProvider
|
||||
alternateRoomSummaryProvider = RoomSummaryProviderMock(.init())
|
||||
@@ -52,13 +55,14 @@ extension ClientProxyMock {
|
||||
canDeactivateAccount = false
|
||||
directRoomForUserIDReturnValue = .failure(.sdkError(ClientProxyMockError.generic))
|
||||
createDirectRoomWithExpectedRoomNameReturnValue = .failure(.sdkError(ClientProxyMockError.generic))
|
||||
createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLReturnValue = .failure(.sdkError(ClientProxyMockError.generic))
|
||||
createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartReturnValue = .failure(.sdkError(ClientProxyMockError.generic))
|
||||
uploadMediaReturnValue = .failure(.sdkError(ClientProxyMockError.generic))
|
||||
loadUserDisplayNameReturnValue = .failure(.sdkError(ClientProxyMockError.generic))
|
||||
setUserDisplayNameReturnValue = .failure(.sdkError(ClientProxyMockError.generic))
|
||||
loadUserAvatarURLReturnValue = .failure(.sdkError(ClientProxyMockError.generic))
|
||||
setUserAvatarMediaReturnValue = .failure(.sdkError(ClientProxyMockError.generic))
|
||||
removeUserAvatarReturnValue = .failure(.sdkError(ClientProxyMockError.generic))
|
||||
isAliasAvailableReturnValue = .success(true)
|
||||
logoutReturnValue = nil
|
||||
searchUsersSearchTermLimitReturnValue = .success(.init(results: [], limited: false))
|
||||
profileForReturnValue = .success(.init(userID: "@a:b.com", displayName: "Some user"))
|
||||
|
||||
@@ -2628,15 +2628,15 @@ class ClientProxyMock: ClientProxyProtocol {
|
||||
}
|
||||
//MARK: - createRoom
|
||||
|
||||
var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingCallsCount = 0
|
||||
var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCallsCount: Int {
|
||||
var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartUnderlyingCallsCount = 0
|
||||
var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingCallsCount
|
||||
return createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingCallsCount
|
||||
returnValue = createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
@@ -2644,29 +2644,29 @@ class ClientProxyMock: ClientProxyProtocol {
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingCallsCount = newValue
|
||||
createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingCallsCount = newValue
|
||||
createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCalled: Bool {
|
||||
return createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCallsCount > 0
|
||||
var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartCalled: Bool {
|
||||
return createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartCallsCount > 0
|
||||
}
|
||||
var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLReceivedArguments: (name: String, topic: String?, isRoomPrivate: Bool, isKnockingOnly: Bool, userIDs: [String], avatarURL: URL?)?
|
||||
var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLReceivedInvocations: [(name: String, topic: String?, isRoomPrivate: Bool, isKnockingOnly: Bool, userIDs: [String], avatarURL: URL?)] = []
|
||||
var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartReceivedArguments: (name: String, topic: String?, isRoomPrivate: Bool, isKnockingOnly: Bool, userIDs: [String], avatarURL: URL?, aliasLocalPart: String?)?
|
||||
var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartReceivedInvocations: [(name: String, topic: String?, isRoomPrivate: Bool, isKnockingOnly: Bool, userIDs: [String], avatarURL: URL?, aliasLocalPart: String?)] = []
|
||||
|
||||
var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingReturnValue: Result<String, ClientProxyError>!
|
||||
var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLReturnValue: Result<String, ClientProxyError>! {
|
||||
var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartUnderlyingReturnValue: Result<String, ClientProxyError>!
|
||||
var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartReturnValue: Result<String, ClientProxyError>! {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingReturnValue
|
||||
return createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: Result<String, ClientProxyError>? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingReturnValue
|
||||
returnValue = createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartUnderlyingReturnValue
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
@@ -2674,26 +2674,26 @@ class ClientProxyMock: ClientProxyProtocol {
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingReturnValue = newValue
|
||||
createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartUnderlyingReturnValue = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingReturnValue = newValue
|
||||
createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartUnderlyingReturnValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLClosure: ((String, String?, Bool, Bool, [String], URL?) async -> Result<String, ClientProxyError>)?
|
||||
var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartClosure: ((String, String?, Bool, Bool, [String], URL?, String?) async -> Result<String, ClientProxyError>)?
|
||||
|
||||
func createRoom(name: String, topic: String?, isRoomPrivate: Bool, isKnockingOnly: Bool, userIDs: [String], avatarURL: URL?) async -> Result<String, ClientProxyError> {
|
||||
createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCallsCount += 1
|
||||
createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLReceivedArguments = (name: name, topic: topic, isRoomPrivate: isRoomPrivate, isKnockingOnly: isKnockingOnly, userIDs: userIDs, avatarURL: avatarURL)
|
||||
func createRoom(name: String, topic: String?, isRoomPrivate: Bool, isKnockingOnly: Bool, userIDs: [String], avatarURL: URL?, aliasLocalPart: String?) async -> Result<String, ClientProxyError> {
|
||||
createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartCallsCount += 1
|
||||
createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartReceivedArguments = (name: name, topic: topic, isRoomPrivate: isRoomPrivate, isKnockingOnly: isKnockingOnly, userIDs: userIDs, avatarURL: avatarURL, aliasLocalPart: aliasLocalPart)
|
||||
DispatchQueue.main.async {
|
||||
self.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLReceivedInvocations.append((name: name, topic: topic, isRoomPrivate: isRoomPrivate, isKnockingOnly: isKnockingOnly, userIDs: userIDs, avatarURL: avatarURL))
|
||||
self.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartReceivedInvocations.append((name: name, topic: topic, isRoomPrivate: isRoomPrivate, isKnockingOnly: isKnockingOnly, userIDs: userIDs, avatarURL: avatarURL, aliasLocalPart: aliasLocalPart))
|
||||
}
|
||||
if let createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLClosure = createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLClosure {
|
||||
return await createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLClosure(name, topic, isRoomPrivate, isKnockingOnly, userIDs, avatarURL)
|
||||
if let createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartClosure = createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartClosure {
|
||||
return await createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartClosure(name, topic, isRoomPrivate, isKnockingOnly, userIDs, avatarURL, aliasLocalPart)
|
||||
} else {
|
||||
return createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLReturnValue
|
||||
return createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - joinRoom
|
||||
@@ -3973,6 +3973,76 @@ class ClientProxyMock: ClientProxyProtocol {
|
||||
return resolveRoomAliasReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - isAliasAvailable
|
||||
|
||||
var isAliasAvailableUnderlyingCallsCount = 0
|
||||
var isAliasAvailableCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return isAliasAvailableUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = isAliasAvailableUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
isAliasAvailableUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
isAliasAvailableUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var isAliasAvailableCalled: Bool {
|
||||
return isAliasAvailableCallsCount > 0
|
||||
}
|
||||
var isAliasAvailableReceivedAlias: String?
|
||||
var isAliasAvailableReceivedInvocations: [String] = []
|
||||
|
||||
var isAliasAvailableUnderlyingReturnValue: Result<Bool, ClientProxyError>!
|
||||
var isAliasAvailableReturnValue: Result<Bool, ClientProxyError>! {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return isAliasAvailableUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: Result<Bool, ClientProxyError>? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = isAliasAvailableUnderlyingReturnValue
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
isAliasAvailableUnderlyingReturnValue = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
isAliasAvailableUnderlyingReturnValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var isAliasAvailableClosure: ((String) async -> Result<Bool, ClientProxyError>)?
|
||||
|
||||
func isAliasAvailable(_ alias: String) async -> Result<Bool, ClientProxyError> {
|
||||
isAliasAvailableCallsCount += 1
|
||||
isAliasAvailableReceivedAlias = alias
|
||||
DispatchQueue.main.async {
|
||||
self.isAliasAvailableReceivedInvocations.append(alias)
|
||||
}
|
||||
if let isAliasAvailableClosure = isAliasAvailableClosure {
|
||||
return await isAliasAvailableClosure(alias)
|
||||
} else {
|
||||
return isAliasAvailableReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - getElementWellKnown
|
||||
|
||||
var getElementWellKnownUnderlyingCallsCount = 0
|
||||
|
||||
@@ -25,20 +25,32 @@ enum CreateRoomViewModelAction {
|
||||
}
|
||||
|
||||
struct CreateRoomViewState: BindableState {
|
||||
var roomName: String
|
||||
let serverName: String
|
||||
let isKnockingFeatureEnabled: Bool
|
||||
var selectedUsers: [UserProfileProxy]
|
||||
var aliasLocalPart: String
|
||||
var bindings: CreateRoomViewStateBindings
|
||||
var avatarURL: URL?
|
||||
var canCreateRoom: Bool {
|
||||
!bindings.roomName.isEmpty
|
||||
!roomName.isEmpty && aliasErrors.isEmpty
|
||||
}
|
||||
|
||||
var aliasErrors: Set<CreateRoomAliasErrorState> = []
|
||||
var aliasErrorDescription: String? {
|
||||
if aliasErrors.contains(.alreadyExists) {
|
||||
return L10n.screenCreateRoomRoomAddressNotAvailableErrorDescription
|
||||
} else if aliasErrors.contains(.invalidSymbols) {
|
||||
return L10n.screenCreateRoomRoomAddressInvalidSymbolsErrorDescription
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
struct CreateRoomViewStateBindings {
|
||||
var roomName: String
|
||||
var roomTopic: String
|
||||
var isRoomPrivate: Bool
|
||||
var isKnockingOnly = false
|
||||
var isKnockingOnly: Bool
|
||||
var showAttachmentConfirmationDialog = false
|
||||
|
||||
/// Information describing the currently displayed alert.
|
||||
@@ -51,4 +63,11 @@ enum CreateRoomViewAction {
|
||||
case displayCameraPicker
|
||||
case displayMediaPicker
|
||||
case removeImage
|
||||
case updateRoomName(String)
|
||||
case updateAliasLocalPart(String)
|
||||
}
|
||||
|
||||
enum CreateRoomAliasErrorState {
|
||||
case alreadyExists
|
||||
case invalidSymbols
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -16,7 +16,23 @@ struct CreateRoomScreen: View {
|
||||
case name
|
||||
case topic
|
||||
}
|
||||
|
||||
|
||||
private var aliasBinding: Binding<String> {
|
||||
.init(get: {
|
||||
context.viewState.aliasLocalPart
|
||||
}, set: {
|
||||
context.send(viewAction: .updateAliasLocalPart($0))
|
||||
})
|
||||
}
|
||||
|
||||
private var roomNameBinding: Binding<String> {
|
||||
.init(get: {
|
||||
context.viewState.roomName
|
||||
}, set: {
|
||||
context.send(viewAction: .updateRoomName($0))
|
||||
})
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
roomSection
|
||||
@@ -25,6 +41,7 @@ struct CreateRoomScreen: View {
|
||||
if context.viewState.isKnockingFeatureEnabled,
|
||||
!context.isRoomPrivate {
|
||||
roomAccessSection
|
||||
roomAliasSection
|
||||
}
|
||||
}
|
||||
.compoundList()
|
||||
@@ -48,7 +65,7 @@ struct CreateRoomScreen: View {
|
||||
.compoundListSectionHeader()
|
||||
|
||||
TextField(L10n.screenCreateRoomRoomNameLabel,
|
||||
text: $context.roomName,
|
||||
text: roomNameBinding,
|
||||
prompt: Text(L10n.commonRoomNamePlaceholder).foregroundColor(.compound.textPlaceholder),
|
||||
axis: .horizontal)
|
||||
.focused($focus, equals: .name)
|
||||
@@ -150,25 +167,65 @@ struct CreateRoomScreen: View {
|
||||
iconAlignment: .top),
|
||||
kind: .selection(isSelected: !context.isRoomPrivate) { context.isRoomPrivate = false })
|
||||
} header: {
|
||||
Text(L10n.commonSecurity.uppercased())
|
||||
Text(L10n.screenCreateRoomRoomVisibilitySectionTitle)
|
||||
.compoundListSectionHeader()
|
||||
}
|
||||
}
|
||||
|
||||
private var roomAccessSection: some View {
|
||||
Section {
|
||||
ListRow(label: .plain(title: L10n.screenCreateRoomAccessSectionAnyoneOptionTitle,
|
||||
description: L10n.screenCreateRoomAccessSectionAnyoneOptionDescription),
|
||||
ListRow(label: .plain(title: L10n.screenCreateRoomRoomAccessSectionAnyoneOptionTitle,
|
||||
description: L10n.screenCreateRoomRoomAccessSectionAnyoneOptionDescription),
|
||||
kind: .selection(isSelected: !context.isKnockingOnly) { context.isKnockingOnly = false })
|
||||
ListRow(label: .plain(title: L10n.screenCreateRoomAccessSectionKnockingOptionTitle,
|
||||
description: L10n.screenCreateRoomAccessSectionKnockingOptionDescription),
|
||||
ListRow(label: .plain(title: L10n.screenCreateRoomRoomAccessSectionKnockingOptionTitle,
|
||||
description: L10n.screenCreateRoomRoomAccessSectionKnockingOptionDescription),
|
||||
kind: .selection(isSelected: context.isKnockingOnly) { context.isKnockingOnly = true })
|
||||
} header: {
|
||||
Text(L10n.screenCreateRoomAccessSectionHeader.uppercased())
|
||||
Text(L10n.screenCreateRoomRoomAccessSectionHeader)
|
||||
.compoundListSectionHeader()
|
||||
}
|
||||
}
|
||||
|
||||
private var roomAliasSection: some View {
|
||||
Section {
|
||||
ListRow(kind: .custom {
|
||||
HStack(spacing: 0) {
|
||||
Text("#")
|
||||
.font(.compound.bodyLG)
|
||||
.foregroundStyle(.compound.textSecondary)
|
||||
|
||||
TextField("", text: aliasBinding)
|
||||
.autocapitalization(.none)
|
||||
.textCase(.lowercase)
|
||||
.tint(.compound.iconAccentTertiary)
|
||||
.font(.compound.bodyLG)
|
||||
.foregroundStyle(.compound.textPrimary)
|
||||
.padding(.horizontal, 8)
|
||||
Text(":\(context.viewState.serverName)")
|
||||
.font(.compound.bodyLG)
|
||||
.foregroundStyle(.compound.textSecondary)
|
||||
}
|
||||
.padding(ListRowPadding.textFieldInsets)
|
||||
.environment(\.layoutDirection, .leftToRight)
|
||||
.errorBackground(!context.viewState.aliasErrors.isEmpty)
|
||||
})
|
||||
} header: {
|
||||
Text(L10n.screenCreateRoomRoomAddressSectionTitle)
|
||||
.compoundListSectionHeader()
|
||||
} footer: {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
if let errorDescription = context.viewState.aliasErrorDescription {
|
||||
Label(errorDescription, icon: \.error, iconSize: .xSmall, relativeTo: .compound.bodySM)
|
||||
.foregroundStyle(.compound.textCriticalPrimary)
|
||||
.font(.compound.bodySM)
|
||||
}
|
||||
Text(L10n.screenCreateRoomRoomAddressSectionFooter)
|
||||
.compoundListSectionFooter()
|
||||
.font(.compound.bodySM)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var toolbar: some ToolbarContent {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button(L10n.actionCreate) {
|
||||
@@ -180,6 +237,15 @@ struct CreateRoomScreen: View {
|
||||
}
|
||||
}
|
||||
|
||||
private extension View {
|
||||
func errorBackground(_ shouldDisplay: Bool) -> some View {
|
||||
listRowBackground(shouldDisplay ? AnyView(RoundedRectangle(cornerRadius: 10)
|
||||
.inset(by: 1)
|
||||
.fill(.compound.bgCriticalSubtleHovered)
|
||||
.stroke(Color.compound.borderCriticalPrimary)) : AnyView(Color.compound.bgCanvasDefaultLevel1))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct CreateRoom_Previews: PreviewProvider, TestablePreview {
|
||||
@@ -208,7 +274,7 @@ struct CreateRoom_Previews: PreviewProvider, TestablePreview {
|
||||
}()
|
||||
|
||||
static let publicRoomViewModel = {
|
||||
let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: "@userid:example.com"))))
|
||||
let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userIDServerName: "example.org", userID: "@userid:example.com"))))
|
||||
let parameters = CreateRoomFlowParameters(isRoomPrivate: false)
|
||||
let selectedUsers: [UserProfileProxy] = [.mockAlice, .mockBob, .mockCharlie]
|
||||
ServiceLocator.shared.settings.knockingEnabled = true
|
||||
@@ -220,6 +286,32 @@ struct CreateRoom_Previews: PreviewProvider, TestablePreview {
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
}()
|
||||
|
||||
static let publicRoomInvalidAliasViewModel = {
|
||||
let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userIDServerName: "example.org", userID: "@userid:example.com"))))
|
||||
let parameters = CreateRoomFlowParameters(isRoomPrivate: false, aliasLocalPart: "#:")
|
||||
ServiceLocator.shared.settings.knockingEnabled = true
|
||||
return CreateRoomViewModel(userSession: userSession,
|
||||
createRoomParameters: .init(parameters),
|
||||
selectedUsers: .init([]),
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
userIndicatorController: UserIndicatorControllerMock(),
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
}()
|
||||
|
||||
static let publicRoomExistingAliasViewModel = {
|
||||
let clientProxy = ClientProxyMock(.init(userIDServerName: "example.org", userID: "@userid:example.com"))
|
||||
clientProxy.isAliasAvailableReturnValue = .success(false)
|
||||
let userSession = UserSessionMock(.init(clientProxy: clientProxy))
|
||||
let parameters = CreateRoomFlowParameters(isRoomPrivate: false, aliasLocalPart: "existing")
|
||||
ServiceLocator.shared.settings.knockingEnabled = true
|
||||
return CreateRoomViewModel(userSession: userSession,
|
||||
createRoomParameters: .init(parameters),
|
||||
selectedUsers: .init([]),
|
||||
analytics: ServiceLocator.shared.analytics,
|
||||
userIndicatorController: UserIndicatorControllerMock(),
|
||||
appSettings: ServiceLocator.shared.settings)
|
||||
}()
|
||||
|
||||
static var previews: some View {
|
||||
NavigationStack {
|
||||
CreateRoomScreen(context: viewModel.context)
|
||||
@@ -233,5 +325,15 @@ struct CreateRoom_Previews: PreviewProvider, TestablePreview {
|
||||
CreateRoomScreen(context: publicRoomViewModel.context)
|
||||
}
|
||||
.previewDisplayName("Create Public Room")
|
||||
NavigationStack {
|
||||
CreateRoomScreen(context: publicRoomInvalidAliasViewModel.context)
|
||||
}
|
||||
.snapshotPreferences(delay: 1.5)
|
||||
.previewDisplayName("Create Public Room, invalid alias")
|
||||
NavigationStack {
|
||||
CreateRoomScreen(context: publicRoomExistingAliasViewModel.context)
|
||||
}
|
||||
.snapshotPreferences(delay: 1.5)
|
||||
.previewDisplayName("Create Public Room, existing alias")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,13 +239,13 @@ struct SecureBackupRecoveryKeyScreen_Previews: PreviewProvider, TestablePreview
|
||||
SecureBackupRecoveryKeyScreen(context: generatingViewModel.context)
|
||||
}
|
||||
.previewDisplayName("Generating")
|
||||
.snapshot(delay: 0.25)
|
||||
.snapshotPreferences(delay: 0.25)
|
||||
|
||||
NavigationStack {
|
||||
SecureBackupRecoveryKeyScreen(context: setupViewModel.context)
|
||||
}
|
||||
.previewDisplayName("Set up")
|
||||
.snapshot(delay: 0.25)
|
||||
.snapshotPreferences(delay: 0.25)
|
||||
|
||||
NavigationStack {
|
||||
SecureBackupRecoveryKeyScreen(context: incompleteViewModel.context)
|
||||
|
||||
@@ -67,6 +67,22 @@ class ClientProxy: ClientProxyProtocol {
|
||||
"org.matrix.msc3401.call.member": Int32(0)
|
||||
])
|
||||
}
|
||||
|
||||
private static var knockingRoomCreationPowerLevelOverrides: PowerLevels {
|
||||
.init(usersDefault: nil,
|
||||
eventsDefault: nil,
|
||||
stateDefault: nil,
|
||||
ban: nil,
|
||||
kick: nil,
|
||||
redact: nil,
|
||||
invite: Int32(50),
|
||||
notifications: nil,
|
||||
users: [:],
|
||||
events: [
|
||||
"m.call.member": Int32(0),
|
||||
"org.matrix.msc3401.call.member": Int32(0)
|
||||
])
|
||||
}
|
||||
|
||||
private var loadCachedAvatarURLTask: Task<Void, Never>?
|
||||
private let userAvatarURLSubject = CurrentValueSubject<URL?, Never>(nil)
|
||||
@@ -377,9 +393,14 @@ class ClientProxy: ClientProxyProtocol {
|
||||
}
|
||||
|
||||
// swiftlint:disable:next function_parameter_count
|
||||
func createRoom(name: String, topic: String?, isRoomPrivate: Bool, isKnockingOnly: Bool, userIDs: [String], avatarURL: URL?) async -> Result<String, ClientProxyError> {
|
||||
func createRoom(name: String,
|
||||
topic: String?,
|
||||
isRoomPrivate: Bool,
|
||||
isKnockingOnly: Bool,
|
||||
userIDs: [String],
|
||||
avatarURL: URL?,
|
||||
aliasLocalPart: String?) async -> Result<String, ClientProxyError> {
|
||||
do {
|
||||
// TODO: Revisit once the SDK supports the knocking API
|
||||
let parameters = CreateRoomParameters(name: name,
|
||||
topic: topic,
|
||||
isEncrypted: isRoomPrivate,
|
||||
@@ -388,7 +409,9 @@ class ClientProxy: ClientProxyProtocol {
|
||||
preset: isRoomPrivate ? .privateChat : .publicChat,
|
||||
invite: userIDs,
|
||||
avatar: avatarURL?.absoluteString,
|
||||
powerLevelContentOverride: Self.roomCreationPowerLevelOverrides)
|
||||
powerLevelContentOverride: isKnockingOnly ? Self.knockingRoomCreationPowerLevelOverrides : Self.roomCreationPowerLevelOverrides,
|
||||
// This is an FFI naming mistake, what is required is the `aliasLocalPart` not the whole alias
|
||||
canonicalAlias: aliasLocalPart)
|
||||
let roomID = try await client.createRoom(request: parameters)
|
||||
|
||||
await waitForRoomToSync(roomID: roomID)
|
||||
@@ -626,6 +649,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
func resolveRoomAlias(_ alias: String) async -> Result<ResolvedRoomAlias, ClientProxyError> {
|
||||
do {
|
||||
guard let resolvedAlias = try await client.resolveRoomAlias(roomAlias: alias) else {
|
||||
MXLog.error("Failed resolving room alias, is nil")
|
||||
return .failure(.failedResolvingRoomAlias)
|
||||
}
|
||||
|
||||
@@ -641,6 +665,16 @@ class ClientProxy: ClientProxyProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
func isAliasAvailable(_ alias: String) async -> Result<Bool, ClientProxyError> {
|
||||
do {
|
||||
let result = try await client.isRoomAliasAvailable(alias: alias)
|
||||
return .success(result)
|
||||
} catch {
|
||||
MXLog.error("Failed checking if alias: \(alias) is available with error: \(error)")
|
||||
return .failure(.sdkError(error))
|
||||
}
|
||||
}
|
||||
|
||||
func getElementWellKnown() async -> Result<ElementWellKnown?, ClientProxyError> {
|
||||
await client.getElementWellKnown().map(ElementWellKnown.init)
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
|
||||
var deviceID: String? { get }
|
||||
|
||||
var homeserver: String { get }
|
||||
|
||||
|
||||
var slidingSyncVersion: SlidingSyncVersion { get }
|
||||
var availableSlidingSyncVersions: [SlidingSyncVersion] { get async }
|
||||
|
||||
@@ -133,7 +133,13 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
|
||||
func createDirectRoom(with userID: String, expectedRoomName: String?) async -> Result<String, ClientProxyError>
|
||||
|
||||
// swiftlint:disable:next function_parameter_count
|
||||
func createRoom(name: String, topic: String?, isRoomPrivate: Bool, isKnockingOnly: Bool, userIDs: [String], avatarURL: URL?) async -> Result<String, ClientProxyError>
|
||||
func createRoom(name: String,
|
||||
topic: String?,
|
||||
isRoomPrivate: Bool,
|
||||
isKnockingOnly: Bool,
|
||||
userIDs: [String],
|
||||
avatarURL: URL?,
|
||||
aliasLocalPart: String?) async -> Result<String, ClientProxyError>
|
||||
|
||||
func joinRoom(_ roomID: String, via: [String]) async -> Result<Void, ClientProxyError>
|
||||
|
||||
@@ -173,6 +179,8 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
|
||||
|
||||
func resolveRoomAlias(_ alias: String) async -> Result<ResolvedRoomAlias, ClientProxyError>
|
||||
|
||||
func isAliasAvailable(_ alias: String) async -> Result<Bool, ClientProxyError>
|
||||
|
||||
func getElementWellKnown() async -> Result<ElementWellKnown?, ClientProxyError>
|
||||
|
||||
// MARK: - Ignored users
|
||||
|
||||
@@ -14,4 +14,5 @@ struct CreateRoomFlowParameters {
|
||||
var isRoomPrivate = true
|
||||
var isKnockingOnly = false
|
||||
var avatarImageMedia: MediaInfo?
|
||||
var aliasLocalPart: String?
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9650c6b430799996d02b0d34cf9d8657c94a5b5627dde3cb74755b5020619cf6
|
||||
size 202009
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7563d6820945006a48db647fed692d4458338d63669ea4c344052f5a63a4a2ee
|
||||
size 204578
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d3a984e4fc4d1aade45c5e8b98dcba2f1ecc0e50965e2ca64ad6faad777de26c
|
||||
size 172668
|
||||
oid sha256:6f630b2ae196d0f8d9b98b68482863ea78aa84eb5e9b82e78d77430ec81dc007
|
||||
size 194309
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:49a518ac1fe0ffd8719d757b5c5f2decde92b237937f68df15a239c228cf24b8
|
||||
size 144080
|
||||
oid sha256:860a4367f4ab161be805df36b2723829e023db753c45f9f49ecc126f6a2cc128
|
||||
size 145345
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:467d13af04d60db15fb1586df63b7589a59ae6f2cb4c7482b79cc0c56d7af0ce
|
||||
size 143822
|
||||
oid sha256:b0a28cc070627724c44111ee0736006650e1453d85be6038783073d911f3d909
|
||||
size 144730
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f255ed8f26b1f9efd108b7b0422262185f45152495727578822fb0691bfe08a9
|
||||
size 228341
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b42242042ad3adf3b953a29ff58792511076930892a7bda4d813b3e3218f39e0
|
||||
size 226407
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0f493e2b0a85be6d277f7138a11f0f02510b04df3fc22a8c76391fbc3444abd0
|
||||
size 208736
|
||||
oid sha256:a6f47df77fca87c83e7668b95a500c5b7d0a0640d694a7b92c58365573d0abd6
|
||||
size 224351
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c03a493641d0f7c21680bec4ccea98428f210d642011feb3d61988aa09af2612
|
||||
size 168502
|
||||
oid sha256:b9311de66c92307e15ac5d62c025c953d20ce42e946eaaab30604f754ce6d395
|
||||
size 171925
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1b8555d5ddea49cf85fc2fc9cb8981f9453f30152f8befc4d596358a7c79f63d
|
||||
size 169943
|
||||
oid sha256:662701144b88061103f80d08c078a28ca918e768e4907e4100fd56941098acfa
|
||||
size 173078
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:acf39359c47be4101e19fe3d0bfcedb7f0a262b418f3ab3f90da38c9ad3b88f0
|
||||
size 130131
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:66610bab01ad7ed399d8c4f72fd8d04990eafbf911c8b666c3ca21beba88a58b
|
||||
size 128602
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7f54b4053b937e1fc6cee22dfff905bead3ccfbd45bf6607fb8492c74dda15fd
|
||||
size 118717
|
||||
oid sha256:311d57b9b2cc2ad3b459c5817e6dd605740abef4559b571492de22a81b160285
|
||||
size 126941
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:133221a0b8d4b9aed307a19043b323ca99c3cf639b2097c3713209c6c69c70e5
|
||||
size 94718
|
||||
oid sha256:d637d85d789f0d3d8756719f0b277a23b07730a21d46fe599afdb4b1143ce814
|
||||
size 95980
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5c99bf8cd5ba59b6343a3e61af3f2b45ebbe3cab0dda38ef6aa45750b7c13558
|
||||
size 94927
|
||||
oid sha256:c89527ca06fb7a7b03cb6bb2feda23cd9325e98378eecc364f537c29d6878b08
|
||||
size 96262
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d191b67a8b246e26c7567427f09cd7562fc88402b06a150212a74eb55fb58831
|
||||
size 164266
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d191b67a8b246e26c7567427f09cd7562fc88402b06a150212a74eb55fb58831
|
||||
size 164266
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2155e204f2bf31a62cc09e3656e77fa58ee327a1ba37de0a307ba7e4fdd9a836
|
||||
size 162088
|
||||
oid sha256:d191b67a8b246e26c7567427f09cd7562fc88402b06a150212a74eb55fb58831
|
||||
size 164266
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:608dbe5519a764e6a9dd8c1d909b5d53ec969d34a03251cdedaabdc1508d661a
|
||||
size 124622
|
||||
oid sha256:61ca01f8203d3c668ca2183f46d2954a32284624c8b7caaffd37124e2d454997
|
||||
size 126803
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8f5dd8e86929e05e9a30b27fe8895668246da009696ac614f8f4fbf4bf78d0d8
|
||||
size 125070
|
||||
oid sha256:ab43faa0b020b9628d10999a93d9c70e90e007eb788ce9a14eaca5330b9c5843
|
||||
size 127313
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2b4eb7771b9156b75c3812751b1219ba337e4d9b1b465dedd09e7784b1f009e9
|
||||
size 120296
|
||||
oid sha256:02851bc8304737c8eee1ea76ddee133ec47feb49e0471b8e6c1213696851e6e4
|
||||
size 140058
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e80a71b9eaeb41bd700cf22e98b0290c06e6786cdffea72c0973549b4ba5bd43
|
||||
size 141940
|
||||
oid sha256:b694da58f6895e51248e8368a2baf1f7b9c4eef3119533ea2eca3c13407412d5
|
||||
size 171446
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:872d515fe6aae36c12d21a3e6757abcddf9e03e51e6421324505a528b5946376
|
||||
size 74557
|
||||
oid sha256:bb8bc4cd519ee62becb550537da7bf15566b178330101c21c839d9d93bbd34ed
|
||||
size 95875
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:88b07c92a3f9763eb361918da508af0e140bce7241ebbc92c38f3ac38cc9394d
|
||||
size 100293
|
||||
oid sha256:8db80a8ad96649272dac60a2a95aade580a6b5b09cbb79c49e81d210d41b4f0c
|
||||
size 121490
|
||||
|
||||
@@ -25,7 +25,7 @@ class CreateRoomScreenViewModelTests: XCTestCase {
|
||||
|
||||
override func setUpWithError() throws {
|
||||
cancellables.removeAll()
|
||||
clientProxy = ClientProxyMock(.init(userID: "@a:b.com"))
|
||||
clientProxy = ClientProxyMock(.init(userIDServerName: "matrix.org", userID: "@a:b.com"))
|
||||
userSession = UserSessionMock(.init(clientProxy: clientProxy))
|
||||
let parameters = CreateRoomFlowParameters()
|
||||
usersSubject.send([.mockAlice, .mockBob, .mockCharlie])
|
||||
@@ -68,21 +68,25 @@ class CreateRoomScreenViewModelTests: XCTestCase {
|
||||
|
||||
func testCreateRoomRequirements() {
|
||||
XCTAssertFalse(context.viewState.canCreateRoom)
|
||||
context.roomName = "A"
|
||||
context.send(viewAction: .updateRoomName("A"))
|
||||
XCTAssertTrue(context.viewState.canCreateRoom)
|
||||
}
|
||||
|
||||
func testCreateKnockingRoom() async {
|
||||
context.roomName = "A"
|
||||
context.send(viewAction: .updateRoomName("A"))
|
||||
context.roomTopic = "B"
|
||||
context.isRoomPrivate = false
|
||||
// When setting the room as private we always reset the knocking state to the default value of false
|
||||
// so we need to wait a main actor cycle to ensure the view state is updated
|
||||
await Task.yield()
|
||||
context.isKnockingOnly = true
|
||||
XCTAssertTrue(context.viewState.canCreateRoom)
|
||||
|
||||
let expectation = expectation(description: "Wait for the room to be created")
|
||||
clientProxy.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLClosure = { _, _, isPrivate, isKnockingOnly, _, _ in
|
||||
clientProxy.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartClosure = { _, _, isPrivate, isKnockingOnly, _, _, localAliasPart in
|
||||
XCTAssertTrue(isKnockingOnly)
|
||||
XCTAssertFalse(isPrivate)
|
||||
XCTAssertEqual(localAliasPart, "a")
|
||||
defer { expectation.fulfill() }
|
||||
return .success("")
|
||||
}
|
||||
@@ -90,14 +94,67 @@ class CreateRoomScreenViewModelTests: XCTestCase {
|
||||
await fulfillment(of: [expectation])
|
||||
}
|
||||
|
||||
func testCreatePublicRoomFailsForInvalidAlias() async throws {
|
||||
context.send(viewAction: .updateRoomName("A"))
|
||||
context.roomTopic = "B"
|
||||
context.isRoomPrivate = false
|
||||
// When setting the room as private we always reset the alias
|
||||
// so we need to wait a main actor cycle to ensure the view state is updated
|
||||
await Task.yield()
|
||||
|
||||
// we wait for the debounce to show the error
|
||||
let deferred = deferFulfillment(context.$viewState) { viewState in
|
||||
viewState.aliasErrors.contains(.invalidSymbols) && !viewState.canCreateRoom
|
||||
}
|
||||
context.send(viewAction: .updateAliasLocalPart("#:"))
|
||||
try await deferred.fulfill()
|
||||
|
||||
// We also want to force the room creation in case the user may tap the button before the debounce
|
||||
// blocked it
|
||||
context.send(viewAction: .createRoom)
|
||||
await Task.yield()
|
||||
XCTAssertFalse(clientProxy.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartCalled)
|
||||
}
|
||||
|
||||
func testCreatePublicRoomFailsForExistingAlias() async throws {
|
||||
clientProxy.isAliasAvailableReturnValue = .success(false)
|
||||
context.send(viewAction: .updateRoomName("A"))
|
||||
context.roomTopic = "B"
|
||||
context.isRoomPrivate = false
|
||||
// When setting the room as private we always reset the alias
|
||||
// so we need to wait a main actor cycle to ensure the view state is updated
|
||||
await Task.yield()
|
||||
|
||||
// we wait for the debounce to show the error
|
||||
let deferred = deferFulfillment(context.$viewState) { viewState in
|
||||
viewState.aliasErrors.contains(.alreadyExists) && !viewState.canCreateRoom
|
||||
}
|
||||
context.send(viewAction: .updateAliasLocalPart("abc"))
|
||||
try await deferred.fulfill()
|
||||
|
||||
// We also want to force the room creation in case the user may tap the button before the debounce
|
||||
// blocked it
|
||||
let expectation = expectation(description: "Wait for the alias to be checked again")
|
||||
clientProxy.isAliasAvailableClosure = { _ in
|
||||
defer {
|
||||
expectation.fulfill()
|
||||
}
|
||||
return .success(false)
|
||||
}
|
||||
context.send(viewAction: .createRoom)
|
||||
await fulfillment(of: [expectation])
|
||||
XCTAssertEqual(clientProxy.isAliasAvailableCallsCount, 2)
|
||||
XCTAssertFalse(clientProxy.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartCalled)
|
||||
}
|
||||
|
||||
func testCreatePrivateRoomCantHaveKnockRule() async {
|
||||
context.roomName = "A"
|
||||
context.send(viewAction: .updateRoomName("A"))
|
||||
context.roomTopic = "B"
|
||||
context.isRoomPrivate = true
|
||||
context.isKnockingOnly = true
|
||||
context.send(viewAction: .createRoom)
|
||||
let expectation = expectation(description: "Wait for the room to be created")
|
||||
clientProxy.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLClosure = { _, _, isPrivate, isKnockingOnly, _, _ in
|
||||
clientProxy.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartClosure = { _, _, isPrivate, isKnockingOnly, _, _, _ in
|
||||
XCTAssertFalse(isKnockingOnly)
|
||||
XCTAssertTrue(isPrivate)
|
||||
expectation.fulfill()
|
||||
@@ -105,4 +162,35 @@ class CreateRoomScreenViewModelTests: XCTestCase {
|
||||
}
|
||||
await fulfillment(of: [expectation])
|
||||
}
|
||||
|
||||
func testNameAndAddressSync() async {
|
||||
context.isRoomPrivate = true
|
||||
await Task.yield()
|
||||
context.send(viewAction: .updateRoomName("abc"))
|
||||
XCTAssertEqual(context.viewState.aliasLocalPart, "abc")
|
||||
XCTAssertEqual(context.viewState.roomName, "abc")
|
||||
context.send(viewAction: .updateRoomName("DEF"))
|
||||
XCTAssertEqual(context.viewState.roomName, "DEF")
|
||||
XCTAssertEqual(context.viewState.aliasLocalPart, "def")
|
||||
context.send(viewAction: .updateRoomName("a b c"))
|
||||
XCTAssertEqual(context.viewState.aliasLocalPart, "a-b-c")
|
||||
XCTAssertEqual(context.viewState.roomName, "a b c")
|
||||
context.send(viewAction: .updateAliasLocalPart("hello-world"))
|
||||
// This removes the sync
|
||||
XCTAssertEqual(context.viewState.aliasLocalPart, "hello-world")
|
||||
XCTAssertEqual(context.viewState.roomName, "a b c")
|
||||
|
||||
context.send(viewAction: .updateRoomName("Hello Matrix!"))
|
||||
XCTAssertEqual(context.viewState.aliasLocalPart, "hello-world")
|
||||
XCTAssertEqual(context.viewState.roomName, "Hello Matrix!")
|
||||
|
||||
// Deleting the whole name will restore the sync
|
||||
context.send(viewAction: .updateRoomName(""))
|
||||
XCTAssertEqual(context.viewState.aliasLocalPart, "")
|
||||
XCTAssertEqual(context.viewState.roomName, "")
|
||||
|
||||
context.send(viewAction: .updateRoomName("Hello# Matrix!"))
|
||||
XCTAssertEqual(context.viewState.aliasLocalPart, "hello-matrix!")
|
||||
XCTAssertEqual(context.viewState.roomName, "Hello# Matrix!")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ packages:
|
||||
# Element/Matrix dependencies
|
||||
MatrixRustSDK:
|
||||
url: https://github.com/element-hq/matrix-rust-components-swift
|
||||
exactVersion: 1.0.65
|
||||
exactVersion: 1.0.66
|
||||
# path: ../matrix-rust-sdk
|
||||
Compound:
|
||||
url: https://github.com/element-hq/compound-ios
|
||||
|
||||
Reference in New Issue
Block a user