From 061a6ce58f1fc7e1affb8af38f03417e9e991776 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Mon, 21 Oct 2024 18:32:57 +0200 Subject: [PATCH 01/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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) }