From 6b37ea87780eedbc9183d5b167a24dd538f46419 Mon Sep 17 00:00:00 2001 From: Zeedif Date: Sun, 23 Aug 2026 20:23:50 -0600 Subject: [PATCH 1/4] feat(setup-wizard): improve E2EE dialog UX and password field - Only show the passphrase and "Obfuscate Properties" controls once end-to-end encryption is enabled, instead of leaving them visible but disabled. - Replace the password show/hide checkbox with an icon toggle button. - Let translated labels wrap to their content instead of being clipped into a fixed-width column (this clipped longer translated labels, e.g. in German and Spanish). - Scope the onboarding invitation link's 44px touch-target padding to mobile, so on desktop it renders as a normal inline link instead of a stray, oddly-padded button. Co-Authored-By: Claude Sonnet 5 --- src/common/messages/combinedMessages.prod.ts | 8 +++ src/common/messagesJson/en.json | 2 + src/common/messagesJson/es.json | 2 + src/common/messagesYAML/en.yaml | 2 + src/common/messagesYAML/es.yaml | 2 + .../dialogs/SetupRemoteE2EE.svelte | 46 +++++++------- .../services/LiveSyncUI/DialogHost.svelte | 2 +- .../LiveSyncUI/components/Password.svelte | 63 ++++++++++++++++++- styles.css | 6 ++ 9 files changed, 108 insertions(+), 25 deletions(-) diff --git a/src/common/messages/combinedMessages.prod.ts b/src/common/messages/combinedMessages.prod.ts index 7cc161e3..27e69c31 100644 --- a/src/common/messages/combinedMessages.prod.ts +++ b/src/common/messages/combinedMessages.prod.ts @@ -2530,6 +2530,10 @@ export const allMessages: Readonly ${remote})": { def: "Higher (${local} > ${remote})", es: "Superior (${local} > ${remote})", @@ -9570,6 +9574,10 @@ export const allMessages: Readonly ${remote})": "Higher (${local} > ${remote})", "Highlight diff": "Highlight diff", "How to display network errors when the sync server is unreachable.": "How to display network errors when the sync server is unreachable.", @@ -1028,6 +1029,7 @@ "Show history": "Show history", "Show icon only": "Show icon only", "Show only notifications": "Show only notifications", + "Show password": "Show password", "Show status as icons only": "Show status as icons only", "Show status icon instead of file warnings banner": "Show status icon instead of file warnings banner", "Show status inside the editor": "Show status inside the editor", diff --git a/src/common/messagesJson/es.json b/src/common/messagesJson/es.json index 957b1b60..a36a5d12 100644 --- a/src/common/messagesJson/es.json +++ b/src/common/messagesJson/es.json @@ -304,6 +304,7 @@ "Hidden Files": "Archivos ocultos", "Hide completely": "Ocultar por completo", "Hide not applicable items": "Ocultar elementos no aplicables", + "Hide password": "Ocultar contraseña", "Higher (${local} > ${remote})": "Superior (${local} > ${remote})", "Highlight diff": "Resaltar las diferencias", "How to display network errors when the sync server is unreachable.": "Cómo mostrar los errores de red cuando el servidor de sincronización no está disponible.", @@ -1069,6 +1070,7 @@ "Show history": "Mostrar el historial", "Show icon only": "Mostrar solo el icono", "Show only notifications": "Mostrar solo notificaciones", + "Show password": "Mostrar contraseña", "Show status as icons only": "Mostrar estado solo con íconos", "Show status icon instead of file warnings banner": "Mostrar icono de estado en lugar del banner de advertencia de archivos", "Show status inside the editor": "Mostrar estado dentro del editor", diff --git a/src/common/messagesYAML/en.yaml b/src/common/messagesYAML/en.yaml index 844842c5..4560fe54 100644 --- a/src/common/messagesYAML/en.yaml +++ b/src/common/messagesYAML/en.yaml @@ -401,6 +401,7 @@ Hidden file synchronization have been temporarily disabled. Please enable them a Hidden Files: Hidden Files Hide completely: Hide completely Hide not applicable items: Hide not applicable items +Hide password: Hide password Higher (${local} > ${remote}): Higher (${local} > ${remote}) Highlight diff: Highlight diff How to display network errors when the sync server is unreachable.: How to display network errors when the sync server is unreachable. @@ -1830,6 +1831,7 @@ Show full banner: Show full banner Show history: Show history Show icon only: Show icon only Show only notifications: Show only notifications +Show password: Show password Show status as icons only: Show status as icons only Show status icon instead of file warnings banner: Show status icon instead of file warnings banner Show status inside the editor: Show status inside the editor diff --git a/src/common/messagesYAML/es.yaml b/src/common/messagesYAML/es.yaml index 6b79fddc..7a6b0d49 100644 --- a/src/common/messagesYAML/es.yaml +++ b/src/common/messagesYAML/es.yaml @@ -425,6 +425,7 @@ Hidden file synchronization have been temporarily disabled. Please enable them a Hidden Files: Archivos ocultos Hide completely: Ocultar por completo Hide not applicable items: Ocultar elementos no aplicables +Hide password: Ocultar contraseña Higher (${local} > ${remote}): Superior (${local} > ${remote}) Highlight diff: Resaltar las diferencias How to display network errors when the sync server is unreachable.: @@ -1950,6 +1951,7 @@ Show full banner: Mostrar banner completo Show history: Mostrar el historial Show icon only: Mostrar solo el icono Show only notifications: Mostrar solo notificaciones +Show password: Mostrar contraseña Show status as icons only: Mostrar estado solo con íconos Show status icon instead of file warnings banner: Mostrar icono de estado en lugar del banner de advertencia de archivos Show status inside the editor: Mostrar estado dentro del editor diff --git a/src/modules/features/SetupWizard/dialogs/SetupRemoteE2EE.svelte b/src/modules/features/SetupWizard/dialogs/SetupRemoteE2EE.svelte index efce2429..0c6c00a1 100644 --- a/src/modules/features/SetupWizard/dialogs/SetupRemoteE2EE.svelte +++ b/src/modules/features/SetupWizard/dialogs/SetupRemoteE2EE.svelte @@ -52,13 +52,6 @@ {translateMessage("Please configure your end-to-end encryption settings.")} - {translateMessage( @@ -69,22 +62,29 @@ "Also, please note that if you are using Peer-to-Peer synchronization, this configuration will be used when you switch to other methods and connect to a remote server in the future." )} - - {translateMessage("This setting must be the same even when connecting to multiple synchronisation destinations.")} - - - - - - - {translateMessage( - "Obfuscating properties (e.g., path of file, size, creation and modification dates) adds an additional layer of security by making it harder to identify the structure and names of your files and folders on the remote server. This helps protect your privacy and makes it more difficult for unauthorized users to infer information about your data." - )} - +{#if encryptionSettings.encrypt} + + + + + {translateMessage( + "This setting must be the same even when connecting to multiple synchronisation destinations." + )} + + + + + + {translateMessage( + "Obfuscating properties (e.g., path of file, size, creation and modification dates) adds an additional layer of security by making it harder to identify the structure and names of your files and folders on the remote server. This helps protect your privacy and makes it more difficult for unauthorized users to infer information about your data." + )} + +{/if} diff --git a/src/modules/services/LiveSyncUI/DialogHost.svelte b/src/modules/services/LiveSyncUI/DialogHost.svelte index 6265c7bf..c71c257a 100644 --- a/src/modules/services/LiveSyncUI/DialogHost.svelte +++ b/src/modules/services/LiveSyncUI/DialogHost.svelte @@ -110,7 +110,7 @@ .dialog-host :global(label > span) { display: block; - width: 8em; + min-width: 8em; } .dialog-host :global(.note), diff --git a/src/modules/services/LiveSyncUI/components/Password.svelte b/src/modules/services/LiveSyncUI/components/Password.svelte index 5ebf3884..b3be8c80 100644 --- a/src/modules/services/LiveSyncUI/components/Password.svelte +++ b/src/modules/services/LiveSyncUI/components/Password.svelte @@ -18,6 +18,7 @@ let showPassword = $state(false); const type = $derived.by(() => (showPassword ? "text" : "password")); const translatedPlaceholder = $derived.by(() => translate(placeholder)); + const toggleLabel = $derived.by(() => translate(showPassword ? "Hide password" : "Show password")); - + + + diff --git a/styles.css b/styles.css index b5bf7ac6..5a537c3d 100644 --- a/styles.css +++ b/styles.css @@ -353,6 +353,12 @@ body { } .sls-onboarding-invitation-action { + display: inline; +} + +/* Touch devices need a larger tap target; on desktop this would just pad the + link with blank space and make it look like a misplaced button. */ +body.is-mobile .sls-onboarding-invitation-action { display: inline-flex; min-width: 44px; min-height: 44px; From 2c35f45765307c26ac7c5d61ca22ce6dc01f3b2d Mon Sep 17 00:00:00 2001 From: Zeedif Date: Wed, 26 Aug 2026 20:45:44 -0600 Subject: [PATCH 2/4] =?UTF-8?q?fix(setup-wizard):=20address=20review=20?= =?UTF-8?q?=E2=80=94=20icon,=20CSS=20scope,=20E2E=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use a plain emoji glyph for the password visibility toggle instead of embedded SVG paths, matching the same pragmatic approach already used for the browser build's menu icons in this codebase. - Scope the wider label width to the E2EE dialogue instead of changing it for every InputRow in every Svelte dialogue. - Extend the existing CouchDB manual setup E2E workflow to assert the passphrase and Obfuscate Properties fields are hidden until encryption is enabled, then appear, and that the passphrase value survives toggling its visibility. --- .../dialogs/SetupRemoteE2EE.svelte | 158 ++++++++++-------- .../services/LiveSyncUI/DialogHost.svelte | 2 +- .../LiveSyncUI/components/Password.svelte | 35 +--- .../scripts/couchdb-manual-setup-workflow.ts | 38 ++++- 4 files changed, 126 insertions(+), 107 deletions(-) diff --git a/src/modules/features/SetupWizard/dialogs/SetupRemoteE2EE.svelte b/src/modules/features/SetupWizard/dialogs/SetupRemoteE2EE.svelte index 0c6c00a1..7340f31d 100644 --- a/src/modules/features/SetupWizard/dialogs/SetupRemoteE2EE.svelte +++ b/src/modules/features/SetupWizard/dialogs/SetupRemoteE2EE.svelte @@ -48,80 +48,94 @@ } - -{translateMessage("Please configure your end-to-end encryption settings.")} - - - - - {translateMessage( - "Enabling end-to-end encryption ensures that your data is encrypted on your device before being sent to the remote server. This means that even if someone gains access to the server, they won't be able to read your data without the passphrase. Make sure to remember your passphrase, as it will be required to decrypt your data on other devices." - )} -
- {translateMessage( - "Also, please note that if you are using Peer-to-Peer synchronization, this configuration will be used when you switch to other methods and connect to a remote server in the future." - )} -
-{#if encryptionSettings.encrypt} - - +
+ + {translateMessage("Please configure your end-to-end encryption settings.")} + + + + {translateMessage( + "Enabling end-to-end encryption ensures that your data is encrypted on your device before being sent to the remote server. This means that even if someone gains access to the server, they won't be able to read your data without the passphrase. Make sure to remember your passphrase, as it will be required to decrypt your data on other devices." + )} +
+ {translateMessage( + "Also, please note that if you are using Peer-to-Peer synchronization, this configuration will be used when you switch to other methods and connect to a remote server in the future." + )} +
+ {#if encryptionSettings.encrypt} + + + + + {translateMessage( + "This setting must be the same even when connecting to multiple synchronisation destinations." + )} + + + + + + {translateMessage( + "Obfuscating properties (e.g., path of file, size, creation and modification dates) adds an additional layer of security by making it harder to identify the structure and names of your files and folders on the remote server. This helps protect your privacy and makes it more difficult for unauthorized users to infer information about your data." + )} + + {/if} + + + + + + + {translateMessage( + "In most cases, you should stick with the default algorithm (${algorithm}), This setting is only required if you have an existing Vault encrypted in a different format.", + { algorithm: E2EEAlgorithmNames[DEFAULT_SETTINGS.E2EEAlgorithm] } + )} + + + {translateMessage( + "Changing the encryption algorithm will prevent access to any data previously encrypted with a different algorithm. Ensure that all your devices are configured to use the same algorithm to maintain access to your data." + )} + + + - {translateMessage( - "This setting must be the same even when connecting to multiple synchronisation destinations." - )} +

+ {translateMessage( + "Please be aware that the End-to-End Encryption passphrase is not validated until the synchronisation process actually commences. This is a security measure designed to protect your data." + )} +

+

+ {translateMessage( + "Therefore, we ask that you exercise extreme caution when configuring server information manually. If an incorrect passphrase is entered, the data on the server will become corrupted." + )}

+ {translateMessage("Please understand that this is intended behaviour.")} +

- - - - - {translateMessage( - "Obfuscating properties (e.g., path of file, size, creation and modification dates) adds an additional layer of security by making it harder to identify the structure and names of your files and folders on the remote server. This helps protect your privacy and makes it more difficult for unauthorized users to infer information about your data." - )} - -{/if} - - - - - - {translateMessage( - "In most cases, you should stick with the default algorithm (${algorithm}), This setting is only required if you have an existing Vault encrypted in a different format.", - { algorithm: E2EEAlgorithmNames[DEFAULT_SETTINGS.E2EEAlgorithm] } - )} - - - {translateMessage( - "Changing the encryption algorithm will prevent access to any data previously encrypted with a different algorithm. Ensure that all your devices are configured to use the same algorithm to maintain access to your data." - )} - - + + commit()} /> + setResult(TYPE_CANCELLED)} /> + +
- -

