From 6457647afcd1686aee9c9c1e899758f2faf58283 Mon Sep 17 00:00:00 2001 From: Mauro <34335419+Velin92@users.noreply.github.com> Date: Fri, 8 Nov 2024 15:49:20 +0100 Subject: [PATCH] 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 --- ElementX.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../en.lproj/Localizable.strings | 12 +- ElementX/Sources/Generated/Strings.swift | 24 +-- ElementX/Sources/Mocks/ClientProxyMock.swift | 8 +- .../Mocks/Generated/GeneratedMocks.swift | 118 +++++++++++--- .../Screens/CreateRoom/CreateRoomModels.swift | 25 ++- .../CreateRoom/CreateRoomViewModel.swift | 147 ++++++++++++++++-- .../CreateRoom/View/CreateRoomScreen.swift | 120 ++++++++++++-- .../View/SecureBackupRecoveryKeyScreen.swift | 4 +- .../Sources/Services/Client/ClientProxy.swift | 40 ++++- .../Services/Client/ClientProxyProtocol.swift | 12 +- .../CreateRoom/CreateRoomFlowParameters.swift | 1 + ...n-GB.Create-Public-Room-existing-alias.png | 3 + ...en-GB.Create-Public-Room-invalid-alias.png | 3 + ...eateRoom-iPad-en-GB.Create-Public-Room.png | 4 +- ...m-iPad-en-GB.Create-Room-without-users.png | 4 +- ...test_createRoom-iPad-en-GB.Create-Room.png | 4 +- ...eudo.Create-Public-Room-existing-alias.png | 3 + ...seudo.Create-Public-Room-invalid-alias.png | 3 + ...ateRoom-iPad-pseudo.Create-Public-Room.png | 4 +- ...-iPad-pseudo.Create-Room-without-users.png | 4 +- ...est_createRoom-iPad-pseudo.Create-Room.png | 4 +- ...n-GB.Create-Public-Room-existing-alias.png | 3 + ...en-GB.Create-Public-Room-invalid-alias.png | 3 + ...oom-iPhone-16-en-GB.Create-Public-Room.png | 4 +- ...one-16-en-GB.Create-Room-without-users.png | 4 +- ...createRoom-iPhone-16-en-GB.Create-Room.png | 4 +- ...eudo.Create-Public-Room-existing-alias.png | 3 + ...seudo.Create-Public-Room-invalid-alias.png | 3 + ...om-iPhone-16-pseudo.Create-Public-Room.png | 4 +- ...ne-16-pseudo.Create-Room-without-users.png | 4 +- ...reateRoom-iPhone-16-pseudo.Create-Room.png | 4 +- ...kupRecoveryKeyScreen-iPad-en-GB.Set-up.png | 4 +- ...upRecoveryKeyScreen-iPad-pseudo.Set-up.png | 4 +- ...coveryKeyScreen-iPhone-16-en-GB.Set-up.png | 4 +- ...overyKeyScreen-iPhone-16-pseudo.Set-up.png | 4 +- .../Sources/CreateRoomViewModelTests.swift | 100 +++++++++++- project.yml | 2 +- 39 files changed, 592 insertions(+), 115 deletions(-) create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPad-en-GB.Create-Public-Room-existing-alias.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPad-en-GB.Create-Public-Room-invalid-alias.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPad-pseudo.Create-Public-Room-existing-alias.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPad-pseudo.Create-Public-Room-invalid-alias.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPhone-16-en-GB.Create-Public-Room-existing-alias.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPhone-16-en-GB.Create-Public-Room-invalid-alias.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPhone-16-pseudo.Create-Public-Room-existing-alias.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPhone-16-pseudo.Create-Public-Room-invalid-alias.png diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index fe0c6e310..24ab4952a 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -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" */ = { diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 57400ca50..0d3eb3946 100644 --- a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -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" } }, { diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index 4200cde50..23b47f1e3 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -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?"; diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index 2c89b62e8..f807da070 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -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 diff --git a/ElementX/Sources/Mocks/ClientProxyMock.swift b/ElementX/Sources/Mocks/ClientProxyMock.swift index b5b52018b..9a21f9120 100644 --- a/ElementX/Sources/Mocks/ClientProxyMock.swift +++ b/ElementX/Sources/Mocks/ClientProxyMock.swift @@ -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")) diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index c7524379c..51c386fda 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -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! - var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLReturnValue: Result! { + var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartUnderlyingReturnValue: Result! + var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartReturnValue: Result! { get { if Thread.isMainThread { - return createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingReturnValue + return createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartUnderlyingReturnValue } else { var returnValue: Result? = 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)? + var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartClosure: ((String, String?, Bool, Bool, [String], URL?, String?) async -> Result)? - func createRoom(name: String, topic: String?, isRoomPrivate: Bool, isKnockingOnly: Bool, userIDs: [String], avatarURL: URL?) async -> Result { - 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 { + 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! + var isAliasAvailableReturnValue: Result! { + get { + if Thread.isMainThread { + return isAliasAvailableUnderlyingReturnValue + } else { + var returnValue: Result? = 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)? + + func isAliasAvailable(_ alias: String) async -> Result { + 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 diff --git a/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift b/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift index 44cefd052..d225589be 100644 --- a/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift +++ b/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift @@ -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 = [] + 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 } diff --git a/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift b/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift index da70c7afb..31e40fd5d 100644 --- a/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift +++ b/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift @@ -6,6 +6,7 @@ // import Combine +import MatrixRustSDK import SwiftUI typealias CreateRoomViewModelType = StateStoreViewModel @@ -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? private var actionsSubject: PassthroughSubject = .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" diff --git a/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift b/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift index 726cf99f0..be52464a9 100644 --- a/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift +++ b/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift @@ -16,7 +16,23 @@ struct CreateRoomScreen: View { case name case topic } - + + private var aliasBinding: Binding { + .init(get: { + context.viewState.aliasLocalPart + }, set: { + context.send(viewAction: .updateAliasLocalPart($0)) + }) + } + + private var roomNameBinding: Binding { + .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") } } diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/View/SecureBackupRecoveryKeyScreen.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/View/SecureBackupRecoveryKeyScreen.swift index acd39bd18..51314243e 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/View/SecureBackupRecoveryKeyScreen.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/View/SecureBackupRecoveryKeyScreen.swift @@ -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) diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index 801ce4e8c..8516ab021 100644 --- a/ElementX/Sources/Services/Client/ClientProxy.swift +++ b/ElementX/Sources/Services/Client/ClientProxy.swift @@ -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? private let userAvatarURLSubject = CurrentValueSubject(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 { + func createRoom(name: String, + topic: String?, + isRoomPrivate: Bool, + isKnockingOnly: Bool, + userIDs: [String], + avatarURL: URL?, + aliasLocalPart: String?) async -> Result { 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 { 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 { + 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 { await client.getElementWellKnown().map(ElementWellKnown.init) } diff --git a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift index 85bd4f7ad..0feae1793 100644 --- a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift +++ b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift @@ -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 // swiftlint:disable:next function_parameter_count - func createRoom(name: String, topic: String?, isRoomPrivate: Bool, isKnockingOnly: Bool, userIDs: [String], avatarURL: URL?) async -> Result + func createRoom(name: String, + topic: String?, + isRoomPrivate: Bool, + isKnockingOnly: Bool, + userIDs: [String], + avatarURL: URL?, + aliasLocalPart: String?) async -> Result func joinRoom(_ roomID: String, via: [String]) async -> Result @@ -173,6 +179,8 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol { func resolveRoomAlias(_ alias: String) async -> Result + func isAliasAvailable(_ alias: String) async -> Result + func getElementWellKnown() async -> Result // MARK: - Ignored users diff --git a/ElementX/Sources/Services/CreateRoom/CreateRoomFlowParameters.swift b/ElementX/Sources/Services/CreateRoom/CreateRoomFlowParameters.swift index d2e03d96d..736e99c4b 100644 --- a/ElementX/Sources/Services/CreateRoom/CreateRoomFlowParameters.swift +++ b/ElementX/Sources/Services/CreateRoom/CreateRoomFlowParameters.swift @@ -14,4 +14,5 @@ struct CreateRoomFlowParameters { var isRoomPrivate = true var isKnockingOnly = false var avatarImageMedia: MediaInfo? + var aliasLocalPart: String? } diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPad-en-GB.Create-Public-Room-existing-alias.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPad-en-GB.Create-Public-Room-existing-alias.png new file mode 100644 index 000000000..a048c452d --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPad-en-GB.Create-Public-Room-existing-alias.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9650c6b430799996d02b0d34cf9d8657c94a5b5627dde3cb74755b5020619cf6 +size 202009 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPad-en-GB.Create-Public-Room-invalid-alias.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPad-en-GB.Create-Public-Room-invalid-alias.png new file mode 100644 index 000000000..8ce2cc596 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPad-en-GB.Create-Public-Room-invalid-alias.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7563d6820945006a48db647fed692d4458338d63669ea4c344052f5a63a4a2ee +size 204578 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPad-en-GB.Create-Public-Room.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPad-en-GB.Create-Public-Room.png index dd7544c1e..93564aa4d 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPad-en-GB.Create-Public-Room.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPad-en-GB.Create-Public-Room.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d3a984e4fc4d1aade45c5e8b98dcba2f1ecc0e50965e2ca64ad6faad777de26c -size 172668 +oid sha256:6f630b2ae196d0f8d9b98b68482863ea78aa84eb5e9b82e78d77430ec81dc007 +size 194309 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPad-en-GB.Create-Room-without-users.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPad-en-GB.Create-Room-without-users.png index 57d2a7842..107a06bd6 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPad-en-GB.Create-Room-without-users.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPad-en-GB.Create-Room-without-users.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:49a518ac1fe0ffd8719d757b5c5f2decde92b237937f68df15a239c228cf24b8 -size 144080 +oid sha256:860a4367f4ab161be805df36b2723829e023db753c45f9f49ecc126f6a2cc128 +size 145345 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPad-en-GB.Create-Room.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPad-en-GB.Create-Room.png index 6c198b387..c06ac0558 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPad-en-GB.Create-Room.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPad-en-GB.Create-Room.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:467d13af04d60db15fb1586df63b7589a59ae6f2cb4c7482b79cc0c56d7af0ce -size 143822 +oid sha256:b0a28cc070627724c44111ee0736006650e1453d85be6038783073d911f3d909 +size 144730 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPad-pseudo.Create-Public-Room-existing-alias.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPad-pseudo.Create-Public-Room-existing-alias.png new file mode 100644 index 000000000..35ad91399 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPad-pseudo.Create-Public-Room-existing-alias.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f255ed8f26b1f9efd108b7b0422262185f45152495727578822fb0691bfe08a9 +size 228341 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPad-pseudo.Create-Public-Room-invalid-alias.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPad-pseudo.Create-Public-Room-invalid-alias.png new file mode 100644 index 000000000..8b49055f9 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPad-pseudo.Create-Public-Room-invalid-alias.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b42242042ad3adf3b953a29ff58792511076930892a7bda4d813b3e3218f39e0 +size 226407 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPad-pseudo.Create-Public-Room.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPad-pseudo.Create-Public-Room.png index 2bb300bfb..e383e0a7c 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPad-pseudo.Create-Public-Room.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPad-pseudo.Create-Public-Room.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0f493e2b0a85be6d277f7138a11f0f02510b04df3fc22a8c76391fbc3444abd0 -size 208736 +oid sha256:a6f47df77fca87c83e7668b95a500c5b7d0a0640d694a7b92c58365573d0abd6 +size 224351 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPad-pseudo.Create-Room-without-users.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPad-pseudo.Create-Room-without-users.png index d4accf96e..8ca3e931d 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPad-pseudo.Create-Room-without-users.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPad-pseudo.Create-Room-without-users.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c03a493641d0f7c21680bec4ccea98428f210d642011feb3d61988aa09af2612 -size 168502 +oid sha256:b9311de66c92307e15ac5d62c025c953d20ce42e946eaaab30604f754ce6d395 +size 171925 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPad-pseudo.Create-Room.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPad-pseudo.Create-Room.png index 927c1e3d6..800e0982e 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPad-pseudo.Create-Room.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPad-pseudo.Create-Room.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1b8555d5ddea49cf85fc2fc9cb8981f9453f30152f8befc4d596358a7c79f63d -size 169943 +oid sha256:662701144b88061103f80d08c078a28ca918e768e4907e4100fd56941098acfa +size 173078 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPhone-16-en-GB.Create-Public-Room-existing-alias.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPhone-16-en-GB.Create-Public-Room-existing-alias.png new file mode 100644 index 000000000..5c280a583 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPhone-16-en-GB.Create-Public-Room-existing-alias.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:acf39359c47be4101e19fe3d0bfcedb7f0a262b418f3ab3f90da38c9ad3b88f0 +size 130131 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPhone-16-en-GB.Create-Public-Room-invalid-alias.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPhone-16-en-GB.Create-Public-Room-invalid-alias.png new file mode 100644 index 000000000..5d536e0a5 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPhone-16-en-GB.Create-Public-Room-invalid-alias.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:66610bab01ad7ed399d8c4f72fd8d04990eafbf911c8b666c3ca21beba88a58b +size 128602 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPhone-16-en-GB.Create-Public-Room.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPhone-16-en-GB.Create-Public-Room.png index 53af40f6d..8f0fea0f9 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPhone-16-en-GB.Create-Public-Room.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPhone-16-en-GB.Create-Public-Room.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f54b4053b937e1fc6cee22dfff905bead3ccfbd45bf6607fb8492c74dda15fd -size 118717 +oid sha256:311d57b9b2cc2ad3b459c5817e6dd605740abef4559b571492de22a81b160285 +size 126941 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPhone-16-en-GB.Create-Room-without-users.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPhone-16-en-GB.Create-Room-without-users.png index 9280f4861..3e986cdcb 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPhone-16-en-GB.Create-Room-without-users.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPhone-16-en-GB.Create-Room-without-users.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:133221a0b8d4b9aed307a19043b323ca99c3cf639b2097c3713209c6c69c70e5 -size 94718 +oid sha256:d637d85d789f0d3d8756719f0b277a23b07730a21d46fe599afdb4b1143ce814 +size 95980 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPhone-16-en-GB.Create-Room.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPhone-16-en-GB.Create-Room.png index 969911967..215efe85a 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPhone-16-en-GB.Create-Room.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPhone-16-en-GB.Create-Room.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5c99bf8cd5ba59b6343a3e61af3f2b45ebbe3cab0dda38ef6aa45750b7c13558 -size 94927 +oid sha256:c89527ca06fb7a7b03cb6bb2feda23cd9325e98378eecc364f537c29d6878b08 +size 96262 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPhone-16-pseudo.Create-Public-Room-existing-alias.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPhone-16-pseudo.Create-Public-Room-existing-alias.png new file mode 100644 index 000000000..96122c784 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPhone-16-pseudo.Create-Public-Room-existing-alias.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d191b67a8b246e26c7567427f09cd7562fc88402b06a150212a74eb55fb58831 +size 164266 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPhone-16-pseudo.Create-Public-Room-invalid-alias.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPhone-16-pseudo.Create-Public-Room-invalid-alias.png new file mode 100644 index 000000000..96122c784 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPhone-16-pseudo.Create-Public-Room-invalid-alias.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d191b67a8b246e26c7567427f09cd7562fc88402b06a150212a74eb55fb58831 +size 164266 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPhone-16-pseudo.Create-Public-Room.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPhone-16-pseudo.Create-Public-Room.png index cfb427967..96122c784 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPhone-16-pseudo.Create-Public-Room.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPhone-16-pseudo.Create-Public-Room.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2155e204f2bf31a62cc09e3656e77fa58ee327a1ba37de0a307ba7e4fdd9a836 -size 162088 +oid sha256:d191b67a8b246e26c7567427f09cd7562fc88402b06a150212a74eb55fb58831 +size 164266 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPhone-16-pseudo.Create-Room-without-users.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPhone-16-pseudo.Create-Room-without-users.png index 6848b26ad..ebeef4fda 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPhone-16-pseudo.Create-Room-without-users.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPhone-16-pseudo.Create-Room-without-users.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:608dbe5519a764e6a9dd8c1d909b5d53ec969d34a03251cdedaabdc1508d661a -size 124622 +oid sha256:61ca01f8203d3c668ca2183f46d2954a32284624c8b7caaffd37124e2d454997 +size 126803 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPhone-16-pseudo.Create-Room.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPhone-16-pseudo.Create-Room.png index 20411e7c1..89c4aae8f 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPhone-16-pseudo.Create-Room.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_createRoom-iPhone-16-pseudo.Create-Room.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8f5dd8e86929e05e9a30b27fe8895668246da009696ac614f8f4fbf4bf78d0d8 -size 125070 +oid sha256:ab43faa0b020b9628d10999a93d9c70e90e007eb788ce9a14eaca5330b9c5843 +size 127313 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_secureBackupRecoveryKeyScreen-iPad-en-GB.Set-up.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_secureBackupRecoveryKeyScreen-iPad-en-GB.Set-up.png index ebbd6a69f..02ab29e32 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_secureBackupRecoveryKeyScreen-iPad-en-GB.Set-up.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_secureBackupRecoveryKeyScreen-iPad-en-GB.Set-up.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b4eb7771b9156b75c3812751b1219ba337e4d9b1b465dedd09e7784b1f009e9 -size 120296 +oid sha256:02851bc8304737c8eee1ea76ddee133ec47feb49e0471b8e6c1213696851e6e4 +size 140058 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_secureBackupRecoveryKeyScreen-iPad-pseudo.Set-up.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_secureBackupRecoveryKeyScreen-iPad-pseudo.Set-up.png index 37f1a320a..83bba7ef5 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_secureBackupRecoveryKeyScreen-iPad-pseudo.Set-up.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_secureBackupRecoveryKeyScreen-iPad-pseudo.Set-up.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e80a71b9eaeb41bd700cf22e98b0290c06e6786cdffea72c0973549b4ba5bd43 -size 141940 +oid sha256:b694da58f6895e51248e8368a2baf1f7b9c4eef3119533ea2eca3c13407412d5 +size 171446 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_secureBackupRecoveryKeyScreen-iPhone-16-en-GB.Set-up.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_secureBackupRecoveryKeyScreen-iPhone-16-en-GB.Set-up.png index 8d002a493..49cd7a983 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_secureBackupRecoveryKeyScreen-iPhone-16-en-GB.Set-up.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_secureBackupRecoveryKeyScreen-iPhone-16-en-GB.Set-up.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:872d515fe6aae36c12d21a3e6757abcddf9e03e51e6421324505a528b5946376 -size 74557 +oid sha256:bb8bc4cd519ee62becb550537da7bf15566b178330101c21c839d9d93bbd34ed +size 95875 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_secureBackupRecoveryKeyScreen-iPhone-16-pseudo.Set-up.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_secureBackupRecoveryKeyScreen-iPhone-16-pseudo.Set-up.png index 09ee04312..7c5fb0f79 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_secureBackupRecoveryKeyScreen-iPhone-16-pseudo.Set-up.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_secureBackupRecoveryKeyScreen-iPhone-16-pseudo.Set-up.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:88b07c92a3f9763eb361918da508af0e140bce7241ebbc92c38f3ac38cc9394d -size 100293 +oid sha256:8db80a8ad96649272dac60a2a95aade580a6b5b09cbb79c49e81d210d41b4f0c +size 121490 diff --git a/UnitTests/Sources/CreateRoomViewModelTests.swift b/UnitTests/Sources/CreateRoomViewModelTests.swift index b145dfbfd..110d78846 100644 --- a/UnitTests/Sources/CreateRoomViewModelTests.swift +++ b/UnitTests/Sources/CreateRoomViewModelTests.swift @@ -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!") + } } diff --git a/project.yml b/project.yml index 5eb74d64f..649c0bf73 100644 --- a/project.yml +++ b/project.yml @@ -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