From 061a6ce58f1fc7e1affb8af38f03417e9e991776 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Mon, 21 Oct 2024 18:32:57 +0200 Subject: [PATCH 01/22] added the address section --- ElementX/Sources/Mocks/ClientProxyMock.swift | 3 ++- .../Screens/CreateRoom/CreateRoomModels.swift | 2 ++ .../CreateRoom/CreateRoomViewModel.swift | 2 +- .../CreateRoom/View/CreateRoomScreen.swift | 25 ++++++++++++++++++- 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/ElementX/Sources/Mocks/ClientProxyMock.swift b/ElementX/Sources/Mocks/ClientProxyMock.swift index ac086c1c43..34ef905c66 100644 --- a/ElementX/Sources/Mocks/ClientProxyMock.swift +++ b/ElementX/Sources/Mocks/ClientProxyMock.swift @@ -9,6 +9,7 @@ import Combine import Foundation struct ClientProxyMockConfiguration { + var homeserver = "" var userID: String = RoomMemberProxyMock.mockMe.userID var deviceID: String? var roomSummaryProvider: RoomSummaryProviderProtocol? = RoomSummaryProviderMock(.init()) @@ -26,7 +27,7 @@ extension ClientProxyMock { userID = configuration.userID deviceID = configuration.deviceID - homeserver = "" + homeserver = configuration.homeserver roomSummaryProvider = configuration.roomSummaryProvider alternateRoomSummaryProvider = RoomSummaryProviderMock(.init()) diff --git a/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift b/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift index 44cefd052c..5d1ee21af7 100644 --- a/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift +++ b/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift @@ -25,6 +25,7 @@ enum CreateRoomViewModelAction { } struct CreateRoomViewState: BindableState { + let homeserver: String let isKnockingFeatureEnabled: Bool var selectedUsers: [UserProfileProxy] var bindings: CreateRoomViewStateBindings @@ -40,6 +41,7 @@ struct CreateRoomViewStateBindings { var isRoomPrivate: Bool var isKnockingOnly = false var showAttachmentConfirmationDialog = false + var address = "" /// Information describing the currently displayed alert. var alertInfo: AlertInfo? diff --git a/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift b/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift index da70c7afb2..77214ae875 100644 --- a/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift +++ b/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift @@ -37,7 +37,7 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol let bindings = CreateRoomViewStateBindings(roomName: parameters.name, roomTopic: parameters.topic, isRoomPrivate: parameters.isRoomPrivate) - super.init(initialViewState: CreateRoomViewState(isKnockingFeatureEnabled: appSettings.knockingEnabled, selectedUsers: selectedUsers.value, bindings: bindings), mediaProvider: userSession.mediaProvider) + super.init(initialViewState: CreateRoomViewState(homeserver: ":\(userSession.clientProxy.homeserver)", isKnockingFeatureEnabled: appSettings.knockingEnabled, selectedUsers: selectedUsers.value, bindings: bindings), mediaProvider: userSession.mediaProvider) createRoomParameters .map(\.avatarImageMedia) diff --git a/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift b/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift index 726cf99f0e..ded4080762 100644 --- a/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift +++ b/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift @@ -25,6 +25,7 @@ struct CreateRoomScreen: View { if context.viewState.isKnockingFeatureEnabled, !context.isRoomPrivate { roomAccessSection + roomAddressSection } } .compoundList() @@ -169,6 +170,28 @@ struct CreateRoomScreen: View { } } + private var roomAddressSection: some View { + Section { + HStack(spacing: 0) { + Text("#") + .font(.compound.bodyLG) + .foregroundStyle(.compound.textSecondary) + TextField("", text: $context.address) + .font(.compound.bodyLG) + .foregroundStyle(.compound.textPrimary) + Text(context.viewState.homeserver) + .font(.compound.bodyLG) + .foregroundStyle(.compound.textSecondary) + } + } header: { + Text("Room address".uppercased()) + .compoundListSectionHeader() + } footer: { + Text("In order for this room to be visible in the public room directory, you will need a room address. ") + .compoundListSectionFooter() + } + } + private var toolbar: some ToolbarContent { ToolbarItem(placement: .confirmationAction) { Button(L10n.actionCreate) { @@ -208,7 +231,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(homeserver: "example.org", userID: "@userid:example.com")))) let parameters = CreateRoomFlowParameters(isRoomPrivate: false) let selectedUsers: [UserProfileProxy] = [.mockAlice, .mockBob, .mockCharlie] ServiceLocator.shared.settings.knockingEnabled = true From 2bde9f8c02b5175467432bef25892007d91e50df Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Thu, 24 Oct 2024 19:54:42 +0200 Subject: [PATCH 02/22] updated code and strings --- .../en.lproj/Localizable.strings | 19 ++++++++----- ElementX/Sources/Generated/Strings.swift | 27 ++++++++++++++----- ElementX/Sources/Mocks/ClientProxyMock.swift | 2 ++ .../Mocks/Generated/GeneratedMocks.swift | 1 + .../Screens/CreateRoom/CreateRoomModels.swift | 2 +- .../CreateRoom/CreateRoomViewModel.swift | 6 ++++- .../CreateRoom/View/CreateRoomScreen.swift | 16 ++++++----- .../Sources/Services/Client/ClientProxy.swift | 4 +++ .../Services/Client/ClientProxyProtocol.swift | 2 ++ 9 files changed, 59 insertions(+), 20 deletions(-) diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index 4aeb3bef7b..612e96c04e 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -139,6 +139,7 @@ "common_edited_suffix" = "(edited)"; "common_editing" = "Editing"; "common_emote" = "* %1$@ %2$@"; +"common_encryption" = "Encryption"; "common_encryption_enabled" = "Encryption enabled"; "common_enter_your_pin" = "Enter your PIN"; "common_error" = "Error"; @@ -342,6 +343,9 @@ "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_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"; @@ -449,9 +453,12 @@ "screen_change_server_title" = "Select your server"; "screen_chat_backup_key_backup_action_disable" = "Turn off backup"; "screen_chat_backup_key_backup_action_enable" = "Turn on backup"; -"screen_chat_backup_key_backup_description" = "Backup ensures that you don't lose your message history. %1$@."; -"screen_chat_backup_key_backup_title" = "Backup"; +"screen_chat_backup_key_backup_description" = "Store your cryptographic identity and message keys securely on the server. This will allow you to view your message history on any new devices. %1$@."; +"screen_chat_backup_key_backup_title" = "Key storage"; +"screen_chat_backup_key_storage_toggle_description" = "Upload keys from this device"; +"screen_chat_backup_key_storage_toggle_title" = "Allow key storage"; "screen_chat_backup_recovery_action_change" = "Change recovery key"; +"screen_chat_backup_recovery_action_change_description" = "Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices."; "screen_chat_backup_recovery_action_confirm_description" = "Your chat backup is currently out of sync."; "screen_chat_backup_recovery_action_setup" = "Set up recovery"; "screen_chat_backup_recovery_action_setup_description" = "Get access to your encrypted messages if you lose all your devices or are signed out of %1$@ everywhere."; @@ -473,10 +480,10 @@ "screen_create_poll_title" = "Create Poll"; "screen_create_room_action_create_room" = "New room"; "screen_create_room_error_creating_room" = "An error occurred when creating the room"; -"screen_create_room_private_option_description" = "Messages in this room are encrypted. Encryption can’t be disabled afterwards."; -"screen_create_room_private_option_title" = "Private room (invite only)"; -"screen_create_room_public_option_description" = "Messages are not encrypted and anyone can read them. You can enable encryption at a later date."; -"screen_create_room_public_option_title" = "Public room (anyone)"; +"screen_create_room_private_option_description" = "Only people invited can access this room. All messages are end-to-end encrypted."; +"screen_create_room_private_option_title" = "Private room"; +"screen_create_room_public_option_description" = "Anyone can find this room.\nYou can change this anytime in room settings."; +"screen_create_room_public_option_title" = "Public room"; "screen_create_room_topic_label" = "Topic (optional)"; "screen_deactivate_account_confirmation_dialog_content" = "Please confirm that you want to deactivate your account. This action cannot be undone."; "screen_deactivate_account_delete_all_messages" = "Delete all my messages"; diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index a9fef6619b..68f54bee57 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -312,6 +312,8 @@ internal enum L10n { internal static func commonEmote(_ p1: Any, _ p2: Any) -> String { return L10n.tr("Localizable", "common_emote", String(describing: p1), String(describing: p2)) } + /// Encryption + internal static var commonEncryption: String { return L10n.tr("Localizable", "common_encryption") } /// Encryption enabled internal static var commonEncryptionEnabled: String { return L10n.tr("Localizable", "common_encryption_enabled") } /// Enter your PIN @@ -1005,14 +1007,20 @@ internal enum L10n { internal static var screenChatBackupKeyBackupActionDisable: String { return L10n.tr("Localizable", "screen_chat_backup_key_backup_action_disable") } /// Turn on backup internal static var screenChatBackupKeyBackupActionEnable: String { return L10n.tr("Localizable", "screen_chat_backup_key_backup_action_enable") } - /// Backup ensures that you don't lose your message history. %1$@. + /// Store your cryptographic identity and message keys securely on the server. This will allow you to view your message history on any new devices. %1$@. internal static func screenChatBackupKeyBackupDescription(_ p1: Any) -> String { return L10n.tr("Localizable", "screen_chat_backup_key_backup_description", String(describing: p1)) } - /// Backup + /// Key storage internal static var screenChatBackupKeyBackupTitle: String { return L10n.tr("Localizable", "screen_chat_backup_key_backup_title") } + /// Upload keys from this device + internal static var screenChatBackupKeyStorageToggleDescription: String { return L10n.tr("Localizable", "screen_chat_backup_key_storage_toggle_description") } + /// Allow key storage + internal static var screenChatBackupKeyStorageToggleTitle: String { return L10n.tr("Localizable", "screen_chat_backup_key_storage_toggle_title") } /// Change recovery key internal static var screenChatBackupRecoveryActionChange: String { return L10n.tr("Localizable", "screen_chat_backup_recovery_action_change") } + /// Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices. + internal static var screenChatBackupRecoveryActionChangeDescription: String { return L10n.tr("Localizable", "screen_chat_backup_recovery_action_change_description") } /// Enter recovery key internal static var screenChatBackupRecoveryActionConfirm: String { return L10n.tr("Localizable", "screen_chat_backup_recovery_action_confirm") } /// Your chat backup is currently out of sync. @@ -1079,16 +1087,23 @@ internal enum L10n { internal static var screenCreateRoomAddPeopleTitle: String { return L10n.tr("Localizable", "screen_create_room_add_people_title") } /// An error occurred when creating the room internal static var screenCreateRoomErrorCreatingRoom: String { return L10n.tr("Localizable", "screen_create_room_error_creating_room") } - /// Messages in this room are encrypted. Encryption can’t be disabled afterwards. + /// Only people invited can access this room. All messages are end-to-end encrypted. internal static var screenCreateRoomPrivateOptionDescription: String { return L10n.tr("Localizable", "screen_create_room_private_option_description") } - /// Private room (invite only) + /// Private room internal static var screenCreateRoomPrivateOptionTitle: String { return L10n.tr("Localizable", "screen_create_room_private_option_title") } - /// Messages are not encrypted and anyone can read them. You can enable encryption at a later date. + /// Anyone can find this room. + /// You can change this anytime in room settings. internal static var screenCreateRoomPublicOptionDescription: String { return L10n.tr("Localizable", "screen_create_room_public_option_description") } - /// Public room (anyone) + /// Public room internal static var screenCreateRoomPublicOptionTitle: String { return L10n.tr("Localizable", "screen_create_room_public_option_title") } + /// 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 + internal static var screenCreateRoomRoomAddressSectionTitle: String { return L10n.tr("Localizable", "screen_create_room_room_address_section_title") } /// Room name internal static var screenCreateRoomRoomNameLabel: String { return L10n.tr("Localizable", "screen_create_room_room_name_label") } + /// Room visibility + internal static var screenCreateRoomRoomVisibilitySectionTitle: String { return L10n.tr("Localizable", "screen_create_room_room_visibility_section_title") } /// Create a room internal static var screenCreateRoomTitle: String { return L10n.tr("Localizable", "screen_create_room_title") } /// Topic (optional) diff --git a/ElementX/Sources/Mocks/ClientProxyMock.swift b/ElementX/Sources/Mocks/ClientProxyMock.swift index 34ef905c66..34375a839e 100644 --- a/ElementX/Sources/Mocks/ClientProxyMock.swift +++ b/ElementX/Sources/Mocks/ClientProxyMock.swift @@ -10,6 +10,7 @@ import Foundation struct ClientProxyMockConfiguration { var homeserver = "" + var serverName: String? var userID: String = RoomMemberProxyMock.mockMe.userID var deviceID: String? var roomSummaryProvider: RoomSummaryProviderProtocol? = RoomSummaryProviderMock(.init()) @@ -28,6 +29,7 @@ extension ClientProxyMock { deviceID = configuration.deviceID homeserver = configuration.homeserver + serverName = configuration.serverName roomSummaryProvider = configuration.roomSummaryProvider alternateRoomSummaryProvider = RoomSummaryProviderMock(.init()) diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 9755771e0b..4efb3787db 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -2149,6 +2149,7 @@ class ClientProxyMock: ClientProxyProtocol { set(value) { underlyingHomeserver = value } } var underlyingHomeserver: String! + var serverName: String? var slidingSyncVersion: SlidingSyncVersion { get { return underlyingSlidingSyncVersion } set(value) { underlyingSlidingSyncVersion = value } diff --git a/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift b/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift index 5d1ee21af7..ae83e1d9a2 100644 --- a/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift +++ b/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift @@ -41,7 +41,7 @@ struct CreateRoomViewStateBindings { var isRoomPrivate: Bool var isKnockingOnly = false var showAttachmentConfirmationDialog = false - var address = "" + var addressName = "" /// Information describing the currently displayed alert. var alertInfo: AlertInfo? diff --git a/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift b/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift index 77214ae875..d1f2ace1e6 100644 --- a/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift +++ b/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift @@ -37,7 +37,11 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol let bindings = CreateRoomViewStateBindings(roomName: parameters.name, roomTopic: parameters.topic, isRoomPrivate: parameters.isRoomPrivate) - super.init(initialViewState: CreateRoomViewState(homeserver: ":\(userSession.clientProxy.homeserver)", isKnockingFeatureEnabled: appSettings.knockingEnabled, selectedUsers: selectedUsers.value, bindings: bindings), mediaProvider: userSession.mediaProvider) + super.init(initialViewState: CreateRoomViewState(homeserver: ":\(userSession.clientProxy.serverName ?? "")", + isKnockingFeatureEnabled: appSettings.knockingEnabled, + selectedUsers: selectedUsers.value, + bindings: bindings), + mediaProvider: userSession.mediaProvider) createRoomParameters .map(\.avatarImageMedia) diff --git a/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift b/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift index ded4080762..d801d7ee63 100644 --- a/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift +++ b/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift @@ -16,7 +16,7 @@ struct CreateRoomScreen: View { case name case topic } - + var body: some View { Form { roomSection @@ -151,7 +151,7 @@ struct CreateRoomScreen: View { iconAlignment: .top), kind: .selection(isSelected: !context.isRoomPrivate) { context.isRoomPrivate = false }) } header: { - Text(L10n.commonSecurity.uppercased()) + Text(L10n.screenCreateRoomRoomVisibilitySectionTitle.uppercased()) .compoundListSectionHeader() } } @@ -176,18 +176,22 @@ struct CreateRoomScreen: View { Text("#") .font(.compound.bodyLG) .foregroundStyle(.compound.textSecondary) - TextField("", text: $context.address) + TextField("", text: $context.addressName) + .autocapitalization(.none) + .textCase(.lowercase) .font(.compound.bodyLG) .foregroundStyle(.compound.textPrimary) + .padding(.horizontal, 8) Text(context.viewState.homeserver) .font(.compound.bodyLG) .foregroundStyle(.compound.textSecondary) } + .environment(\.layoutDirection, .leftToRight) } header: { - Text("Room address".uppercased()) + Text(L10n.screenCreateRoomRoomAddressSectionTitle.uppercased()) .compoundListSectionHeader() } footer: { - Text("In order for this room to be visible in the public room directory, you will need a room address. ") + Text(L10n.screenCreateRoomRoomAddressSectionFooter) .compoundListSectionFooter() } } @@ -231,7 +235,7 @@ struct CreateRoom_Previews: PreviewProvider, TestablePreview { }() static let publicRoomViewModel = { - let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(homeserver: "example.org", userID: "@userid:example.com")))) + let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(serverName: "example.org", userID: "@userid:example.com")))) let parameters = CreateRoomFlowParameters(isRoomPrivate: false) let selectedUsers: [UserProfileProxy] = [.mockAlice, .mockBob, .mockCharlie] ServiceLocator.shared.settings.knockingEnabled = true diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index 0bba67ad5e..5863709727 100644 --- a/ElementX/Sources/Services/Client/ClientProxy.swift +++ b/ElementX/Sources/Services/Client/ClientProxy.swift @@ -203,6 +203,10 @@ class ClientProxy: ClientProxyProtocol { client.homeserver() } + var serverName: String? { + try? client.userIdServerName() + } + var slidingSyncVersion: SlidingSyncVersion { client.slidingSyncVersion() } diff --git a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift index 8bbf4ecb55..e364752bf4 100644 --- a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift +++ b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift @@ -88,6 +88,8 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol { var homeserver: String { get } + var serverName: String? { get } + var slidingSyncVersion: SlidingSyncVersion { get } var availableSlidingSyncVersions: [SlidingSyncVersion] { get async } From 860848c511c259661c9b1d9923c0ec40a860ec96 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Thu, 24 Oct 2024 20:15:09 +0200 Subject: [PATCH 03/22] syncing name and address --- .../Screens/CreateRoom/CreateRoomModels.swift | 8 ++-- .../CreateRoom/CreateRoomViewModel.swift | 37 +++++++++++++------ .../CreateRoom/View/CreateRoomScreen.swift | 16 +++++++- 3 files changed, 44 insertions(+), 17 deletions(-) diff --git a/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift b/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift index ae83e1d9a2..d66b0cf19e 100644 --- a/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift +++ b/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift @@ -25,23 +25,23 @@ enum CreateRoomViewModelAction { } struct CreateRoomViewState: BindableState { + var roomName: String let homeserver: String let isKnockingFeatureEnabled: Bool var selectedUsers: [UserProfileProxy] + var addressName: String var bindings: CreateRoomViewStateBindings var avatarURL: URL? var canCreateRoom: Bool { - !bindings.roomName.isEmpty + roomName.isEmpty } } struct CreateRoomViewStateBindings { - var roomName: String var roomTopic: String var isRoomPrivate: Bool var isKnockingOnly = false var showAttachmentConfirmationDialog = false - var addressName = "" /// Information describing the currently displayed alert. var alertInfo: AlertInfo? @@ -53,4 +53,6 @@ enum CreateRoomViewAction { case displayCameraPicker case displayMediaPicker case removeImage + case updateName(String) + case updateAddress(String) } diff --git a/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift b/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift index d1f2ace1e6..0c716e523a 100644 --- a/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift +++ b/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift @@ -15,6 +15,7 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol private var createRoomParameters: CreateRoomFlowParameters private let analytics: AnalyticsService private let userIndicatorController: UserIndicatorControllerProtocol + private var syncNameAndAddress = true private var actionsSubject: PassthroughSubject = .init() @@ -35,11 +36,13 @@ 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) - super.init(initialViewState: CreateRoomViewState(homeserver: ":\(userSession.clientProxy.serverName ?? "")", + super.init(initialViewState: CreateRoomViewState(roomName: parameters.name, + homeserver: ":\(userSession.clientProxy.serverName ?? "")", isKnockingFeatureEnabled: appSettings.knockingEnabled, selectedUsers: selectedUsers.value, + addressName: parameters.name.split(separator: " ").joined(separator: "-").lowercased(), bindings: bindings), mediaProvider: userSession.mediaProvider) @@ -84,6 +87,17 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol actionsSubject.send(.displayMediaPicker) case .removeImage: actionsSubject.send(.removeImage) + case .updateAddress(let address): + state.addressName = address + syncNameAndAddress = false + case .updateName(let name): + if name.isEmpty { + syncNameAndAddress = true + } + state.roomName = name + if syncNameAndAddress { + state.addressName = name.split(separator: " ").joined(separator: "-").lowercased() + } } } @@ -91,24 +105,23 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol private func setupBindings() { 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 } - .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) } - 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 } private func createRoom() async { @@ -118,7 +131,7 @@ 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) let avatarURL: URL? if let media = createRoomParameters.avatarImageMedia { switch await userSession.clientProxy.uploadMedia(media) { diff --git a/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift b/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift index d801d7ee63..4fe0d6b319 100644 --- a/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift +++ b/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift @@ -48,8 +48,13 @@ struct CreateRoomScreen: View { .padding(.leading, ListRowPadding.horizontal) .compoundListSectionHeader() + let binding = Binding(get: { + context.viewState.roomName + }, set: { + context.send(viewAction: .updateName($0)) + }) TextField(L10n.screenCreateRoomRoomNameLabel, - text: $context.roomName, + text: binding, prompt: Text(L10n.commonRoomNamePlaceholder).foregroundColor(.compound.textPlaceholder), axis: .horizontal) .focused($focus, equals: .name) @@ -176,7 +181,14 @@ struct CreateRoomScreen: View { Text("#") .font(.compound.bodyLG) .foregroundStyle(.compound.textSecondary) - TextField("", text: $context.addressName) + + let binding = Binding(get: { + context.viewState.addressName + }, set: { + context.send(viewAction: .updateAddress($0)) + }) + + TextField("", text: binding) .autocapitalization(.none) .textCase(.lowercase) .font(.compound.bodyLG) From 045f1ca7feaa410f7937f1d2ff7663669f33de78 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Thu, 24 Oct 2024 20:18:41 +0200 Subject: [PATCH 04/22] improved code --- .../Screens/CreateRoom/CreateRoomViewModel.swift | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift b/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift index 0c716e523a..f9a3c526e3 100644 --- a/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift +++ b/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift @@ -42,7 +42,7 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol homeserver: ":\(userSession.clientProxy.serverName ?? "")", isKnockingFeatureEnabled: appSettings.knockingEnabled, selectedUsers: selectedUsers.value, - addressName: parameters.name.split(separator: " ").joined(separator: "-").lowercased(), + addressName: parameters.name.toValidAddress, bindings: bindings), mediaProvider: userSession.mediaProvider) @@ -88,7 +88,7 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol case .removeImage: actionsSubject.send(.removeImage) case .updateAddress(let address): - state.addressName = address + state.addressName = address.toValidAddress syncNameAndAddress = false case .updateName(let name): if name.isEmpty { @@ -96,7 +96,7 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol } state.roomName = name if syncNameAndAddress { - state.addressName = name.split(separator: " ").joined(separator: "-").lowercased() + state.addressName = name.toValidAddress } } } @@ -190,3 +190,9 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier) } } + +private extension String { + var toValidAddress: Self { + split(separator: " ").joined(separator: "-").lowercased() + } +} From e63e1a3ac8e7d262c9d6bb41fe7e2a70809438bb Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Thu, 24 Oct 2024 20:27:10 +0200 Subject: [PATCH 05/22] added a way to reset the state --- .../Screens/CreateRoom/CreateRoomViewModel.swift | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift b/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift index f9a3c526e3..eaa02b37a3 100644 --- a/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift +++ b/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift @@ -104,6 +104,21 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol // 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.addressName = state.roomName.toValidAddress + syncNameAndAddress = true + } + .store(in: &cancellables) + context.$viewState .throttle(for: 0.5, scheduler: DispatchQueue.main, latest: true) .removeDuplicates { old, new in From d40022f3a3444dcb742538c2ec0a1711ea9ef744 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Thu, 24 Oct 2024 20:28:09 +0200 Subject: [PATCH 06/22] better documentation --- ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift b/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift index eaa02b37a3..fe4718c53d 100644 --- a/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift +++ b/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift @@ -89,8 +89,11 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol actionsSubject.send(.removeImage) case .updateAddress(let address): state.addressName = address.toValidAddress + // 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. syncNameAndAddress = false case .updateName(let name): + // Reset the syncing if the name is fully cancelled if name.isEmpty { syncNameAndAddress = true } From cf5ede5700c313000f99f46d0c5d2c261bf1df2c Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Mon, 4 Nov 2024 10:11:01 +0100 Subject: [PATCH 07/22] update strings --- .../en.lproj/Localizable.strings | 47 ++++++++------- ElementX/Sources/Generated/Strings.swift | 57 ++++++++++++------- 2 files changed, 63 insertions(+), 41 deletions(-) diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index 612e96c04e..4200cde50f 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -115,8 +115,8 @@ "banner_migrate_to_native_sliding_sync_description" = "Your server now supports a new, faster protocol. Log out and log back in to upgrade now. Doing this now will help you avoid a forced logout when the old protocol is removed later."; "banner_migrate_to_native_sliding_sync_force_logout_title" = "Your homeserver no longer supports the old protocol. Please log out and log back in to continue using the app."; "banner_migrate_to_native_sliding_sync_title" = "Upgrade available"; -"banner.set_up_recovery.content" = "Generate a new recovery key that can be used to restore your encrypted message history in case you lose access to your devices."; -"banner.set_up_recovery.title" = "Set up recovery"; +"banner_set_up_recovery_content" = "Recover your cryptographic identity and message history with a recovery key if you have lost all your existing devices."; +"banner_set_up_recovery_title" = "Set up recovery to protect your account"; "common_about" = "About"; "common_acceptable_use_policy" = "Acceptable use policy"; "common_advanced_settings" = "Advanced settings"; @@ -150,6 +150,7 @@ "common_favourited" = "Favourited"; "common_file" = "File"; "common_forward_message" = "Forward message"; +"common_frequently_used" = "Frequently used"; "common_gif" = "GIF"; "common_image" = "Image"; "common_in_reply_to" = "In reply to %1$@"; @@ -232,6 +233,7 @@ "common_verification_failed" = "Verification failed"; "common_verified" = "Verified"; "common_verify_device" = "Verify device"; +"common_verify_identity" = "Verify identity"; "common_video" = "Video"; "common_voice_message" = "Voice message"; "common_waiting" = "Waiting…"; @@ -244,8 +246,10 @@ "common.you" = "You"; "common_unable_to_decrypt_insecure_device" = "Sent from an insecure device"; "common_unable_to_decrypt_verification_violation" = "Sender's verified identity has changed"; -"confirm_recovery_key_banner_message" = "Your chat backup is currently out of sync. You need to enter your recovery key to maintain access to your chat backup."; -"confirm_recovery_key_banner_title" = "Enter your recovery key"; +"confirm_recovery_key_banner_message" = "Confirm your recovery key to maintain access to your key storage and message history."; +"confirm_recovery_key_banner_primary_button_title" = "Enter your recovery key"; +"confirm_recovery_key_banner_secondary_button_title" = "Forgot your recovery key?"; +"confirm_recovery_key_banner_title" = "Your key storage is out of sync"; "crash_detection_dialog_content" = "%1$@ crashed the last time it was used. Would you like to share a crash report with us?"; "crypto_identity_change_pin_violation" = "%1$@'s identity appears to have changed. %2$@"; "crypto_identity_change_pin_violation_new" = "%1$@’s %2$@ identity appears to have changed. %3$@"; @@ -340,6 +344,7 @@ "rich_text_editor_unindent" = "Unindent"; "rich_text_editor_url_placeholder" = "Link"; "rich_text_editor_a11y_add_attachment" = "Add attachment"; +"rich_text_editor_composer_caption_placeholder" = "Optional caption…"; "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."; @@ -387,6 +392,8 @@ "screen_account_provider_signup_title" = "You’re about to create an account on %@"; "screen_advanced_settings_developer_mode" = "Developer mode"; "screen_advanced_settings_developer_mode_description" = "Enable to have access to features and functionality for developers."; +"screen_advanced_settings_media_compression_description" = "Upload photos and videos faster and reduce data usage"; +"screen_advanced_settings_media_compression_title" = "Optimise media quality"; "screen_advanced_settings_rich_text_editor_description" = "Disable the rich text editor to type Markdown manually."; "screen_advanced_settings_send_read_receipts" = "Read receipts"; "screen_advanced_settings_send_read_receipts_description" = "If turned off, your read receipts won't be sent to anyone. You will still receive read receipts from other users."; @@ -451,16 +458,16 @@ "screen_change_server_form_notice" = "You can only connect to an existing server that supports sliding sync. Your homeserver admin will need to configure it. %1$@"; "screen_change_server_subtitle" = "What is the address of your server?"; "screen_change_server_title" = "Select your server"; -"screen_chat_backup_key_backup_action_disable" = "Turn off backup"; +"screen_chat_backup_key_backup_action_disable" = "Delete key storage"; "screen_chat_backup_key_backup_action_enable" = "Turn on backup"; "screen_chat_backup_key_backup_description" = "Store your cryptographic identity and message keys securely on the server. This will allow you to view your message history on any new devices. %1$@."; "screen_chat_backup_key_backup_title" = "Key storage"; +"screen_chat_backup_key_storage_disabled_error" = "Key storage must be turned on to set up recovery."; "screen_chat_backup_key_storage_toggle_description" = "Upload keys from this device"; "screen_chat_backup_key_storage_toggle_title" = "Allow key storage"; "screen_chat_backup_recovery_action_change" = "Change recovery key"; "screen_chat_backup_recovery_action_change_description" = "Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices."; -"screen_chat_backup_recovery_action_confirm_description" = "Your chat backup is currently out of sync."; -"screen_chat_backup_recovery_action_setup" = "Set up recovery"; +"screen_chat_backup_recovery_action_confirm_description" = "Your key storage is currently out of sync."; "screen_chat_backup_recovery_action_setup_description" = "Get access to your encrypted messages if you lose all your devices or are signed out of %1$@ everywhere."; "screen_create_account_title" = "Create account"; "screen_create_new_recovery_key_list_item_1" = "Open %1$@ in a desktop device"; @@ -535,10 +542,10 @@ "screen_key_backup_disable_confirmation_action_turn_off" = "Turn off"; "screen_key_backup_disable_confirmation_description" = "You will lose your encrypted messages if you are signed out of all devices."; "screen_key_backup_disable_confirmation_title" = "Are you sure you want to turn off backup?"; -"screen_key_backup_disable_description" = "Turning off backup will remove your current encryption key backup and turn off other security features. In this case, you will:"; -"screen_key_backup_disable_description_point_1" = "Not have encrypted message history on new devices"; -"screen_key_backup_disable_description_point_2" = "Lose access to your encrypted messages if you are signed out of %1$@ everywhere"; -"screen_key_backup_disable_title" = "Are you sure you want to turn off backup?"; +"screen_key_backup_disable_description" = "Deleting key storage will remove your cryptographic identity and message keys from the server and turn off the following security features:"; +"screen_key_backup_disable_description_point_1" = "You will not have encrypted message history on new devices"; +"screen_key_backup_disable_description_point_2" = "You will lose access to your encrypted messages if you are signed out of %1$@ everywhere"; +"screen_key_backup_disable_title" = "Are you sure you want to turn off key storage and delete it?"; "screen_login_error_deactivated_account" = "This account has been deactivated."; "screen_login_error_invalid_credentials" = "Incorrect username and/or password"; "screen_login_error_invalid_user_id" = "This is not a valid user identifier. Expected format: ‘@user:homeserver.org’"; @@ -631,12 +638,11 @@ "screen_qr_code_login_verify_code_title" = "Your verification code"; "screen_recovery_key_change_description" = "Get a new recovery key if you've lost your existing one. After changing your recovery key, your old one will no longer work."; "screen_recovery_key_change_generate_key" = "Generate a new recovery key"; -"screen_recovery_key_change_generate_key_description" = "Make sure you can store your recovery key somewhere safe"; "screen_recovery_key_change_success" = "Recovery key changed"; "screen_recovery_key_change_title" = "Change recovery key?"; "screen_recovery_key_confirm_create_new_recovery_key" = "Create new recovery key"; "screen_recovery_key_confirm_description" = "Make sure nobody can see this screen!"; -"screen_recovery_key_confirm_error_content" = "Please try again to confirm access to your chat backup."; +"screen_recovery_key_confirm_error_content" = "Please try again to confirm access to your key storage."; "screen_recovery_key_confirm_error_title" = "Incorrect recovery key"; "screen_recovery_key_confirm_key_description" = "If you have a security key or security phrase, this will work too."; "screen_recovery_key_confirm_key_placeholder" = "Enter…"; @@ -645,14 +651,14 @@ "screen_recovery_key_copied_to_clipboard" = "Copied recovery key"; "screen_recovery_key_generating_key" = "Generating…"; "screen_recovery_key_save_action" = "Save recovery key"; -"screen_recovery_key_save_description" = "Write down your recovery key somewhere safe or save it in a password manager."; +"screen_recovery_key_save_description" = "Write down this recovery key somewhere safe, like a password manager, encrypted note, or a physical safe."; "screen_recovery_key_save_key_description" = "Tap to copy recovery key"; -"screen_recovery_key_save_title" = "Save your recovery key"; +"screen_recovery_key_save_title" = "Save your recovery key somewhere safe"; "screen_recovery_key_setup_confirmation_description" = "You will not be able to access your new recovery key after this step."; "screen_recovery_key_setup_confirmation_title" = "Have you saved your recovery key?"; -"screen_recovery_key_setup_description" = "Your chat backup is protected by a recovery key. If you need a new recovery key after setup you can recreate by selecting ‘Change recovery key’."; +"screen_recovery_key_setup_description" = "Your key storage is protected by a recovery key. If you need a new recovery key after setup, you can recreate it by selecting ‘Change recovery key’."; "screen_recovery_key_setup_generate_key" = "Generate your recovery key"; -"screen_recovery_key_setup_generate_key_description" = "Make sure you can store your recovery key somewhere safe"; +"screen_recovery_key_setup_generate_key_description" = "Do not share this with anyone!"; "screen_recovery_key_setup_success" = "Recovery setup successful"; "screen_recovery_key_setup_title" = "Set up recovery"; "screen_report_content_block_user_hint" = "Check if you want to hide all current and future messages from this user"; @@ -784,7 +790,6 @@ "screen_room_timeline_less_reactions" = "Show less"; "screen_room_timeline_message_copied" = "Message copied"; "screen_room_timeline_no_permission_to_post" = "You do not have permission to post to this room"; -"screen_room_timeline_reactions_show_less" = "Show less"; "screen_room_timeline_reactions_show_more" = "Show more"; "screen_room_timeline_read_marker_title" = "New"; "screen_room_title" = "Chat"; @@ -834,7 +839,6 @@ "screen_session_verification_ready_subtitle" = "Compare a unique set of emojis."; "screen_session_verification_request_accepted_subtitle" = "Compare the unique emoji, ensuring they appear in the same order."; "screen_session_verification_request_details_timestamp" = "Signed in"; -"screen_session_verification_request_failure_subtitle" = "Either the request timed out, the request was denied, or there was a verification mismatch."; "screen_session_verification_request_failure_title" = "Verification failed"; "screen_session_verification_request_footer" = "Only continue if you initiated this verification."; "screen_session_verification_request_subtitle" = "Verify the other device to keep your message history secure."; @@ -990,6 +994,7 @@ "troubleshoot_notifications_test_unified_push_failure" = "No push distributors found."; "troubleshoot_notifications_test_unified_push_title" = "Check UnifiedPush"; "a11y_poll" = "Poll"; +"banner_set_up_recovery_submit" = "Set up recovery"; "dialog_title_error" = "Error"; "dialog_title_success" = "Success"; "notification_fallback_content" = "Notification"; @@ -1010,6 +1015,7 @@ "screen_blocked_users_unblock_alert_title" = "Unblock user"; "screen_bug_report_rash_logs_alert_title" = "%1$@ crashed the last time it was used. Would you like to share a crash report with us?"; "screen_chat_backup_recovery_action_confirm" = "Enter recovery key"; +"screen_chat_backup_recovery_action_setup" = "Set up recovery"; "screen_create_poll_cancel_confirmation_content_ios" = "Your changes won’t be saved"; "screen_create_room_add_people_title" = "Invite people"; "screen_create_room_room_name_label" = "Room name"; @@ -1026,6 +1032,7 @@ "screen_login_subtitle" = "Matrix is an open network for secure, decentralised communication."; "screen_notification_settings_mentions_section_title" = "Mentions"; "screen_qr_code_login_invalid_scan_state_retry_button" = "Try again"; +"screen_recovery_key_change_generate_key_description" = "Do not share this with anyone!"; "screen_recovery_key_confirm_title" = "Enter your recovery key"; "screen_report_content_block_user" = "Block user"; "screen_reset_encryption_password_placeholder" = "Enter…"; @@ -1049,8 +1056,10 @@ "screen_room_error_failed_processing_media" = "Failed processing media to upload, please try again."; "screen_room_member_list_manage_member_remove_confirmation_ban" = "Remove and ban member"; "screen_room_notification_settings_mode_mentions_and_keywords" = "Mentions and Keywords only"; +"screen_room_timeline_reactions_show_less" = "Show less"; "screen_roomlist_filter_people" = "People"; "screen_server_confirmation_change_server" = "Change account provider"; +"screen_session_verification_request_failure_subtitle" = "Either the request timed out, the request was denied, or there was a verification mismatch."; "screen_signout_confirmation_dialog_submit" = "Sign out"; "screen_signout_confirmation_dialog_title" = "Sign out"; "screen_signout_key_backup_offline_title" = "Your keys are still being backed up"; diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index 68f54bee57..2c89b62e8c 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -266,6 +266,12 @@ internal enum L10n { internal static var bannerMigrateToNativeSlidingSyncForceLogoutTitle: String { return L10n.tr("Localizable", "banner_migrate_to_native_sliding_sync_force_logout_title") } /// Upgrade available internal static var bannerMigrateToNativeSlidingSyncTitle: String { return L10n.tr("Localizable", "banner_migrate_to_native_sliding_sync_title") } + /// Recover your cryptographic identity and message history with a recovery key if you have lost all your existing devices. + internal static var bannerSetUpRecoveryContent: String { return L10n.tr("Localizable", "banner_set_up_recovery_content") } + /// Set up recovery + internal static var bannerSetUpRecoverySubmit: String { return L10n.tr("Localizable", "banner_set_up_recovery_submit") } + /// Set up recovery to protect your account + internal static var bannerSetUpRecoveryTitle: String { return L10n.tr("Localizable", "banner_set_up_recovery_title") } /// About internal static var commonAbout: String { return L10n.tr("Localizable", "common_about") } /// Acceptable use policy @@ -334,6 +340,8 @@ internal enum L10n { internal static var commonFile: String { return L10n.tr("Localizable", "common_file") } /// Forward message internal static var commonForwardMessage: String { return L10n.tr("Localizable", "common_forward_message") } + /// Frequently used + internal static var commonFrequentlyUsed: String { return L10n.tr("Localizable", "common_frequently_used") } /// GIF internal static var commonGif: String { return L10n.tr("Localizable", "common_gif") } /// Image @@ -518,6 +526,8 @@ internal enum L10n { internal static var commonVerified: String { return L10n.tr("Localizable", "common_verified") } /// Verify device internal static var commonVerifyDevice: String { return L10n.tr("Localizable", "common_verify_device") } + /// Verify identity + internal static var commonVerifyIdentity: String { return L10n.tr("Localizable", "common_verify_identity") } /// Video internal static var commonVideo: String { return L10n.tr("Localizable", "common_video") } /// Voice message @@ -526,9 +536,13 @@ internal enum L10n { internal static var commonWaiting: String { return L10n.tr("Localizable", "common_waiting") } /// Waiting for this message internal static var commonWaitingForDecryptionKey: String { return L10n.tr("Localizable", "common_waiting_for_decryption_key") } - /// Your chat backup is currently out of sync. You need to enter your recovery key to maintain access to your chat backup. + /// Confirm your recovery key to maintain access to your key storage and message history. internal static var confirmRecoveryKeyBannerMessage: String { return L10n.tr("Localizable", "confirm_recovery_key_banner_message") } /// Enter your recovery key + internal static var confirmRecoveryKeyBannerPrimaryButtonTitle: String { return L10n.tr("Localizable", "confirm_recovery_key_banner_primary_button_title") } + /// Forgot your recovery key? + internal static var confirmRecoveryKeyBannerSecondaryButtonTitle: String { return L10n.tr("Localizable", "confirm_recovery_key_banner_secondary_button_title") } + /// Your key storage is out of sync internal static var confirmRecoveryKeyBannerTitle: String { return L10n.tr("Localizable", "confirm_recovery_key_banner_title") } /// %1$@ crashed the last time it was used. Would you like to share a crash report with us? internal static func crashDetectionDialogContent(_ p1: Any) -> String { @@ -766,6 +780,8 @@ internal enum L10n { internal static var richTextEditorCloseFormattingOptions: String { return L10n.tr("Localizable", "rich_text_editor_close_formatting_options") } /// Toggle code block internal static var richTextEditorCodeBlock: String { return L10n.tr("Localizable", "rich_text_editor_code_block") } + /// Optional caption… + internal static var richTextEditorComposerCaptionPlaceholder: String { return L10n.tr("Localizable", "rich_text_editor_composer_caption_placeholder") } /// Message… internal static var richTextEditorComposerPlaceholder: String { return L10n.tr("Localizable", "rich_text_editor_composer_placeholder") } /// Create a link @@ -832,6 +848,10 @@ internal enum L10n { internal static var screenAdvancedSettingsElementCallBaseUrlDescription: String { return L10n.tr("Localizable", "screen_advanced_settings_element_call_base_url_description") } /// Invalid URL, please make sure you include the protocol (http/https) and the correct address. internal static var screenAdvancedSettingsElementCallBaseUrlValidationError: String { return L10n.tr("Localizable", "screen_advanced_settings_element_call_base_url_validation_error") } + /// Upload photos and videos faster and reduce data usage + internal static var screenAdvancedSettingsMediaCompressionDescription: String { return L10n.tr("Localizable", "screen_advanced_settings_media_compression_description") } + /// Optimise media quality + internal static var screenAdvancedSettingsMediaCompressionTitle: String { return L10n.tr("Localizable", "screen_advanced_settings_media_compression_title") } /// Disable the rich text editor to type Markdown manually. internal static var screenAdvancedSettingsRichTextEditorDescription: String { return L10n.tr("Localizable", "screen_advanced_settings_rich_text_editor_description") } /// Read receipts @@ -1003,7 +1023,7 @@ internal enum L10n { internal static var screenChangeServerSubtitle: String { return L10n.tr("Localizable", "screen_change_server_subtitle") } /// Select your server internal static var screenChangeServerTitle: String { return L10n.tr("Localizable", "screen_change_server_title") } - /// Turn off backup + /// Delete key storage internal static var screenChatBackupKeyBackupActionDisable: String { return L10n.tr("Localizable", "screen_chat_backup_key_backup_action_disable") } /// Turn on backup internal static var screenChatBackupKeyBackupActionEnable: String { return L10n.tr("Localizable", "screen_chat_backup_key_backup_action_enable") } @@ -1013,6 +1033,8 @@ internal enum L10n { } /// Key storage internal static var screenChatBackupKeyBackupTitle: String { return L10n.tr("Localizable", "screen_chat_backup_key_backup_title") } + /// Key storage must be turned on to set up recovery. + internal static var screenChatBackupKeyStorageDisabledError: String { return L10n.tr("Localizable", "screen_chat_backup_key_storage_disabled_error") } /// Upload keys from this device internal static var screenChatBackupKeyStorageToggleDescription: String { return L10n.tr("Localizable", "screen_chat_backup_key_storage_toggle_description") } /// Allow key storage @@ -1023,7 +1045,7 @@ internal enum L10n { internal static var screenChatBackupRecoveryActionChangeDescription: String { return L10n.tr("Localizable", "screen_chat_backup_recovery_action_change_description") } /// Enter recovery key internal static var screenChatBackupRecoveryActionConfirm: String { return L10n.tr("Localizable", "screen_chat_backup_recovery_action_confirm") } - /// Your chat backup is currently out of sync. + /// Your key storage is currently out of sync. internal static var screenChatBackupRecoveryActionConfirmDescription: String { return L10n.tr("Localizable", "screen_chat_backup_recovery_action_confirm_description") } /// Set up recovery internal static var screenChatBackupRecoveryActionSetup: String { return L10n.tr("Localizable", "screen_chat_backup_recovery_action_setup") } @@ -1252,15 +1274,15 @@ internal enum L10n { internal static var screenKeyBackupDisableConfirmationDescription: String { return L10n.tr("Localizable", "screen_key_backup_disable_confirmation_description") } /// Are you sure you want to turn off backup? internal static var screenKeyBackupDisableConfirmationTitle: String { return L10n.tr("Localizable", "screen_key_backup_disable_confirmation_title") } - /// Turning off backup will remove your current encryption key backup and turn off other security features. In this case, you will: + /// Deleting key storage will remove your cryptographic identity and message keys from the server and turn off the following security features: internal static var screenKeyBackupDisableDescription: String { return L10n.tr("Localizable", "screen_key_backup_disable_description") } - /// Not have encrypted message history on new devices + /// You will not have encrypted message history on new devices internal static var screenKeyBackupDisableDescriptionPoint1: String { return L10n.tr("Localizable", "screen_key_backup_disable_description_point_1") } - /// Lose access to your encrypted messages if you are signed out of %1$@ everywhere + /// You will lose access to your encrypted messages if you are signed out of %1$@ everywhere internal static func screenKeyBackupDisableDescriptionPoint2(_ p1: Any) -> String { return L10n.tr("Localizable", "screen_key_backup_disable_description_point_2", String(describing: p1)) } - /// Are you sure you want to turn off backup? + /// Are you sure you want to turn off key storage and delete it? internal static var screenKeyBackupDisableTitle: String { return L10n.tr("Localizable", "screen_key_backup_disable_title") } /// This account has been deactivated. internal static var screenLoginErrorDeactivatedAccount: String { return L10n.tr("Localizable", "screen_login_error_deactivated_account") } @@ -1490,7 +1512,7 @@ internal enum L10n { internal static var screenRecoveryKeyChangeDescription: String { return L10n.tr("Localizable", "screen_recovery_key_change_description") } /// Generate a new recovery key internal static var screenRecoveryKeyChangeGenerateKey: String { return L10n.tr("Localizable", "screen_recovery_key_change_generate_key") } - /// Make sure you can store your recovery key somewhere safe + /// Do not share this with anyone! internal static var screenRecoveryKeyChangeGenerateKeyDescription: String { return L10n.tr("Localizable", "screen_recovery_key_change_generate_key_description") } /// Recovery key changed internal static var screenRecoveryKeyChangeSuccess: String { return L10n.tr("Localizable", "screen_recovery_key_change_success") } @@ -1500,7 +1522,7 @@ internal enum L10n { internal static var screenRecoveryKeyConfirmCreateNewRecoveryKey: String { return L10n.tr("Localizable", "screen_recovery_key_confirm_create_new_recovery_key") } /// Make sure nobody can see this screen! internal static var screenRecoveryKeyConfirmDescription: String { return L10n.tr("Localizable", "screen_recovery_key_confirm_description") } - /// Please try again to confirm access to your chat backup. + /// Please try again to confirm access to your key storage. internal static var screenRecoveryKeyConfirmErrorContent: String { return L10n.tr("Localizable", "screen_recovery_key_confirm_error_content") } /// Incorrect recovery key internal static var screenRecoveryKeyConfirmErrorTitle: String { return L10n.tr("Localizable", "screen_recovery_key_confirm_error_title") } @@ -1520,21 +1542,21 @@ internal enum L10n { internal static var screenRecoveryKeyGeneratingKey: String { return L10n.tr("Localizable", "screen_recovery_key_generating_key") } /// Save recovery key internal static var screenRecoveryKeySaveAction: String { return L10n.tr("Localizable", "screen_recovery_key_save_action") } - /// Write down your recovery key somewhere safe or save it in a password manager. + /// Write down this recovery key somewhere safe, like a password manager, encrypted note, or a physical safe. internal static var screenRecoveryKeySaveDescription: String { return L10n.tr("Localizable", "screen_recovery_key_save_description") } /// Tap to copy recovery key internal static var screenRecoveryKeySaveKeyDescription: String { return L10n.tr("Localizable", "screen_recovery_key_save_key_description") } - /// Save your recovery key + /// Save your recovery key somewhere safe internal static var screenRecoveryKeySaveTitle: String { return L10n.tr("Localizable", "screen_recovery_key_save_title") } /// You will not be able to access your new recovery key after this step. internal static var screenRecoveryKeySetupConfirmationDescription: String { return L10n.tr("Localizable", "screen_recovery_key_setup_confirmation_description") } /// Have you saved your recovery key? internal static var screenRecoveryKeySetupConfirmationTitle: String { return L10n.tr("Localizable", "screen_recovery_key_setup_confirmation_title") } - /// Your chat backup is protected by a recovery key. If you need a new recovery key after setup you can recreate by selecting ‘Change recovery key’. + /// Your key storage is protected by a recovery key. If you need a new recovery key after setup, you can recreate it by selecting ‘Change recovery key’. internal static var screenRecoveryKeySetupDescription: String { return L10n.tr("Localizable", "screen_recovery_key_setup_description") } /// Generate your recovery key internal static var screenRecoveryKeySetupGenerateKey: String { return L10n.tr("Localizable", "screen_recovery_key_setup_generate_key") } - /// Make sure you can store your recovery key somewhere safe + /// Do not share this with anyone! internal static var screenRecoveryKeySetupGenerateKeyDescription: String { return L10n.tr("Localizable", "screen_recovery_key_setup_generate_key_description") } /// Recovery setup successful internal static var screenRecoveryKeySetupSuccess: String { return L10n.tr("Localizable", "screen_recovery_key_setup_success") } @@ -2491,15 +2513,6 @@ internal enum L10n { /// Check UnifiedPush internal static var troubleshootNotificationsTestUnifiedPushTitle: String { return L10n.tr("Localizable", "troubleshoot_notifications_test_unified_push_title") } - internal enum Banner { - internal enum SetUpRecovery { - /// Generate a new recovery key that can be used to restore your encrypted message history in case you lose access to your devices. - internal static var content: String { return L10n.tr("Localizable", "banner.set_up_recovery.content") } - /// Set up recovery - internal static var title: String { return L10n.tr("Localizable", "banner.set_up_recovery.title") } - } - } - internal enum Common { /// Copied to clipboard internal static var copiedToClipboard: String { return L10n.tr("Localizable", "common.copied_to_clipboard") } From 5e61df64c66bdff6f0ac55a683b92413615b7337 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Tue, 5 Nov 2024 14:48:17 +0100 Subject: [PATCH 08/22] handling the alias --- .../Mocks/Generated/GeneratedMocks.swift | 118 ++++++++++--- .../Mocks/Generated/SDKGeneratedMocks.swift | 160 +++++++++++++++++- .../Screens/CreateRoom/CreateRoomModels.swift | 7 + .../CreateRoom/CreateRoomViewModel.swift | 33 +++- .../Sources/Services/Client/ClientProxy.swift | 43 ++++- .../Services/Client/ClientProxyProtocol.swift | 11 +- .../CreateRoom/CreateRoomFlowParameters.swift | 1 + 7 files changed, 337 insertions(+), 36 deletions(-) diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index a7ff24b3df..6deff1cafd 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -2629,15 +2629,15 @@ class ClientProxyMock: ClientProxyProtocol { } //MARK: - createRoom - var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingCallsCount = 0 - var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCallsCount: Int { + var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasUnderlyingCallsCount = 0 + var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasCallsCount: Int { get { if Thread.isMainThread { - return createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingCallsCount + return createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingCallsCount + returnValue = createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasUnderlyingCallsCount } return returnValue! @@ -2645,29 +2645,29 @@ class ClientProxyMock: ClientProxyProtocol { } set { if Thread.isMainThread { - createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingCallsCount = newValue + createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingCallsCount = newValue + createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasUnderlyingCallsCount = newValue } } } } - var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCalled: Bool { - return createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCallsCount > 0 + var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasCalled: Bool { + return createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasCallsCount > 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 createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasReceivedArguments: (name: String, topic: String?, isRoomPrivate: Bool, isKnockingOnly: Bool, userIDs: [String], avatarURL: URL?, canonicalAlias: String?)? + var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasReceivedInvocations: [(name: String, topic: String?, isRoomPrivate: Bool, isKnockingOnly: Bool, userIDs: [String], avatarURL: URL?, canonicalAlias: String?)] = [] - var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingReturnValue: Result! - var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLReturnValue: Result! { + var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasUnderlyingReturnValue: Result! + var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasReturnValue: Result! { get { if Thread.isMainThread { - return createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingReturnValue + return createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasUnderlyingReturnValue } else { var returnValue: Result? = nil DispatchQueue.main.sync { - returnValue = createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingReturnValue + returnValue = createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasUnderlyingReturnValue } return returnValue! @@ -2675,26 +2675,26 @@ class ClientProxyMock: ClientProxyProtocol { } set { if Thread.isMainThread { - createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingReturnValue = newValue + createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingReturnValue = newValue + createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasUnderlyingReturnValue = newValue } } } } - var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLClosure: ((String, String?, Bool, Bool, [String], URL?) async -> Result)? + var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasClosure: ((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?, canonicalAlias: String?) async -> Result { + createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasCallsCount += 1 + createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasReceivedArguments = (name: name, topic: topic, isRoomPrivate: isRoomPrivate, isKnockingOnly: isKnockingOnly, userIDs: userIDs, avatarURL: avatarURL, canonicalAlias: canonicalAlias) DispatchQueue.main.async { - self.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLReceivedInvocations.append((name: name, topic: topic, isRoomPrivate: isRoomPrivate, isKnockingOnly: isKnockingOnly, userIDs: userIDs, avatarURL: avatarURL)) + self.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasReceivedInvocations.append((name: name, topic: topic, isRoomPrivate: isRoomPrivate, isKnockingOnly: isKnockingOnly, userIDs: userIDs, avatarURL: avatarURL, canonicalAlias: canonicalAlias)) } - if let createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLClosure = createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLClosure { - return await createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLClosure(name, topic, isRoomPrivate, isKnockingOnly, userIDs, avatarURL) + if let createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasClosure = createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasClosure { + return await createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasClosure(name, topic, isRoomPrivate, isKnockingOnly, userIDs, avatarURL, canonicalAlias) } else { - return createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLReturnValue + return createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasReturnValue } } //MARK: - joinRoom @@ -3974,6 +3974,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/Mocks/Generated/SDKGeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift index 94918f2205..44c527cc32 100644 --- a/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift @@ -2056,6 +2056,81 @@ open class ClientSDKMock: MatrixRustSDK.Client { } } + //MARK: - isRoomAliasAvailable + + open var isRoomAliasAvailableAliasThrowableError: Error? + var isRoomAliasAvailableAliasUnderlyingCallsCount = 0 + open var isRoomAliasAvailableAliasCallsCount: Int { + get { + if Thread.isMainThread { + return isRoomAliasAvailableAliasUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = isRoomAliasAvailableAliasUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + isRoomAliasAvailableAliasUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + isRoomAliasAvailableAliasUnderlyingCallsCount = newValue + } + } + } + } + open var isRoomAliasAvailableAliasCalled: Bool { + return isRoomAliasAvailableAliasCallsCount > 0 + } + open var isRoomAliasAvailableAliasReceivedAlias: String? + open var isRoomAliasAvailableAliasReceivedInvocations: [String] = [] + + var isRoomAliasAvailableAliasUnderlyingReturnValue: Bool! + open var isRoomAliasAvailableAliasReturnValue: Bool! { + get { + if Thread.isMainThread { + return isRoomAliasAvailableAliasUnderlyingReturnValue + } else { + var returnValue: Bool? = nil + DispatchQueue.main.sync { + returnValue = isRoomAliasAvailableAliasUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + isRoomAliasAvailableAliasUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + isRoomAliasAvailableAliasUnderlyingReturnValue = newValue + } + } + } + } + open var isRoomAliasAvailableAliasClosure: ((String) async throws -> Bool)? + + open override func isRoomAliasAvailable(alias: String) async throws -> Bool { + if let error = isRoomAliasAvailableAliasThrowableError { + throw error + } + isRoomAliasAvailableAliasCallsCount += 1 + isRoomAliasAvailableAliasReceivedAlias = alias + DispatchQueue.main.async { + self.isRoomAliasAvailableAliasReceivedInvocations.append(alias) + } + if let isRoomAliasAvailableAliasClosure = isRoomAliasAvailableAliasClosure { + return try await isRoomAliasAvailableAliasClosure(alias) + } else { + return isRoomAliasAvailableAliasReturnValue + } + } + //MARK: - joinRoomById open var joinRoomByIdRoomIdThrowableError: Error? @@ -2676,13 +2751,13 @@ open class ClientSDKMock: MatrixRustSDK.Client { open var resolveRoomAliasRoomAliasReceivedRoomAlias: String? open var resolveRoomAliasRoomAliasReceivedInvocations: [String] = [] - var resolveRoomAliasRoomAliasUnderlyingReturnValue: ResolvedRoomAlias! - open var resolveRoomAliasRoomAliasReturnValue: ResolvedRoomAlias! { + var resolveRoomAliasRoomAliasUnderlyingReturnValue: ResolvedRoomAlias? + open var resolveRoomAliasRoomAliasReturnValue: ResolvedRoomAlias? { get { if Thread.isMainThread { return resolveRoomAliasRoomAliasUnderlyingReturnValue } else { - var returnValue: ResolvedRoomAlias? = nil + var returnValue: ResolvedRoomAlias?? = nil DispatchQueue.main.sync { returnValue = resolveRoomAliasRoomAliasUnderlyingReturnValue } @@ -2700,9 +2775,9 @@ open class ClientSDKMock: MatrixRustSDK.Client { } } } - open var resolveRoomAliasRoomAliasClosure: ((String) async throws -> ResolvedRoomAlias)? + open var resolveRoomAliasRoomAliasClosure: ((String) async throws -> ResolvedRoomAlias?)? - open override func resolveRoomAlias(roomAlias: String) async throws -> ResolvedRoomAlias { + open override func resolveRoomAlias(roomAlias: String) async throws -> ResolvedRoomAlias? { if let error = resolveRoomAliasRoomAliasThrowableError { throw error } @@ -2764,6 +2839,81 @@ open class ClientSDKMock: MatrixRustSDK.Client { try await restoreSessionSessionClosure?(session) } + //MARK: - roomAliasExists + + open var roomAliasExistsRoomAliasThrowableError: Error? + var roomAliasExistsRoomAliasUnderlyingCallsCount = 0 + open var roomAliasExistsRoomAliasCallsCount: Int { + get { + if Thread.isMainThread { + return roomAliasExistsRoomAliasUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = roomAliasExistsRoomAliasUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + roomAliasExistsRoomAliasUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + roomAliasExistsRoomAliasUnderlyingCallsCount = newValue + } + } + } + } + open var roomAliasExistsRoomAliasCalled: Bool { + return roomAliasExistsRoomAliasCallsCount > 0 + } + open var roomAliasExistsRoomAliasReceivedRoomAlias: String? + open var roomAliasExistsRoomAliasReceivedInvocations: [String] = [] + + var roomAliasExistsRoomAliasUnderlyingReturnValue: Bool! + open var roomAliasExistsRoomAliasReturnValue: Bool! { + get { + if Thread.isMainThread { + return roomAliasExistsRoomAliasUnderlyingReturnValue + } else { + var returnValue: Bool? = nil + DispatchQueue.main.sync { + returnValue = roomAliasExistsRoomAliasUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + roomAliasExistsRoomAliasUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + roomAliasExistsRoomAliasUnderlyingReturnValue = newValue + } + } + } + } + open var roomAliasExistsRoomAliasClosure: ((String) async throws -> Bool)? + + open override func roomAliasExists(roomAlias: String) async throws -> Bool { + if let error = roomAliasExistsRoomAliasThrowableError { + throw error + } + roomAliasExistsRoomAliasCallsCount += 1 + roomAliasExistsRoomAliasReceivedRoomAlias = roomAlias + DispatchQueue.main.async { + self.roomAliasExistsRoomAliasReceivedInvocations.append(roomAlias) + } + if let roomAliasExistsRoomAliasClosure = roomAliasExistsRoomAliasClosure { + return try await roomAliasExistsRoomAliasClosure(roomAlias) + } else { + return roomAliasExistsRoomAliasReturnValue + } + } + //MARK: - roomDirectorySearch var roomDirectorySearchUnderlyingCallsCount = 0 diff --git a/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift b/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift index d66b0cf19e..7cae74d221 100644 --- a/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift +++ b/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift @@ -35,6 +35,8 @@ struct CreateRoomViewState: BindableState { var canCreateRoom: Bool { roomName.isEmpty } + + var errorState: CreateRoomAliasErrorState? } struct CreateRoomViewStateBindings { @@ -56,3 +58,8 @@ enum CreateRoomViewAction { case updateName(String) case updateAddress(String) } + +enum CreateRoomAliasErrorState { + case alreadyExists + case invalidSymbols +} diff --git a/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift b/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift index fe4718c53d..d714b16dd8 100644 --- a/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift +++ b/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift @@ -140,6 +140,11 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol createRoomParameters.topic = state.bindings.roomTopic createRoomParameters.isRoomPrivate = state.bindings.isRoomPrivate createRoomParameters.isKnockingOnly = state.bindings.isKnockingOnly + if !state.addressName.isEmpty { + createRoomParameters.canonicalAlias = "#\(state.addressName)\(state.homeserver)" + } else { + createRoomParameters.canonicalAlias = nil + } } private func createRoom() async { @@ -150,6 +155,25 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol // Since the parameters are throttled, we need to make sure that the latest values are used updateParameters(state: state) + + if !createRoomParameters.isRoomPrivate { + guard let alias = createRoomParameters.canonicalAlias else { + state.errorState = .invalidSymbols + return + } + + switch await userSession.clientProxy.isAliasAvailable(alias) { + case .success(true): + break + case .success(false): + state.errorState = .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) { @@ -182,7 +206,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, + canonicalAlias: createRoomParameters.isRoomPrivate ? nil : createRoomParameters.canonicalAlias) { case .success(let roomId): analytics.trackCreatedRoom(isDM: false) actionsSubject.send(.openRoom(withIdentifier: roomId)) @@ -211,6 +236,10 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol private extension String { var toValidAddress: Self { - split(separator: " ").joined(separator: "-").lowercased() + split(separator: " ") + .joined(separator: "-") + // Characters that can be % encoded need to be excluded + .replacingOccurrences(of: "[!#$&'()*+,/:;=?@\\[\\]]", with: "", options: .regularExpression) + .lowercased() } } diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index 46ba30a3d2..8842cd4cfa 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) @@ -381,9 +397,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?, + canonicalAlias: String?) async -> Result { do { - // TODO: Revisit once the SDK supports the knocking API let parameters = CreateRoomParameters(name: name, topic: topic, isEncrypted: isRoomPrivate, @@ -392,7 +413,8 @@ class ClientProxy: ClientProxyProtocol { preset: isRoomPrivate ? .privateChat : .publicChat, invite: userIDs, avatar: avatarURL?.absoluteString, - powerLevelContentOverride: Self.roomCreationPowerLevelOverrides) + powerLevelContentOverride: isKnockingOnly ? Self.knockingRoomCreationPowerLevelOverrides : Self.roomCreationPowerLevelOverrides, + canonicalAlias: canonicalAlias) let roomID = try await client.createRoom(request: parameters) await waitForRoomToSync(roomID: roomID) @@ -629,7 +651,10 @@ class ClientProxy: ClientProxyProtocol { func resolveRoomAlias(_ alias: String) async -> Result { do { - let resolvedAlias = try await client.resolveRoomAlias(roomAlias: alias) + guard let resolvedAlias = try await client.resolveRoomAlias(roomAlias: alias) else { + MXLog.error("Failed resolving room alias, is nil") + return .failure(.failedResolvingRoomAlias) + } // Resolving aliases is done through the directory/room API which returns too many / all known // vias, which in turn results in invalid join requests. Trim them to something manageable @@ -643,6 +668,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 5957a66133..4b629b1924 100644 --- a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift +++ b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift @@ -36,6 +36,7 @@ enum ClientProxyError: Error { case failedUploadingMedia(Error, MatrixErrorCode) case roomPreviewIsPrivate case failedRetrievingUserIdentity + case failedResolvingRoomAlias } enum SlidingSyncConstants { @@ -134,7 +135,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?, + canonicalAlias: String?) async -> Result func joinRoom(_ roomID: String, via: [String]) async -> Result @@ -174,6 +181,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 d2e03d96dd..d02d47a245 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 canonicalAlias: String? } From b1b2bf34abb37fa27e1a0c13ecddf5c119f9d3b0 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Tue, 5 Nov 2024 23:09:49 +0100 Subject: [PATCH 09/22] alias error state --- ElementX/Sources/Mocks/ClientProxyMock.swift | 2 +- .../Screens/CreateRoom/CreateRoomModels.swift | 2 +- .../CreateRoom/CreateRoomViewModel.swift | 31 ++++++++--- .../CreateRoom/View/CreateRoomScreen.swift | 53 +++++++++++++++++-- .../CreateRoom/CreateRoomFlowParameters.swift | 9 +++- .../Sources/CreateRoomViewModelTests.swift | 10 ++-- 6 files changed, 90 insertions(+), 17 deletions(-) diff --git a/ElementX/Sources/Mocks/ClientProxyMock.swift b/ElementX/Sources/Mocks/ClientProxyMock.swift index d71335c02d..c3cdf00d44 100644 --- a/ElementX/Sources/Mocks/ClientProxyMock.swift +++ b/ElementX/Sources/Mocks/ClientProxyMock.swift @@ -53,7 +53,7 @@ extension ClientProxyMock { canDeactivateAccount = false directRoomForUserIDReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) createDirectRoomWithExpectedRoomNameReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) - createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) + createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) uploadMediaReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) loadUserDisplayNameReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) setUserDisplayNameReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) diff --git a/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift b/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift index 7cae74d221..0c91b62aa4 100644 --- a/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift +++ b/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift @@ -33,7 +33,7 @@ struct CreateRoomViewState: BindableState { var bindings: CreateRoomViewStateBindings var avatarURL: URL? var canCreateRoom: Bool { - roomName.isEmpty + !roomName.isEmpty } var errorState: CreateRoomAliasErrorState? diff --git a/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift b/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift index d714b16dd8..69bfb47e88 100644 --- a/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift +++ b/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift @@ -39,10 +39,10 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol let bindings = CreateRoomViewStateBindings(roomTopic: parameters.topic, isRoomPrivate: parameters.isRoomPrivate) super.init(initialViewState: CreateRoomViewState(roomName: parameters.name, - homeserver: ":\(userSession.clientProxy.serverName ?? "")", + homeserver: userSession.clientProxy.serverName ?? "", isKnockingFeatureEnabled: appSettings.knockingEnabled, selectedUsers: selectedUsers.value, - addressName: parameters.name.toValidAddress, + addressName: parameters.addressName ?? parameters.name.toValidAddress, bindings: bindings), mediaProvider: userSession.mediaProvider) @@ -133,6 +133,23 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol actionsSubject.send(.updateDetails(createRoomParameters)) } .store(in: &cancellables) + + context.$viewState + .map(\.addressName) + .removeDuplicates() + .debounce(for: 0.5, scheduler: DispatchQueue.main) + .sink { [weak self] addressName in + guard let self else { + return + } + // TODO: Check if the alias is valid throught the SDK + if addressName.contains("wrong") { + state.errorState = .invalidSymbols + } else { + state.errorState = nil + } + } + .store(in: &cancellables) } private func updateParameters(state: CreateRoomViewState) { @@ -141,9 +158,9 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol createRoomParameters.isRoomPrivate = state.bindings.isRoomPrivate createRoomParameters.isKnockingOnly = state.bindings.isKnockingOnly if !state.addressName.isEmpty { - createRoomParameters.canonicalAlias = "#\(state.addressName)\(state.homeserver)" + createRoomParameters.addressName = state.addressName } else { - createRoomParameters.canonicalAlias = nil + createRoomParameters.addressName = nil } } @@ -156,8 +173,9 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol // Since the parameters are throttled, we need to make sure that the latest values are used updateParameters(state: state) + let alias = createRoomParameters.canonicalAlias(homeserver: userSession.clientProxy.serverName ?? "") if !createRoomParameters.isRoomPrivate { - guard let alias = createRoomParameters.canonicalAlias else { + guard let alias else { state.errorState = .invalidSymbols return } @@ -207,7 +225,7 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol isKnockingOnly: createRoomParameters.isRoomPrivate ? false : createRoomParameters.isKnockingOnly, userIDs: state.selectedUsers.map(\.userID), avatarURL: avatarURL, - canonicalAlias: createRoomParameters.isRoomPrivate ? nil : createRoomParameters.canonicalAlias) { + canonicalAlias: createRoomParameters.isRoomPrivate ? nil : alias) { case .success(let roomId): analytics.trackCreatedRoom(isDM: false) actionsSubject.send(.openRoom(withIdentifier: roomId)) @@ -235,6 +253,7 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol } private extension String { + // TODO: This will be soon done by the SDK directly var toValidAddress: Self { split(separator: " ") .joined(separator: "-") diff --git a/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift b/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift index 4fe0d6b319..7a0baf8ec1 100644 --- a/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift +++ b/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift @@ -194,7 +194,7 @@ struct CreateRoomScreen: View { .font(.compound.bodyLG) .foregroundStyle(.compound.textPrimary) .padding(.horizontal, 8) - Text(context.viewState.homeserver) + Text(":\(context.viewState.homeserver)") .font(.compound.bodyLG) .foregroundStyle(.compound.textSecondary) } @@ -203,9 +203,25 @@ struct CreateRoomScreen: View { Text(L10n.screenCreateRoomRoomAddressSectionTitle.uppercased()) .compoundListSectionHeader() } footer: { - Text(L10n.screenCreateRoomRoomAddressSectionFooter) - .compoundListSectionFooter() + VStack(alignment: .leading, spacing: 12) { + if let error = context.viewState.errorState { + switch error { + case .alreadyExists: + Label("Already exists", icon: \.error, iconSize: .xSmall, relativeTo: .compound.bodySM) + .foregroundStyle(.compound.textCriticalPrimary) + .font(.compound.bodySM) + case .invalidSymbols: + Label("Invalid symbols", icon: \.error, iconSize: .xSmall, relativeTo: .compound.bodySM) + .foregroundStyle(.compound.textCriticalPrimary) + .font(.compound.bodySM) + } + } + Text(L10n.screenCreateRoomRoomAddressSectionFooter) + .compoundListSectionFooter() + .font(.compound.bodySM) + } } + .errorBackground(context.viewState.errorState != nil) } private var toolbar: some ToolbarContent { @@ -219,6 +235,20 @@ struct CreateRoomScreen: View { } } +private extension View { + @ViewBuilder + func errorBackground(_ shouldDisplay: Bool) -> some View { + if shouldDisplay { + listRowBackground(RoundedRectangle(cornerRadius: 10) + .inset(by: 1) + .fill(.compound.bgCriticalSubtle) + .stroke(.compound.borderCriticalSubtle)) + } else { + self + } + } +} + // MARK: - Previews struct CreateRoom_Previews: PreviewProvider, TestablePreview { @@ -259,6 +289,19 @@ struct CreateRoom_Previews: PreviewProvider, TestablePreview { appSettings: ServiceLocator.shared.settings) }() + static let publicRoomInvalidAliasViewModel = { + let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(serverName: "example.org", userID: "@userid:example.com")))) + let parameters = CreateRoomFlowParameters(isRoomPrivate: false, addressName: "wrong") + let selectedUsers: [UserProfileProxy] = [.mockAlice, .mockBob, .mockCharlie] + 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) @@ -272,5 +315,9 @@ struct CreateRoom_Previews: PreviewProvider, TestablePreview { CreateRoomScreen(context: publicRoomViewModel.context) } .previewDisplayName("Create Public Room") + NavigationStack { + CreateRoomScreen(context: publicRoomInvalidAliasViewModel.context) + } + .previewDisplayName("Create Public Room, invalid alias") } } diff --git a/ElementX/Sources/Services/CreateRoom/CreateRoomFlowParameters.swift b/ElementX/Sources/Services/CreateRoom/CreateRoomFlowParameters.swift index d02d47a245..c00c1f4eb8 100644 --- a/ElementX/Sources/Services/CreateRoom/CreateRoomFlowParameters.swift +++ b/ElementX/Sources/Services/CreateRoom/CreateRoomFlowParameters.swift @@ -14,5 +14,12 @@ struct CreateRoomFlowParameters { var isRoomPrivate = true var isKnockingOnly = false var avatarImageMedia: MediaInfo? - var canonicalAlias: String? + var addressName: String? + + func canonicalAlias(homeserver: String) -> String? { + guard let addressName = addressName else { + return nil + } + return "#\(addressName):\(homeserver)" + } } diff --git a/UnitTests/Sources/CreateRoomViewModelTests.swift b/UnitTests/Sources/CreateRoomViewModelTests.swift index b145dfbfd9..7007984ead 100644 --- a/UnitTests/Sources/CreateRoomViewModelTests.swift +++ b/UnitTests/Sources/CreateRoomViewModelTests.swift @@ -68,19 +68,19 @@ class CreateRoomScreenViewModelTests: XCTestCase { func testCreateRoomRequirements() { XCTAssertFalse(context.viewState.canCreateRoom) - context.roomName = "A" + context.send(viewAction: .updateName("A")) XCTAssertTrue(context.viewState.canCreateRoom) } func testCreateKnockingRoom() async { - context.roomName = "A" + context.send(viewAction: .updateName("A")) context.roomTopic = "B" context.isRoomPrivate = false context.isKnockingOnly = true XCTAssertTrue(context.viewState.canCreateRoom) let expectation = expectation(description: "Wait for the room to be created") - clientProxy.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLClosure = { _, _, isPrivate, isKnockingOnly, _, _ in + clientProxy.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasClosure = { _, _, isPrivate, isKnockingOnly, _, _, _ in XCTAssertTrue(isKnockingOnly) XCTAssertFalse(isPrivate) defer { expectation.fulfill() } @@ -91,13 +91,13 @@ class CreateRoomScreenViewModelTests: XCTestCase { } func testCreatePrivateRoomCantHaveKnockRule() async { - context.roomName = "A" + context.send(viewAction: .updateName("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.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasClosure = { _, _, isPrivate, isKnockingOnly, _, _, _ in XCTAssertFalse(isKnockingOnly) XCTAssertTrue(isPrivate) expectation.fulfill() From dcd2b49e2f546e93319c1bff616c5c2e569e514f Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Tue, 5 Nov 2024 23:20:06 +0100 Subject: [PATCH 10/22] update strings --- .../en.lproj/Localizable.strings | 12 ++++++---- ElementX/Sources/Generated/Strings.swift | 24 +++++++++++-------- .../CreateRoom/View/CreateRoomScreen.swift | 17 ++++++------- 3 files changed, 30 insertions(+), 23 deletions(-) diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index 4200cde50f..23b47f1e3c 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 2c89b62e8c..f807da0708 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/Screens/CreateRoom/View/CreateRoomScreen.swift b/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift index 7a0baf8ec1..1b7c071444 100644 --- a/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift +++ b/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift @@ -163,14 +163,15 @@ struct CreateRoomScreen: View { private var roomAccessSection: some View { Section { - ListRow(label: .plain(title: L10n.screenCreateRoomAccessSectionAnyoneOptionTitle, - description: L10n.screenCreateRoomAccessSectionAnyoneOptionDescription), - kind: .selection(isSelected: !context.isKnockingOnly) { context.isKnockingOnly = false }) - ListRow(label: .plain(title: L10n.screenCreateRoomAccessSectionKnockingOptionTitle, - description: L10n.screenCreateRoomAccessSectionKnockingOptionDescription), + ListRow(label: .plain(title: + L10n.screenCreateRoomRoomAccessSectionAnyoneOptionTitle, + description: L10n.screenCreateRoomRoomAccessSectionAnyoneOptionDescription), + kind: .selection(isSelected: !context.isKnockingOnly) { context.isKnockingOnly = false }) + 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.uppercased()) .compoundListSectionHeader() } } @@ -207,11 +208,11 @@ struct CreateRoomScreen: View { if let error = context.viewState.errorState { switch error { case .alreadyExists: - Label("Already exists", icon: \.error, iconSize: .xSmall, relativeTo: .compound.bodySM) + Label(L10n.screenCreateRoomRoomAddressNotAvailableErrorDescription, icon: \.error, iconSize: .xSmall, relativeTo: .compound.bodySM) .foregroundStyle(.compound.textCriticalPrimary) .font(.compound.bodySM) case .invalidSymbols: - Label("Invalid symbols", icon: \.error, iconSize: .xSmall, relativeTo: .compound.bodySM) + Label(L10n.screenCreateRoomRoomAddressInvalidSymbolsErrorDescription, icon: \.error, iconSize: .xSmall, relativeTo: .compound.bodySM) .foregroundStyle(.compound.textCriticalPrimary) .font(.compound.bodySM) } From 3607863ef5bb942fdd10b90ff25a3db4dbaa0e22 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Wed, 6 Nov 2024 13:14:12 +0100 Subject: [PATCH 11/22] error handling --- ElementX/Sources/Mocks/ClientProxyMock.swift | 1 + .../Screens/CreateRoom/CreateRoomModels.swift | 4 +- .../CreateRoom/CreateRoomViewModel.swift | 69 ++++++++++------ .../CreateRoom/View/CreateRoomScreen.swift | 79 +++++++++---------- .../CreateRoom/CreateRoomFlowParameters.swift | 7 -- 5 files changed, 85 insertions(+), 75 deletions(-) diff --git a/ElementX/Sources/Mocks/ClientProxyMock.swift b/ElementX/Sources/Mocks/ClientProxyMock.swift index c3cdf00d44..e2e7618b60 100644 --- a/ElementX/Sources/Mocks/ClientProxyMock.swift +++ b/ElementX/Sources/Mocks/ClientProxyMock.swift @@ -60,6 +60,7 @@ extension ClientProxyMock { 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/Screens/CreateRoom/CreateRoomModels.swift b/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift index 0c91b62aa4..0082f12ffe 100644 --- a/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift +++ b/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift @@ -33,10 +33,10 @@ struct CreateRoomViewState: BindableState { var bindings: CreateRoomViewStateBindings var avatarURL: URL? var canCreateRoom: Bool { - !roomName.isEmpty + !roomName.isEmpty && errors.isEmpty } - var errorState: CreateRoomAliasErrorState? + var errors: Set = [] } struct CreateRoomViewStateBindings { diff --git a/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift b/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift index 69bfb47e88..cd909726fe 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 @@ -16,6 +17,7 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol private let analytics: AnalyticsService private let userIndicatorController: UserIndicatorControllerProtocol private var syncNameAndAddress = true + @CancellableTask private var checkAliasAvailabilityTask: Task? private var actionsSubject: PassthroughSubject = .init() @@ -42,7 +44,7 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol homeserver: userSession.clientProxy.serverName ?? "", isKnockingFeatureEnabled: appSettings.knockingEnabled, selectedUsers: selectedUsers.value, - addressName: parameters.addressName ?? parameters.name.toValidAddress, + addressName: parameters.addressName ?? roomAliasNameFromRoomDisplayName(roomName: parameters.name), bindings: bindings), mediaProvider: userSession.mediaProvider) @@ -88,7 +90,7 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol case .removeImage: actionsSubject.send(.removeImage) case .updateAddress(let address): - state.addressName = address.toValidAddress + state.addressName = address // 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. syncNameAndAddress = false @@ -99,7 +101,7 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol } state.roomName = name if syncNameAndAddress { - state.addressName = name.toValidAddress + state.addressName = roomAliasNameFromRoomDisplayName(roomName: name) } } } @@ -117,7 +119,7 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol .sink { [weak self] _ in guard let self else { return } state.bindings.isKnockingOnly = false - state.addressName = state.roomName.toValidAddress + state.addressName = roomAliasNameFromRoomDisplayName(roomName: state.roomName) syncNameAndAddress = true } .store(in: &cancellables) @@ -142,11 +144,32 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol guard let self else { return } - // TODO: Check if the alias is valid throught the SDK - if addressName.contains("wrong") { - state.errorState = .invalidSymbols + + guard state.isKnockingFeatureEnabled, + !state.bindings.isRoomPrivate, + let canonicalAlias = canonicalAlias(addressName: addressName) else { + // While is empty or private room we don't change or display the error + return + } + + if !isRoomAliasFormatValid(alias: canonicalAlias) { + state.errors.insert(.invalidSymbols) } else { - state.errorState = nil + state.errors.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.errors.insert(.alreadyExists) + } else { + guard !Task.isCancelled else { return } + state.errors.remove(.alreadyExists) + } } } .store(in: &cancellables) @@ -173,10 +196,11 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol // Since the parameters are throttled, we need to make sure that the latest values are used updateParameters(state: state) - let alias = createRoomParameters.canonicalAlias(homeserver: userSession.clientProxy.serverName ?? "") - if !createRoomParameters.isRoomPrivate { - guard let alias else { - state.errorState = .invalidSymbols + // Better to double check the errors also when trying to create the room + let alias = canonicalAlias(addressName: createRoomParameters.addressName) + if state.isKnockingFeatureEnabled, !createRoomParameters.isRoomPrivate { + guard let alias, isRoomAliasFormatValid(alias: alias) else { + state.errors.insert(.invalidSymbols) return } @@ -184,7 +208,7 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol case .success(true): break case .success(false): - state.errorState = .alreadyExists + state.errors.insert(.alreadyExists) return case .failure: state.bindings.alertInfo = AlertInfo(id: .unknown) @@ -236,6 +260,14 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol } } + func canonicalAlias(addressName: String?) -> String? { + guard let addressName, + !addressName.isEmpty else { + return nil + } + return "#\(addressName):\(state.homeserver)" + } + // MARK: Loading indicator private static let loadingIndicatorIdentifier = "\(CreateRoomViewModel.self)-Loading" @@ -251,14 +283,3 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier) } } - -private extension String { - // TODO: This will be soon done by the SDK directly - var toValidAddress: Self { - split(separator: " ") - .joined(separator: "-") - // Characters that can be % encoded need to be excluded - .replacingOccurrences(of: "[!#$&'()*+,/:;=?@\\[\\]]", with: "", options: .regularExpression) - .lowercased() - } -} diff --git a/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift b/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift index 1b7c071444..f69b0cf850 100644 --- a/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift +++ b/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift @@ -178,51 +178,51 @@ struct CreateRoomScreen: View { private var roomAddressSection: some View { Section { - HStack(spacing: 0) { - Text("#") - .font(.compound.bodyLG) - .foregroundStyle(.compound.textSecondary) - - let binding = Binding(get: { - context.viewState.addressName - }, set: { - context.send(viewAction: .updateAddress($0)) - }) - - TextField("", text: binding) - .autocapitalization(.none) - .textCase(.lowercase) - .font(.compound.bodyLG) - .foregroundStyle(.compound.textPrimary) - .padding(.horizontal, 8) - Text(":\(context.viewState.homeserver)") - .font(.compound.bodyLG) - .foregroundStyle(.compound.textSecondary) - } - .environment(\.layoutDirection, .leftToRight) + ListRow(kind: .custom { + HStack(spacing: 0) { + Text("#") + .font(.compound.bodyLG) + .foregroundStyle(.compound.textSecondary) + + let binding = Binding(get: { + context.viewState.addressName + }, set: { + context.send(viewAction: .updateAddress($0)) + }) + + TextField("", text: binding) + .autocapitalization(.none) + .textCase(.lowercase) + .font(.compound.bodyLG) + .foregroundStyle(.compound.textPrimary) + .padding(.horizontal, 8) + Text(":\(context.viewState.homeserver)") + .font(.compound.bodyLG) + .foregroundStyle(.compound.textSecondary) + } + .padding(ListRowPadding.textFieldInsets) + .environment(\.layoutDirection, .leftToRight) + .errorBackground(!context.viewState.errors.isEmpty) + }) } header: { Text(L10n.screenCreateRoomRoomAddressSectionTitle.uppercased()) .compoundListSectionHeader() } footer: { VStack(alignment: .leading, spacing: 12) { - if let error = context.viewState.errorState { - switch error { - case .alreadyExists: - Label(L10n.screenCreateRoomRoomAddressNotAvailableErrorDescription, icon: \.error, iconSize: .xSmall, relativeTo: .compound.bodySM) - .foregroundStyle(.compound.textCriticalPrimary) - .font(.compound.bodySM) - case .invalidSymbols: - Label(L10n.screenCreateRoomRoomAddressInvalidSymbolsErrorDescription, icon: \.error, iconSize: .xSmall, relativeTo: .compound.bodySM) - .foregroundStyle(.compound.textCriticalPrimary) - .font(.compound.bodySM) - } + if context.viewState.errors.contains(.alreadyExists) { + Label(L10n.screenCreateRoomRoomAddressNotAvailableErrorDescription, icon: \.error, iconSize: .xSmall, relativeTo: .compound.bodySM) + .foregroundStyle(.compound.textCriticalPrimary) + .font(.compound.bodySM) + } else if context.viewState.errors.contains(.invalidSymbols) { + Label(L10n.screenCreateRoomRoomAddressInvalidSymbolsErrorDescription, icon: \.error, iconSize: .xSmall, relativeTo: .compound.bodySM) + .foregroundStyle(.compound.textCriticalPrimary) + .font(.compound.bodySM) } Text(L10n.screenCreateRoomRoomAddressSectionFooter) .compoundListSectionFooter() .font(.compound.bodySM) } } - .errorBackground(context.viewState.errorState != nil) } private var toolbar: some ToolbarContent { @@ -237,16 +237,11 @@ struct CreateRoomScreen: View { } private extension View { - @ViewBuilder func errorBackground(_ shouldDisplay: Bool) -> some View { - if shouldDisplay { - listRowBackground(RoundedRectangle(cornerRadius: 10) + listRowBackground(shouldDisplay ? AnyView(RoundedRectangle(cornerRadius: 10) .inset(by: 1) .fill(.compound.bgCriticalSubtle) - .stroke(.compound.borderCriticalSubtle)) - } else { - self - } + .stroke(Color.compound.borderCriticalSubtle)) : AnyView(Color.compound.bgCanvasDefaultLevel1)) } } @@ -292,7 +287,7 @@ struct CreateRoom_Previews: PreviewProvider, TestablePreview { static let publicRoomInvalidAliasViewModel = { let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(serverName: "example.org", userID: "@userid:example.com")))) - let parameters = CreateRoomFlowParameters(isRoomPrivate: false, addressName: "wrong") + let parameters = CreateRoomFlowParameters(isRoomPrivate: false, addressName: "#:") let selectedUsers: [UserProfileProxy] = [.mockAlice, .mockBob, .mockCharlie] ServiceLocator.shared.settings.knockingEnabled = true return CreateRoomViewModel(userSession: userSession, diff --git a/ElementX/Sources/Services/CreateRoom/CreateRoomFlowParameters.swift b/ElementX/Sources/Services/CreateRoom/CreateRoomFlowParameters.swift index c00c1f4eb8..f5195103bd 100644 --- a/ElementX/Sources/Services/CreateRoom/CreateRoomFlowParameters.swift +++ b/ElementX/Sources/Services/CreateRoom/CreateRoomFlowParameters.swift @@ -15,11 +15,4 @@ struct CreateRoomFlowParameters { var isKnockingOnly = false var avatarImageMedia: MediaInfo? var addressName: String? - - func canonicalAlias(homeserver: String) -> String? { - guard let addressName = addressName else { - return nil - } - return "#\(addressName):\(homeserver)" - } } From 25553fd614ff47bb0aa2efcf0bb23bf545e64c1d Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Wed, 6 Nov 2024 14:45:40 +0100 Subject: [PATCH 12/22] improved the error handling --- .../Screens/CreateRoom/CreateRoomModels.swift | 6 ++-- .../CreateRoom/CreateRoomViewModel.swift | 31 +++++++++++++------ .../CreateRoom/View/CreateRoomScreen.swift | 7 +++-- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift b/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift index 0082f12ffe..652e6b6def 100644 --- a/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift +++ b/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift @@ -33,16 +33,16 @@ struct CreateRoomViewState: BindableState { var bindings: CreateRoomViewStateBindings var avatarURL: URL? var canCreateRoom: Bool { - !roomName.isEmpty && errors.isEmpty + !roomName.isEmpty && aliasErrors.isEmpty } - var errors: Set = [] + var aliasErrors: Set = [] } struct CreateRoomViewStateBindings { var roomTopic: String var isRoomPrivate: Bool - var isKnockingOnly = false + var isKnockingOnly: Bool var showAttachmentConfirmationDialog = false /// Information describing the currently displayed alert. diff --git a/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift b/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift index cd909726fe..67fbb0fa9b 100644 --- a/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift +++ b/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift @@ -38,7 +38,9 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol self.analytics = analytics self.userIndicatorController = userIndicatorController - let bindings = CreateRoomViewStateBindings(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(roomName: parameters.name, homeserver: userSession.clientProxy.serverName ?? "", @@ -119,6 +121,7 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol .sink { [weak self] _ in guard let self else { return } state.bindings.isKnockingOnly = false + state.aliasErrors = [] state.addressName = roomAliasNameFromRoomDisplayName(roomName: state.roomName) syncNameAndAddress = true } @@ -127,7 +130,11 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol context.$viewState .throttle(for: 0.5, scheduler: DispatchQueue.main, latest: true) .removeDuplicates { old, new in - old.roomName == new.roomName && old.bindings.roomTopic == new.bindings.roomTopic && old.bindings.isRoomPrivate == new.bindings.isRoomPrivate + old.roomName == new.roomName && + old.bindings.roomTopic == new.bindings.roomTopic && + old.bindings.isRoomPrivate == new.bindings.isRoomPrivate && + old.bindings.isKnockingOnly == new.bindings.isKnockingOnly && + old.addressName == new.addressName } .sink { [weak self] state in guard let self else { return } @@ -139,7 +146,7 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol context.$viewState .map(\.addressName) .removeDuplicates() - .debounce(for: 0.5, scheduler: DispatchQueue.main) + .debounce(for: 1, scheduler: DispatchQueue.main) .sink { [weak self] addressName in guard let self else { return @@ -153,11 +160,15 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol } if !isRoomAliasFormatValid(alias: canonicalAlias) { - state.errors.insert(.invalidSymbols) - } else { - state.errors.remove(.invalidSymbols) + 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 @@ -165,10 +176,10 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol if case .success(false) = await self.userSession.clientProxy.isAliasAvailable(canonicalAlias) { guard !Task.isCancelled else { return } - state.errors.insert(.alreadyExists) + state.aliasErrors.insert(.alreadyExists) } else { guard !Task.isCancelled else { return } - state.errors.remove(.alreadyExists) + state.aliasErrors.remove(.alreadyExists) } } } @@ -200,7 +211,7 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol let alias = canonicalAlias(addressName: createRoomParameters.addressName) if state.isKnockingFeatureEnabled, !createRoomParameters.isRoomPrivate { guard let alias, isRoomAliasFormatValid(alias: alias) else { - state.errors.insert(.invalidSymbols) + state.aliasErrors = [.invalidSymbols] return } @@ -208,7 +219,7 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol case .success(true): break case .success(false): - state.errors.insert(.alreadyExists) + state.aliasErrors = [.alreadyExists] return case .failure: state.bindings.alertInfo = AlertInfo(id: .unknown) diff --git a/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift b/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift index f69b0cf850..39bf18818b 100644 --- a/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift +++ b/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift @@ -193,6 +193,7 @@ struct CreateRoomScreen: View { TextField("", text: binding) .autocapitalization(.none) .textCase(.lowercase) + .tint(.compound.iconAccentTertiary) .font(.compound.bodyLG) .foregroundStyle(.compound.textPrimary) .padding(.horizontal, 8) @@ -202,18 +203,18 @@ struct CreateRoomScreen: View { } .padding(ListRowPadding.textFieldInsets) .environment(\.layoutDirection, .leftToRight) - .errorBackground(!context.viewState.errors.isEmpty) + .errorBackground(!context.viewState.aliasErrors.isEmpty) }) } header: { Text(L10n.screenCreateRoomRoomAddressSectionTitle.uppercased()) .compoundListSectionHeader() } footer: { VStack(alignment: .leading, spacing: 12) { - if context.viewState.errors.contains(.alreadyExists) { + if context.viewState.aliasErrors.contains(.alreadyExists) { Label(L10n.screenCreateRoomRoomAddressNotAvailableErrorDescription, icon: \.error, iconSize: .xSmall, relativeTo: .compound.bodySM) .foregroundStyle(.compound.textCriticalPrimary) .font(.compound.bodySM) - } else if context.viewState.errors.contains(.invalidSymbols) { + } else if context.viewState.aliasErrors.contains(.invalidSymbols) { Label(L10n.screenCreateRoomRoomAddressInvalidSymbolsErrorDescription, icon: \.error, iconSize: .xSmall, relativeTo: .compound.bodySM) .foregroundStyle(.compound.textCriticalPrimary) .font(.compound.bodySM) From 2c2be08a8d4cd0020035c9e54db582b21ef1bc19 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Wed, 6 Nov 2024 19:03:46 +0100 Subject: [PATCH 13/22] new preview tests, even if they do not work well --- .../CreateRoom/View/CreateRoomScreen.swift | 25 +++++++++++++++++-- ...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 +-- 21 files changed, 71 insertions(+), 26 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/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift b/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift index 39bf18818b..e572bfccdb 100644 --- a/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift +++ b/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift @@ -241,8 +241,8 @@ private extension View { func errorBackground(_ shouldDisplay: Bool) -> some View { listRowBackground(shouldDisplay ? AnyView(RoundedRectangle(cornerRadius: 10) .inset(by: 1) - .fill(.compound.bgCriticalSubtle) - .stroke(Color.compound.borderCriticalSubtle)) : AnyView(Color.compound.bgCanvasDefaultLevel1)) + .fill(.compound.bgCriticalSubtleHovered) + .stroke(Color.compound.borderCriticalPrimary)) : AnyView(Color.compound.bgCanvasDefaultLevel1)) } } @@ -298,6 +298,21 @@ struct CreateRoom_Previews: PreviewProvider, TestablePreview { userIndicatorController: UserIndicatorControllerMock(), appSettings: ServiceLocator.shared.settings) }() + + static let publicRoomExistingAliasViewModel = { + let clientProxy = ClientProxyMock(.init(serverName: "example.org", userID: "@userid:example.com")) + clientProxy.isAliasAvailableReturnValue = .success(false) + let userSession = UserSessionMock(.init(clientProxy: clientProxy)) + let parameters = CreateRoomFlowParameters(isRoomPrivate: false, addressName: "existing") + let selectedUsers: [UserProfileProxy] = [.mockAlice, .mockBob, .mockCharlie] + 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 { @@ -315,6 +330,12 @@ struct CreateRoom_Previews: PreviewProvider, TestablePreview { NavigationStack { CreateRoomScreen(context: publicRoomInvalidAliasViewModel.context) } + .snapshot(delay: 2) .previewDisplayName("Create Public Room, invalid alias") + NavigationStack { + CreateRoomScreen(context: publicRoomExistingAliasViewModel.context) + } + .snapshot(delay: 2) + .previewDisplayName("Create Public Room, existing alias") } } 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 0000000000..d8d3ef2a7b --- /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:a7685544adc2865c4277cee8e7d33170886110659843309dfe0b4a5ccf0bdce4 +size 197256 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 0000000000..54e64d55fc --- /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:cb61c5abfa7d5553fc9dd8d5706db4c981fa62e02f609aa7aa6bd62be12cd37d +size 195237 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 dd7544c1ee..93564aa4d5 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 57d2a78428..107a06bd65 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 6c198b387e..c06ac05586 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 0000000000..cce490c162 --- /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:4d9250379488fcac7f169b79dc5bc486849f2d1c35ab6842f79173e35d6118ba +size 227227 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 0000000000..3fe552501c --- /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:0065c54fa87a25898a8d6d5aa6308d23ebade22fb525484a23d1416879844045 +size 225259 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 2bb300bfb9..e383e0a7ce 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 d4accf96e4..8ca3e931d6 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 927c1e3d61..800e0982e7 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 0000000000..661b3ce3c1 --- /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:086f19e8b6e6ac2082abd41b7f1595284b13c8757b0cbfdd7340eb0445a4d5b0 +size 129240 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 0000000000..95301486c3 --- /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:87db9862d62e07e348eeb337c0c2556784f3065ebd903caf0f8d8f43bcb78150 +size 127743 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 53af40f6dd..8f0fea0f9b 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 9280f48615..3e986cdcbb 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 9699119670..215efe85a6 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 0000000000..96122c7846 --- /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 0000000000..96122c7846 --- /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 cfb427967f..96122c7846 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 6848b26ade..ebeef4fda4 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 20411e7c1c..89c4aae8fa 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 From eb6a3ba281eb83cdd964166f47a0464f2b53b921 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Wed, 6 Nov 2024 19:11:48 +0100 Subject: [PATCH 14/22] improved tests --- .../Sources/Screens/CreateRoom/View/CreateRoomScreen.swift | 6 ++---- .../View/SecureBackupRecoveryKeyScreen.swift | 4 ++-- ...ateRoom-iPad-en-GB.Create-Public-Room-existing-alias.png | 4 ++-- ...eateRoom-iPad-en-GB.Create-Public-Room-invalid-alias.png | 4 ++-- ...teRoom-iPad-pseudo.Create-Public-Room-existing-alias.png | 4 ++-- ...ateRoom-iPad-pseudo.Create-Public-Room-invalid-alias.png | 4 ++-- ...om-iPhone-16-en-GB.Create-Public-Room-existing-alias.png | 4 ++-- ...oom-iPhone-16-en-GB.Create-Public-Room-invalid-alias.png | 4 ++-- 8 files changed, 16 insertions(+), 18 deletions(-) diff --git a/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift b/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift index e572bfccdb..86a8ca7eb8 100644 --- a/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift +++ b/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift @@ -289,7 +289,6 @@ struct CreateRoom_Previews: PreviewProvider, TestablePreview { static let publicRoomInvalidAliasViewModel = { let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(serverName: "example.org", userID: "@userid:example.com")))) let parameters = CreateRoomFlowParameters(isRoomPrivate: false, addressName: "#:") - let selectedUsers: [UserProfileProxy] = [.mockAlice, .mockBob, .mockCharlie] ServiceLocator.shared.settings.knockingEnabled = true return CreateRoomViewModel(userSession: userSession, createRoomParameters: .init(parameters), @@ -304,7 +303,6 @@ struct CreateRoom_Previews: PreviewProvider, TestablePreview { clientProxy.isAliasAvailableReturnValue = .success(false) let userSession = UserSessionMock(.init(clientProxy: clientProxy)) let parameters = CreateRoomFlowParameters(isRoomPrivate: false, addressName: "existing") - let selectedUsers: [UserProfileProxy] = [.mockAlice, .mockBob, .mockCharlie] ServiceLocator.shared.settings.knockingEnabled = true return CreateRoomViewModel(userSession: userSession, createRoomParameters: .init(parameters), @@ -330,12 +328,12 @@ struct CreateRoom_Previews: PreviewProvider, TestablePreview { NavigationStack { CreateRoomScreen(context: publicRoomInvalidAliasViewModel.context) } - .snapshot(delay: 2) + .snapshotPreferences(delay: 1.5) .previewDisplayName("Create Public Room, invalid alias") NavigationStack { CreateRoomScreen(context: publicRoomExistingAliasViewModel.context) } - .snapshot(delay: 2) + .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 acd39bd180..51314243eb 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/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 index d8d3ef2a7b..a048c452d3 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a7685544adc2865c4277cee8e7d33170886110659843309dfe0b4a5ccf0bdce4 -size 197256 +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 index 54e64d55fc..8ce2cc596c 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb61c5abfa7d5553fc9dd8d5706db4c981fa62e02f609aa7aa6bd62be12cd37d -size 195237 +oid sha256:7563d6820945006a48db647fed692d4458338d63669ea4c344052f5a63a4a2ee +size 204578 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 index cce490c162..35ad913994 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4d9250379488fcac7f169b79dc5bc486849f2d1c35ab6842f79173e35d6118ba -size 227227 +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 index 3fe552501c..8b49055f95 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0065c54fa87a25898a8d6d5aa6308d23ebade22fb525484a23d1416879844045 -size 225259 +oid sha256:b42242042ad3adf3b953a29ff58792511076930892a7bda4d813b3e3218f39e0 +size 226407 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 index 661b3ce3c1..5c280a5838 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:086f19e8b6e6ac2082abd41b7f1595284b13c8757b0cbfdd7340eb0445a4d5b0 -size 129240 +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 index 95301486c3..5d536e0a57 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:87db9862d62e07e348eeb337c0c2556784f3065ebd903caf0f8d8f43bcb78150 -size 127743 +oid sha256:66610bab01ad7ed399d8c4f72fd8d04990eafbf911c8b666c3ca21beba88a58b +size 128602 From ac5123ccc518f6e67b7bc688baf28033af456fd4 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Thu, 7 Nov 2024 10:11:36 +0100 Subject: [PATCH 15/22] unit tests --- .../Sources/CreateRoomViewModelTests.swift | 92 ++++++++++++++++++- 1 file changed, 90 insertions(+), 2 deletions(-) diff --git a/UnitTests/Sources/CreateRoomViewModelTests.swift b/UnitTests/Sources/CreateRoomViewModelTests.swift index 7007984ead..6ed493e14a 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(serverName: "matrix.org", userID: "@a:b.com")) userSession = UserSessionMock(.init(clientProxy: clientProxy)) let parameters = CreateRoomFlowParameters() usersSubject.send([.mockAlice, .mockBob, .mockCharlie]) @@ -76,13 +76,17 @@ class CreateRoomScreenViewModelTests: XCTestCase { context.send(viewAction: .updateName("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.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasClosure = { _, _, isPrivate, isKnockingOnly, _, _, _ in + clientProxy.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasClosure = { _, _, isPrivate, isKnockingOnly, _, _, canonicalAlias in XCTAssertTrue(isKnockingOnly) XCTAssertFalse(isPrivate) + XCTAssertEqual(canonicalAlias, "#a:matrix.org") defer { expectation.fulfill() } return .success("") } @@ -90,6 +94,59 @@ class CreateRoomScreenViewModelTests: XCTestCase { await fulfillment(of: [expectation]) } + func testCreatePublicRoomFailsForInvalidAlias() async throws { + context.send(viewAction: .updateName("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: .updateAddress("#:")) + 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.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasCalled) + } + + func testCreatePublicRoomFailsForExistingAlias() async throws { + clientProxy.isAliasAvailableReturnValue = .success(false) + context.send(viewAction: .updateName("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: .updateAddress("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.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasCalled) + } + func testCreatePrivateRoomCantHaveKnockRule() async { context.send(viewAction: .updateName("A")) context.roomTopic = "B" @@ -105,4 +162,35 @@ class CreateRoomScreenViewModelTests: XCTestCase { } await fulfillment(of: [expectation]) } + + func testNameAndAddressSync() async { + context.isRoomPrivate = true + await Task.yield() + context.send(viewAction: .updateName("abc")) + XCTAssertEqual(context.viewState.addressName, "abc") + XCTAssertEqual(context.viewState.roomName, "abc") + context.send(viewAction: .updateName("DEF")) + XCTAssertEqual(context.viewState.roomName, "DEF") + XCTAssertEqual(context.viewState.addressName, "def") + context.send(viewAction: .updateName("a b c")) + XCTAssertEqual(context.viewState.addressName, "a-b-c") + XCTAssertEqual(context.viewState.roomName, "a b c") + context.send(viewAction: .updateAddress("hello-world")) + // This removes the sync + XCTAssertEqual(context.viewState.addressName, "hello-world") + XCTAssertEqual(context.viewState.roomName, "a b c") + + context.send(viewAction: .updateName("Hello Matrix!")) + XCTAssertEqual(context.viewState.addressName, "hello-world") + XCTAssertEqual(context.viewState.roomName, "Hello Matrix!") + + // Deleting the whole name will restore the sync + context.send(viewAction: .updateName("")) + XCTAssertEqual(context.viewState.addressName, "") + XCTAssertEqual(context.viewState.roomName, "") + + context.send(viewAction: .updateName("Hello# Matrix!")) + XCTAssertEqual(context.viewState.addressName, "hello-matrix!") + XCTAssertEqual(context.viewState.roomName, "Hello# Matrix!") + } } From 8c1c23563e5f4aa7961b614f8e5d269959f8c875 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Thu, 7 Nov 2024 17:28:26 +0100 Subject: [PATCH 16/22] pr comments and using the correct value --- ElementX/Sources/Mocks/ClientProxyMock.swift | 6 +-- .../Mocks/Generated/GeneratedMocks.swift | 49 +++++++++--------- .../Screens/CreateRoom/CreateRoomModels.swift | 12 ++++- .../CreateRoom/CreateRoomViewModel.swift | 50 +++++++++---------- .../CreateRoom/View/CreateRoomScreen.swift | 37 ++++++-------- .../Sources/Services/Client/ClientProxy.swift | 9 ++-- .../Services/Client/ClientProxyProtocol.swift | 6 +-- .../CreateRoom/CreateRoomFlowParameters.swift | 2 +- .../Sources/CreateRoomViewModelTests.swift | 24 ++++----- 9 files changed, 96 insertions(+), 99 deletions(-) diff --git a/ElementX/Sources/Mocks/ClientProxyMock.swift b/ElementX/Sources/Mocks/ClientProxyMock.swift index 25173c46fa..9a21f91205 100644 --- a/ElementX/Sources/Mocks/ClientProxyMock.swift +++ b/ElementX/Sources/Mocks/ClientProxyMock.swift @@ -10,7 +10,7 @@ import Foundation struct ClientProxyMockConfiguration { var homeserver = "" - var serverName: String? + var userIDServerName: String? var userID: String = RoomMemberProxyMock.mockMe.userID var deviceID: String? var roomSummaryProvider: RoomSummaryProviderProtocol? = RoomSummaryProviderMock(.init()) @@ -31,7 +31,7 @@ extension ClientProxyMock { deviceID = configuration.deviceID homeserver = configuration.homeserver - serverName = configuration.serverName + userIDServerName = configuration.userIDServerName roomSummaryProvider = configuration.roomSummaryProvider alternateRoomSummaryProvider = RoomSummaryProviderMock(.init()) @@ -55,7 +55,7 @@ extension ClientProxyMock { canDeactivateAccount = false directRoomForUserIDReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) createDirectRoomWithExpectedRoomNameReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) - createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) + createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) uploadMediaReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) loadUserDisplayNameReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) setUserDisplayNameReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 6deff1cafd..51c386fda5 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -2149,7 +2149,6 @@ class ClientProxyMock: ClientProxyProtocol { set(value) { underlyingHomeserver = value } } var underlyingHomeserver: String! - var serverName: String? var slidingSyncVersion: SlidingSyncVersion { get { return underlyingSlidingSyncVersion } set(value) { underlyingSlidingSyncVersion = value } @@ -2629,15 +2628,15 @@ class ClientProxyMock: ClientProxyProtocol { } //MARK: - createRoom - var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasUnderlyingCallsCount = 0 - var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasCallsCount: Int { + var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartUnderlyingCallsCount = 0 + var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartCallsCount: Int { get { if Thread.isMainThread { - return createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasUnderlyingCallsCount + return createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasUnderlyingCallsCount + returnValue = createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartUnderlyingCallsCount } return returnValue! @@ -2645,29 +2644,29 @@ class ClientProxyMock: ClientProxyProtocol { } set { if Thread.isMainThread { - createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasUnderlyingCallsCount = newValue + createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasUnderlyingCallsCount = newValue + createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartUnderlyingCallsCount = newValue } } } } - var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasCalled: Bool { - return createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasCallsCount > 0 + var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartCalled: Bool { + return createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartCallsCount > 0 } - var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasReceivedArguments: (name: String, topic: String?, isRoomPrivate: Bool, isKnockingOnly: Bool, userIDs: [String], avatarURL: URL?, canonicalAlias: String?)? - var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasReceivedInvocations: [(name: String, topic: String?, isRoomPrivate: Bool, isKnockingOnly: Bool, userIDs: [String], avatarURL: URL?, canonicalAlias: String?)] = [] + 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 createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasUnderlyingReturnValue: Result! - var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasReturnValue: Result! { + var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartUnderlyingReturnValue: Result! + var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartReturnValue: Result! { get { if Thread.isMainThread { - return createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasUnderlyingReturnValue + return createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartUnderlyingReturnValue } else { var returnValue: Result? = nil DispatchQueue.main.sync { - returnValue = createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasUnderlyingReturnValue + returnValue = createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartUnderlyingReturnValue } return returnValue! @@ -2675,26 +2674,26 @@ class ClientProxyMock: ClientProxyProtocol { } set { if Thread.isMainThread { - createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasUnderlyingReturnValue = newValue + createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasUnderlyingReturnValue = newValue + createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartUnderlyingReturnValue = newValue } } } } - var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasClosure: ((String, String?, Bool, Bool, [String], URL?, String?) 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?, canonicalAlias: String?) async -> Result { - createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasCallsCount += 1 - createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasReceivedArguments = (name: name, topic: topic, isRoomPrivate: isRoomPrivate, isKnockingOnly: isKnockingOnly, userIDs: userIDs, avatarURL: avatarURL, canonicalAlias: canonicalAlias) + 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.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasReceivedInvocations.append((name: name, topic: topic, isRoomPrivate: isRoomPrivate, isKnockingOnly: isKnockingOnly, userIDs: userIDs, avatarURL: avatarURL, canonicalAlias: canonicalAlias)) + self.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartReceivedInvocations.append((name: name, topic: topic, isRoomPrivate: isRoomPrivate, isKnockingOnly: isKnockingOnly, userIDs: userIDs, avatarURL: avatarURL, aliasLocalPart: aliasLocalPart)) } - if let createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasClosure = createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasClosure { - return await createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasClosure(name, topic, isRoomPrivate, isKnockingOnly, userIDs, avatarURL, canonicalAlias) + if let createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartClosure = createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartClosure { + return await createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartClosure(name, topic, isRoomPrivate, isKnockingOnly, userIDs, avatarURL, aliasLocalPart) } else { - return createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasReturnValue + return createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartReturnValue } } //MARK: - joinRoom diff --git a/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift b/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift index 652e6b6def..cd3f2049d5 100644 --- a/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift +++ b/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift @@ -26,10 +26,10 @@ enum CreateRoomViewModelAction { struct CreateRoomViewState: BindableState { var roomName: String - let homeserver: String + let serverName: String let isKnockingFeatureEnabled: Bool var selectedUsers: [UserProfileProxy] - var addressName: String + var aliasLocalPart: String var bindings: CreateRoomViewStateBindings var avatarURL: URL? var canCreateRoom: Bool { @@ -37,6 +37,14 @@ struct CreateRoomViewState: BindableState { } 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 { diff --git a/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift b/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift index 67fbb0fa9b..2ad212ba1a 100644 --- a/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift +++ b/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift @@ -16,7 +16,7 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol private var createRoomParameters: CreateRoomFlowParameters private let analytics: AnalyticsService private let userIndicatorController: UserIndicatorControllerProtocol - private var syncNameAndAddress = true + private var syncNameAndAlias = true @CancellableTask private var checkAliasAvailabilityTask: Task? private var actionsSubject: PassthroughSubject = .init() @@ -43,10 +43,10 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol isKnockingOnly: appSettings.knockingEnabled ? parameters.isKnockingOnly : false) super.init(initialViewState: CreateRoomViewState(roomName: parameters.name, - homeserver: userSession.clientProxy.serverName ?? "", + serverName: userSession.clientProxy.userIDServerName ?? "", isKnockingFeatureEnabled: appSettings.knockingEnabled, selectedUsers: selectedUsers.value, - addressName: parameters.addressName ?? roomAliasNameFromRoomDisplayName(roomName: parameters.name), + aliasLocalPart: parameters.aliasLocalPart ?? roomAliasNameFromRoomDisplayName(roomName: parameters.name), bindings: bindings), mediaProvider: userSession.mediaProvider) @@ -92,18 +92,18 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol case .removeImage: actionsSubject.send(.removeImage) case .updateAddress(let address): - state.addressName = address + state.aliasLocalPart = address // 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. - syncNameAndAddress = false + syncNameAndAlias = false case .updateName(let name): // Reset the syncing if the name is fully cancelled if name.isEmpty { - syncNameAndAddress = true + syncNameAndAlias = true } state.roomName = name - if syncNameAndAddress { - state.addressName = roomAliasNameFromRoomDisplayName(roomName: name) + if syncNameAndAlias { + state.aliasLocalPart = roomAliasNameFromRoomDisplayName(roomName: name) } } } @@ -122,8 +122,8 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol guard let self else { return } state.bindings.isKnockingOnly = false state.aliasErrors = [] - state.addressName = roomAliasNameFromRoomDisplayName(roomName: state.roomName) - syncNameAndAddress = true + state.aliasLocalPart = roomAliasNameFromRoomDisplayName(roomName: state.roomName) + syncNameAndAlias = true } .store(in: &cancellables) @@ -134,7 +134,7 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol old.bindings.roomTopic == new.bindings.roomTopic && old.bindings.isRoomPrivate == new.bindings.isRoomPrivate && old.bindings.isKnockingOnly == new.bindings.isKnockingOnly && - old.addressName == new.addressName + old.aliasLocalPart == new.aliasLocalPart } .sink { [weak self] state in guard let self else { return } @@ -144,17 +144,17 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol .store(in: &cancellables) context.$viewState - .map(\.addressName) + .map(\.aliasLocalPart) .removeDuplicates() .debounce(for: 1, scheduler: DispatchQueue.main) - .sink { [weak self] addressName in + .sink { [weak self] aliasLocalPart in guard let self else { return } guard state.isKnockingFeatureEnabled, !state.bindings.isRoomPrivate, - let canonicalAlias = canonicalAlias(addressName: addressName) else { + let canonicalAlias = canonicalAlias(aliasLocalPart: aliasLocalPart) else { // While is empty or private room we don't change or display the error return } @@ -191,10 +191,10 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol createRoomParameters.topic = state.bindings.roomTopic createRoomParameters.isRoomPrivate = state.bindings.isRoomPrivate createRoomParameters.isKnockingOnly = state.bindings.isKnockingOnly - if !state.addressName.isEmpty { - createRoomParameters.addressName = state.addressName + if !state.aliasLocalPart.isEmpty { + createRoomParameters.aliasLocalPart = state.aliasLocalPart } else { - createRoomParameters.addressName = nil + createRoomParameters.aliasLocalPart = nil } } @@ -208,14 +208,14 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol updateParameters(state: state) // Better to double check the errors also when trying to create the room - let alias = canonicalAlias(addressName: createRoomParameters.addressName) if state.isKnockingFeatureEnabled, !createRoomParameters.isRoomPrivate { - guard let alias, isRoomAliasFormatValid(alias: alias) else { + guard let canonicalAlias = canonicalAlias(aliasLocalPart: createRoomParameters.aliasLocalPart), + isRoomAliasFormatValid(alias: canonicalAlias) else { state.aliasErrors = [.invalidSymbols] return } - switch await userSession.clientProxy.isAliasAvailable(alias) { + switch await userSession.clientProxy.isAliasAvailable(canonicalAlias) { case .success(true): break case .success(false): @@ -260,7 +260,7 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol isKnockingOnly: createRoomParameters.isRoomPrivate ? false : createRoomParameters.isKnockingOnly, userIDs: state.selectedUsers.map(\.userID), avatarURL: avatarURL, - canonicalAlias: createRoomParameters.isRoomPrivate ? nil : alias) { + aliasLocalPart: createRoomParameters.isRoomPrivate ? nil : createRoomParameters.aliasLocalPart) { case .success(let roomId): analytics.trackCreatedRoom(isDM: false) actionsSubject.send(.openRoom(withIdentifier: roomId)) @@ -271,12 +271,12 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol } } - func canonicalAlias(addressName: String?) -> String? { - guard let addressName, - !addressName.isEmpty else { + func canonicalAlias(aliasLocalPart: String?) -> String? { + guard let aliasLocalPart, + !aliasLocalPart.isEmpty else { return nil } - return "#\(addressName):\(state.homeserver)" + return "#\(aliasLocalPart):\(state.serverName)" } // MARK: Loading indicator diff --git a/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift b/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift index 86a8ca7eb8..41e92034b7 100644 --- a/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift +++ b/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift @@ -25,7 +25,7 @@ struct CreateRoomScreen: View { if context.viewState.isKnockingFeatureEnabled, !context.isRoomPrivate { roomAccessSection - roomAddressSection + roomAliasSection } } .compoundList() @@ -156,27 +156,26 @@ struct CreateRoomScreen: View { iconAlignment: .top), kind: .selection(isSelected: !context.isRoomPrivate) { context.isRoomPrivate = false }) } header: { - Text(L10n.screenCreateRoomRoomVisibilitySectionTitle.uppercased()) + Text(L10n.screenCreateRoomRoomVisibilitySectionTitle) .compoundListSectionHeader() } } private var roomAccessSection: some View { Section { - ListRow(label: .plain(title: - L10n.screenCreateRoomRoomAccessSectionAnyoneOptionTitle, - description: L10n.screenCreateRoomRoomAccessSectionAnyoneOptionDescription), - kind: .selection(isSelected: !context.isKnockingOnly) { context.isKnockingOnly = false }) + ListRow(label: .plain(title: L10n.screenCreateRoomRoomAccessSectionAnyoneOptionTitle, + description: L10n.screenCreateRoomRoomAccessSectionAnyoneOptionDescription), + kind: .selection(isSelected: !context.isKnockingOnly) { context.isKnockingOnly = false }) ListRow(label: .plain(title: L10n.screenCreateRoomRoomAccessSectionKnockingOptionTitle, description: L10n.screenCreateRoomRoomAccessSectionKnockingOptionDescription), kind: .selection(isSelected: context.isKnockingOnly) { context.isKnockingOnly = true }) } header: { - Text(L10n.screenCreateRoomRoomAccessSectionHeader.uppercased()) + Text(L10n.screenCreateRoomRoomAccessSectionHeader) .compoundListSectionHeader() } } - private var roomAddressSection: some View { + private var roomAliasSection: some View { Section { ListRow(kind: .custom { HStack(spacing: 0) { @@ -185,7 +184,7 @@ struct CreateRoomScreen: View { .foregroundStyle(.compound.textSecondary) let binding = Binding(get: { - context.viewState.addressName + context.viewState.aliasLocalPart }, set: { context.send(viewAction: .updateAddress($0)) }) @@ -197,7 +196,7 @@ struct CreateRoomScreen: View { .font(.compound.bodyLG) .foregroundStyle(.compound.textPrimary) .padding(.horizontal, 8) - Text(":\(context.viewState.homeserver)") + Text(":\(context.viewState.serverName)") .font(.compound.bodyLG) .foregroundStyle(.compound.textSecondary) } @@ -206,18 +205,14 @@ struct CreateRoomScreen: View { .errorBackground(!context.viewState.aliasErrors.isEmpty) }) } header: { - Text(L10n.screenCreateRoomRoomAddressSectionTitle.uppercased()) + Text(L10n.screenCreateRoomRoomAddressSectionTitle) .compoundListSectionHeader() } footer: { VStack(alignment: .leading, spacing: 12) { - if context.viewState.aliasErrors.contains(.alreadyExists) { + if let errorDescription = context.viewState.aliasErrorDescription { Label(L10n.screenCreateRoomRoomAddressNotAvailableErrorDescription, icon: \.error, iconSize: .xSmall, relativeTo: .compound.bodySM) .foregroundStyle(.compound.textCriticalPrimary) .font(.compound.bodySM) - } else if context.viewState.aliasErrors.contains(.invalidSymbols) { - Label(L10n.screenCreateRoomRoomAddressInvalidSymbolsErrorDescription, icon: \.error, iconSize: .xSmall, relativeTo: .compound.bodySM) - .foregroundStyle(.compound.textCriticalPrimary) - .font(.compound.bodySM) } Text(L10n.screenCreateRoomRoomAddressSectionFooter) .compoundListSectionFooter() @@ -274,7 +269,7 @@ struct CreateRoom_Previews: PreviewProvider, TestablePreview { }() static let publicRoomViewModel = { - let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(serverName: "example.org", 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 @@ -287,8 +282,8 @@ struct CreateRoom_Previews: PreviewProvider, TestablePreview { }() static let publicRoomInvalidAliasViewModel = { - let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(serverName: "example.org", userID: "@userid:example.com")))) - let parameters = CreateRoomFlowParameters(isRoomPrivate: false, addressName: "#:") + 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), @@ -299,10 +294,10 @@ struct CreateRoom_Previews: PreviewProvider, TestablePreview { }() static let publicRoomExistingAliasViewModel = { - let clientProxy = ClientProxyMock(.init(serverName: "example.org", userID: "@userid:example.com")) + 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, addressName: "existing") + let parameters = CreateRoomFlowParameters(isRoomPrivate: false, aliasLocalPart: "existing") ServiceLocator.shared.settings.knockingEnabled = true return CreateRoomViewModel(userSession: userSession, createRoomParameters: .init(parameters), diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index 78f7916c34..8516ab0211 100644 --- a/ElementX/Sources/Services/Client/ClientProxy.swift +++ b/ElementX/Sources/Services/Client/ClientProxy.swift @@ -221,10 +221,6 @@ class ClientProxy: ClientProxyProtocol { client.homeserver() } - var serverName: String? { - try? client.userIdServerName() - } - var slidingSyncVersion: SlidingSyncVersion { client.slidingSyncVersion() } @@ -403,7 +399,7 @@ class ClientProxy: ClientProxyProtocol { isKnockingOnly: Bool, userIDs: [String], avatarURL: URL?, - canonicalAlias: String?) async -> Result { + aliasLocalPart: String?) async -> Result { do { let parameters = CreateRoomParameters(name: name, topic: topic, @@ -414,7 +410,8 @@ class ClientProxy: ClientProxyProtocol { invite: userIDs, avatar: avatarURL?.absoluteString, powerLevelContentOverride: isKnockingOnly ? Self.knockingRoomCreationPowerLevelOverrides : Self.roomCreationPowerLevelOverrides, - canonicalAlias: canonicalAlias) + // 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) diff --git a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift index 4b629b1924..0feae1793e 100644 --- a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift +++ b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift @@ -88,9 +88,7 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol { var deviceID: String? { get } var homeserver: String { get } - - var serverName: String? { get } - + var slidingSyncVersion: SlidingSyncVersion { get } var availableSlidingSyncVersions: [SlidingSyncVersion] { get async } @@ -141,7 +139,7 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol { isKnockingOnly: Bool, userIDs: [String], avatarURL: URL?, - canonicalAlias: String?) async -> Result + aliasLocalPart: String?) async -> Result func joinRoom(_ roomID: String, via: [String]) async -> Result diff --git a/ElementX/Sources/Services/CreateRoom/CreateRoomFlowParameters.swift b/ElementX/Sources/Services/CreateRoom/CreateRoomFlowParameters.swift index f5195103bd..736e99c4b3 100644 --- a/ElementX/Sources/Services/CreateRoom/CreateRoomFlowParameters.swift +++ b/ElementX/Sources/Services/CreateRoom/CreateRoomFlowParameters.swift @@ -14,5 +14,5 @@ struct CreateRoomFlowParameters { var isRoomPrivate = true var isKnockingOnly = false var avatarImageMedia: MediaInfo? - var addressName: String? + var aliasLocalPart: String? } diff --git a/UnitTests/Sources/CreateRoomViewModelTests.swift b/UnitTests/Sources/CreateRoomViewModelTests.swift index 6ed493e14a..6271bbbc83 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(serverName: "matrix.org", 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]) @@ -83,7 +83,7 @@ class CreateRoomScreenViewModelTests: XCTestCase { XCTAssertTrue(context.viewState.canCreateRoom) let expectation = expectation(description: "Wait for the room to be created") - clientProxy.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasClosure = { _, _, isPrivate, isKnockingOnly, _, _, canonicalAlias in + clientProxy.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartClosure = { _, _, isPrivate, isKnockingOnly, _, _, canonicalAlias in XCTAssertTrue(isKnockingOnly) XCTAssertFalse(isPrivate) XCTAssertEqual(canonicalAlias, "#a:matrix.org") @@ -113,7 +113,7 @@ class CreateRoomScreenViewModelTests: XCTestCase { // blocked it context.send(viewAction: .createRoom) await Task.yield() - XCTAssertFalse(clientProxy.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasCalled) + XCTAssertFalse(clientProxy.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartCalled) } func testCreatePublicRoomFailsForExistingAlias() async throws { @@ -144,7 +144,7 @@ class CreateRoomScreenViewModelTests: XCTestCase { context.send(viewAction: .createRoom) await fulfillment(of: [expectation]) XCTAssertEqual(clientProxy.isAliasAvailableCallsCount, 2) - XCTAssertFalse(clientProxy.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasCalled) + XCTAssertFalse(clientProxy.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartCalled) } func testCreatePrivateRoomCantHaveKnockRule() async { @@ -154,7 +154,7 @@ class CreateRoomScreenViewModelTests: XCTestCase { context.isKnockingOnly = true context.send(viewAction: .createRoom) let expectation = expectation(description: "Wait for the room to be created") - clientProxy.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCanonicalAliasClosure = { _, _, isPrivate, isKnockingOnly, _, _, _ in + clientProxy.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartClosure = { _, _, isPrivate, isKnockingOnly, _, _, _ in XCTAssertFalse(isKnockingOnly) XCTAssertTrue(isPrivate) expectation.fulfill() @@ -167,30 +167,30 @@ class CreateRoomScreenViewModelTests: XCTestCase { context.isRoomPrivate = true await Task.yield() context.send(viewAction: .updateName("abc")) - XCTAssertEqual(context.viewState.addressName, "abc") + XCTAssertEqual(context.viewState.aliasLocalPart, "abc") XCTAssertEqual(context.viewState.roomName, "abc") context.send(viewAction: .updateName("DEF")) XCTAssertEqual(context.viewState.roomName, "DEF") - XCTAssertEqual(context.viewState.addressName, "def") + XCTAssertEqual(context.viewState.aliasLocalPart, "def") context.send(viewAction: .updateName("a b c")) - XCTAssertEqual(context.viewState.addressName, "a-b-c") + XCTAssertEqual(context.viewState.aliasLocalPart, "a-b-c") XCTAssertEqual(context.viewState.roomName, "a b c") context.send(viewAction: .updateAddress("hello-world")) // This removes the sync - XCTAssertEqual(context.viewState.addressName, "hello-world") + XCTAssertEqual(context.viewState.aliasLocalPart, "hello-world") XCTAssertEqual(context.viewState.roomName, "a b c") context.send(viewAction: .updateName("Hello Matrix!")) - XCTAssertEqual(context.viewState.addressName, "hello-world") + XCTAssertEqual(context.viewState.aliasLocalPart, "hello-world") XCTAssertEqual(context.viewState.roomName, "Hello Matrix!") // Deleting the whole name will restore the sync context.send(viewAction: .updateName("")) - XCTAssertEqual(context.viewState.addressName, "") + XCTAssertEqual(context.viewState.aliasLocalPart, "") XCTAssertEqual(context.viewState.roomName, "") context.send(viewAction: .updateName("Hello# Matrix!")) - XCTAssertEqual(context.viewState.addressName, "hello-matrix!") + XCTAssertEqual(context.viewState.aliasLocalPart, "hello-matrix!") XCTAssertEqual(context.viewState.roomName, "Hello# Matrix!") } } From 98ffd6f83bc5993750b47f0f178ae02d1d202223 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Thu, 7 Nov 2024 17:30:39 +0100 Subject: [PATCH 17/22] fix --- ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift b/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift index 41e92034b7..6f78f0fc98 100644 --- a/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift +++ b/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift @@ -210,7 +210,7 @@ struct CreateRoomScreen: View { } footer: { VStack(alignment: .leading, spacing: 12) { if let errorDescription = context.viewState.aliasErrorDescription { - Label(L10n.screenCreateRoomRoomAddressNotAvailableErrorDescription, icon: \.error, iconSize: .xSmall, relativeTo: .compound.bodySM) + Label(errorDescription, icon: \.error, iconSize: .xSmall, relativeTo: .compound.bodySM) .foregroundStyle(.compound.textCriticalPrimary) .font(.compound.bodySM) } From 1ccb19c46b2c8ea0439a03621310ee47a8400d11 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Thu, 7 Nov 2024 17:38:50 +0100 Subject: [PATCH 18/22] pr comments --- .../Screens/CreateRoom/CreateRoomModels.swift | 4 +-- .../CreateRoom/CreateRoomViewModel.swift | 6 ++-- .../CreateRoom/View/CreateRoomScreen.swift | 31 +++++++++++-------- .../Sources/CreateRoomViewModelTests.swift | 28 ++++++++--------- 4 files changed, 37 insertions(+), 32 deletions(-) diff --git a/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift b/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift index cd3f2049d5..d225589bec 100644 --- a/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift +++ b/ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift @@ -63,8 +63,8 @@ enum CreateRoomViewAction { case displayCameraPicker case displayMediaPicker case removeImage - case updateName(String) - case updateAddress(String) + case updateRoomName(String) + case updateAliasLocalPart(String) } enum CreateRoomAliasErrorState { diff --git a/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift b/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift index 2ad212ba1a..07df3777b3 100644 --- a/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift +++ b/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift @@ -91,12 +91,12 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol actionsSubject.send(.displayMediaPicker) case .removeImage: actionsSubject.send(.removeImage) - case .updateAddress(let address): - state.aliasLocalPart = address + 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 .updateName(let name): + case .updateRoomName(let name): // Reset the syncing if the name is fully cancelled if name.isEmpty { syncNameAndAlias = true diff --git a/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift b/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift index 6f78f0fc98..be52464a9f 100644 --- a/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift +++ b/ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift @@ -17,6 +17,22 @@ struct CreateRoomScreen: View { 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 @@ -48,13 +64,8 @@ struct CreateRoomScreen: View { .padding(.leading, ListRowPadding.horizontal) .compoundListSectionHeader() - let binding = Binding(get: { - context.viewState.roomName - }, set: { - context.send(viewAction: .updateName($0)) - }) TextField(L10n.screenCreateRoomRoomNameLabel, - text: binding, + text: roomNameBinding, prompt: Text(L10n.commonRoomNamePlaceholder).foregroundColor(.compound.textPlaceholder), axis: .horizontal) .focused($focus, equals: .name) @@ -183,13 +194,7 @@ struct CreateRoomScreen: View { .font(.compound.bodyLG) .foregroundStyle(.compound.textSecondary) - let binding = Binding(get: { - context.viewState.aliasLocalPart - }, set: { - context.send(viewAction: .updateAddress($0)) - }) - - TextField("", text: binding) + TextField("", text: aliasBinding) .autocapitalization(.none) .textCase(.lowercase) .tint(.compound.iconAccentTertiary) diff --git a/UnitTests/Sources/CreateRoomViewModelTests.swift b/UnitTests/Sources/CreateRoomViewModelTests.swift index 6271bbbc83..9d1050ba2b 100644 --- a/UnitTests/Sources/CreateRoomViewModelTests.swift +++ b/UnitTests/Sources/CreateRoomViewModelTests.swift @@ -68,12 +68,12 @@ class CreateRoomScreenViewModelTests: XCTestCase { func testCreateRoomRequirements() { XCTAssertFalse(context.viewState.canCreateRoom) - context.send(viewAction: .updateName("A")) + context.send(viewAction: .updateRoomName("A")) XCTAssertTrue(context.viewState.canCreateRoom) } func testCreateKnockingRoom() async { - context.send(viewAction: .updateName("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 @@ -95,7 +95,7 @@ class CreateRoomScreenViewModelTests: XCTestCase { } func testCreatePublicRoomFailsForInvalidAlias() async throws { - context.send(viewAction: .updateName("A")) + context.send(viewAction: .updateRoomName("A")) context.roomTopic = "B" context.isRoomPrivate = false // When setting the room as private we always reset the alias @@ -106,7 +106,7 @@ class CreateRoomScreenViewModelTests: XCTestCase { let deferred = deferFulfillment(context.$viewState) { viewState in viewState.aliasErrors.contains(.invalidSymbols) && !viewState.canCreateRoom } - context.send(viewAction: .updateAddress("#:")) + 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 @@ -118,7 +118,7 @@ class CreateRoomScreenViewModelTests: XCTestCase { func testCreatePublicRoomFailsForExistingAlias() async throws { clientProxy.isAliasAvailableReturnValue = .success(false) - context.send(viewAction: .updateName("A")) + context.send(viewAction: .updateRoomName("A")) context.roomTopic = "B" context.isRoomPrivate = false // When setting the room as private we always reset the alias @@ -129,7 +129,7 @@ class CreateRoomScreenViewModelTests: XCTestCase { let deferred = deferFulfillment(context.$viewState) { viewState in viewState.aliasErrors.contains(.alreadyExists) && !viewState.canCreateRoom } - context.send(viewAction: .updateAddress("abc")) + 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 @@ -148,7 +148,7 @@ class CreateRoomScreenViewModelTests: XCTestCase { } func testCreatePrivateRoomCantHaveKnockRule() async { - context.send(viewAction: .updateName("A")) + context.send(viewAction: .updateRoomName("A")) context.roomTopic = "B" context.isRoomPrivate = true context.isKnockingOnly = true @@ -166,30 +166,30 @@ class CreateRoomScreenViewModelTests: XCTestCase { func testNameAndAddressSync() async { context.isRoomPrivate = true await Task.yield() - context.send(viewAction: .updateName("abc")) + context.send(viewAction: .updateRoomName("abc")) XCTAssertEqual(context.viewState.aliasLocalPart, "abc") XCTAssertEqual(context.viewState.roomName, "abc") - context.send(viewAction: .updateName("DEF")) + context.send(viewAction: .updateRoomName("DEF")) XCTAssertEqual(context.viewState.roomName, "DEF") XCTAssertEqual(context.viewState.aliasLocalPart, "def") - context.send(viewAction: .updateName("a b c")) + context.send(viewAction: .updateRoomName("a b c")) XCTAssertEqual(context.viewState.aliasLocalPart, "a-b-c") XCTAssertEqual(context.viewState.roomName, "a b c") - context.send(viewAction: .updateAddress("hello-world")) + 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: .updateName("Hello Matrix!")) + 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: .updateName("")) + context.send(viewAction: .updateRoomName("")) XCTAssertEqual(context.viewState.aliasLocalPart, "") XCTAssertEqual(context.viewState.roomName, "") - context.send(viewAction: .updateName("Hello# Matrix!")) + context.send(viewAction: .updateRoomName("Hello# Matrix!")) XCTAssertEqual(context.viewState.aliasLocalPart, "hello-matrix!") XCTAssertEqual(context.viewState.roomName, "Hello# Matrix!") } From edeccb8a60cffe71fd376c1693655b2111a9554c Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Thu, 7 Nov 2024 17:50:03 +0100 Subject: [PATCH 19/22] to improve safety and control of the FF --- ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift b/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift index 07df3777b3..31e40fd5d6 100644 --- a/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift +++ b/ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift @@ -191,7 +191,7 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol createRoomParameters.topic = state.bindings.roomTopic createRoomParameters.isRoomPrivate = state.bindings.isRoomPrivate createRoomParameters.isKnockingOnly = state.bindings.isKnockingOnly - if !state.aliasLocalPart.isEmpty { + if state.isKnockingFeatureEnabled, !state.aliasLocalPart.isEmpty { createRoomParameters.aliasLocalPart = state.aliasLocalPart } else { createRoomParameters.aliasLocalPart = nil From a45089182c5f38fdba90b1123acd6c9134ce5dc7 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Fri, 8 Nov 2024 13:14:26 +0100 Subject: [PATCH 20/22] fixed a test --- UnitTests/Sources/CreateRoomViewModelTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/UnitTests/Sources/CreateRoomViewModelTests.swift b/UnitTests/Sources/CreateRoomViewModelTests.swift index 9d1050ba2b..110d788468 100644 --- a/UnitTests/Sources/CreateRoomViewModelTests.swift +++ b/UnitTests/Sources/CreateRoomViewModelTests.swift @@ -83,10 +83,10 @@ class CreateRoomScreenViewModelTests: XCTestCase { XCTAssertTrue(context.viewState.canCreateRoom) let expectation = expectation(description: "Wait for the room to be created") - clientProxy.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartClosure = { _, _, isPrivate, isKnockingOnly, _, _, canonicalAlias in + clientProxy.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLAliasLocalPartClosure = { _, _, isPrivate, isKnockingOnly, _, _, localAliasPart in XCTAssertTrue(isKnockingOnly) XCTAssertFalse(isPrivate) - XCTAssertEqual(canonicalAlias, "#a:matrix.org") + XCTAssertEqual(localAliasPart, "a") defer { expectation.fulfill() } return .success("") } From a9e883b2827dfa47fa8e4142404f0f57e4e1bbb8 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Fri, 8 Nov 2024 14:14:34 +0100 Subject: [PATCH 21/22] updated tests --- .../test_secureBackupRecoveryKeyScreen-iPad-en-GB.Set-up.png | 4 ++-- .../test_secureBackupRecoveryKeyScreen-iPad-pseudo.Set-up.png | 4 ++-- ...t_secureBackupRecoveryKeyScreen-iPhone-16-en-GB.Set-up.png | 4 ++-- ..._secureBackupRecoveryKeyScreen-iPhone-16-pseudo.Set-up.png | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) 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 ebbd6a69f3..02ab29e321 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 37f1a320a7..83bba7ef50 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 8d002a493b..49cd7a983f 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 09ee043125..7c5fb0f79c 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 From 01c7f3e50b6ada9671ed5ae08d76636ede07ff23 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Fri, 8 Nov 2024 15:28:32 +0100 Subject: [PATCH 22/22] update SDK --- ElementX.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- project.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index fe0c6e3102..24ab4952ad 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 57400ca50d..0d3eb39468 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/project.yml b/project.yml index 5eb74d64fa..649c0bf736 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