- {translateMessage( - "Please be aware that the End-to-End Encryption passphrase is not validated until the synchronisation process actually commences. This is a security measure designed to protect your data." - )} -

-

- {translateMessage( - "Therefore, we ask that you exercise extreme caution when configuring server information manually. If an incorrect passphrase is entered, the data on the server will become corrupted." - )}

- {translateMessage("Please understand that this is intended behaviour.")} -

-
- - - commit()} /> - setResult(TYPE_CANCELLED)} /> - + diff --git a/src/modules/services/LiveSyncUI/DialogHost.svelte b/src/modules/services/LiveSyncUI/DialogHost.svelte index c71c257a..6265c7bf 100644 --- a/src/modules/services/LiveSyncUI/DialogHost.svelte +++ b/src/modules/services/LiveSyncUI/DialogHost.svelte @@ -110,7 +110,7 @@ .dialog-host :global(label > span) { display: block; - min-width: 8em; + width: 8em; } .dialog-host :global(.note), diff --git a/src/modules/services/LiveSyncUI/components/Password.svelte b/src/modules/services/LiveSyncUI/components/Password.svelte index b3be8c80..1a674ee8 100644 --- a/src/modules/services/LiveSyncUI/components/Password.svelte +++ b/src/modules/services/LiveSyncUI/components/Password.svelte @@ -41,37 +41,7 @@ {disabled} onclick={() => (showPassword = !showPassword)} > - {#if showPassword} - - - - - - - {:else} - - - - - {/if} + 👁️ diff --git a/test/e2e-obsidian/scripts/couchdb-manual-setup-workflow.ts b/test/e2e-obsidian/scripts/couchdb-manual-setup-workflow.ts index ef44ae87..fc832bd1 100644 --- a/test/e2e-obsidian/scripts/couchdb-manual-setup-workflow.ts +++ b/test/e2e-obsidian/scripts/couchdb-manual-setup-workflow.ts @@ -117,19 +117,55 @@ async function enterManualCouchDBSettings(port: number, couchDb: CouchDbConfig, const encryption = modalByTitle(page, "End-to-End Encryption"); await encryption.waitFor({ state: "visible", timeout: uiTimeoutMs }); + assertEqual( + await encryption.locator('input[name="e2ee-passphrase"]').count(), + 0, + "The passphrase field was present before end-to-end encryption was enabled." + ); + assertEqual( + await encryption.locator("label.row").filter({ hasText: "Obfuscate Properties" }).count(), + 0, + "The Obfuscate Properties row was present before end-to-end encryption was enabled." + ); await encryption .locator("label.row") .filter({ hasText: "End-to-End Encryption" }) .locator('input[type="checkbox"]') .first() .check({ timeout: uiTimeoutMs }); + const passphraseInput = encryption.locator('input[name="e2ee-passphrase"]'); + await passphraseInput.waitFor({ state: "visible", timeout: uiTimeoutMs }); await encryption .locator("label.row") .filter({ hasText: "Obfuscate Properties" }) .locator('input[type="checkbox"]') .first() .check({ timeout: uiTimeoutMs }); - await encryption.locator('input[name="e2ee-passphrase"]').fill(randomBytes(24).toString("base64url")); + const passphraseValue = randomBytes(24).toString("base64url"); + await passphraseInput.fill(passphraseValue); + const passwordToggle = encryption.locator("button.sls-password-toggle"); + await passwordToggle.click({ timeout: uiTimeoutMs }); + assertEqual( + await passphraseInput.getAttribute("type"), + "text", + "Toggling visibility did not reveal the passphrase." + ); + assertEqual( + await passphraseInput.inputValue(), + passphraseValue, + "Toggling visibility changed the passphrase value." + ); + await passwordToggle.click({ timeout: uiTimeoutMs }); + assertEqual( + await passphraseInput.getAttribute("type"), + "password", + "Toggling visibility again did not re-mask the passphrase." + ); + assertEqual( + await passphraseInput.inputValue(), + passphraseValue, + "Re-masking the passphrase changed its value." + ); }); screenshots.push(await captureGuideDialogue(port, "guide-couchdb-manual-encryption.png", "End-to-End Encryption")); await withObsidianPage(port, async (page) => { From a9b146a0ab84400b75dde1d98e0898760e6c1242 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Wed, 2 Sep 2026 14:45:19 +0000 Subject: [PATCH 3/4] test(setup-wizard): enforce mobile password touch target --- .../scripts/onboarding-invitation.ts | 70 ++++++++++++++++--- 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/test/e2e-obsidian/scripts/onboarding-invitation.ts b/test/e2e-obsidian/scripts/onboarding-invitation.ts index fbc8189a..be7bfa94 100644 --- a/test/e2e-obsidian/scripts/onboarding-invitation.ts +++ b/test/e2e-obsidian/scripts/onboarding-invitation.ts @@ -134,7 +134,57 @@ async function captureAndSelectMobileInvitation(): Promise { return screenshot; } -async function captureAndCloseIntro(filename: string, mobile: boolean): Promise { +async function captureMobilePasswordToggle(): Promise { + const port = obsidianRemoteDebuggingPort(); + await withObsidianPage(port, async (page) => { + const intro = onboardingDialogue(page); + await intro + .locator("label") + .filter({ hasText: "I am setting this up for the first time" }) + .locator('input[type="radio"]') + .first() + .check({ timeout: uiTimeoutMs }); + await intro + .getByRole("button", { name: "Yes, I want to set up a new synchronisation" }) + .click({ timeout: uiTimeoutMs }); + + const method = page.locator(".modal-container").filter({ hasText: "Connection Method" }); + await method.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await method + .locator("label") + .filter({ hasText: "Configure a remote manually" }) + .locator('input[type="radio"]') + .first() + .check({ timeout: uiTimeoutMs }); + await method.getByRole("button", { name: "Proceed with manual configuration" }).click({ timeout: uiTimeoutMs }); + + const encryption = page.locator(".modal-container").filter({ hasText: "End-to-End Encryption" }); + await encryption.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await encryption + .locator("label.row") + .filter({ hasText: "End-to-End Encryption" }) + .locator('input[type="checkbox"]') + .first() + .check({ timeout: uiTimeoutMs }); + }); + const screenshot = await captureObsidianDialogue(port, "onboarding-e2ee-mobile.png", async (page) => { + const encryption = page.locator(".modal-container").filter({ hasText: "End-to-End Encryption" }); + const passwordToggle = encryption.locator("button.sls-password-toggle"); + await passwordToggle.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await passwordToggle.evaluate((element) => element.scrollIntoView({ block: "center" })); + await assertLocatorHasMinimumTouchTarget(page, passwordToggle, { + label: "mobile password visibility button", + }); + }); + await withObsidianPage(port, async (page) => { + const encryption = page.locator(".modal-container").filter({ hasText: "End-to-End Encryption" }); + await encryption.getByRole("button", { name: "Cancel", exact: true }).click({ timeout: uiTimeoutMs }); + await encryption.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + return screenshot; +} + +async function captureIntro(filename: string, mobile: boolean, closeAfterCapture = true): Promise { const port = obsidianRemoteDebuggingPort(); const screenshot = await captureObsidianDialogue(port, filename, async (page) => { const container = onboardingDialogue(page); @@ -148,11 +198,13 @@ async function captureAndCloseIntro(filename: string, mobile: boolean): Promise< .waitFor({ state: "visible", timeout: uiTimeoutMs }); if (mobile) await assertMobileDialogueLayout(page, container, "mobile onboarding introduction"); }); - await withObsidianPage(port, async (page) => { - const container = onboardingDialogue(page); - await container.getByRole("button", { name: "No, please take me back" }).click({ timeout: uiTimeoutMs }); - await container.waitFor({ state: "hidden", timeout: uiTimeoutMs }); - }); + if (closeAfterCapture) { + await withObsidianPage(port, async (page) => { + const container = onboardingDialogue(page); + await container.getByRole("button", { name: "No, please take me back" }).click({ timeout: uiTimeoutMs }); + await container.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + } return screenshot; } @@ -235,10 +287,11 @@ async function main(): Promise { const desktopInvitation = await captureDesktopInvitation(); const mobileInvitation = await captureAndSelectMobileInvitation(); - const mobileIntro = await captureAndCloseIntro("onboarding-intro-mobile.png", true); + const mobileIntro = await captureIntro("onboarding-intro-mobile.png", true, false); + const mobileEncryption = await captureMobilePasswordToggle(); await setObsidianMobileTestMode(obsidianRemoteDebuggingPort(), false, uiTimeoutMs); await openOnboardingFromSettings(); - const settingsIntro = await captureAndCloseIntro("onboarding-intro-settings-desktop.png", false); + const settingsIntro = await captureIntro("onboarding-intro-settings-desktop.png", false); await dismissVisibleNotices(); await closeSettings(); @@ -247,6 +300,7 @@ async function main(): Promise { desktopInvitation, mobileInvitation, mobileIntro, + mobileEncryption, settingsIntro, ].join(", ")}` ); From be9c328f4a8e0a6073397065476d96f81d626497 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Wed, 2 Sep 2026 14:45:33 +0000 Subject: [PATCH 4/4] fix(setup-wizard): enlarge mobile password toggle --- src/modules/services/LiveSyncUI/components/Password.svelte | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/modules/services/LiveSyncUI/components/Password.svelte b/src/modules/services/LiveSyncUI/components/Password.svelte index 1a674ee8..6642ae0b 100644 --- a/src/modules/services/LiveSyncUI/components/Password.svelte +++ b/src/modules/services/LiveSyncUI/components/Password.svelte @@ -61,4 +61,7 @@ .sls-password-toggle:hover { background: var(--background-modifier-hover); } + :global(body.is-mobile) .sls-password-toggle { + min-width: 44px; + }