From d36293747474099bf4f62195722a344e5decce9d Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Sat, 21 Feb 2026 17:47:45 -0500 Subject: [PATCH] Improve profile editing UX, especially around unsaved changes As seen with a real life user (and myself) I will sometimes back out of editing user information, despite there being a Save button. That's because we make no attempt to warn or stop the user before doing so. Now that is fixed, and the window will be prevented from closing or the page popping. Additionally, there's now a "Reset Changes" button that works as you'd expect. I also made it so this new button and the existing "Save" button are only enabled when there's unsaved changes. I also did some work on the UX flow to get to this account editing page. There used to be a dedicated button in the account menu, but that's duplicative with the new "Open Profile" option. That's removed, but I also replaced the useless "Message" and "Ignore" (what? ignoring yourself?) buttons with a button to edit the account. Related to removing UI cruft, the "Show QR code" button on this page is removed. There's an easier way to access that through your own profile. --- src/app/qml/AccountMenu.qml | 6 -- src/app/qml/UserDetailDialog.qml | 9 +++ src/settings/AccountEditorPage.qml | 120 ++++++++++++++++++++++------- 3 files changed, 101 insertions(+), 34 deletions(-) diff --git a/src/app/qml/AccountMenu.qml b/src/app/qml/AccountMenu.qml index 0488f47ea..53c35565e 100644 --- a/src/app/qml/AccountMenu.qml +++ b/src/app/qml/AccountMenu.qml @@ -40,12 +40,6 @@ KirigamiComponents.ConvergentContextMenu { }) } - Kirigami.Action { - text: i18nc("@action:inmenu", "Edit This Account") - icon.name: "document-edit" - onTriggered: NeoChatSettingsView.openWithInitialProperties("accounts", {initialAccount: root.connection}); - } - Kirigami.Action { text: i18nc("@action:inmenu", "Notification Settings") icon.name: "notifications" diff --git a/src/app/qml/UserDetailDialog.qml b/src/app/qml/UserDetailDialog.qml index d4253bb2d..94f6d8427 100644 --- a/src/app/qml/UserDetailDialog.qml +++ b/src/app/qml/UserDetailDialog.qml @@ -149,15 +149,24 @@ Kirigami.Dialog { Kirigami.Action { text: i18nc("@action:intoolbar Message this user directly", "Message") icon.name: "document-send-symbolic" + visible: !root.isSelf onTriggered: { root.close(); root.connection.requestDirectChat(root.user.id); } }, + Kirigami.Action { + text: i18nc("@action:intoolbar Edit This Account", "Edit") + icon.name: "document-edit" + visible: root.isSelf + + onTriggered: NeoChatSettingsView.openWithInitialProperties("accounts", {initialAccount: root.connection}); + }, Kirigami.Action { icon.name: "im-invisible-user-symbolic" text: root.connection.isIgnored(root.user.id) ? i18nc("@action:intoolbar Unignore or 'unblock' this user", "Unignore") : i18nc("@action:intoolbar Ignore or 'block' this user", "Ignore") + visible: !root.isSelf onTriggered: { root.close(); diff --git a/src/settings/AccountEditorPage.qml b/src/settings/AccountEditorPage.qml index 1c3e14823..e0c9b78c3 100644 --- a/src/settings/AccountEditorPage.qml +++ b/src/settings/AccountEditorPage.qml @@ -19,6 +19,91 @@ FormCard.FormCardPage { title: i18nc("@title:window", "Edit Account") property NeoChatConnection connection + readonly property bool hasUnsavedChanges: root.connection.localUser.displayName !== name.text + || avatar.source != avatar.findOriginalAvatarUrl() + || root.connection.label !== accountLabel.text; + + function resetChanges(): void { + name.text = root.connection ? root.connection.localUser.displayName : ""; + accountLabel.text = root.connection ? root.connection.label : ""; + avatar.source = avatar.findOriginalAvatarUrl(); + } + + function saveChanges(): void { + if (avatar.source != avatar.findOriginalAvatarUrl() && !root.connection.setAvatar(avatar.source)) { + (root.Window.window as Kirigami.ApplicationWindow).showPassiveNotification(i18nc("@info", "New avatar could not be set.")); + } + if (root.connection.localUser.displayName !== name.text) { + root.connection.localUser.rename(name.text); + } + if (root.connection.label !== accountLabel.text) { + root.connection.label = accountLabel.text; + } + } + + function checkForUnsavedChanges(): bool { + if (root.hasUnsavedChanges) { + resetChangesDialog.open(); + return true; + } + return false; + } + + onBackRequested: event => { + if (checkForUnsavedChanges(event)) { + event.accepted = true; // Prevent the page from popping + } + } + + Connections { + target: root.Window.window + + function onClosing(event): void { + if (root.checkForUnsavedChanges(event)) { + event.accepted = false; // Prevent the window from closing + } + } + } + + Kirigami.PromptDialog { + id: resetChangesDialog + + parent: root.QQC2.Overlay.overlay + preferredWidth: Kirigami.Units.gridUnit * 24 + + title: i18nc("@title:dialog Apply unsaved settings", "Apply Settings") + subtitle: i18nc("@info", "There are unsaved changes to user information. Apply the changes or discard them?") + + standardButtons: QQC2.Dialog.Cancel + + footer: QQC2.DialogButtonBox { + QQC2.Button { + text: i18nc("@action:button As in 'Remove this device'", "Apply") + icon.name: "dialog-ok-apply" + + onClicked: { + root.saveChanges(); + resetChangesDialog.close(); + } + + QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.ApplyRole + } + + QQC2.Button { + text: i18nc("@action:button As in 'Remove this device'", "Reset") + icon.name: "edit-reset-symbolic" + + onClicked: { + root.resetChanges(); + resetChangesDialog.close(); + } + + QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.ResetRole + } + } + } + + Component.onCompleted: resetChanges() KirigamiComponents.AvatarButton { id: avatar @@ -34,8 +119,6 @@ FormCard.FormCardPage { padding: 0 - // Note: User::avatarUrl does not set user_id, and thus cannot be used directly here. Hence the makeMediaUrl. - source: findOriginalAvatarUrl() name: root.connection.localUser.displayName function findOriginalAvatarUrl(): string { @@ -109,14 +192,12 @@ FormCard.FormCardPage { FormCard.FormTextFieldDelegate { id: name label: i18n("Display Name:") - text: root.connection ? root.connection.localUser.displayName : "" } FormCard.FormDelegateSeparator {} FormCard.FormTextFieldDelegate { id: accountLabel label: i18n("Label:") placeholderText: i18n("Work") - text: root.connection ? root.connection.label : "" } FormCard.FormDelegateSeparator {} FormCard.FormTextDelegate { @@ -140,34 +221,17 @@ FormCard.FormCardPage { } FormCard.FormDelegateSeparator {} FormCard.FormButtonDelegate { - text: i18nc("@action:button", "Show QR Code") - icon.name: "view-barcode-qr-symbolic" - onClicked: { - let qrMax = Qt.createComponent('org.kde.neochat', 'QrCodeMaximizeComponent').createObject(QQC2.Overlay.overlay, { - text: "https://matrix.to/#/" + root.connection.localUser.id, - title: root.connection.localUser.displayName, - subtitle: root.connection.localUser.id, - // Note: User::avatarUrl does not set user_id, and thus cannot be used directly here. Hence the makeMediaUrl. - avatarSource: root.connection && (root.connection.localUser.avatarUrl.toString().length > 0 ? root.connection.makeMediaUrl(root.connection.localUser.avatarUrl) : "") - }); - qrMax.open(); - } + text: i18nc("@action:button", "Reset Changes") + icon.name: "edit-reset-symbolic" + enabled: root.hasUnsavedChanges + onClicked: root.resetChanges() } FormCard.FormDelegateSeparator {} FormCard.FormButtonDelegate { - text: i18n("Save") + text: i18nc("@action:button Save changes to user", "Save") icon.name: "document-save-symbolic" - onClicked: { - if (avatar.source != avatar.findOriginalAvatarUrl() && !root.connection.setAvatar(avatar.source)) { - (root.Window.window as Kirigami.ApplicationWindow).showPassiveNotification("The Avatar could not be set"); - } - if (root.connection.localUser.displayName !== name.text) { - root.connection.localUser.rename(name.text); - } - if (root.connection.label !== accountLabel.text) { - root.connection.label = accountLabel.text; - } - } + enabled: root.hasUnsavedChanges + onClicked: root.saveChanges() } }