From d42ad04aa3e94e53e0df75815cc48e6917365026 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Wed, 8 Apr 2026 23:55:46 -0300 Subject: [PATCH 01/45] chore: fix snjs tests [snjs docker only] --- packages/snjs/mocha/sync_tests/online.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/snjs/mocha/sync_tests/online.test.js b/packages/snjs/mocha/sync_tests/online.test.js index 86c20a097..66d29bb36 100644 --- a/packages/snjs/mocha/sync_tests/online.test.js +++ b/packages/snjs/mocha/sync_tests/online.test.js @@ -452,7 +452,7 @@ describe('online syncing', function () { } expect(safeGuard.isSyncCallsThresholdReachedThisMinute()).to.equal(true) - expect(syncCount).to.equal(200) + expect(syncCount).to.be.at.least(200) }) it('items that are never synced and deleted should not be uploaded to server', async function () { From 437ebddbd0c0d8a66a8e4e4c573d7cac460a00c9 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Thu, 9 Apr 2026 15:25:46 -0300 Subject: [PATCH 02/45] chore: split snjs session tests and increase token ttl [snjs docker only] (#3001) --- packages/snjs/mocha/TestRegistry/BaseTests.js | 2 -- packages/snjs/mocha/TestRegistry/MainRegistry.js | 2 ++ packages/snjs/mocha/TestRegistry/SessionTests.js | 8 ++++++++ packages/snjs/mocha/test.html | 5 +++++ 4 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 packages/snjs/mocha/TestRegistry/SessionTests.js diff --git a/packages/snjs/mocha/TestRegistry/BaseTests.js b/packages/snjs/mocha/TestRegistry/BaseTests.js index f0c296012..df0db7faa 100644 --- a/packages/snjs/mocha/TestRegistry/BaseTests.js +++ b/packages/snjs/mocha/TestRegistry/BaseTests.js @@ -55,8 +55,6 @@ export const BaseTests = { 'actions.test.js', 'preferences.test.js', 'files.test.js', - 'session.test.js', - 'session-invalidation.test.js', 'subscriptions.test.js', 'recovery.test.js', ] diff --git a/packages/snjs/mocha/TestRegistry/MainRegistry.js b/packages/snjs/mocha/TestRegistry/MainRegistry.js index a93e318a5..b9596b4b7 100644 --- a/packages/snjs/mocha/TestRegistry/MainRegistry.js +++ b/packages/snjs/mocha/TestRegistry/MainRegistry.js @@ -1,7 +1,9 @@ import { BaseTests } from './BaseTests.js' +import { SessionTests } from './SessionTests.js' import { VaultTests } from './VaultTests.js' export default { BaseTests, + SessionTests, VaultTests, } diff --git a/packages/snjs/mocha/TestRegistry/SessionTests.js b/packages/snjs/mocha/TestRegistry/SessionTests.js new file mode 100644 index 000000000..832e36fec --- /dev/null +++ b/packages/snjs/mocha/TestRegistry/SessionTests.js @@ -0,0 +1,8 @@ +export const SessionTests = { + enabled: true, + exclusive: false, + files: [ + 'session.test.js', + 'session-invalidation.test.js', + ], +} diff --git a/packages/snjs/mocha/test.html b/packages/snjs/mocha/test.html index b0a53963e..b751d6fff 100644 --- a/packages/snjs/mocha/test.html +++ b/packages/snjs/mocha/test.html @@ -44,6 +44,8 @@ MainRegistry.VaultTests.exclusive = true; } else if (testSuite === 'base') { MainRegistry.BaseTests.exclusive = true; + } else if (testSuite === 'session') { + MainRegistry.SessionTests.exclusive = true; } const loadTest = (fileName) => { @@ -70,9 +72,12 @@ await loadTests(MainRegistry.VaultTests.files); } else if (MainRegistry.BaseTests.exclusive) { await loadTests(MainRegistry.BaseTests.files); + } else if (MainRegistry.SessionTests.exclusive) { + await loadTests(MainRegistry.SessionTests.files); } else { await loadTests([ ...MainRegistry.BaseTests.files, + ...MainRegistry.SessionTests.files, ...MainRegistry.VaultTests.files ]); } From cc1f16c70e6ccfc5d43c0d4e7af6c65d5fd51104 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Thu, 9 Apr 2026 15:37:47 -0300 Subject: [PATCH 03/45] Revert "chore: split snjs session tests and increase token ttl [snjs docker only] (#3001)" This reverts commit 437ebddbd0c0d8a66a8e4e4c573d7cac460a00c9. --- packages/snjs/mocha/TestRegistry/BaseTests.js | 2 ++ packages/snjs/mocha/TestRegistry/MainRegistry.js | 2 -- packages/snjs/mocha/TestRegistry/SessionTests.js | 8 -------- packages/snjs/mocha/test.html | 5 ----- 4 files changed, 2 insertions(+), 15 deletions(-) delete mode 100644 packages/snjs/mocha/TestRegistry/SessionTests.js diff --git a/packages/snjs/mocha/TestRegistry/BaseTests.js b/packages/snjs/mocha/TestRegistry/BaseTests.js index df0db7faa..f0c296012 100644 --- a/packages/snjs/mocha/TestRegistry/BaseTests.js +++ b/packages/snjs/mocha/TestRegistry/BaseTests.js @@ -55,6 +55,8 @@ export const BaseTests = { 'actions.test.js', 'preferences.test.js', 'files.test.js', + 'session.test.js', + 'session-invalidation.test.js', 'subscriptions.test.js', 'recovery.test.js', ] diff --git a/packages/snjs/mocha/TestRegistry/MainRegistry.js b/packages/snjs/mocha/TestRegistry/MainRegistry.js index b9596b4b7..a93e318a5 100644 --- a/packages/snjs/mocha/TestRegistry/MainRegistry.js +++ b/packages/snjs/mocha/TestRegistry/MainRegistry.js @@ -1,9 +1,7 @@ import { BaseTests } from './BaseTests.js' -import { SessionTests } from './SessionTests.js' import { VaultTests } from './VaultTests.js' export default { BaseTests, - SessionTests, VaultTests, } diff --git a/packages/snjs/mocha/TestRegistry/SessionTests.js b/packages/snjs/mocha/TestRegistry/SessionTests.js deleted file mode 100644 index 832e36fec..000000000 --- a/packages/snjs/mocha/TestRegistry/SessionTests.js +++ /dev/null @@ -1,8 +0,0 @@ -export const SessionTests = { - enabled: true, - exclusive: false, - files: [ - 'session.test.js', - 'session-invalidation.test.js', - ], -} diff --git a/packages/snjs/mocha/test.html b/packages/snjs/mocha/test.html index b751d6fff..b0a53963e 100644 --- a/packages/snjs/mocha/test.html +++ b/packages/snjs/mocha/test.html @@ -44,8 +44,6 @@ MainRegistry.VaultTests.exclusive = true; } else if (testSuite === 'base') { MainRegistry.BaseTests.exclusive = true; - } else if (testSuite === 'session') { - MainRegistry.SessionTests.exclusive = true; } const loadTest = (fileName) => { @@ -72,12 +70,9 @@ await loadTests(MainRegistry.VaultTests.files); } else if (MainRegistry.BaseTests.exclusive) { await loadTests(MainRegistry.BaseTests.files); - } else if (MainRegistry.SessionTests.exclusive) { - await loadTests(MainRegistry.SessionTests.files); } else { await loadTests([ ...MainRegistry.BaseTests.files, - ...MainRegistry.SessionTests.files, ...MainRegistry.VaultTests.files ]); } From 12230e28a488514658bcddb0c44556bb275ed625 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Fri, 10 Apr 2026 23:07:15 -0300 Subject: [PATCH 04/45] chore: fix snjs tests [snjs docker only] --- packages/snjs/mocha/recovery.test.js | 24 ++++++++++++++---------- packages/snjs/mocha/session.test.js | 9 +++++++-- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/packages/snjs/mocha/recovery.test.js b/packages/snjs/mocha/recovery.test.js index b81b4dd7b..0b09ca6e3 100644 --- a/packages/snjs/mocha/recovery.test.js +++ b/packages/snjs/mocha/recovery.test.js @@ -26,21 +26,25 @@ describe('account recovery', function () { }) it('should get the same recovery code at each consecutive call', async () => { - let recoveryCodesSetting = await application.settings.getSetting(SettingName.create(SettingName.NAMES.RecoveryCodes).getValue()) + const recoveryCodesSettingName = SettingName.create(SettingName.NAMES.RecoveryCodes).getValue() + const rootKey = await application.encryption.computeRootKey(context.password, await application.encryption.getRootKeyParams()) + const serverPassword = rootKey.serverPassword + + let recoveryCodesSetting = await application.settings.getSetting(recoveryCodesSettingName, serverPassword) expect(recoveryCodesSetting).to.equal(undefined) - const generatedRecoveryCodesAfterFirstCall = await application.getRecoveryCodes.execute() + const generatedRecoveryCodesAfterFirstCall = await application.getRecoveryCodes.execute({ password: context.password }) expect(generatedRecoveryCodesAfterFirstCall.getValue().length).to.equal(49) - recoveryCodesSetting = await application.settings.getSetting(SettingName.create(SettingName.NAMES.RecoveryCodes).getValue()) + recoveryCodesSetting = await application.settings.getSetting(recoveryCodesSettingName, serverPassword) expect(recoveryCodesSetting).to.equal(generatedRecoveryCodesAfterFirstCall.getValue()) - const fetchedRecoveryCodesOnTheSecondCall = await application.getRecoveryCodes.execute() + const fetchedRecoveryCodesOnTheSecondCall = await application.getRecoveryCodes.execute({ password: context.password }) expect(generatedRecoveryCodesAfterFirstCall.getValue()).to.equal(fetchedRecoveryCodesOnTheSecondCall.getValue()) }) it('should allow to sign in with recovery code', async () => { - const generatedRecoveryCodes = await application.getRecoveryCodes.execute() + const generatedRecoveryCodes = await application.getRecoveryCodes.execute({ password: context.password }) application = await context.signout() @@ -56,7 +60,7 @@ describe('account recovery', function () { }) it('should automatically generate new recovery code after recovery sign in', async () => { - const generatedRecoveryCodes = await application.getRecoveryCodes.execute() + const generatedRecoveryCodes = await application.getRecoveryCodes.execute({ password: context.password }) application = await context.signout() @@ -66,7 +70,7 @@ describe('account recovery', function () { password: context.password, }) - const recoveryCodesAfterRecoverySignIn = await application.getRecoveryCodes.execute() + const recoveryCodesAfterRecoverySignIn = await application.getRecoveryCodes.execute({ password: context.password }) expect(recoveryCodesAfterRecoverySignIn.getValue()).not.to.equal(generatedRecoveryCodes.getValue()) }) @@ -78,7 +82,7 @@ describe('account recovery', function () { expect(await application.mfa.isMfaActivated()).to.equal(true) - const generatedRecoveryCodes = await application.getRecoveryCodes.execute() + const generatedRecoveryCodes = await application.getRecoveryCodes.execute({ password: context.password }) application = await context.signout() @@ -92,7 +96,7 @@ describe('account recovery', function () { }) it('should not allow to sign in with recovery code and invalid credentials', async () => { - const generatedRecoveryCodes = await application.getRecoveryCodes.execute() + const generatedRecoveryCodes = await application.getRecoveryCodes.execute({ password: context.password }) application = await context.signout() @@ -108,7 +112,7 @@ describe('account recovery', function () { }) it('should not allow to sign in with invalid recovery code', async () => { - await application.getRecoveryCodes.execute() + await application.getRecoveryCodes.execute({ password: context.password }) application = await context.signout() diff --git a/packages/snjs/mocha/session.test.js b/packages/snjs/mocha/session.test.js index 3ca733e19..0da230e5b 100644 --- a/packages/snjs/mocha/session.test.js +++ b/packages/snjs/mocha/session.test.js @@ -3,6 +3,8 @@ import WebDeviceInterface from './lib/web_device_interface.js' chai.use(chaiAsPromised) const expect = chai.expect +const urlSearchParams = new URLSearchParams(window.location.search) +const skipSessionCooldownRefreshTest = urlSearchParams.get('skip_session_cooldown_refresh_test') === 'true' describe('server session', function () { this.timeout(Factory.TenSecondTimeout) @@ -481,7 +483,9 @@ describe('server session', function () { await refreshPromise }) - it('should tell the client to refresh the token if one is used during the cooldown period after a refresh', async function () { + ;(skipSessionCooldownRefreshTest ? it.skip : it)( + 'should tell the client to refresh the token if one is used during the cooldown period after a refresh', + async function () { await Factory.registerUserToApplication({ application: application, email: email, @@ -508,7 +512,8 @@ describe('server session', function () { application.http.setSession = originalSetSessionFn application.http.refreshSessionCallback = originalRefreshSessionCallbackFn - }) + }, + ) it('if session renewal response is dropped, next sync with server should return a 498 and successfully renew the session', async function () { await Factory.registerUserToApplication({ From b111c3b2bc0b231aabfe456e6103fadf86efe828 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Fri, 10 Apr 2026 23:32:42 -0300 Subject: [PATCH 05/45] chore: fix snjs tests [snjs docker only] --- packages/snjs/mocha/session.test.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/snjs/mocha/session.test.js b/packages/snjs/mocha/session.test.js index 0da230e5b..f23d3228a 100644 --- a/packages/snjs/mocha/session.test.js +++ b/packages/snjs/mocha/session.test.js @@ -4,7 +4,7 @@ import WebDeviceInterface from './lib/web_device_interface.js' chai.use(chaiAsPromised) const expect = chai.expect const urlSearchParams = new URLSearchParams(window.location.search) -const skipSessionCooldownRefreshTest = urlSearchParams.get('skip_session_cooldown_refresh_test') === 'true' +const skipSessionCooldownTests = urlSearchParams.get('skipSessionCooldownTests') === 'true' describe('server session', function () { this.timeout(Factory.TenSecondTimeout) @@ -483,7 +483,7 @@ describe('server session', function () { await refreshPromise }) - ;(skipSessionCooldownRefreshTest ? it.skip : it)( + ;(skipSessionCooldownTests ? it.skip : it)( 'should tell the client to refresh the token if one is used during the cooldown period after a refresh', async function () { await Factory.registerUserToApplication({ @@ -515,7 +515,9 @@ describe('server session', function () { }, ) - it('if session renewal response is dropped, next sync with server should return a 498 and successfully renew the session', async function () { + ;(skipSessionCooldownTests ? it.skip : it)( + 'if session renewal response is dropped, next sync with server should return a 498 and successfully renew the session', + async function () { await Factory.registerUserToApplication({ application: application, email: email, @@ -534,8 +536,9 @@ describe('server session', function () { await application.sync.sync(syncOptions) await application.sync.sync(syncOptions) - expect(refreshSpy.callCount).to.equal(2) - }) + expect(refreshSpy.callCount).to.equal(2) + }, + ) it('notes should be synced as expected after refreshing a session', async function () { await Factory.registerUserToApplication({ From 150e1a315c8ea0c09c6ad3a02d9adc0533308199 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Thu, 23 Apr 2026 14:45:32 -0300 Subject: [PATCH 06/45] chore: improve import tools (#3000) --- .../EvernoteConverter/EvernoteConverter.ts | 22 +++++++++++++------ .../GoogleKeepConverter.ts | 9 ++++---- .../src/Import/ImportLimits.spec.ts | 15 +++++++++++++ .../ui-services/src/Import/ImportLimits.ts | 7 ++++++ packages/ui-services/src/Import/Importer.ts | 6 +++++ 5 files changed, 48 insertions(+), 11 deletions(-) create mode 100644 packages/ui-services/src/Import/ImportLimits.spec.ts create mode 100644 packages/ui-services/src/Import/ImportLimits.ts diff --git a/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.ts b/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.ts index f19fc7b14..e377e4a1c 100644 --- a/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.ts +++ b/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.ts @@ -388,15 +388,23 @@ export class EvernoteConverter implements Converter { } stripHTML(html: string) { - const tmp = document.createElement('html') - tmp.innerHTML = html - return tmp.textContent || tmp.innerText || '' + const doc = new DOMParser().parseFromString(html, 'text/html') + return doc.body.textContent || '' } } function changeElementTag(element: HTMLElement, newTag: string) { - const attributes = Array.prototype.slice.call(element.attributes) - element.outerHTML = `<${newTag} ${attributes.map((attr) => attr.name + '="' + attr.value + '"').join(' ')}>${ - element.innerHTML - }` + const doc = element.ownerDocument + const parent = element.parentElement + if (!parent) { + return + } + const replacement = doc.createElement(newTag) + for (const attr of Array.from(element.attributes)) { + replacement.setAttribute(attr.name, attr.value) + } + while (element.firstChild) { + replacement.appendChild(element.firstChild) + } + parent.replaceChild(replacement, element) } diff --git a/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.ts b/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.ts index 5f11f5da5..00acfa510 100644 --- a/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.ts +++ b/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.ts @@ -74,8 +74,8 @@ export class GoogleKeepConverter implements Converter { convertHTMLToSuper: HTMLToSuperConverterFunction, canUseSuper: boolean, ): Promise { - const rootElement = document.createElement('html') - rootElement.innerHTML = data + const doc = new DOMParser().parseFromString(data, 'text/html') + const rootElement = doc.documentElement const headingElement = rootElement.getElementsByClassName('heading')[0] const parsedDate = new Date(headingElement?.textContent || '') @@ -110,8 +110,9 @@ export class GoogleKeepConverter implements Converter { }) if (!canUseSuper) { - // Replace
with \n so line breaks get recognised - contentElement.innerHTML = contentElement.innerHTML.replace(/
/g, '\n') + Array.from(contentElement.querySelectorAll('br')).forEach((br) => { + br.replaceWith(doc.createTextNode('\n')) + }) content = contentElement.textContent } else { content = convertHTMLToSuper(rootElement.innerHTML, { diff --git a/packages/ui-services/src/Import/ImportLimits.spec.ts b/packages/ui-services/src/Import/ImportLimits.spec.ts new file mode 100644 index 000000000..b25b4b0a8 --- /dev/null +++ b/packages/ui-services/src/Import/ImportLimits.spec.ts @@ -0,0 +1,15 @@ +import { assertImportFileWithinSizeLimit, MaxImportFileSizeBytes } from './ImportLimits' + +describe('ImportLimits', () => { + it('rejects files over the configured limit', () => { + const file = new File(['x'], 'note.html', { type: 'text/html' }) + Object.defineProperty(file, 'size', { value: MaxImportFileSizeBytes + 1 }) + expect(() => assertImportFileWithinSizeLimit(file)).toThrow('Import file is too large') + }) + + it('allows files at or below max import size', () => { + const file = new File(['x'], 'note.html', { type: 'text/html' }) + Object.defineProperty(file, 'size', { value: MaxImportFileSizeBytes }) + expect(() => assertImportFileWithinSizeLimit(file)).not.toThrow() + }) +}) diff --git a/packages/ui-services/src/Import/ImportLimits.ts b/packages/ui-services/src/Import/ImportLimits.ts new file mode 100644 index 000000000..bd8d04a30 --- /dev/null +++ b/packages/ui-services/src/Import/ImportLimits.ts @@ -0,0 +1,7 @@ +export const MaxImportFileSizeBytes = 50 * 1_000_000 + +export function assertImportFileWithinSizeLimit(file: File): void { + if (file.size > MaxImportFileSizeBytes) { + throw new Error('Import file is too large') + } +} diff --git a/packages/ui-services/src/Import/Importer.ts b/packages/ui-services/src/Import/Importer.ts index a8af94e8d..9a5c04857 100644 --- a/packages/ui-services/src/Import/Importer.ts +++ b/packages/ui-services/src/Import/Importer.ts @@ -12,6 +12,7 @@ import { EvernoteConverter } from './EvernoteConverter/EvernoteConverter' import { GoogleKeepConverter } from './GoogleKeepConverter/GoogleKeepConverter' import { PlaintextConverter } from './PlaintextConverter/PlaintextConverter' import { SimplenoteConverter } from './SimplenoteConverter/SimplenoteConverter' +import { assertImportFileWithinSizeLimit, MaxImportFileSizeBytes } from './ImportLimits' import { readFileAsText } from './Utils' import { DecryptedItemInterface, @@ -74,6 +75,9 @@ export class Importer { } detectService = async (file: File): Promise => { + if (file.size > MaxImportFileSizeBytes) { + return null + } const content = await readFileAsText(file) const { ext } = parseFileName(file.name) @@ -232,6 +236,8 @@ export class Importer { throw new Error('Importing Super notes requires a subscription') } + assertImportFileWithinSizeLimit(file) + const successful: ConversionResult['successful'] = [] const errored: ConversionResult['errored'] = [] From b33089029f5979adf0c8e80a65c29a38945cf899 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Thu, 23 Apr 2026 14:48:30 -0300 Subject: [PATCH 07/45] fix: Fixes Super sections drag and drop issues on mobile (#3003) --- .../SuperEditor/Plugins/DraggableBlockPlugin/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/DraggableBlockPlugin/index.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/DraggableBlockPlugin/index.tsx index 8b01fe1f8..d624ef807 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/DraggableBlockPlugin/index.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/DraggableBlockPlugin/index.tsx @@ -310,7 +310,7 @@ function useDraggableBlockMenu(editor: LexicalEditor, anchorElem: HTMLElement, i return false } - const dragData = dataTransfer?.getData(DRAG_DATA_FORMAT) || '' + const dragData = dataTransfer?.getData(DRAG_DATA_FORMAT) || draggedNodeKey || '' const draggedNode = $getNodeByKey(dragData) if (!draggedNode) { return false From 90ca0c2cfb648d34c97d58b332a10c61daa20fa4 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Thu, 23 Apr 2026 14:48:52 -0300 Subject: [PATCH 08/45] fix: Prevents unhandled error when attempting to import a blank note (#3004) --- .../Components/ImportModal/ImportModalFileItem.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/web/src/javascripts/Components/ImportModal/ImportModalFileItem.tsx b/packages/web/src/javascripts/Components/ImportModal/ImportModalFileItem.tsx index 6f3f18ad7..fbaba7ff8 100644 --- a/packages/web/src/javascripts/Components/ImportModal/ImportModalFileItem.tsx +++ b/packages/web/src/javascripts/Components/ImportModal/ImportModalFileItem.tsx @@ -126,9 +126,14 @@ const ImportModalFileItem = ({ useEffect(() => { const detect = async () => { setIsDetectingService(true) - const detectedService = await importer.detectService(file.file) - void setFileService(detectedService) - setIsDetectingService(false) + try { + const detectedService = await importer.detectService(file.file) + void setFileService(detectedService) + } catch { + void setFileService(null) + } finally { + setIsDetectingService(false) + } } if (file.service === undefined) { void detect() From 6261fd8241b36be0e73dd84b39d31059d6077c16 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Thu, 23 Apr 2026 14:49:29 -0300 Subject: [PATCH 09/45] fix: Adds dynamic type support for iOS (#3005) * feat: Adds dynamic type support for iOS * fix: Replace hardcoded pixel font sizes * fix: fixes eslint error --- packages/mobile/src/MobileWebAppContainer.tsx | 36 ++++++++++++++++++- .../ChangeEditor/ChangeEditorMenu.tsx | 2 +- .../Preferences/Panes/WhatsNew/WhatsNew.tsx | 6 ++-- .../PreferencesComponents/Content.tsx | 4 ++- .../Subviews/SuccessPrompt.tsx | 2 +- .../SuperEditor/Lexical/Theme/editor.scss | 6 ++-- packages/web/src/stylesheets/_editor.scss | 2 +- .../web/src/stylesheets/_items-column.scss | 2 +- packages/web/src/stylesheets/_main.scss | 2 +- packages/web/src/stylesheets/_navigation.scss | 6 ++-- 10 files changed, 52 insertions(+), 16 deletions(-) diff --git a/packages/mobile/src/MobileWebAppContainer.tsx b/packages/mobile/src/MobileWebAppContainer.tsx index c0373fcc8..c36bcac19 100644 --- a/packages/mobile/src/MobileWebAppContainer.tsx +++ b/packages/mobile/src/MobileWebAppContainer.tsx @@ -2,7 +2,7 @@ import { ApplicationEvent, ReactNativeToWebEvent } from '@standardnotes/snjs' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { Button, Dimensions, Keyboard, KeyboardEvent, Platform, Text, View } from 'react-native' +import { AppState, Button, Dimensions, Keyboard, KeyboardEvent, Platform, Text, View } from 'react-native' import VersionInfo from 'react-native-version-info' import { WebView, WebViewMessageEvent } from 'react-native-webview' import { OnShouldStartLoadWithRequest, WebViewNativeConfig } from 'react-native-webview/lib/WebViewTypes' @@ -52,6 +52,18 @@ const MobileWebAppContents = ({ destroyAndReload }: { destroyAndReload: () => vo const useFlexLayout = Platform.OS === 'ios' || androidVersion < 35 const [webViewContainerHeight, setWebViewContainerHeight] = useState(screenHeight) + const applyDynamicTypeFontScale = useCallback((fontScale?: number) => { + if (Platform.OS !== 'ios') { + return + } + + const scale = fontScale ?? Dimensions.get('window').fontScale + webViewRef.current?.injectJavaScript(` + document.documentElement.style.fontSize = 'calc(1rem * ${scale})'; + true; + `) + }, []) + useEffect(() => { const removeStateServiceListener = stateService.addEventObserver((event: ReactNativeToWebEvent) => { webViewRef.current?.postMessage(JSON.stringify({ reactNativeEvent: event, messageType: 'event' })) @@ -150,6 +162,27 @@ const MobileWebAppContents = ({ destroyAndReload }: { destroyAndReload: () => vo } }, [webViewRef, stateService, device, androidBackHandlerService, colorSchemeService, insets.bottom, screenHeight]) + useEffect(() => { + if (Platform.OS !== 'ios') { + return + } + + const dimensionsListener = Dimensions.addEventListener('change', ({ window }) => { + applyDynamicTypeFontScale(window.fontScale) + }) + + const appStateListener = AppState.addEventListener('change', (state) => { + if (state === 'active') { + applyDynamicTypeFontScale() + } + }) + + return () => { + dimensionsListener.remove() + appStateListener.remove() + } + }, [applyDynamicTypeFontScale]) + useEffect(() => { return notifee.onForegroundEvent(({ type, detail }) => { if (type !== EventType.ACTION_PRESS) { @@ -305,6 +338,7 @@ const MobileWebAppContents = ({ destroyAndReload }: { destroyAndReload: () => vo } if (message === 'appLoaded') { setDidLoadEnd(true) + applyDynamicTypeFontScale() return } try { diff --git a/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx b/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx index 1a4aad1cd..d92d2cd76 100644 --- a/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx +++ b/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx @@ -301,7 +301,7 @@ const ChangeEditorMenu: FunctionComponent = ({ {menuItem.uiFeature.featureIdentifier === NativeFeatureIdentifier.TYPES.SuperEditor && !isSelected(menuItem) && recommendSuper && ( - + Recommended )} diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/WhatsNew/WhatsNew.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/WhatsNew/WhatsNew.tsx index 4c967d171..0b3f9d4d6 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/WhatsNew/WhatsNew.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/WhatsNew/WhatsNew.tsx @@ -80,17 +80,17 @@ const WhatsNew = ({ application }: { application: WebApplication }) => {
{version.version} {version.version === appVersion && ( -
+
Your Version
)} {isLatest && ( -
+
Latest Version
)} {isUnreadVersion && ( -
+
New
)} diff --git a/packages/web/src/javascripts/Components/Preferences/PreferencesComponents/Content.tsx b/packages/web/src/javascripts/Components/Preferences/PreferencesComponents/Content.tsx index 281289ad1..1a23979d2 100644 --- a/packages/web/src/javascripts/Components/Preferences/PreferencesComponents/Content.tsx +++ b/packages/web/src/javascripts/Components/Preferences/PreferencesComponents/Content.tsx @@ -53,6 +53,8 @@ type PillProps = Props & { export const Pill: FunctionComponent = ({ children, className, style }) => { const colorClass = getColorsForPrimaryVariant(style) return ( -
{children}
+
+ {children} +
) } diff --git a/packages/web/src/javascripts/Components/PremiumFeaturesModal/Subviews/SuccessPrompt.tsx b/packages/web/src/javascripts/Components/PremiumFeaturesModal/Subviews/SuccessPrompt.tsx index 51c3fcae4..cddd34c54 100644 --- a/packages/web/src/javascripts/Components/PremiumFeaturesModal/Subviews/SuccessPrompt.tsx +++ b/packages/web/src/javascripts/Components/PremiumFeaturesModal/Subviews/SuccessPrompt.tsx @@ -23,7 +23,7 @@ export const SuccessPrompt = ({ className="mx-auto mb-5 flex h-24 w-24 items-center justify-center rounded-[50%] bg-contrast" aria-hidden={true} > - +
Your purchase was successful!
diff --git a/packages/web/src/javascripts/Components/SuperEditor/Lexical/Theme/editor.scss b/packages/web/src/javascripts/Components/SuperEditor/Lexical/Theme/editor.scss index b806a0e9c..dbe536ded 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Lexical/Theme/editor.scss +++ b/packages/web/src/javascripts/Components/SuperEditor/Lexical/Theme/editor.scss @@ -34,19 +34,19 @@ } } .Lexical__h1 { - font-size: 26px; + font-size: 1.625rem; color: var(--sn-stylekit-editor-foreground-color); font-weight: 700; margin: 0; } .Lexical__h2 { - font-size: 22px; + font-size: 1.375rem; color: var(--sn-stylekit-editor-foreground-color); font-weight: 700; margin: 0; } .Lexical__h3 { - font-size: 19px; + font-size: 1.1875rem; font-weight: 700; margin: 0; } diff --git a/packages/web/src/stylesheets/_editor.scss b/packages/web/src/stylesheets/_editor.scss index afd94cec0..080fff86f 100644 --- a/packages/web/src/stylesheets/_editor.scss +++ b/packages/web/src/stylesheets/_editor.scss @@ -71,7 +71,7 @@ $heading-height: 75px; } #save-status { - font-size: calc(var(--sn-stylekit-base-font-size) - 2px); + font-size: calc(var(--sn-stylekit-base-font-size) - 0.125rem); text-transform: none; font-weight: normal; diff --git a/packages/web/src/stylesheets/_items-column.scss b/packages/web/src/stylesheets/_items-column.scss index 4630ac0fa..8f73e4629 100644 --- a/packages/web/src/stylesheets/_items-column.scss +++ b/packages/web/src/stylesheets/_items-column.scss @@ -89,7 +89,7 @@ cursor: default; background-color: var(--sn-stylekit-neutral-color); color: var(--sn-stylekit-neutral-contrast-color); - font-size: 10px; + font-size: 0.625rem; line-height: 17px; text-align: center; position: absolute; diff --git a/packages/web/src/stylesheets/_main.scss b/packages/web/src/stylesheets/_main.scss index 0d280c956..ce1ed6508 100644 --- a/packages/web/src/stylesheets/_main.scss +++ b/packages/web/src/stylesheets/_main.scss @@ -160,7 +160,7 @@ body, .section-title-bar { .add-button { - font-size: 12px; + font-size: 0.75rem; } .section-title-bar-header { diff --git a/packages/web/src/stylesheets/_navigation.scss b/packages/web/src/stylesheets/_navigation.scss index 4ddb5198d..05bb882c6 100644 --- a/packages/web/src/stylesheets/_navigation.scss +++ b/packages/web/src/stylesheets/_navigation.scss @@ -23,7 +23,7 @@ $content-horizontal-padding: 16px; padding-bottom: 8px; padding-left: $content-horizontal-padding; padding-right: $content-horizontal-padding; - font-size: 12px; + font-size: 0.75rem; } .root-drop { @@ -54,7 +54,7 @@ $content-horizontal-padding: 16px; .tag, .root-drop { - font-size: 14px; + font-size: 0.875rem; cursor: pointer; transition: height 0.1s ease-in-out; width: 100%; @@ -136,7 +136,7 @@ $content-horizontal-padding: 16px; .meta { > .menu { - font-size: 11px; + font-size: 0.6875rem; > .item { margin-right: 4px; From 93196a04f78cfd70074d76f1a97f0cb41bee321e Mon Sep 17 00:00:00 2001 From: StandardNotes CI Date: Thu, 23 Apr 2026 17:55:08 +0000 Subject: [PATCH 10/45] chore(release): publish - @standardnotes/clipper@1.1.584 - @standardnotes/desktop@3.110.189 - @standardnotes/mobile@3.58.249 - @standardnotes/releases@1.4.889 - @standardnotes/snjs@2.211.6 - @standardnotes/ui-services@1.39.3 - @standardnotes/web@3.201.25 --- packages/clipper/CHANGELOG.md | 4 ++++ packages/clipper/package.json | 2 +- packages/desktop/CHANGELOG.md | 4 ++++ packages/desktop/package.json | 2 +- packages/mobile/CHANGELOG.md | 6 ++++++ packages/mobile/package.json | 2 +- packages/releases/CHANGELOG.md | 4 ++++ packages/releases/package.json | 2 +- packages/snjs/CHANGELOG.md | 6 ++++++ packages/snjs/package.json | 2 +- packages/ui-services/CHANGELOG.md | 4 ++++ packages/ui-services/package.json | 2 +- packages/web/CHANGELOG.md | 8 ++++++++ packages/web/CHANGELOG.md.json | 18 ++++++++++++++++++ packages/web/package.json | 2 +- 15 files changed, 61 insertions(+), 7 deletions(-) diff --git a/packages/clipper/CHANGELOG.md b/packages/clipper/CHANGELOG.md index 53a2bea76..5fc6edcb8 100644 --- a/packages/clipper/CHANGELOG.md +++ b/packages/clipper/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.1.584](https://github.com/standardnotes/app/compare/@standardnotes/clipper@1.1.583...@standardnotes/clipper@1.1.584) (2026-04-23) + +**Note:** Version bump only for package @standardnotes/clipper + ## [1.1.583](https://github.com/standardnotes/app/compare/@standardnotes/clipper@1.1.582...@standardnotes/clipper@1.1.583) (2026-04-07) **Note:** Version bump only for package @standardnotes/clipper diff --git a/packages/clipper/package.json b/packages/clipper/package.json index 426b95d84..b4d3ccda1 100644 --- a/packages/clipper/package.json +++ b/packages/clipper/package.json @@ -1,7 +1,7 @@ { "name": "@standardnotes/clipper", "description": "Web clipper browser extension for Standard Notes", - "version": "1.1.583", + "version": "1.1.584", "private": true, "scripts": { "build-mv2": "yarn clean && webpack --config ./webpack.config.prod.js", diff --git a/packages/desktop/CHANGELOG.md b/packages/desktop/CHANGELOG.md index 2e7e849ee..72c9f2da1 100644 --- a/packages/desktop/CHANGELOG.md +++ b/packages/desktop/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [3.110.189](https://github.com/standardnotes/app/compare/@standardnotes/desktop@3.201.24...@standardnotes/desktop@3.110.189) (2026-04-23) + +**Note:** Version bump only for package @standardnotes/desktop + ## [3.110.188](https://github.com/standardnotes/app/compare/@standardnotes/desktop@3.201.23...@standardnotes/desktop@3.110.188) (2026-04-07) **Note:** Version bump only for package @standardnotes/desktop diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 9e8234db7..dceec7b15 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@standardnotes/desktop", "main": "./app/dist/index.js", - "version": "3.110.188", + "version": "3.110.189", "license": "AGPL-3.0", "author": "Standard Notes.", "private": true, diff --git a/packages/mobile/CHANGELOG.md b/packages/mobile/CHANGELOG.md index 6f34b7708..970d938f4 100644 --- a/packages/mobile/CHANGELOG.md +++ b/packages/mobile/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [3.58.249](https://github.com/standardnotes/app/compare/@standardnotes/mobile@3.58.248...@standardnotes/mobile@3.58.249) (2026-04-23) + +### Bug Fixes + +* Adds dynamic type support for iOS ([#3005](https://github.com/standardnotes/app/issues/3005)) ([6261fd8](https://github.com/standardnotes/app/commit/6261fd8241b36be0e73dd84b39d31059d6077c16)) + ## [3.58.248](https://github.com/standardnotes/app/compare/@standardnotes/mobile@3.58.247...@standardnotes/mobile@3.58.248) (2026-04-07) **Note:** Version bump only for package @standardnotes/mobile diff --git a/packages/mobile/package.json b/packages/mobile/package.json index b43d7f380..0045d055a 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@standardnotes/mobile", - "version": "3.58.248", + "version": "3.58.249", "author": "Standard Notes.", "private": true, "license": "AGPL-3.0", diff --git a/packages/releases/CHANGELOG.md b/packages/releases/CHANGELOG.md index d4642d5ed..056bb0a53 100644 --- a/packages/releases/CHANGELOG.md +++ b/packages/releases/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.4.889](https://github.com/standardnotes/app/compare/@standardnotes/releases@1.4.888...@standardnotes/releases@1.4.889) (2026-04-23) + +**Note:** Version bump only for package @standardnotes/releases + ## [1.4.888](https://github.com/standardnotes/app/compare/@standardnotes/releases@1.4.887...@standardnotes/releases@1.4.888) (2026-04-07) **Note:** Version bump only for package @standardnotes/releases diff --git a/packages/releases/package.json b/packages/releases/package.json index 6a0c3e770..5fb700bc3 100644 --- a/packages/releases/package.json +++ b/packages/releases/package.json @@ -1,6 +1,6 @@ { "name": "@standardnotes/releases", - "version": "1.4.888", + "version": "1.4.889", "repository": { "type": "git", "url": "https://github.com/standardnotes/app", diff --git a/packages/snjs/CHANGELOG.md b/packages/snjs/CHANGELOG.md index a7c1dd7a2..9eb9b2008 100644 --- a/packages/snjs/CHANGELOG.md +++ b/packages/snjs/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [2.211.6](https://github.com/standardnotes/app/compare/@standardnotes/snjs@2.211.5...@standardnotes/snjs@2.211.6) (2026-04-23) + +### Reverts + +* Revert "chore: split snjs session tests and increase token ttl [snjs docker only] (#3001)" ([cc1f16c](https://github.com/standardnotes/app/commit/cc1f16c70e6ccfc5d43c0d4e7af6c65d5fd51104)), closes [#3001](https://github.com/standardnotes/app/issues/3001) + ## [2.211.5](https://github.com/standardnotes/app/compare/@standardnotes/snjs@2.211.4...@standardnotes/snjs@2.211.5) (2026-04-07) **Note:** Version bump only for package @standardnotes/snjs diff --git a/packages/snjs/package.json b/packages/snjs/package.json index 7836e924c..394a9407b 100644 --- a/packages/snjs/package.json +++ b/packages/snjs/package.json @@ -1,6 +1,6 @@ { "name": "@standardnotes/snjs", - "version": "2.211.5", + "version": "2.211.6", "repository": { "type": "git", "url": "https://github.com/standardnotes/app", diff --git a/packages/ui-services/CHANGELOG.md b/packages/ui-services/CHANGELOG.md index 11350c9e2..493c020f7 100644 --- a/packages/ui-services/CHANGELOG.md +++ b/packages/ui-services/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.39.3](https://github.com/standardnotes/app/compare/@standardnotes/ui-services@1.39.2...@standardnotes/ui-services@1.39.3) (2026-04-23) + +**Note:** Version bump only for package @standardnotes/ui-services + ## [1.39.2](https://github.com/standardnotes/app/compare/@standardnotes/ui-services@1.39.1...@standardnotes/ui-services@1.39.2) (2025-12-18) **Note:** Version bump only for package @standardnotes/ui-services diff --git a/packages/ui-services/package.json b/packages/ui-services/package.json index 97add7948..dbcd99413 100644 --- a/packages/ui-services/package.json +++ b/packages/ui-services/package.json @@ -1,6 +1,6 @@ { "name": "@standardnotes/ui-services", - "version": "1.39.2", + "version": "1.39.3", "engines": { "node": ">=16.0.0 <17.0.0" }, diff --git a/packages/web/CHANGELOG.md b/packages/web/CHANGELOG.md index 8350995ac..5bb94b345 100644 --- a/packages/web/CHANGELOG.md +++ b/packages/web/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [3.201.25](https://github.com/standardnotes/app/compare/@standardnotes/web@3.201.24...@standardnotes/web@3.201.25) (2026-04-23) + +### Bug Fixes + +* Adds dynamic type support for iOS ([#3005](https://github.com/standardnotes/app/issues/3005)) ([6261fd8](https://github.com/standardnotes/app/commit/6261fd8241b36be0e73dd84b39d31059d6077c16)) +* Fixes Super sections drag and drop issues on mobile ([#3003](https://github.com/standardnotes/app/issues/3003)) ([b330890](https://github.com/standardnotes/app/commit/b33089029f5979adf0c8e80a65c29a38945cf899)) +* Prevents unhandled error when attempting to import a blank note ([#3004](https://github.com/standardnotes/app/issues/3004)) ([90ca0c2](https://github.com/standardnotes/app/commit/90ca0c2cfb648d34c97d58b332a10c61daa20fa4)) + ## [3.201.24](https://github.com/standardnotes/app/compare/@standardnotes/web@3.201.23...@standardnotes/web@3.201.24) (2026-04-07) **Note:** Version bump only for package @standardnotes/web diff --git a/packages/web/CHANGELOG.md.json b/packages/web/CHANGELOG.md.json index 11041f358..532f00f1e 100644 --- a/packages/web/CHANGELOG.md.json +++ b/packages/web/CHANGELOG.md.json @@ -1,5 +1,23 @@ { "versions": [ + { + "version": "3.201.25", + "title": "[3.201.25](https://github.com/standardnotes/app/compare/@standardnotes/web@3.201.24...@standardnotes/web@3.201.25) (2026-04-23)", + "date": null, + "body": "### Bug Fixes\n\n* Adds dynamic type support for iOS ([#3005](https://github.com/standardnotes/app/issues/3005)) ([6261fd8](https://github.com/standardnotes/app/commit/6261fd8241b36be0e73dd84b39d31059d6077c16))\n* Fixes Super sections drag and drop issues on mobile ([#3003](https://github.com/standardnotes/app/issues/3003)) ([b330890](https://github.com/standardnotes/app/commit/b33089029f5979adf0c8e80a65c29a38945cf899))\n* Prevents unhandled error when attempting to import a blank note ([#3004](https://github.com/standardnotes/app/issues/3004)) ([90ca0c2](https://github.com/standardnotes/app/commit/90ca0c2cfb648d34c97d58b332a10c61daa20fa4))", + "parsed": { + "_": [ + "Adds dynamic type support for iOS (#3005) (6261fd8)", + "Fixes Super sections drag and drop issues on mobile (#3003) (b330890)", + "Prevents unhandled error when attempting to import a blank note (#3004) (90ca0c2)" + ], + "Bug Fixes": [ + "Adds dynamic type support for iOS (#3005) (6261fd8)", + "Fixes Super sections drag and drop issues on mobile (#3003) (b330890)", + "Prevents unhandled error when attempting to import a blank note (#3004) (90ca0c2)" + ] + } + }, { "version": "3.201.24", "title": "[3.201.24](https://github.com/standardnotes/app/compare/@standardnotes/web@3.201.23...@standardnotes/web@3.201.24) (2026-04-07)", diff --git a/packages/web/package.json b/packages/web/package.json index 1c2eea937..533d931bd 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,6 +1,6 @@ { "name": "@standardnotes/web", - "version": "3.201.24", + "version": "3.201.25", "license": "AGPL-3.0", "main": "dist/app.js", "author": "Standard Notes", From 073db143144bf88007010e4f44003198b327b4da Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Thu, 23 Apr 2026 22:41:45 -0300 Subject: [PATCH 11/45] fix: Shows info on UI and hides Manage button for shared subs (#3002) * fix: Shows info on UI and hides Manage button for shared subs * chore: dry --- packages/responses/src/Domain/Temp/Subscription.ts | 1 + .../Preferences/Panes/Account/AccountPreferences.tsx | 3 ++- .../Account/Subscription/SubscriptionInformation.tsx | 5 ++++- .../Account/Subscription/SubscriptionStatusText.tsx | 12 +++++++++++- .../Subscription/SubscriptionController.ts | 5 +++++ 5 files changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/responses/src/Domain/Temp/Subscription.ts b/packages/responses/src/Domain/Temp/Subscription.ts index e749413a9..269499378 100644 --- a/packages/responses/src/Domain/Temp/Subscription.ts +++ b/packages/responses/src/Domain/Temp/Subscription.ts @@ -4,4 +4,5 @@ export type Subscription = { createdAt: number updatedAt: number cancelled: boolean + subscriptionType?: 'regular' | 'shared' } diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Account/AccountPreferences.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Account/AccountPreferences.tsx index 7fe9aa7da..2e1dec482 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Account/AccountPreferences.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Account/AccountPreferences.tsx @@ -18,6 +18,7 @@ type Props = { const AccountPreferences = ({ application }: Props) => { const isUsingThirdPartyServer = !application.sessions.isSignedIntoFirstPartyServer() + const isSharedSubscription = application.subscriptionController.isSharedSubscription return ( @@ -30,7 +31,7 @@ const AccountPreferences = ({ application }: Props) => { )} - + {!isSharedSubscription && } {application.hasAccount() && application.featuresController.entitledToFiles && ( )} diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Account/Subscription/SubscriptionInformation.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Account/Subscription/SubscriptionInformation.tsx index 5e02fc7d7..f945da571 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Account/Subscription/SubscriptionInformation.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Account/Subscription/SubscriptionInformation.tsx @@ -5,6 +5,7 @@ import { useApplication } from '@/Components/ApplicationProvider' const SubscriptionInformation = () => { const application = useApplication() + const isSharedSubscription = application.subscriptionController.isSharedSubscription const manageSubscription = async () => { void application.openSubscriptionDashboard.execute() @@ -13,7 +14,9 @@ const SubscriptionInformation = () => { return ( <> - @@ -124,12 +125,17 @@ const AdvancedOptions: FunctionComponent = ({ @@ -140,7 +146,7 @@ const AdvancedOptions: FunctionComponent = ({ className={{ container: 'mb-2' }} left={[]} type="text" - placeholder="Username" + placeholder={c('Label').t`Username`} value={privateUsername} onChange={handlePrivateUsernameNameChange} disabled={disabled || isRecoveryCodes} @@ -154,7 +160,7 @@ const AdvancedOptions: FunctionComponent = ({
= ({ href="https://standardnotes.com/help/security" target="_blank" rel="noopener noreferrer" - title="Learn more" + title={c('Action').t`Learn more`} > @@ -174,7 +180,7 @@ const AdvancedOptions: FunctionComponent = ({
= ({ className={{ container: 'mb-2' }} left={[]} type="text" - placeholder="Recovery code" + placeholder={c('Label').t`Recovery code`} value={recoveryCodes} onChange={handleRecoveryCodesChange} disabled={disabled} diff --git a/packages/web/src/javascripts/Components/AccountMenu/ConfirmNoMergeDialog.tsx b/packages/web/src/javascripts/Components/AccountMenu/ConfirmNoMergeDialog.tsx index 7888be217..e1719a591 100644 --- a/packages/web/src/javascripts/Components/AccountMenu/ConfirmNoMergeDialog.tsx +++ b/packages/web/src/javascripts/Components/AccountMenu/ConfirmNoMergeDialog.tsx @@ -2,6 +2,7 @@ import AlertDialog from '@/Components/AlertDialog/AlertDialog' import Button from '@/Components/Button/Button' import Icon from '@/Components/Icon/Icon' import { FunctionComponent } from 'react' +import { c } from 'ttag' type Props = { onClose: () => void @@ -12,7 +13,7 @@ const ConfirmNoMergeDialog: FunctionComponent = ({ onClose, onConfirm }) return (
- Delete local data? + {c('Title').t`Delete local data?`} @@ -20,18 +21,18 @@ const ConfirmNoMergeDialog: FunctionComponent = ({ onClose, onConfirm })

- You have chosen not to merge your local data. If you proceed, your local notes and tags will be permanently - deleted and replaced with data from your account. This action cannot be undone. + {c('Info') + .t`You have chosen not to merge your local data. If you proceed, your local notes and tags will be permanently deleted and replaced with data from your account. This action cannot be undone.`}

- Are you sure you want to continue without merging? + {c('Info').t`Are you sure you want to continue without merging?`}

- +
diff --git a/packages/web/src/javascripts/Components/AccountMenu/ConfirmPassword.tsx b/packages/web/src/javascripts/Components/AccountMenu/ConfirmPassword.tsx index 77ef5cdae..1124f4617 100644 --- a/packages/web/src/javascripts/Components/AccountMenu/ConfirmPassword.tsx +++ b/packages/web/src/javascripts/Components/AccountMenu/ConfirmPassword.tsx @@ -122,7 +122,7 @@ const ConfirmPassword: FunctionComponent = ({ setMenuPane, email, passwor } if (password !== confirmPassword) { - setError(STRING_NON_MATCHING_PASSWORDS) + setError(STRING_NON_MATCHING_PASSWORDS()) setConfirmPassword('') passwordInputRef.current?.focus() return diff --git a/packages/web/src/javascripts/Components/AccountMenu/GeneralAccountMenu.tsx b/packages/web/src/javascripts/Components/AccountMenu/GeneralAccountMenu.tsx index 1e5ff8114..6eb132811 100644 --- a/packages/web/src/javascripts/Components/AccountMenu/GeneralAccountMenu.tsx +++ b/packages/web/src/javascripts/Components/AccountMenu/GeneralAccountMenu.tsx @@ -47,7 +47,7 @@ const GeneralAccountMenu: FunctionComponent = ({ setMenuPane, closeMenu, } }) .catch(() => { - application.alerts.alert(STRING_GENERIC_SYNC_ERROR).catch(console.error) + application.alerts.alert(STRING_GENERIC_SYNC_ERROR()).catch(console.error) }) .finally(() => { setIsSyncingInProgress(false) @@ -69,9 +69,9 @@ const GeneralAccountMenu: FunctionComponent = ({ setMenuPane, closeMenu, }, [application]) const openEmail = useCallback(() => { - const subject = 'Standard Notes Feedback' + const subject = c('MailtoSubject').t`Standard Notes Feedback` - const body = `App Version: ${application.version}` + const body = c('MailtoBody').t`App Version: ${application.version}` application.device.openUrl( `mailto:help@standardnotes.com?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`, @@ -103,7 +103,7 @@ const GeneralAccountMenu: FunctionComponent = ({ setMenuPane, closeMenu, return ( <>
-
Account
+
{c('Title').t`Account`}
@@ -111,7 +111,7 @@ const GeneralAccountMenu: FunctionComponent = ({ setMenuPane, closeMenu, {user ? ( <>
-
You're signed in as:
+
{c('Info').t`You're signed in as:`}
{user.email}
{application.getHost.execute().getValue()}
@@ -119,13 +119,13 @@ const GeneralAccountMenu: FunctionComponent = ({ setMenuPane, closeMenu, {isSyncingInProgress ? (
- Syncing... + {c('Status').t`Syncing...`}
) : (
-
Last synced:
+
{c('Label').t`Last synced:`}
{lastSyncDate}
@@ -161,7 +161,7 @@ const GeneralAccountMenu: FunctionComponent = ({ setMenuPane, closeMenu, {user ? ( - Account settings + {c('Action').t`Account settings`} ) : ( <> @@ -182,18 +182,18 @@ const GeneralAccountMenu: FunctionComponent = ({ setMenuPane, closeMenu, }} > - Import + {c('Action').t`Import`} {application.isNativeMobileWeb() && ( - Email us + {c('Action').t`Email us`} )}
- Help & feedback + {c('Action').t`Help & feedback`}
v{application.version}
@@ -205,7 +205,7 @@ const GeneralAccountMenu: FunctionComponent = ({ setMenuPane, closeMenu, }} > - Keyboard shortcuts + {c('Action').t`Keyboard shortcuts`} {keyboardShortcutsHelpShortcut && ( )} @@ -216,7 +216,7 @@ const GeneralAccountMenu: FunctionComponent = ({ setMenuPane, closeMenu, }} > - Command palette + {c('Action').t`Command palette`} {commandPaletteShortcut && ( )} @@ -228,7 +228,7 @@ const GeneralAccountMenu: FunctionComponent = ({ setMenuPane, closeMenu, - Sign out workspace + {c('Action').t`Sign out workspace`} ) : null} diff --git a/packages/web/src/javascripts/Components/AccountMenu/MergeLocalDataCheckbox.tsx b/packages/web/src/javascripts/Components/AccountMenu/MergeLocalDataCheckbox.tsx index cd2e10b2c..39e1826af 100644 --- a/packages/web/src/javascripts/Components/AccountMenu/MergeLocalDataCheckbox.tsx +++ b/packages/web/src/javascripts/Components/AccountMenu/MergeLocalDataCheckbox.tsx @@ -1,6 +1,7 @@ import Icon from '@/Components/Icon/Icon' import StyledTooltip from '@/Components/StyledTooltip/StyledTooltip' import { ChangeEventHandler, FunctionComponent } from 'react' +import { c } from 'ttag' type Props = { checked: boolean @@ -21,9 +22,10 @@ const MergeLocalDataCheckbox: FunctionComponent = ({ checked, onChange, d onChange={onChange} disabled={disabled} /> - Merge local data ({notesAndTagsCount} notes and tags) + {c('Option').t`Merge local data (${notesAndTagsCount} notes and tags)`} diff --git a/packages/web/src/javascripts/Components/AccountMenu/ServerPicker/ServerPicker.tsx b/packages/web/src/javascripts/Components/AccountMenu/ServerPicker/ServerPicker.tsx index 45f1ba51f..4e91bad8f 100644 --- a/packages/web/src/javascripts/Components/AccountMenu/ServerPicker/ServerPicker.tsx +++ b/packages/web/src/javascripts/Components/AccountMenu/ServerPicker/ServerPicker.tsx @@ -6,6 +6,7 @@ import { useApplication } from '@/Components/ApplicationProvider' import { isDesktopApplication } from '@/Utils' import RadioButtonGroup from '@/Components/RadioButtonGroup/RadioButtonGroup' import { DefaultHost } from '@standardnotes/snjs' +import { c } from 'ttag' type Props = { className?: string @@ -49,7 +50,7 @@ const ServerPicker = ({ className }: Props) => { } else if (type === 'home server') { if (!application.homeServer) { application.alerts - .alert('Home server is not running. Please open the prefences and home server tab to start it.') + .alert(c('Error').t`Home server is not running. Please open the prefences and home server tab to start it.`) .catch(console.error) return @@ -58,7 +59,7 @@ const ServerPicker = ({ className }: Props) => { const homeServerUrl = await application.homeServer.getHomeServerUrl() if (!homeServerUrl) { application.alerts - .alert('Home server is not running. Please open the prefences and home server tab to start it.') + .alert(c('Error').t`Home server is not running. Please open the prefences and home server tab to start it.`) .catch(console.error) return @@ -71,9 +72,9 @@ const ServerPicker = ({ className }: Props) => { const options = useMemo( () => [ - { label: 'Default', value: 'standard' }, - { label: 'Custom', value: 'custom' }, - ].concat(isDesktopApplication() ? [{ label: 'Home Server', value: 'home server' }] : []) as { + { label: c('Option').t`Default`, value: 'standard' }, + { label: c('Option').t`Custom`, value: 'custom' }, + ].concat(isDesktopApplication() ? [{ label: c('Option').t`Home Server`, value: 'home server' }] : []) as { label: string value: ServerType }[], @@ -82,7 +83,7 @@ const ServerPicker = ({ className }: Props) => { return (
-
Sync Server
+
{c('Label').t`Sync Server`}
{currentType === 'custom' && ( { const application = useApplication() @@ -12,9 +13,10 @@ const User = () => {
{application.syncStatusController.errorMessage && (
-
Sync Unreachable
+
{c('Title').t`Sync Unreachable`}
- Hmm...we can't seem to sync your account. The reason: {application.syncStatusController.errorMessage} + {c('Error') + .t`Hmm...we can't seem to sync your account. The reason: ${application.syncStatusController.errorMessage}`}
{ rel="noopener" target="_blank" > - Need help? + {c('Action').t`Need help?`}
)} diff --git a/packages/web/src/javascripts/Components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherMenu.tsx b/packages/web/src/javascripts/Components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherMenu.tsx index e601fc897..30d5535f7 100644 --- a/packages/web/src/javascripts/Components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherMenu.tsx +++ b/packages/web/src/javascripts/Components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherMenu.tsx @@ -8,6 +8,7 @@ import MenuItem from '@/Components/Menu/MenuItem' import WorkspaceMenuItem from './WorkspaceMenuItem' import { useApplication } from '@/Components/ApplicationProvider' import MenuSection from '@/Components/Menu/MenuSection' +import { c } from 'ttag' type Props = { mainApplicationGroup: WebApplicationGroup @@ -42,9 +43,9 @@ const WorkspaceSwitcherMenu: FunctionComponent = ({ const signoutAll = useCallback(async () => { const confirmed = await application.alerts.confirm( - 'Are you sure you want to sign out of all workspaces on this device?', + c('Info').t`Are you sure you want to sign out of all workspaces on this device?`, undefined, - 'Sign out all', + c('Action').t`Sign out all`, ButtonType.Danger, ) if (!confirmed) { @@ -86,12 +87,12 @@ const WorkspaceSwitcherMenu: FunctionComponent = ({ - Add another workspace + {c('Action').t`Add another workspace`} {!hideWorkspaceOptions && ( - Sign out all workspaces + {c('Action').t`Sign out all workspaces`} )} diff --git a/packages/web/src/javascripts/Components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherOption.tsx b/packages/web/src/javascripts/Components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherOption.tsx index 845b3b7f0..4228eff34 100644 --- a/packages/web/src/javascripts/Components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherOption.tsx +++ b/packages/web/src/javascripts/Components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherOption.tsx @@ -7,6 +7,7 @@ import WorkspaceSwitcherMenu from './WorkspaceSwitcherMenu' import MenuItem from '@/Components/Menu/MenuItem' import Popover from '@/Components/Popover/Popover' import { MenuItemIconSize } from '@/Constants/TailwindClassNames' +import { c } from 'ttag' type Props = { mainApplicationGroup: WebApplicationGroup @@ -25,12 +26,12 @@ const WorkspaceSwitcherOption: FunctionComponent = ({ mainApplicationGrou
- Switch workspace + {c('Action').t`Switch workspace`}
{ return (
- Delete account? + {c('Title').t`Delete account?`}
-

{STRING_DELETE_ACCOUNT_CONFIRMATION}

+

{STRING_DELETE_ACCOUNT_CONFIRMATION()}

diff --git a/packages/web/src/javascripts/Components/ConfirmSignoutModal/ConfirmSignoutModal.tsx b/packages/web/src/javascripts/Components/ConfirmSignoutModal/ConfirmSignoutModal.tsx index bd75a7a80..fadc31d46 100644 --- a/packages/web/src/javascripts/Components/ConfirmSignoutModal/ConfirmSignoutModal.tsx +++ b/packages/web/src/javascripts/Components/ConfirmSignoutModal/ConfirmSignoutModal.tsx @@ -8,6 +8,7 @@ import Button from '@/Components/Button/Button' import Icon from '../Icon/Icon' import AlertDialog from '../AlertDialog/AlertDialog' import HorizontalSeparator from '../Shared/HorizontalSeparator' +import { c } from 'ttag' type Props = { application: WebApplication @@ -37,22 +38,21 @@ const ConfirmSignoutModal: FunctionComponent = ({ application, applicatio return (
- Sign out workspace? + {c('Title').t`Sign out workspace?`}
-

{STRING_SIGN_OUT_CONFIRMATION}

+

{STRING_SIGN_OUT_CONFIRMATION()}

{showWorkspaceWarning && ( <>

- Note: - Because you have other workspaces signed in, this sign out may leave logs and other metadata of your - session on this device. For a more robust sign out that performs a hard clear of all app-related data, - use the Sign out all workspaces option under Switch workspace. + {c('Label').t`Note:`} + {c('Info') + .t`Because you have other workspaces signed in, this sign out may leave logs and other metadata of your session on this device. For a more robust sign out that performs a hard clear of all app-related data, use the "Sign out all workspaces" option under "Switch workspace".`}

)} @@ -66,7 +66,8 @@ const ConfirmSignoutModal: FunctionComponent = ({ application, applicatio

- Local backups are enabled for this workspace. Review your backup files manually to decide what to keep. + {c('Info') + .t`Local backups are enabled for this workspace. Review your backup files manually to decide what to keep.`}

@@ -83,10 +84,10 @@ const ConfirmSignoutModal: FunctionComponent = ({ application, applicatio
diff --git a/packages/web/src/javascripts/Components/Footer/Footer.tsx b/packages/web/src/javascripts/Components/Footer/Footer.tsx index 0bc7195f7..90a4d602b 100644 --- a/packages/web/src/javascripts/Components/Footer/Footer.tsx +++ b/packages/web/src/javascripts/Components/Footer/Footer.tsx @@ -287,12 +287,12 @@ class Footer extends AbstractComponent { securityUpdateClickHandler = async () => { if ( await confirmDialog({ - title: STRING_UPGRADE_ACCOUNT_CONFIRM_TITLE, - text: STRING_UPGRADE_ACCOUNT_CONFIRM_TEXT, - confirmButtonText: STRING_UPGRADE_ACCOUNT_CONFIRM_BUTTON, + title: STRING_UPGRADE_ACCOUNT_CONFIRM_TITLE(), + text: STRING_UPGRADE_ACCOUNT_CONFIRM_TEXT(), + confirmButtonText: STRING_UPGRADE_ACCOUNT_CONFIRM_BUTTON(), }) ) { - preventRefreshing(STRING_CONFIRM_APP_QUIT_DURING_UPGRADE, async () => { + preventRefreshing(STRING_CONFIRM_APP_QUIT_DURING_UPGRADE(), async () => { await this.application.upgradeProtocolVersion() }).catch(console.error) } diff --git a/packages/web/src/javascripts/Components/ImportModal/ImportModal.tsx b/packages/web/src/javascripts/Components/ImportModal/ImportModal.tsx index 930b00d0f..f79460ef8 100644 --- a/packages/web/src/javascripts/Components/ImportModal/ImportModal.tsx +++ b/packages/web/src/javascripts/Components/ImportModal/ImportModal.tsx @@ -13,6 +13,7 @@ import ItemSelectionDropdown from '../ItemSelectionDropdown/ItemSelectionDropdow import { ContentType, SNTag } from '@standardnotes/snjs' import Button from '../Button/Button' import { ClassicFileReader } from '@standardnotes/filepicker' +import { c } from 'ttag' const ImportModal = ({ importModalController }: { importModalController: ImportModalController }) => { const application = useApplication() @@ -41,7 +42,7 @@ const ImportModal = ({ importModalController }: { importModalController: ImportM const modalActions: ModalAction[] = useMemo( () => [ { - label: 'Import', + label: c('Action').t`Import`, type: 'primary', onClick: parseAndImport, hidden: !isReadyToImport, @@ -49,7 +50,7 @@ const ImportModal = ({ importModalController }: { importModalController: ImportM disabled: !isReadyToImport || (addImportsToTag && !shouldCreateTag && !existingTagForImports), }, { - label: importSuccessOrError ? 'Close' : 'Cancel', + label: importSuccessOrError ? c('Action').t`Close` : c('Action').t`Cancel`, type: 'cancel', onClick: close, mobileSlot: 'left', @@ -77,7 +78,7 @@ const ImportModal = ({ importModalController }: { importModalController: ImportM return ( - +
{!files.length && } {files.length > 0 && ( @@ -101,7 +102,7 @@ const ImportModal = ({ importModalController }: { importModalController: ImportM }} small > - Add files + {c('Action').t`Add files`} )} @@ -110,7 +111,7 @@ const ImportModal = ({ importModalController }: { importModalController: ImportM {files.length > 0 && (
- Add all imported notes to tag + {c('Label').t`Add all imported notes to tag`} {addImportsToTag && ( <> @@ -124,7 +125,7 @@ const ImportModal = ({ importModalController }: { importModalController: ImportM setShouldCreateTag(true) }} /> - Create new tag + {c('Label').t`Create new tag`}
@@ -138,7 +139,7 @@ const ImportModal = ({ importModalController }: { importModalController: ImportM setShouldCreateTag(false) }} /> - Add to existing tag + {c('Label').t`Add to existing tag`} {existingTagForImports && ( setExistingTagForImports(tag as SNTag)} - placeholder="Select tag to add imported notes to..." + placeholder={c('Placeholder').t`Select tag to add imported notes to...`} contentTypes={[ContentType.TYPES.Tag]} />
diff --git a/packages/web/src/javascripts/Components/ImportModal/ImportModalFileItem.tsx b/packages/web/src/javascripts/Components/ImportModal/ImportModalFileItem.tsx index fbaba7ff8..cb2f956ee 100644 --- a/packages/web/src/javascripts/Components/ImportModal/ImportModalFileItem.tsx +++ b/packages/web/src/javascripts/Components/ImportModal/ImportModalFileItem.tsx @@ -1,10 +1,11 @@ import { ImportModalController, ImportModalFile } from '@/Components/ImportModal/ImportModalController' -import { classNames, ContentType, pluralize } from '@standardnotes/snjs' +import { classNames, ContentType } from '@standardnotes/snjs' import { ConversionResult, Importer } from '@standardnotes/ui-services' import { observer } from 'mobx-react-lite' import { useCallback, useEffect, useState } from 'react' import Icon from '../Icon/Icon' import { Disclosure, DisclosureContent, DisclosureProvider } from '@ariakit/react' +import { c, msgid, ngettext } from 'ttag' const NoteImportTypeColors: Record = { evernote: 'bg-[#14cc45] text-[#000]', @@ -49,12 +50,17 @@ const countSuccessfulItemsByGroup = (successful: ConversionResult['successful']) } const ImportErroredAccordion = ({ errored }: { errored: ConversionResult['errored'] }) => { + const count = errored.length return (
- Could not import {errored.length} {pluralize(errored.length, 'item', 'items')} (click for details) + {ngettext( + msgid`Could not import ${count} item (click for details)`, + `Could not import ${count} items (click for details)`, + count, + )}
@@ -77,9 +83,9 @@ const ImportFinishedStatus = ({ file }: { file: ImportModalFile }) => { const { notes, tags, files } = countSuccessfulItemsByGroup(file.successful) - const notesStatus = notes > 0 ? `${notes} ${pluralize(notes, 'note', 'notes')}` : '' - const tagsStatus = tags > 0 ? `${tags} ${pluralize(tags, 'tag', 'tags')}` : '' - const filesStatus = files > 0 ? `${files} ${pluralize(files, 'file', 'files')}` : '' + const notesStatus = notes > 0 ? ngettext(msgid`${notes} note`, `${notes} notes`, notes) : '' + const tagsStatus = tags > 0 ? ngettext(msgid`${tags} tag`, `${tags} tags`, tags) : '' + const filesStatus = files > 0 ? ngettext(msgid`${files} file`, `${files} files`, files) : '' const status = [notesStatus, tagsStatus, filesStatus].filter(Boolean).join(', ') return ( @@ -87,7 +93,7 @@ const ImportFinishedStatus = ({ file }: { file: ImportModalFile }) => { {file.successful.length > 0 && (
- {status} imported + {c('Info').t`${status} imported`}
)} {file.errored.length > 0 && } @@ -156,14 +162,16 @@ const ImportModalFileItem = ({
{file.file.name}
{isDetectingService ? ( -
Detecting service...
+
{c('Status').t`Detecting service...`}
) : (
- {file.status === 'pending' && file.service && 'Ready to import'} - {file.status === 'pending' && !file.service && 'Could not auto-detect service. Please select manually.'} - {file.status === 'parsing' && 'Parsing...'} - {file.status === 'importing' && 'Importing...'} - {file.status === 'uploading-files' && 'Uploading and embedding files...'} + {file.status === 'pending' && file.service && c('Status').t`Ready to import`} + {file.status === 'pending' && + !file.service && + c('Status').t`Could not auto-detect service. Please select manually.`} + {file.status === 'parsing' && c('Status').t`Parsing...`} + {file.status === 'importing' && c('Status').t`Importing...`} + {file.status === 'uploading-files' && c('Status').t`Uploading and embedding files...`} {file.status === 'error' && file.error.message}
@@ -197,7 +205,7 @@ const ImportModalFileItem = ({ )} -
or import from:
+
{c('Info').t`or import from:`}
-
Your password has been successfully changed.
-

Ensure you are running the latest version of Standard Notes on all platforms for maximum compatibility.

+
{c('Info').t`Your password has been successfully changed.`}
+

+ {c('Info') + .t`Ensure you are running the latest version of Standard Notes on all platforms for maximum compatibility.`} +

diff --git a/packages/web/src/javascripts/Components/PasswordWizard/PasswordStep.tsx b/packages/web/src/javascripts/Components/PasswordWizard/PasswordStep.tsx index 89bd666c8..9cf1fdf92 100644 --- a/packages/web/src/javascripts/Components/PasswordWizard/PasswordStep.tsx +++ b/packages/web/src/javascripts/Components/PasswordWizard/PasswordStep.tsx @@ -1,4 +1,5 @@ import { useState } from 'react' +import { c } from 'ttag' import DecoratedPasswordInput from '../Input/DecoratedPasswordInput' export const PasswordStep = ({ @@ -33,7 +34,7 @@ export const PasswordStep = ({
void } +enum ContinueTitle { + Default, + Generating, + Finish, +} + type State = { - continueTitle: string + continueTitle: ContinueTitle formData: FormData isContinuing?: boolean lockContinue?: boolean @@ -22,10 +29,6 @@ type State = { step: Steps } -const DEFAULT_CONTINUE_TITLE = 'Continue' -const GENERATING_CONTINUE_TITLE = 'Generating Keys...' -const FINISH_CONTINUE_TITLE = 'Finish' - enum Steps { PreprocessingStep = 'preprocessing-step', PasswordStep = 'password-step', @@ -46,7 +49,7 @@ class PasswordWizard extends AbstractComponent { const baseState = { formData: {}, - continueTitle: DEFAULT_CONTINUE_TITLE, + continueTitle: ContinueTitle.Default, } if (props.application.featuresController.isVaultsEnabled()) { @@ -82,7 +85,7 @@ class PasswordWizard extends AbstractComponent { resetContinueState() { this.setState({ showSpinner: false, - continueTitle: DEFAULT_CONTINUE_TITLE, + continueTitle: ContinueTitle.Default, isContinuing: false, }) } @@ -109,7 +112,7 @@ class PasswordWizard extends AbstractComponent { this.setState({ isContinuing: true, showSpinner: true, - continueTitle: GENERATING_CONTINUE_TITLE, + continueTitle: ContinueTitle.Generating, }) const valid = await this.validateCurrentPassword() @@ -127,7 +130,7 @@ class PasswordWizard extends AbstractComponent { this.setState({ isContinuing: false, showSpinner: false, - continueTitle: FINISH_CONTINUE_TITLE, + continueTitle: ContinueTitle.Finish, step: Steps.FinishStep, }) } @@ -136,16 +139,18 @@ class PasswordWizard extends AbstractComponent { const currentPassword = this.state.formData.currentPassword const newPass = this.state.formData.newPassword if (!currentPassword || currentPassword.length === 0) { - this.application.alerts.alert('Please enter your current password.').catch(console.error) + this.application.alerts.alert(c('Error').t`Please enter your current password.`).catch(console.error) return false } if (!newPass || newPass.length === 0) { - this.application.alerts.alert('Please enter a new password.').catch(console.error) + this.application.alerts.alert(c('Error').t`Please enter a new password.`).catch(console.error) return false } if (newPass !== this.state.formData.newPasswordConfirmation) { - this.application.alerts.alert('Your new password does not match its confirmation.').catch(console.error) + this.application.alerts + .alert(c('Error').t`Your new password does not match its confirmation.`) + .catch(console.error) this.setFormDataState({ status: undefined, }).catch(console.error) @@ -154,7 +159,7 @@ class PasswordWizard extends AbstractComponent { if (!this.application.sessions.getUser()?.email) { this.application.alerts - .alert("We don't have your email stored. Please sign out then log back in to fix this issue.") + .alert(c('Error').t`We don't have your email stored. Please sign out then log back in to fix this issue.`) .catch(console.error) this.setFormDataState({ status: undefined, @@ -165,7 +170,7 @@ class PasswordWizard extends AbstractComponent { const success = await this.application.validateAccountPassword(this.state.formData.currentPassword as string) if (!success) { this.application.alerts - .alert('The current password you entered is not correct. Please try again.') + .alert(c('Error').t`The current password you entered is not correct. Please try again.`) .catch(console.error) } return success @@ -180,7 +185,7 @@ class PasswordWizard extends AbstractComponent { }) await this.setFormDataState({ - status: 'Processing encryption keys…', + status: c('Status').t`Processing encryption keys…`, }) const newPassword = this.state.formData.newPassword @@ -197,13 +202,13 @@ class PasswordWizard extends AbstractComponent { if (!success) { this.setFormDataState({ - status: 'Unable to process your password. Please try again.', + status: c('Status').t`Unable to process your password. Please try again.`, }).catch(console.error) } else { this.setState({ formData: { ...this.state.formData, - status: 'Successfully changed password.', + status: c('Status').t`Successfully changed password.`, }, }) } @@ -212,7 +217,9 @@ class PasswordWizard extends AbstractComponent { dismiss = () => { if (this.state.processing) { - this.application.alerts.alert('Cannot close window until pending tasks are complete.').catch(console.error) + this.application.alerts + .alert(c('Error').t`Cannot close window until pending tasks are complete.`) + .catch(console.error) } else { this.props.dismissModal() } @@ -266,25 +273,37 @@ class PasswordWizard extends AbstractComponent { } } + continueLabel(): string { + switch (this.state.continueTitle) { + case ContinueTitle.Generating: + return c('Action').t`Generating Keys...` + case ContinueTitle.Finish: + return c('Action').t`Finish` + case ContinueTitle.Default: + default: + return c('Action').t`Continue` + } + } + override render() { return (
) : ( - this.state.continueTitle + this.continueLabel() ), onClick: this.nextStep, type: 'primary', diff --git a/packages/web/src/javascripts/Components/PasswordWizard/PreprocessingStep.tsx b/packages/web/src/javascripts/Components/PasswordWizard/PreprocessingStep.tsx index e61fcde03..7a110e300 100644 --- a/packages/web/src/javascripts/Components/PasswordWizard/PreprocessingStep.tsx +++ b/packages/web/src/javascripts/Components/PasswordWizard/PreprocessingStep.tsx @@ -1,6 +1,7 @@ import Spinner from '../Spinner/Spinner' import { useApplication } from '../ApplicationProvider' import { useCallback, useEffect, useState } from 'react' +import { c } from 'ttag' export const PreprocessingStep = ({ onContinue, @@ -76,7 +77,7 @@ export const PreprocessingStep = ({ return (
-

Checking for data conflicts...

+

{c('Status').t`Checking for data conflicts...`}

) } @@ -88,9 +89,8 @@ export const PreprocessingStep = ({ return (

- You have pending vault invites. Changing your password will delete these invites. It is recommended you accept - or decline these invites before changing your password. If you choose to continue, these invites will be - deleted. + {c('Info') + .t`You have pending vault invites. Changing your password will delete these invites. It is recommended you accept or decline these invites before changing your password. If you choose to continue, these invites will be deleted.`}

) diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Account/ChangeEmail/ChangeEmail.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Account/ChangeEmail/ChangeEmail.tsx index 91c72d418..feb613cc7 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Account/ChangeEmail/ChangeEmail.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Account/ChangeEmail/ChangeEmail.tsx @@ -1,4 +1,5 @@ import { FunctionComponent, useCallback, useMemo, useState } from 'react' +import { c } from 'ttag' import { WebApplication } from '@/Application/WebApplication' import { useBeforeUnload } from '@/Hooks/useBeforeUnload' import ChangeEmailForm from './ChangeEmailForm' @@ -6,9 +7,9 @@ import ChangeEmailSuccess from './ChangeEmailSuccess' import Modal, { ModalAction } from '@/Components/Modal/Modal' enum SubmitButtonTitles { - Default = 'Continue', - GeneratingKeys = 'Generating Keys...', - Finish = 'Finish', + Default, + GeneratingKeys, + Finish, } enum Steps { @@ -35,7 +36,7 @@ const ChangeEmail: FunctionComponent = ({ onCloseDialog, application }) = const validateCurrentPassword = useCallback(async () => { if (!currentPassword || currentPassword.length === 0) { - applicationAlertService.alert('Please enter your current password.').catch(console.error) + applicationAlertService.alert(c('Error').t`Please enter your current password.`).catch(console.error) return false } @@ -43,7 +44,7 @@ const ChangeEmail: FunctionComponent = ({ onCloseDialog, application }) = const success = await application.validateAccountPassword(currentPassword) if (!success) { applicationAlertService - .alert('The current password you entered is not correct. Please try again.') + .alert(c('Error').t`The current password you entered is not correct. Please try again.`) .catch(console.error) return false @@ -73,7 +74,9 @@ const ChangeEmail: FunctionComponent = ({ onCloseDialog, application }) = const dismiss = useCallback(() => { if (lockContinue) { - applicationAlertService.alert('Cannot close window until pending tasks are complete.').catch(console.error) + applicationAlertService + .alert(c('Error').t`Cannot close window until pending tasks are complete.`) + .catch(console.error) } else { onCloseDialog() } @@ -115,32 +118,46 @@ const ChangeEmail: FunctionComponent = ({ onCloseDialog, application }) = const handleDialogClose = useCallback(() => { if (lockContinue) { - applicationAlertService.alert('Cannot close window until pending tasks are complete.').catch(console.error) + applicationAlertService + .alert(c('Error').t`Cannot close window until pending tasks are complete.`) + .catch(console.error) } else { onCloseDialog() } }, [applicationAlertService, lockContinue, onCloseDialog]) + const submitButtonLabel = useMemo(() => { + switch (submitButtonTitle) { + case SubmitButtonTitles.GeneratingKeys: + return c('Action').t`Generating Keys...` + case SubmitButtonTitles.Finish: + return c('Action').t`Finish` + case SubmitButtonTitles.Default: + default: + return c('Action').t`Continue` + } + }, [submitButtonTitle]) + const modalActions = useMemo( (): ModalAction[] => [ { - label: 'Cancel', + label: c('Action').t`Cancel`, onClick: handleDialogClose, type: 'cancel', mobileSlot: 'left', }, { - label: submitButtonTitle, + label: submitButtonLabel, onClick: handleSubmit, type: 'primary', mobileSlot: 'right', }, ], - [handleDialogClose, handleSubmit, submitButtonTitle], + [handleDialogClose, handleSubmit, submitButtonLabel], ) return ( - +
{currentStep === Steps.InitialStep && ( diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Account/ChangeEmail/ChangeEmailForm.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Account/ChangeEmail/ChangeEmailForm.tsx index 08b2602f2..aa46b10b9 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Account/ChangeEmail/ChangeEmailForm.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Account/ChangeEmail/ChangeEmailForm.tsx @@ -1,6 +1,7 @@ import DecoratedInput from '@/Components/Input/DecoratedInput' import DecoratedPasswordInput from '@/Components/Input/DecoratedPasswordInput' import { Dispatch, SetStateAction, FunctionComponent } from 'react' +import { c } from 'ttag' type Props = { setNewEmail: Dispatch> @@ -14,7 +15,7 @@ const ChangeEmailForm: FunctionComponent = ({ setNewEmail, setCurrentPass
= ({ setNewEmail, setCurrentPass
{ return (
-
Your email has been successfully changed.
+
{c('Info').t`Your email has been successfully changed.`}

- Please ensure you are running the latest version of Standard Notes on all platforms to ensure maximum - compatibility. + {c('Info') + .t`Please ensure you are running the latest version of Standard Notes on all platforms to ensure maximum compatibility.`}

) diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Account/ClearSessionDataView.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Account/ClearSessionDataView.tsx index 25c7b0128..7e1fa9e68 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Account/ClearSessionDataView.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Account/ClearSessionDataView.tsx @@ -5,6 +5,7 @@ import { Title, Text } from '../../PreferencesComponents/Content' import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup' import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment' import { useApplication } from '@/Components/ApplicationProvider' +import { c } from 'ttag' const ClearSessionDataView: FunctionComponent = () => { const application = useApplication() @@ -12,11 +13,12 @@ const ClearSessionDataView: FunctionComponent = () => { return ( - Clear workspace - Remove all data related to the current workspace from the application. + {c('Title').t`Clear workspace`} + {c('Info') + .t`Remove all data related to the current workspace from the application.`}
- This workspace - Remove all data related to the current workspace from the application. + {c('Subtitle').t`This workspace`} + {c('Info').t`Remove all data related to the current workspace from the application.`}
diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Backups/EmailBackups.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Backups/EmailBackups.tsx index bfe1c3918..680431988 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Backups/EmailBackups.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Backups/EmailBackups.tsx @@ -61,7 +61,7 @@ const EmailBackups = ({ application }: Props) => { await application.settings.updateSetting(settingName, payload, false) return true } catch (e) { - application.alerts.alert(STRING_FAILED_TO_UPDATE_USER_SETTING).catch(console.error) + application.alerts.alert(STRING_FAILED_TO_UPDATE_USER_SETTING()).catch(console.error) return false } } diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Security/Privacy.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Security/Privacy.tsx index 5d8390987..fb4bb633b 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Security/Privacy.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Security/Privacy.tsx @@ -24,7 +24,7 @@ const Privacy: FunctionComponent = ({ application }: Props) => { await application.settings.updateSetting(settingName, payload, false) return true } catch (e) { - application.alerts.alert(STRING_FAILED_TO_UPDATE_USER_SETTING).catch(console.error) + application.alerts.alert(STRING_FAILED_TO_UPDATE_USER_SETTING()).catch(console.error) return false } } diff --git a/packages/web/src/javascripts/Constants/Strings.ts b/packages/web/src/javascripts/Constants/Strings.ts index 770b36868..4e8cb3c50 100644 --- a/packages/web/src/javascripts/Constants/Strings.ts +++ b/packages/web/src/javascripts/Constants/Strings.ts @@ -1,13 +1,15 @@ import { escapeHtmlString, Platform, SNApplication } from '@standardnotes/snjs' import { getPlatform, isDesktopApplication } from '../Utils' +import { c } from 'ttag' /** @generic */ -export const STRING_SESSION_EXPIRED = - 'Your session has expired. New changes will not be pulled in. Please sign in to refresh your session.' -export const STRING_DEFAULT_FILE_ERROR = - 'Please use FileSafe or the Bold Editor to attach images and files. Learn more at standardnotes.com/filesafe.' -export const STRING_GENERIC_SYNC_ERROR = - 'There was an error syncing. Please try again. If all else fails, try signing out and signing back in.' +export const STRING_SESSION_EXPIRED = () => + c('Error').t`Your session has expired. New changes will not be pulled in. Please sign in to refresh your session.` +export const STRING_DEFAULT_FILE_ERROR = () => + c('Error') + .t`Please use FileSafe or the Bold Editor to attach images and files. Learn more at standardnotes.com/filesafe.` +export const STRING_GENERIC_SYNC_ERROR = () => + c('Error').t`There was an error syncing. Please try again. If all else fails, try signing out and signing back in.` export function StringSyncException(data: unknown) { return `There was an error while trying to save your items. Please contact support and share this message: ${JSON.stringify( data, @@ -51,8 +53,9 @@ export function StringEmptyTrash(count: number) { /** @account */ export const STRING_ACCOUNT_MENU_UNCHECK_MERGE = 'Unchecking this option means any of the notes you have written while you were signed out will be deleted. Are you sure you want to discard these notes?' -export const STRING_SIGN_OUT_CONFIRMATION = - 'This action will remove this workspace and its related data from this device. Your synced data will not be affected.' +export const STRING_SIGN_OUT_CONFIRMATION = () => + c('Info') + .t`This action will remove this workspace and its related data from this device. Your synced data will not be affected.` export const STRING_ERROR_DECRYPTING_IMPORT = 'There was an error decrypting your items. Make sure the password you entered is correct and try again.' export const STRING_E2E_ENABLED = @@ -61,28 +64,34 @@ export const STRING_LOCAL_ENC_ENABLED = 'Encryption is enabled. Your data is encrypted using your passcode before it is saved to your device storage.' export const STRING_ENC_NOT_ENABLED = 'Encryption is not enabled. Sign in, register, or add a passcode lock to enable encryption.' -export const STRING_IMPORT_SUCCESS = 'Your data has been successfully imported.' +export const STRING_IMPORT_SUCCESS = () => c('Info').t`Your data has been successfully imported.` export const STRING_REMOVE_PASSCODE_CONFIRMATION = 'Are you sure you want to remove your application passcode?' export const STRING_REMOVE_PASSCODE_OFFLINE_ADDENDUM = ' This will remove encryption from your local data.' export const STRING_NON_MATCHING_PASSCODES = 'The two passcodes you entered do not match. Please try again.' -export const STRING_NON_MATCHING_PASSWORDS = 'The two passwords you entered do not match. Please try again.' +export const STRING_NON_MATCHING_PASSWORDS = () => + c('Error').t`The two passwords you entered do not match. Please try again.` export const STRING_GENERATING_LOGIN_KEYS = 'Generating Login Keys...' export const STRING_GENERATING_REGISTER_KEYS = 'Generating Account Keys...' -export const STRING_INVALID_IMPORT_FILE = 'Unable to open file. Ensure it is a proper JSON file and try again.' -export const STRING_IMPORTING_ZIP_FILE = - 'The file you selected is not a valid backup file. Please extract the contents of the zip file, then upload the contained .txt file.' +export const STRING_INVALID_IMPORT_FILE = () => + c('Error').t`Unable to open file. Ensure it is a proper JSON file and try again.` +export const STRING_IMPORTING_ZIP_FILE = () => + c('Error') + .t`The file you selected is not a valid backup file. Please extract the contents of the zip file, then upload the contained .txt file.` export function StringImportError(errorCount: number) { - return `Import complete. ${errorCount} items were not imported because there was an error decrypting them. Make sure the password is correct and try again.` + return c('Info') + .t`Import complete. ${errorCount} items were not imported because there was an error decrypting them. Make sure the password is correct and try again.` } -export const STRING_UNSUPPORTED_BACKUP_FILE_VERSION = - 'This backup file was created using an unsupported version of the application and cannot be imported here. Please update your application and try again.' +export const STRING_UNSUPPORTED_BACKUP_FILE_VERSION = () => + c('Error') + .t`This backup file was created using an unsupported version of the application and cannot be imported here. Please update your application and try again.` /** @password_change */ export const STRING_FAILED_PASSWORD_CHANGE = 'There was an error re-encrypting your items. Your password was changed, but not all your items were properly re-encrypted and synced. You should try syncing again. If all else fails, you should restore your notes from backup.' -export const STRING_CONFIRM_APP_QUIT_DURING_UPGRADE = - 'The encryption upgrade is in progress. You may lose data if you quit the app. ' + 'Are you sure you want to quit?' +export const STRING_CONFIRM_APP_QUIT_DURING_UPGRADE = () => + c('Info') + .t`The encryption upgrade is in progress. You may lose data if you quit the app. Are you sure you want to quit?` export const STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE = 'A passcode change is in progress. You may lose data if you quit the app. ' + 'Are you sure you want to quit?' @@ -90,21 +99,20 @@ export const STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE = export const STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL = 'A passcode removal is in progress. You may lose data if you quit the app. ' + 'Are you sure you want to quit?' -export const STRING_UPGRADE_ACCOUNT_CONFIRM_TITLE = 'Encryption upgrade available' -export const STRING_UPGRADE_ACCOUNT_CONFIRM_TEXT = - 'Encryption version 004 is available. ' + - 'This version strengthens the encryption algorithms your account and ' + - 'local storage use. To learn more about this upgrade, visit our ' + - 'Security Upgrade page.' -export const STRING_UPGRADE_ACCOUNT_CONFIRM_BUTTON = 'Upgrade' +export const STRING_UPGRADE_ACCOUNT_CONFIRM_TITLE = () => c('Title').t`Encryption upgrade available` +export const STRING_UPGRADE_ACCOUNT_CONFIRM_TEXT = () => + c('Info') + .t`Encryption version 004 is available. This version strengthens the encryption algorithms your account and local storage use. To learn more about this upgrade, visit our Security Upgrade page.` +export const STRING_UPGRADE_ACCOUNT_CONFIRM_BUTTON = () => c('Action').t`Upgrade` export const STRING_REMOVE_OFFLINE_KEY_CONFIRMATION = 'This will delete the previously saved offline key.' -export const STRING_DELETE_ACCOUNT_CONFIRMATION = - "Are you sure you want to permanently delete your account? You will be asked to confirm your account password in the next step. If you have an active paid subscription, cancel the subscription first. Otherwise, if you'd like to keep the subscription, you can re-register with the same email after deletion, and your subscription will be linked back up with your account." +export const STRING_DELETE_ACCOUNT_CONFIRMATION = () => + c('Info') + .t`Are you sure you want to permanently delete your account? You will be asked to confirm your account password in the next step. If you have an active paid subscription, cancel the subscription first. Otherwise, if you'd like to keep the subscription, you can re-register with the same email after deletion, and your subscription will be linked back up with your account.` -export const STRING_FAILED_TO_UPDATE_USER_SETTING = - 'There was an error while trying to update your settings. Please try again.' +export const STRING_FAILED_TO_UPDATE_USER_SETTING = () => + c('Error').t`There was an error while trying to update your settings. Please try again.` export const Strings = { protectingNoteWithoutProtectionSources: From b4faa20ac1089187d2655dc132f8252893a51a9f Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Wed, 3 Jun 2026 15:14:43 -0300 Subject: [PATCH 32/45] fix: Fixes empty lines doubled when importing Evernote notes --- .../EvernoteConverter.spec.ts | 21 ++++++++++++++++++- .../EvernoteConverter/EvernoteConverter.ts | 2 +- .../src/Import/EvernoteConverter/testData.ts | 14 +++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.spec.ts b/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.spec.ts index 47d39f9e8..2715879b8 100644 --- a/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.spec.ts +++ b/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.spec.ts @@ -5,7 +5,7 @@ import { ContentType } from '@standardnotes/domain-core' import { SNNote, SNTag } from '@standardnotes/models' import { EvernoteConverter, EvernoteResource } from './EvernoteConverter' -import { createTestResourceElement, enex } from './testData' +import { createTestResourceElement, emptyLineEnex, enex } from './testData' import { PureCryptoInterface } from '@standardnotes/sncrypto-common' import { GenerateUuid } from '@standardnotes/services' import { Converter } from '../Converter' @@ -132,6 +132,25 @@ describe('EvernoteConverter', () => { expect(unorderedList2.getAttribute('__lexicallisttype')).toBeFalsy() }) + it('should preserve single empty lines from Evernote br-only divs', async () => { + const converter = new EvernoteConverter(generateUuid) + + const { successful } = await converter.convert(emptyLineEnex as unknown as File, dependencies) + + expect((successful?.[0] as SNNote).content.text).toBe('line1\n\nline2') + }) + + it('should convert Evernote br-only divs to empty paragraphs for Super', async () => { + const converter = new EvernoteConverter(generateUuid) + + const { successful } = await converter.convert(emptyLineEnex as unknown as File, { + ...dependencies, + canUseSuper: true, + }) + + expect((successful?.[0] as SNNote).content.text).toBe('

line1

line2

') + }) + it('should replace media elements with resources', async () => { const resources: EvernoteResource[] = [ { diff --git a/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.ts b/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.ts index e377e4a1c..be09e94f8 100644 --- a/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.ts +++ b/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.ts @@ -298,7 +298,7 @@ export class EvernoteConverter implements Converter { const children = Array.from(parent.children) const isEveryChildBR = children.every((child) => child.tagName === 'BR') if (isEveryChildBR) { - parent.replaceWith(children[0]) + parent.replaceChildren() } }) } diff --git a/packages/ui-services/src/Import/EvernoteConverter/testData.ts b/packages/ui-services/src/Import/EvernoteConverter/testData.ts index ffb141189..0151ff118 100644 --- a/packages/ui-services/src/Import/EvernoteConverter/testData.ts +++ b/packages/ui-services/src/Import/EvernoteConverter/testData.ts @@ -36,6 +36,20 @@ export const enex = ` ` +export const emptyLineEnex = ` + + + + Empty line test + 20210308T051614Z + 20210308T051855Z + + +
line1

line2
]]> +
+
+
` + export function createTestResourceElement( shouldHaveMimeType = true, shouldHaveSourceUrl = false, From a62f496ee6ca588876b616261c479f6499b4ec19 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Fri, 5 Jun 2026 00:07:46 -0300 Subject: [PATCH 33/45] fix: Fixes highlight text style lost when importing Evernote notes --- .../EvernoteConverter.spec.ts | 14 +++- .../EvernoteConverter/EvernoteConverter.ts | 2 + .../src/Import/EvernoteConverter/testData.ts | 14 ++++ .../src/Import/EvernoteHighlight.spec.ts | 35 ++++++++ .../src/Import/EvernoteHighlight.ts | 46 +++++++++++ .../SuperEditor/BlocksEditorComposer.tsx | 4 + .../Lexical/Utils/highlightHtmlImport.ts | 42 ++++++++++ .../Tools/HeadlessSuperConverter.spec.ts | 82 +++++++++++++++++++ .../Tools/HeadlessSuperConverter.tsx | 4 + 9 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 packages/ui-services/src/Import/EvernoteHighlight.spec.ts create mode 100644 packages/ui-services/src/Import/EvernoteHighlight.ts create mode 100644 packages/web/src/javascripts/Components/SuperEditor/Lexical/Utils/highlightHtmlImport.ts create mode 100644 packages/web/src/javascripts/Components/SuperEditor/Tools/HeadlessSuperConverter.spec.ts diff --git a/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.spec.ts b/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.spec.ts index 2715879b8..d6b094139 100644 --- a/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.spec.ts +++ b/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.spec.ts @@ -5,7 +5,7 @@ import { ContentType } from '@standardnotes/domain-core' import { SNNote, SNTag } from '@standardnotes/models' import { EvernoteConverter, EvernoteResource } from './EvernoteConverter' -import { createTestResourceElement, emptyLineEnex, enex } from './testData' +import { createTestResourceElement, emptyLineEnex, enex, highlightEnex } from './testData' import { PureCryptoInterface } from '@standardnotes/sncrypto-common' import { GenerateUuid } from '@standardnotes/services' import { Converter } from '../Converter' @@ -140,6 +140,18 @@ describe('EvernoteConverter', () => { expect((successful?.[0] as SNNote).content.text).toBe('line1\n\nline2') }) + it('should convert highlight spans to mark elements before Super import', async () => { + const converter = new EvernoteConverter(generateUuid) + + const { successful } = await converter.convert(highlightEnex as unknown as File, { + ...dependencies, + canUseSuper: true, + }) + + expect((successful?.[0] as SNNote).content.text).toContain(']*--en-highlight/) + }) + it('should convert Evernote br-only divs to empty paragraphs for Super', async () => { const converter = new EvernoteConverter(generateUuid) diff --git a/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.ts b/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.ts index be09e94f8..aa8ec11e7 100644 --- a/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.ts +++ b/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.ts @@ -8,6 +8,7 @@ import Base64 from 'crypto-js/enc-base64' import { Converter, UploadFileFn } from '../Converter' import { ConversionResult } from '../ConversionResult' import { getBlobFromBase64 } from '../Utils' +import { convertEvernoteHighlightSpansToMarks } from '../EvernoteHighlight' dayjs.extend(customParseFormat) dayjs.extend(utc) @@ -92,6 +93,7 @@ export class EvernoteConverter implements Converter { this.convertTopLevelDivsToParagraphs(noteElement) this.convertListsToSuperFormatIfApplicable(unorderedLists) this.convertLeftPaddingToSuperIndent(noteElement) + convertEvernoteHighlightSpansToMarks(noteElement) } this.removeEmptyAndOrphanListElements(noteElement) diff --git a/packages/ui-services/src/Import/EvernoteConverter/testData.ts b/packages/ui-services/src/Import/EvernoteConverter/testData.ts index 0151ff118..67d19f474 100644 --- a/packages/ui-services/src/Import/EvernoteConverter/testData.ts +++ b/packages/ui-services/src/Import/EvernoteConverter/testData.ts @@ -36,6 +36,20 @@ export const enex = ` ` +export const highlightEnex = ` + + + + Highlight test + 20210308T051614Z + 20210308T051855Z + + +
Line 1
Line 2
]]> +
+
+
` + export const emptyLineEnex = ` diff --git a/packages/ui-services/src/Import/EvernoteHighlight.spec.ts b/packages/ui-services/src/Import/EvernoteHighlight.spec.ts new file mode 100644 index 000000000..e913b4eaf --- /dev/null +++ b/packages/ui-services/src/Import/EvernoteHighlight.spec.ts @@ -0,0 +1,35 @@ +/** + * @jest-environment jsdom + */ + +import { + convertEvernoteHighlightSpansToMarks, + isEvernoteHighlightElement, + isEvernoteHighlightStyle, +} from './EvernoteHighlight' + +describe('EvernoteHighlight', () => { + it('detects --en-highlight in style attribute', () => { + expect(isEvernoteHighlightStyle('--en-highlight:yellow;background-color: #ffef9e;')).toBe(true) + }) + + it('detects -evernote-highlight in style attribute', () => { + expect(isEvernoteHighlightStyle('background-color: rgb(255, 250, 165);-evernote-highlight:true;')).toBe(true) + }) + + it('does not treat highlight:false as highlighted', () => { + expect(isEvernoteHighlightStyle('--en-highlight:false;')).toBe(false) + }) + + it('converts highlight spans to mark elements', () => { + const root = document.createElement('div') + root.innerHTML = + 'Line 2plain' + + convertEvernoteHighlightSpansToMarks(root) + + expect(root.querySelector('span')).not.toBeNull() + expect(root.querySelector('mark')?.textContent).toBe('Line 2') + expect(isEvernoteHighlightElement(root.querySelector('mark') as HTMLElement)).toBe(true) + }) +}) diff --git a/packages/ui-services/src/Import/EvernoteHighlight.ts b/packages/ui-services/src/Import/EvernoteHighlight.ts new file mode 100644 index 000000000..b08bb5cf8 --- /dev/null +++ b/packages/ui-services/src/Import/EvernoteHighlight.ts @@ -0,0 +1,46 @@ +const EVERNOTE_HIGHLIGHT_PROPERTY = + /(?:--en-highlight|-en-highlight|--evernote-highlight|-evernote-highlight)\s*:\s*([^;]+)/i + +export function isEvernoteHighlightStyle(styleAttribute: string | null | undefined): boolean { + if (!styleAttribute) { + return false + } + + const match = styleAttribute.match(EVERNOTE_HIGHLIGHT_PROPERTY) + if (!match) { + return false + } + + return match[1].trim().toLowerCase() !== 'false' +} + +export function isEvernoteHighlightElement(element: HTMLElement): boolean { + if (isEvernoteHighlightStyle(element.getAttribute('style'))) { + return true + } + + const enHighlight = element.style.getPropertyValue('--en-highlight') + return enHighlight !== '' && enHighlight.toLowerCase() !== 'false' +} + +export function convertEvernoteHighlightSpansToMarks(root: ParentNode) { + const spans = Array.from(root.querySelectorAll('span')) + + for (const span of spans) { + if (!isEvernoteHighlightElement(span)) { + continue + } + + const mark = document.createElement('mark') + const style = span.getAttribute('style') + if (style) { + mark.setAttribute('style', style) + } + + while (span.firstChild) { + mark.appendChild(span.firstChild) + } + + span.replaceWith(mark) + } +} diff --git a/packages/web/src/javascripts/Components/SuperEditor/BlocksEditorComposer.tsx b/packages/web/src/javascripts/Components/SuperEditor/BlocksEditorComposer.tsx index 71fc81a13..507579b24 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/BlocksEditorComposer.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/BlocksEditorComposer.tsx @@ -2,6 +2,7 @@ import { FunctionComponent } from 'react' import { LexicalComposer, InitialEditorStateType } from '@lexical/react/LexicalComposer' import BlocksEditorTheme from './Lexical/Theme/Theme' import { BlockEditorNodes } from './Lexical/Nodes/AllNodes' +import { highlightHtmlImport } from './Lexical/Utils/highlightHtmlImport' import { Klass, LexicalNode } from 'lexical' type BlocksEditorComposerProps = { @@ -26,6 +27,9 @@ export const BlocksEditorComposer: FunctionComponent onError: (error: Error) => console.error(error), editorState: typeof initialValue === 'string' && initialValue.length === 0 ? undefined : initialValue, nodes: [...nodes, ...BlockEditorNodes], + html: { + import: highlightHtmlImport, + }, }} > <>{children} diff --git a/packages/web/src/javascripts/Components/SuperEditor/Lexical/Utils/highlightHtmlImport.ts b/packages/web/src/javascripts/Components/SuperEditor/Lexical/Utils/highlightHtmlImport.ts new file mode 100644 index 000000000..64e74a71d --- /dev/null +++ b/packages/web/src/javascripts/Components/SuperEditor/Lexical/Utils/highlightHtmlImport.ts @@ -0,0 +1,42 @@ +import { isEvernoteHighlightElement } from '@standardnotes/ui-services/src/Import/EvernoteHighlight' +import { $isTextNode, DOMConversionMap, LexicalNode } from 'lexical' + +function applyHighlightToTextChild(domNode: HTMLElement) { + const backgroundColor = domNode.style.backgroundColor + + return { + forChild: (lexicalNode: LexicalNode) => { + if (!$isTextNode(lexicalNode)) { + return lexicalNode + } + + if (!lexicalNode.hasFormat('highlight')) { + lexicalNode.toggleFormat('highlight') + } + + if (backgroundColor) { + lexicalNode.setStyle(`background-color: ${backgroundColor}`) + } + + return lexicalNode + }, + node: null, + } +} + +export const highlightHtmlImport: DOMConversionMap = { + mark: () => ({ + conversion: applyHighlightToTextChild, + priority: 1, + }), + span: (domNode) => { + if (!isEvernoteHighlightElement(domNode as HTMLElement)) { + return null + } + + return { + conversion: applyHighlightToTextChild, + priority: 1, + } + }, +} diff --git a/packages/web/src/javascripts/Components/SuperEditor/Tools/HeadlessSuperConverter.spec.ts b/packages/web/src/javascripts/Components/SuperEditor/Tools/HeadlessSuperConverter.spec.ts new file mode 100644 index 000000000..fa3bee832 --- /dev/null +++ b/packages/web/src/javascripts/Components/SuperEditor/Tools/HeadlessSuperConverter.spec.ts @@ -0,0 +1,82 @@ +/** + * @jest-environment jsdom + */ + +import { HeadlessSuperConverter } from './HeadlessSuperConverter' +import { EvernoteConverter } from '@standardnotes/ui-services/src/Import/EvernoteConverter/EvernoteConverter' +import { highlightEnex } from '@standardnotes/ui-services/src/Import/EvernoteConverter/testData' +import { GenerateUuid } from '@standardnotes/services' +import { PureCryptoInterface } from '@standardnotes/sncrypto-common' + +describe('HeadlessSuperConverter', () => { + it('imports mark tags as highlight format', () => { + const converter = new HeadlessSuperConverter() + const superString = converter.convertOtherFormatToSuperString('

Line 2

', 'html', { + html: { + addLineBreaks: false, + }, + }) + + expect(superString).toContain('"format":128') + }) + + it('imports Evernote highlight spans as highlight format with background color', () => { + const converter = new HeadlessSuperConverter() + const superString = converter.convertOtherFormatToSuperString( + '

Line 2

', + 'html', + { + html: { + addLineBreaks: false, + }, + }, + ) + + expect(superString).toContain('"format":128') + expect(superString).toContain('background-color') + }) + + it('imports legacy -evernote-highlight spans as highlight format', () => { + const converter = new HeadlessSuperConverter() + const superString = converter.convertOtherFormatToSuperString( + '

Line 2

', + 'html', + { + html: { + addLineBreaks: false, + }, + }, + ) + + expect(superString).toContain('"format":128') + }) + + it('exports imported Evernote highlights as mark elements', async () => { + const crypto = { + generateUUID: () => String(Math.random()), + } as unknown as PureCryptoInterface + const generateUuid = new GenerateUuid(crypto) + const superConverter = new HeadlessSuperConverter() + const evernoteConverter = new EvernoteConverter(generateUuid) + + const readFileAsText = async (file: File) => file as unknown as string + + const { successful } = await evernoteConverter.convert(highlightEnex as unknown as File, { + insertNote: async ({ text }) => ({ content: { text } }) as never, + insertTag: async () => ({ content: { references: [] } }) as never, + convertHTMLToSuper: (html, options) => + superConverter.convertOtherFormatToSuperString(html, 'html', { html: options }), + convertMarkdownToSuper: jest.fn(), + readFileAsText, + canUseSuper: true, + canUploadFiles: false, + uploadFile: async () => void 0, + linkItems: async () => void 0, + cleanupItems: async () => void 0, + }) + + const superString = (successful?.[0] as unknown as { content: { text: string } }).content.text + expect(superString).toContain('"format":128') + expect(superString).toContain('background-color') + }) +}) diff --git a/packages/web/src/javascripts/Components/SuperEditor/Tools/HeadlessSuperConverter.tsx b/packages/web/src/javascripts/Components/SuperEditor/Tools/HeadlessSuperConverter.tsx index 63651062a..6b342adbc 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Tools/HeadlessSuperConverter.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/Tools/HeadlessSuperConverter.tsx @@ -20,6 +20,7 @@ import { parseFileName } from '@standardnotes/utils' import { $dfs } from '@lexical/utils' import { $isFileNode } from '../Plugins/EncryptedFilePlugin/Nodes/FileUtils' import { $generateNodesFromSerializedNodes, $insertGeneratedNodes } from '@lexical/clipboard' +import { highlightHtmlImport } from '../Lexical/Utils/highlightHtmlImport' export class HeadlessSuperConverter implements SuperConverterServiceInterface { private importEditor: LexicalEditor @@ -32,6 +33,9 @@ export class HeadlessSuperConverter implements SuperConverterServiceInterface { editable: false, onError: (error: Error) => console.error(error), nodes: BlockEditorNodes, + html: { + import: highlightHtmlImport, + }, }) this.exportEditor = createHeadlessEditor({ namespace: 'BlocksEditor', From f4885188ad7a289b16f95eeea56de2bed56dfb95 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Fri, 5 Jun 2026 00:33:09 -0300 Subject: [PATCH 34/45] fix: Fixes checkboxes parsed as bullet items when importing Evernote notes --- .../EvernoteConverter.spec.ts | 57 +++++- .../EvernoteConverter/EvernoteConverter.ts | 190 ++++++++++++++++-- .../src/Import/EvernoteConverter/testData.ts | 27 +++ .../src/Import/EvernoteHighlight.spec.ts | 35 ---- .../src/Import/EvernoteHighlight.ts | 46 ----- .../src/Import/HighlightSpanImport.spec.ts | 26 +++ .../src/Import/HighlightSpanImport.ts | 24 +++ .../Lexical/Utils/highlightHtmlImport.ts | 4 +- .../Tools/HeadlessSuperConverter.spec.ts | 30 ++- 9 files changed, 330 insertions(+), 109 deletions(-) delete mode 100644 packages/ui-services/src/Import/EvernoteHighlight.spec.ts delete mode 100644 packages/ui-services/src/Import/EvernoteHighlight.ts create mode 100644 packages/ui-services/src/Import/HighlightSpanImport.spec.ts create mode 100644 packages/ui-services/src/Import/HighlightSpanImport.ts diff --git a/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.spec.ts b/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.spec.ts index d6b094139..1cecbac2b 100644 --- a/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.spec.ts +++ b/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.spec.ts @@ -5,7 +5,7 @@ import { ContentType } from '@standardnotes/domain-core' import { SNNote, SNTag } from '@standardnotes/models' import { EvernoteConverter, EvernoteResource } from './EvernoteConverter' -import { createTestResourceElement, emptyLineEnex, enex, highlightEnex } from './testData' +import { checkboxEnex, createTestResourceElement, emptyLineEnex, enTodoEnex, enex, highlightEnex } from './testData' import { PureCryptoInterface } from '@standardnotes/sncrypto-common' import { GenerateUuid } from '@standardnotes/services' import { Converter } from '../Converter' @@ -109,7 +109,43 @@ describe('EvernoteConverter', () => { ) }) + it('should convert Evernote checkbox lists to super format', async () => { + const converter = new EvernoteConverter(generateUuid) + + const { successful } = await converter.convert(checkboxEnex as unknown as File, { + ...dependencies, + canUseSuper: true, + }) + + expect((successful?.[0] as SNNote).content.text).toContain('__lexicallisttype="check"') + expect((successful?.[0] as SNNote).content.text).toContain('aria-checked="true"') + expect((successful?.[0] as SNNote).content.text).toContain('aria-checked="false"') + }) + + it('should convert Evernote checkbox lists to plaintext checkboxes without super', async () => { + const converter = new EvernoteConverter(generateUuid) + + const { successful } = await converter.convert(checkboxEnex as unknown as File, dependencies) + + expect((successful?.[0] as SNNote).content.text).toBe('- [x] Line 1\n- [ ] Line 2\n') + }) + + it('should convert en-todo tags to super checklist format', async () => { + const converter = new EvernoteConverter(generateUuid) + + const { successful } = await converter.convert(enTodoEnex as unknown as File, { + ...dependencies, + canUseSuper: true, + }) + + expect((successful?.[0] as SNNote).content.text).toContain('__lexicallisttype="check"') + expect((successful?.[0] as SNNote).content.text).toContain('Checked item') + expect((successful?.[0] as SNNote).content.text).toContain('Unchecked item') + }) + it('should convert lists to super format if applicable', () => { + const converter = new EvernoteConverter(generateUuid) + const noteElement = document.createElement('en-note') const unorderedList1 = document.createElement('ul') unorderedList1.style.setProperty('--en-todo', 'true') const listItem1 = document.createElement('li') @@ -120,11 +156,10 @@ describe('EvernoteConverter', () => { unorderedList1.appendChild(listItem2) const unorderedList2 = document.createElement('ul') + noteElement.appendChild(unorderedList1) + noteElement.appendChild(unorderedList2) - const array = [unorderedList1, unorderedList2] - - const converter = new EvernoteConverter(generateUuid) - converter.convertListsToSuperFormatIfApplicable(array) + converter.convertEvernoteChecklists(noteElement, true) expect(unorderedList1.getAttribute('__lexicallisttype')).toBe('check') expect(listItem1.getAttribute('aria-checked')).toBe('true') @@ -140,6 +175,18 @@ describe('EvernoteConverter', () => { expect((successful?.[0] as SNNote).content.text).toBe('line1\n\nline2') }) + it('should convert highlight spans to mark elements', () => { + const converter = new EvernoteConverter(generateUuid) + const root = document.createElement('div') + root.innerHTML = + 'Line 2plain' + + converter.convertHighlightSpansToMarks(root) + + expect(root.querySelector('span')?.textContent).toBe('plain') + expect(root.querySelector('mark')?.textContent).toBe('Line 2') + }) + it('should convert highlight spans to mark elements before Super import', async () => { const converter = new EvernoteConverter(generateUuid) diff --git a/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.ts b/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.ts index aa8ec11e7..4f46633ce 100644 --- a/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.ts +++ b/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.ts @@ -8,7 +8,10 @@ import Base64 from 'crypto-js/enc-base64' import { Converter, UploadFileFn } from '../Converter' import { ConversionResult } from '../ConversionResult' import { getBlobFromBase64 } from '../Utils' -import { convertEvernoteHighlightSpansToMarks } from '../EvernoteHighlight' +import { isHighlightSpanElement } from '../HighlightSpanImport' + +const EVERNOTE_TODO = /--en-todo\s*:\s*true/i +const EVERNOTE_CHECKED = /--en-checked\s*:\s*true/i dayjs.extend(customParseFormat) dayjs.extend(utc) @@ -87,15 +90,13 @@ export class EvernoteConverter implements Converter { const noteElement = content.getElementsByTagName('en-note')[0] as HTMLElement - const unorderedLists = Array.from(noteElement.getElementsByTagName('ul')) - if (canUseSuper) { this.convertTopLevelDivsToParagraphs(noteElement) - this.convertListsToSuperFormatIfApplicable(unorderedLists) this.convertLeftPaddingToSuperIndent(noteElement) - convertEvernoteHighlightSpansToMarks(noteElement) + this.convertHighlightSpansToMarks(noteElement) } + this.convertEvernoteChecklists(noteElement, canUseSuper) this.removeEmptyAndOrphanListElements(noteElement) this.unwrapTopLevelBreaks(noteElement) @@ -244,6 +245,38 @@ export class EvernoteConverter implements Converter { } as EvernoteResource } + convertHighlightSpansToMarks(noteElement: HTMLElement) { + for (const span of Array.from(noteElement.querySelectorAll('span'))) { + if (!isHighlightSpanElement(span)) { + continue + } + + const mark = document.createElement('mark') + const style = span.getAttribute('style') + if (style) { + mark.setAttribute('style', style) + } + + while (span.firstChild) { + mark.appendChild(span.firstChild) + } + + span.replaceWith(mark) + } + } + + convertEvernoteChecklists(noteElement: HTMLElement, forSuper: boolean) { + for (const ul of Array.from(noteElement.getElementsByTagName('ul'))) { + if (isEvernoteTodoList(ul)) { + convertEvernoteTodoList(ul, forSuper) + } + } + + for (const group of getEnTodoBlockGroups(noteElement)) { + convertEvernoteEnTodoGroup(group, forSuper) + } + } + convertTopLevelDivsToParagraphs(noteElement: HTMLElement) { noteElement.querySelectorAll('div').forEach((div) => { if (div.parentElement === noteElement) { @@ -252,21 +285,6 @@ export class EvernoteConverter implements Converter { }) } - convertListsToSuperFormatIfApplicable(unorderedLists: HTMLUListElement[]) { - for (const unorderedList of unorderedLists) { - if (unorderedList.style.getPropertyValue('--en-todo') !== 'true') { - continue - } - - unorderedList.setAttribute('__lexicallisttype', 'check') - - const listItems = unorderedList.getElementsByTagName('li') - for (const listItem of Array.from(listItems)) { - listItem.setAttribute('aria-checked', listItem.style.getPropertyValue('--en-checked')) - } - } - } - convertLeftPaddingToSuperIndent(noteElement: HTMLElement) { noteElement.querySelectorAll('p').forEach((element) => { const paddingLeft = element.style.paddingLeft @@ -410,3 +428,135 @@ function changeElementTag(element: HTMLElement, newTag: string) { } parent.replaceChild(replacement, element) } + +function isEvernoteStyleTrue(element: HTMLElement, property: '--en-todo' | '--en-checked'): boolean { + const style = element.getAttribute('style') ?? '' + const matchesStyleAttribute = property === '--en-todo' ? EVERNOTE_TODO.test(style) : EVERNOTE_CHECKED.test(style) + + return matchesStyleAttribute || element.style.getPropertyValue(property) === 'true' +} + +function isEvernoteTodoList(element: HTMLUListElement): boolean { + return isEvernoteStyleTrue(element, '--en-todo') +} + +function isEvernoteChecked(element: HTMLElement): boolean { + return isEvernoteStyleTrue(element, '--en-checked') +} + +function formatPlaintextCheckbox(checked: boolean, text: string): string { + return `- ${checked ? '[x]' : '[ ]'} ${text}` +} + +function moveEnTodoBlockContent(block: HTMLElement, target: HTMLElement) { + const clone = block.cloneNode(true) as HTMLElement + const enTodo = clone.querySelector('en-todo') + + if (enTodo) { + while (enTodo.firstChild) { + target.appendChild(enTodo.firstChild) + } + enTodo.remove() + } + + while (clone.lastChild?.nodeName === 'BR') { + clone.removeChild(clone.lastChild) + } + + while (clone.firstChild) { + target.appendChild(clone.firstChild) + } +} + +function getEnTodoBlockGroups(noteElement: HTMLElement): HTMLElement[][] { + const groups: HTMLElement[][] = [] + let currentGroup: HTMLElement[] = [] + + for (const child of Array.from(noteElement.children)) { + if (!(child instanceof HTMLElement) || (child.tagName !== 'DIV' && child.tagName !== 'P')) { + if (currentGroup.length > 0) { + groups.push(currentGroup) + currentGroup = [] + } + continue + } + + if (child.querySelector('en-todo')) { + currentGroup.push(child) + } else if (currentGroup.length > 0) { + groups.push(currentGroup) + currentGroup = [] + } + } + + if (currentGroup.length > 0) { + groups.push(currentGroup) + } + + return groups +} + +function replaceBlockGroup(group: HTMLElement[], replacement: HTMLElement) { + group[0].replaceWith(replacement) + for (let index = 1; index < group.length; index++) { + group[index].remove() + } +} + +function convertEvernoteTodoList(ul: HTMLUListElement, forSuper: boolean) { + if (forSuper) { + ul.setAttribute('__lexicallisttype', 'check') + for (const listItem of Array.from(ul.getElementsByTagName('li'))) { + listItem.setAttribute('aria-checked', isEvernoteChecked(listItem) ? 'true' : 'false') + } + return + } + + const lines = Array.from(ul.getElementsByTagName('li')).map((listItem) => + formatPlaintextCheckbox(isEvernoteChecked(listItem), listItem.textContent?.trim() ?? ''), + ) + const replacement = document.createElement('div') + replacement.textContent = `${lines.join('\n')}\n` + ul.replaceWith(replacement) +} + +function convertEvernoteEnTodoGroup(group: HTMLElement[], forSuper: boolean) { + if (forSuper) { + const ul = document.createElement('ul') + ul.setAttribute('__lexicallisttype', 'check') + + for (const block of group) { + const enTodo = block.querySelector('en-todo') + if (!enTodo) { + continue + } + + const listItem = document.createElement('li') + const checked = enTodo.getAttribute('checked')?.toLowerCase() === 'true' + listItem.setAttribute('aria-checked', checked ? 'true' : 'false') + moveEnTodoBlockContent(block, listItem) + ul.appendChild(listItem) + } + + replaceBlockGroup(group, ul) + return + } + + const lines: string[] = [] + + for (const block of group) { + const enTodo = block.querySelector('en-todo') + if (!enTodo) { + continue + } + + const textContainer = document.createElement('div') + moveEnTodoBlockContent(block, textContainer) + const checked = enTodo.getAttribute('checked')?.toLowerCase() === 'true' + lines.push(formatPlaintextCheckbox(checked, textContainer.textContent?.trim() ?? '')) + } + + const replacement = document.createElement('div') + replacement.textContent = `${lines.join('\n')}\n` + replaceBlockGroup(group, replacement) +} diff --git a/packages/ui-services/src/Import/EvernoteConverter/testData.ts b/packages/ui-services/src/Import/EvernoteConverter/testData.ts index 67d19f474..8d1f6fb30 100644 --- a/packages/ui-services/src/Import/EvernoteConverter/testData.ts +++ b/packages/ui-services/src/Import/EvernoteConverter/testData.ts @@ -50,6 +50,33 @@ export const highlightEnex = `
` +export const checkboxEnex = ` + + + + Checkbox test + 20221122T043758Z + 20221122T043813Z + + +
  • Line 1
  • Line 2
]]> +
+
+
` + +export const enTodoEnex = ` + + + + En-todo test + + +
Checked item
Unchecked item
]]>
+ 20200622T091652Z + 20200622T091707Z +
+
` + export const emptyLineEnex = ` diff --git a/packages/ui-services/src/Import/EvernoteHighlight.spec.ts b/packages/ui-services/src/Import/EvernoteHighlight.spec.ts deleted file mode 100644 index e913b4eaf..000000000 --- a/packages/ui-services/src/Import/EvernoteHighlight.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * @jest-environment jsdom - */ - -import { - convertEvernoteHighlightSpansToMarks, - isEvernoteHighlightElement, - isEvernoteHighlightStyle, -} from './EvernoteHighlight' - -describe('EvernoteHighlight', () => { - it('detects --en-highlight in style attribute', () => { - expect(isEvernoteHighlightStyle('--en-highlight:yellow;background-color: #ffef9e;')).toBe(true) - }) - - it('detects -evernote-highlight in style attribute', () => { - expect(isEvernoteHighlightStyle('background-color: rgb(255, 250, 165);-evernote-highlight:true;')).toBe(true) - }) - - it('does not treat highlight:false as highlighted', () => { - expect(isEvernoteHighlightStyle('--en-highlight:false;')).toBe(false) - }) - - it('converts highlight spans to mark elements', () => { - const root = document.createElement('div') - root.innerHTML = - 'Line 2plain' - - convertEvernoteHighlightSpansToMarks(root) - - expect(root.querySelector('span')).not.toBeNull() - expect(root.querySelector('mark')?.textContent).toBe('Line 2') - expect(isEvernoteHighlightElement(root.querySelector('mark') as HTMLElement)).toBe(true) - }) -}) diff --git a/packages/ui-services/src/Import/EvernoteHighlight.ts b/packages/ui-services/src/Import/EvernoteHighlight.ts deleted file mode 100644 index b08bb5cf8..000000000 --- a/packages/ui-services/src/Import/EvernoteHighlight.ts +++ /dev/null @@ -1,46 +0,0 @@ -const EVERNOTE_HIGHLIGHT_PROPERTY = - /(?:--en-highlight|-en-highlight|--evernote-highlight|-evernote-highlight)\s*:\s*([^;]+)/i - -export function isEvernoteHighlightStyle(styleAttribute: string | null | undefined): boolean { - if (!styleAttribute) { - return false - } - - const match = styleAttribute.match(EVERNOTE_HIGHLIGHT_PROPERTY) - if (!match) { - return false - } - - return match[1].trim().toLowerCase() !== 'false' -} - -export function isEvernoteHighlightElement(element: HTMLElement): boolean { - if (isEvernoteHighlightStyle(element.getAttribute('style'))) { - return true - } - - const enHighlight = element.style.getPropertyValue('--en-highlight') - return enHighlight !== '' && enHighlight.toLowerCase() !== 'false' -} - -export function convertEvernoteHighlightSpansToMarks(root: ParentNode) { - const spans = Array.from(root.querySelectorAll('span')) - - for (const span of spans) { - if (!isEvernoteHighlightElement(span)) { - continue - } - - const mark = document.createElement('mark') - const style = span.getAttribute('style') - if (style) { - mark.setAttribute('style', style) - } - - while (span.firstChild) { - mark.appendChild(span.firstChild) - } - - span.replaceWith(mark) - } -} diff --git a/packages/ui-services/src/Import/HighlightSpanImport.spec.ts b/packages/ui-services/src/Import/HighlightSpanImport.spec.ts new file mode 100644 index 000000000..2b942f6b4 --- /dev/null +++ b/packages/ui-services/src/Import/HighlightSpanImport.spec.ts @@ -0,0 +1,26 @@ +/** + * @jest-environment jsdom + */ + +import { isHighlightSpanElement, isHighlightSpanStyle } from './HighlightSpanImport' + +describe('HighlightSpanImport', () => { + it('detects --en-highlight in style attribute', () => { + expect(isHighlightSpanStyle('--en-highlight:yellow;background-color: #ffef9e;')).toBe(true) + }) + + it('detects -evernote-highlight in style attribute', () => { + expect(isHighlightSpanStyle('background-color: rgb(255, 250, 165);-evernote-highlight:true;')).toBe(true) + }) + + it('does not treat highlight:false as highlighted', () => { + expect(isHighlightSpanStyle('--en-highlight:false;')).toBe(false) + }) + + it('detects highlight spans by element style', () => { + const span = document.createElement('span') + span.setAttribute('style', '--en-highlight:yellow;background-color: #ffef9e;') + + expect(isHighlightSpanElement(span)).toBe(true) + }) +}) diff --git a/packages/ui-services/src/Import/HighlightSpanImport.ts b/packages/ui-services/src/Import/HighlightSpanImport.ts new file mode 100644 index 000000000..d7f1e86a0 --- /dev/null +++ b/packages/ui-services/src/Import/HighlightSpanImport.ts @@ -0,0 +1,24 @@ +const HIGHLIGHT_SPAN_PROPERTY = + /(?:--en-highlight|-en-highlight|--evernote-highlight|-evernote-highlight)\s*:\s*([^;]+)/i + +export function isHighlightSpanStyle(styleAttribute: string | null | undefined): boolean { + if (!styleAttribute) { + return false + } + + const match = styleAttribute.match(HIGHLIGHT_SPAN_PROPERTY) + if (!match) { + return false + } + + return match[1].trim().toLowerCase() !== 'false' +} + +export function isHighlightSpanElement(element: HTMLElement): boolean { + if (isHighlightSpanStyle(element.getAttribute('style'))) { + return true + } + + const enHighlight = element.style.getPropertyValue('--en-highlight') + return enHighlight !== '' && enHighlight.toLowerCase() !== 'false' +} diff --git a/packages/web/src/javascripts/Components/SuperEditor/Lexical/Utils/highlightHtmlImport.ts b/packages/web/src/javascripts/Components/SuperEditor/Lexical/Utils/highlightHtmlImport.ts index 64e74a71d..11810d40a 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Lexical/Utils/highlightHtmlImport.ts +++ b/packages/web/src/javascripts/Components/SuperEditor/Lexical/Utils/highlightHtmlImport.ts @@ -1,4 +1,4 @@ -import { isEvernoteHighlightElement } from '@standardnotes/ui-services/src/Import/EvernoteHighlight' +import { isHighlightSpanElement } from '@standardnotes/ui-services/src/Import/HighlightSpanImport' import { $isTextNode, DOMConversionMap, LexicalNode } from 'lexical' function applyHighlightToTextChild(domNode: HTMLElement) { @@ -30,7 +30,7 @@ export const highlightHtmlImport: DOMConversionMap = { priority: 1, }), span: (domNode) => { - if (!isEvernoteHighlightElement(domNode as HTMLElement)) { + if (!isHighlightSpanElement(domNode as HTMLElement)) { return null } diff --git a/packages/web/src/javascripts/Components/SuperEditor/Tools/HeadlessSuperConverter.spec.ts b/packages/web/src/javascripts/Components/SuperEditor/Tools/HeadlessSuperConverter.spec.ts index fa3bee832..0e75fe96f 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Tools/HeadlessSuperConverter.spec.ts +++ b/packages/web/src/javascripts/Components/SuperEditor/Tools/HeadlessSuperConverter.spec.ts @@ -4,7 +4,7 @@ import { HeadlessSuperConverter } from './HeadlessSuperConverter' import { EvernoteConverter } from '@standardnotes/ui-services/src/Import/EvernoteConverter/EvernoteConverter' -import { highlightEnex } from '@standardnotes/ui-services/src/Import/EvernoteConverter/testData' +import { checkboxEnex, highlightEnex } from '@standardnotes/ui-services/src/Import/EvernoteConverter/testData' import { GenerateUuid } from '@standardnotes/services' import { PureCryptoInterface } from '@standardnotes/sncrypto-common' @@ -51,6 +51,34 @@ describe('HeadlessSuperConverter', () => { expect(superString).toContain('"format":128') }) + it('imports Evernote checkbox lists as check list type', async () => { + const crypto = { + generateUUID: () => String(Math.random()), + } as unknown as PureCryptoInterface + const generateUuid = new GenerateUuid(crypto) + const superConverter = new HeadlessSuperConverter() + const evernoteConverter = new EvernoteConverter(generateUuid) + + const readFileAsText = async (file: File) => file as unknown as string + + const { successful } = await evernoteConverter.convert(checkboxEnex as unknown as File, { + insertNote: async ({ text }) => ({ content: { text } }) as never, + insertTag: async () => ({ content: { references: [] } }) as never, + convertHTMLToSuper: (html, options) => + superConverter.convertOtherFormatToSuperString(html, 'html', { html: options }), + convertMarkdownToSuper: jest.fn(), + readFileAsText, + canUseSuper: true, + canUploadFiles: false, + uploadFile: async () => void 0, + linkItems: async () => void 0, + cleanupItems: async () => void 0, + }) + + const superString = (successful?.[0] as unknown as { content: { text: string } }).content.text + expect(superString).toContain('"listType":"check"') + }) + it('exports imported Evernote highlights as mark elements', async () => { const crypto = { generateUUID: () => String(Math.random()), From 81bd2de42528e75c53df9234164186415600c40a Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Fri, 5 Jun 2026 14:55:13 -0300 Subject: [PATCH 35/45] chore: temporarily disallow android purchases (#3018) --- .../src/UseCase/IsNativeAndroid.ts | 13 +++++++++++ .../WebApplication/WebApplicationInterface.ts | 1 + packages/ui-services/src/index.ts | 1 + .../Application/Dependencies/Types.ts | 1 + .../Dependencies/WebDependencies.ts | 6 +++++ .../javascripts/Application/WebApplication.ts | 11 +++++++++- .../Components/Footer/UpgradeNow.tsx | 6 +++++ .../NoSubscriptionBanner.tsx | 8 ++++--- .../Panes/Account/NoProSubscription.tsx | 2 +- .../Account/Subscription/NoSubscription.tsx | 2 +- .../Subviews/UpgradePrompt.tsx | 22 ++++++++++--------- .../RevisionContentLocked.tsx | 20 +++++++++-------- .../PurchaseFlow/PurchaseFlowController.ts | 11 +++++++++- .../Event/ApplicationEventObserver.spec.ts | 12 ++++++++++ .../Event/ApplicationEventObserver.ts | 4 +++- 15 files changed, 93 insertions(+), 27 deletions(-) create mode 100644 packages/ui-services/src/UseCase/IsNativeAndroid.ts diff --git a/packages/ui-services/src/UseCase/IsNativeAndroid.ts b/packages/ui-services/src/UseCase/IsNativeAndroid.ts new file mode 100644 index 000000000..4df080713 --- /dev/null +++ b/packages/ui-services/src/UseCase/IsNativeAndroid.ts @@ -0,0 +1,13 @@ +import { Result, SyncUseCaseInterface } from '@standardnotes/domain-core' +import { Environment, Platform } from '@standardnotes/models' + +export class IsNativeAndroid implements SyncUseCaseInterface { + constructor( + private environment: Environment, + private platform: Platform, + ) {} + + execute(): Result { + return Result.ok(this.environment === Environment.Mobile && this.platform === Platform.Android) + } +} diff --git a/packages/ui-services/src/WebApplication/WebApplicationInterface.ts b/packages/ui-services/src/WebApplication/WebApplicationInterface.ts index f2d2bac3d..7cfad3e7b 100644 --- a/packages/ui-services/src/WebApplication/WebApplicationInterface.ts +++ b/packages/ui-services/src/WebApplication/WebApplicationInterface.ts @@ -26,6 +26,7 @@ export interface WebApplicationInterface extends ApplicationInterface { handleReceivedLinkEvent(item: { link: string; title: string }): Promise handleOpenFilePreviewEvent(item: { id: string }): void isNativeMobileWeb(): boolean + canShowPurchaseFlow(): boolean handleAndroidBackButtonPressed(): void addAndroidBackHandlerEventListener(listener: () => boolean): (() => void) | undefined setAndroidBackHandlerFallbackListener(listener: () => boolean): void diff --git a/packages/ui-services/src/index.ts b/packages/ui-services/src/index.ts index ed05b5614..d033f6920 100644 --- a/packages/ui-services/src/index.ts +++ b/packages/ui-services/src/index.ts @@ -37,6 +37,7 @@ export * from './UseCase/IsGlobalSpellcheckEnabled' export * from './UseCase/IsNativeMobileWeb' export * from './UseCase/IsMobileDevice' export * from './UseCase/IsNativeIOS' +export * from './UseCase/IsNativeAndroid' export * from './UseCase/GetItemTags' export * from './Theme/ThemeManager' diff --git a/packages/web/src/javascripts/Application/Dependencies/Types.ts b/packages/web/src/javascripts/Application/Dependencies/Types.ts index b2ad12352..594dba04e 100644 --- a/packages/web/src/javascripts/Application/Dependencies/Types.ts +++ b/packages/web/src/javascripts/Application/Dependencies/Types.ts @@ -48,6 +48,7 @@ export const Web_TYPES = { IsGlobalSpellcheckEnabled: Symbol.for('IsGlobalSpellcheckEnabled'), IsMobileDevice: Symbol.for('IsMobileDevice'), IsNativeIOS: Symbol.for('IsNativeIOS'), + IsNativeAndroid: Symbol.for('IsNativeAndroid'), IsNativeMobileWeb: Symbol.for('IsNativeMobileWeb'), IsTabletOrMobileScreen: Symbol.for('IsTabletOrMobileScreen'), LoadPurchaseFlowUrl: Symbol.for('LoadPurchaseFlowUrl'), diff --git a/packages/web/src/javascripts/Application/Dependencies/WebDependencies.ts b/packages/web/src/javascripts/Application/Dependencies/WebDependencies.ts index eea0204b4..edf446b8c 100644 --- a/packages/web/src/javascripts/Application/Dependencies/WebDependencies.ts +++ b/packages/web/src/javascripts/Application/Dependencies/WebDependencies.ts @@ -7,6 +7,7 @@ import { IsGlobalSpellcheckEnabled, IsMobileDevice, IsNativeIOS, + IsNativeAndroid, IsNativeMobileWeb, KeyboardService, PluginsService, @@ -77,6 +78,10 @@ export class WebDependencies extends DependencyContainer { return new IsNativeIOS(application.environment, application.platform) }) + this.bind(Web_TYPES.IsNativeAndroid, () => { + return new IsNativeAndroid(application.environment, application.platform) + }) + this.bind(Web_TYPES.OpenSubscriptionDashboard, () => { return new OpenSubscriptionDashboard(application, application.legacyApi) }) @@ -331,6 +336,7 @@ export class WebDependencies extends DependencyContainer { application.mobileDevice, this.get(Web_TYPES.LoadPurchaseFlowUrl), this.get(Web_TYPES.IsNativeIOS), + this.get(Web_TYPES.IsNativeAndroid), application.events, ) }) diff --git a/packages/web/src/javascripts/Application/WebApplication.ts b/packages/web/src/javascripts/Application/WebApplication.ts index aa27bd5b8..317240738 100644 --- a/packages/web/src/javascripts/Application/WebApplication.ts +++ b/packages/web/src/javascripts/Application/WebApplication.ts @@ -37,6 +37,7 @@ import { IsGlobalSpellcheckEnabled, IsMobileDevice, IsNativeIOS, + IsNativeAndroid, IsNativeMobileWeb, KeyboardService, PluginsServiceInterface, @@ -257,12 +258,20 @@ export class WebApplication extends SNApplication implements WebApplicationInter return this.deps.get(Web_TYPES.IsNativeIOS).execute().getValue() } + isNativeAndroid(): boolean { + return this.deps.get(Web_TYPES.IsNativeAndroid).execute().getValue() + } + + canShowPurchaseFlow(): boolean { + return !this.isNativeAndroid() + } + get isMobileDevice(): boolean { return this.deps.get(Web_TYPES.IsMobileDevice).execute().getValue() } get hideOutboundSubscriptionLinks() { - return this.isNativeIOS() + return this.isNativeIOS() || this.isNativeAndroid() } get mobileDevice(): MobileDeviceInterface { diff --git a/packages/web/src/javascripts/Components/Footer/UpgradeNow.tsx b/packages/web/src/javascripts/Components/Footer/UpgradeNow.tsx index 126a5ba5f..39504f163 100644 --- a/packages/web/src/javascripts/Components/Footer/UpgradeNow.tsx +++ b/packages/web/src/javascripts/Components/Footer/UpgradeNow.tsx @@ -18,6 +18,8 @@ const UpgradeNow = ({ application, featuresController, subscriptionContoller }: const onClick = useCallback(() => { if (hasAccount && application.isNativeIOS()) { application.showPremiumModal() + } else if (!application.canShowPurchaseFlow() && !hasAccount) { + application.showAccountMenu() } else { void application.openPurchaseFlow() } @@ -27,6 +29,10 @@ const UpgradeNow = ({ application, featuresController, subscriptionContoller }: return null } + if (!application.canShowPurchaseFlow() && hasAccount) { + return null + } + return (

{message}

- + {application.canShowPurchaseFlow() && ( + + )}
) } diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Account/NoProSubscription.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Account/NoProSubscription.tsx index 8b594e1b4..394504fec 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Account/NoProSubscription.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Account/NoProSubscription.tsx @@ -40,7 +40,7 @@ const NoProSubscription: FunctionComponent = ({ application, text }) => { {!application.hideOutboundSubscriptionLinks && ( )} - {application.hasAccount() && ( + {application.hasAccount() && application.canShowPurchaseFlow() && (
diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Account/Subscription/NoSubscription.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Account/Subscription/NoSubscription.tsx index 90bdd8d36..dbbbe272f 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Account/Subscription/NoSubscription.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Account/Subscription/NoSubscription.tsx @@ -36,7 +36,7 @@ const NoSubscription: FunctionComponent = ({ application }) => { {!application.hideOutboundSubscriptionLinks && ( )} - {application.hasAccount() && ( + {application.hasAccount() && application.canShowPurchaseFlow() && (
diff --git a/packages/web/src/javascripts/Components/PremiumFeaturesModal/Subviews/UpgradePrompt.tsx b/packages/web/src/javascripts/Components/PremiumFeaturesModal/Subviews/UpgradePrompt.tsx index ab39b5749..108ddf159 100644 --- a/packages/web/src/javascripts/Components/PremiumFeaturesModal/Subviews/UpgradePrompt.tsx +++ b/packages/web/src/javascripts/Components/PremiumFeaturesModal/Subviews/UpgradePrompt.tsx @@ -110,16 +110,18 @@ export const UpgradePrompt = ({ )}
- + {application.canShowPurchaseFlow() && ( + + )}
) } diff --git a/packages/web/src/javascripts/Components/RevisionHistoryModal/RevisionContentLocked.tsx b/packages/web/src/javascripts/Components/RevisionHistoryModal/RevisionContentLocked.tsx index 73866f4c1..200ccb4e0 100644 --- a/packages/web/src/javascripts/Components/RevisionHistoryModal/RevisionContentLocked.tsx +++ b/packages/web/src/javascripts/Components/RevisionHistoryModal/RevisionContentLocked.tsx @@ -37,15 +37,17 @@ const RevisionContentLocked: FunctionComponent = () => {
{getPremiumContentCopy(planName)}. Learn more about our other plans to upgrade your history capacity.
-
) diff --git a/packages/web/src/javascripts/Controllers/PurchaseFlow/PurchaseFlowController.ts b/packages/web/src/javascripts/Controllers/PurchaseFlow/PurchaseFlowController.ts index 532b047b5..ebc44ab6e 100644 --- a/packages/web/src/javascripts/Controllers/PurchaseFlow/PurchaseFlowController.ts +++ b/packages/web/src/javascripts/Controllers/PurchaseFlow/PurchaseFlowController.ts @@ -11,7 +11,7 @@ import { action, makeObservable, observable } from 'mobx' import { AbstractViewController } from '../Abstract/AbstractViewController' import { PurchaseFlowPane } from './PurchaseFlowPane' import { LoadPurchaseFlowUrl } from '@/Application/UseCase/LoadPurchaseFlowUrl' -import { IsNativeIOS } from '@standardnotes/ui-services' +import { IsNativeIOS, IsNativeAndroid } from '@standardnotes/ui-services' export class PurchaseFlowController extends AbstractViewController { isOpen = false @@ -25,6 +25,7 @@ export class PurchaseFlowController extends AbstractViewController { private mobileDevice: MobileDeviceInterface | undefined, private _loadPurchaseFlowUrl: LoadPurchaseFlowUrl, private _isNativeIOS: IsNativeIOS, + private _isNativeAndroid: IsNativeAndroid, eventBus: InternalEventBusInterface, ) { super(eventBus) @@ -44,6 +45,10 @@ export class PurchaseFlowController extends AbstractViewController { } openPurchaseFlow = async (plan = AppleIAPProductId.ProPlanYearly) => { + if (this._isNativeAndroid.execute().getValue()) { + return + } + const user = this.sessions.getUser() if (!user) { this.isOpen = true @@ -58,6 +63,10 @@ export class PurchaseFlowController extends AbstractViewController { } openPurchaseWebpage = async () => { + if (this._isNativeAndroid.execute().getValue()) { + return + } + const result = await this._loadPurchaseFlowUrl.execute() if (result.isFailed()) { console.error(result.getError()) diff --git a/packages/web/src/javascripts/Event/ApplicationEventObserver.spec.ts b/packages/web/src/javascripts/Event/ApplicationEventObserver.spec.ts index 8d8bb0eed..cba45958d 100644 --- a/packages/web/src/javascripts/Event/ApplicationEventObserver.spec.ts +++ b/packages/web/src/javascripts/Event/ApplicationEventObserver.spec.ts @@ -60,6 +60,7 @@ describe('ApplicationEventObserver', () => { beforeEach(() => { application = {} as jest.Mocked + application.canShowPurchaseFlow = jest.fn().mockReturnValue(true) routeService = {} as jest.Mocked routeService.getRoute = jest.fn().mockReturnValue({ @@ -109,6 +110,17 @@ describe('ApplicationEventObserver', () => { expect(purchaseFlowController.openPurchaseFlow).toHaveBeenCalled() }) + it('should not open the purchase flow on Android', async () => { + application.canShowPurchaseFlow = jest.fn().mockReturnValue(false) + routeService.getRoute = jest.fn().mockReturnValue({ + type: RouteType.Purchase, + } as jest.Mocked) + + await createObserver().handle(ApplicationEvent.Launched) + + expect(purchaseFlowController.openPurchaseFlow).not.toHaveBeenCalled() + }) + it('should open up settings if user is logged in', async () => { routeService.getRoute = jest.fn().mockReturnValue({ type: RouteType.Settings, diff --git a/packages/web/src/javascripts/Event/ApplicationEventObserver.ts b/packages/web/src/javascripts/Event/ApplicationEventObserver.ts index e8e4c24b5..f386126aa 100644 --- a/packages/web/src/javascripts/Event/ApplicationEventObserver.ts +++ b/packages/web/src/javascripts/Event/ApplicationEventObserver.ts @@ -47,7 +47,9 @@ export class ApplicationEventObserver implements EventObserverInterface { const route = this.routeService.getRoute() switch (route.type) { case RouteType.Purchase: - void this.purchaseFlowController.openPurchaseFlow() + if (this.application.canShowPurchaseFlow()) { + void this.purchaseFlowController.openPurchaseFlow() + } break case RouteType.Settings: { From 09ffb07909a00392826d8b81b8353305412e04ac Mon Sep 17 00:00:00 2001 From: StandardNotes CI Date: Fri, 5 Jun 2026 18:01:22 +0000 Subject: [PATCH 36/45] chore(release): publish - @standardnotes/clipper@1.1.588 - @standardnotes/desktop@3.110.193 - @standardnotes/mobile@3.58.255 - @standardnotes/releases@1.4.895 - @standardnotes/ui-services@1.39.5 - @standardnotes/web@3.201.29 --- packages/clipper/CHANGELOG.md | 4 ++++ packages/clipper/package.json | 2 +- packages/desktop/CHANGELOG.md | 4 ++++ packages/desktop/package.json | 2 +- packages/mobile/CHANGELOG.md | 4 ++++ packages/mobile/package.json | 2 +- packages/releases/CHANGELOG.md | 4 ++++ packages/releases/package.json | 2 +- packages/ui-services/CHANGELOG.md | 8 ++++++++ packages/ui-services/package.json | 2 +- packages/web/CHANGELOG.md | 7 +++++++ packages/web/CHANGELOG.md.json | 16 ++++++++++++++++ packages/web/package.json | 2 +- 13 files changed, 53 insertions(+), 6 deletions(-) diff --git a/packages/clipper/CHANGELOG.md b/packages/clipper/CHANGELOG.md index d2d62f079..da1d3b851 100644 --- a/packages/clipper/CHANGELOG.md +++ b/packages/clipper/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.1.588](https://github.com/standardnotes/app/compare/@standardnotes/clipper@1.1.587...@standardnotes/clipper@1.1.588) (2026-06-05) + +**Note:** Version bump only for package @standardnotes/clipper + ## [1.1.587](https://github.com/standardnotes/app/compare/@standardnotes/clipper@1.1.586...@standardnotes/clipper@1.1.587) (2026-05-27) **Note:** Version bump only for package @standardnotes/clipper diff --git a/packages/clipper/package.json b/packages/clipper/package.json index d9016f99a..c8f1f01de 100644 --- a/packages/clipper/package.json +++ b/packages/clipper/package.json @@ -1,7 +1,7 @@ { "name": "@standardnotes/clipper", "description": "Web clipper browser extension for Standard Notes", - "version": "1.1.587", + "version": "1.1.588", "private": true, "scripts": { "build-mv2": "yarn clean && webpack --config ./webpack.config.prod.js", diff --git a/packages/desktop/CHANGELOG.md b/packages/desktop/CHANGELOG.md index 0364118ad..15f180f46 100644 --- a/packages/desktop/CHANGELOG.md +++ b/packages/desktop/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [3.110.193](https://github.com/standardnotes/app/compare/@standardnotes/desktop@3.201.28...@standardnotes/desktop@3.110.193) (2026-06-05) + +**Note:** Version bump only for package @standardnotes/desktop + ## [3.110.192](https://github.com/standardnotes/app/compare/@standardnotes/desktop@3.201.27...@standardnotes/desktop@3.110.192) (2026-05-27) **Note:** Version bump only for package @standardnotes/desktop diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 9af2a2304..406bb6b4f 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@standardnotes/desktop", "main": "./app/dist/index.js", - "version": "3.110.192", + "version": "3.110.193", "license": "AGPL-3.0", "author": "Standard Notes.", "private": true, diff --git a/packages/mobile/CHANGELOG.md b/packages/mobile/CHANGELOG.md index a297c1af9..bf842dc31 100644 --- a/packages/mobile/CHANGELOG.md +++ b/packages/mobile/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [3.58.255](https://github.com/standardnotes/app/compare/@standardnotes/mobile@3.58.254...@standardnotes/mobile@3.58.255) (2026-06-05) + +**Note:** Version bump only for package @standardnotes/mobile + ## [3.58.254](https://github.com/standardnotes/app/compare/@standardnotes/mobile@3.58.253...@standardnotes/mobile@3.58.254) (2026-05-27) **Note:** Version bump only for package @standardnotes/mobile diff --git a/packages/mobile/package.json b/packages/mobile/package.json index 8c898b3cb..d0f03866b 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@standardnotes/mobile", - "version": "3.58.254", + "version": "3.58.255", "author": "Standard Notes.", "private": true, "license": "AGPL-3.0", diff --git a/packages/releases/CHANGELOG.md b/packages/releases/CHANGELOG.md index 63c87917a..49d8b27ab 100644 --- a/packages/releases/CHANGELOG.md +++ b/packages/releases/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.4.895](https://github.com/standardnotes/app/compare/@standardnotes/releases@1.4.894...@standardnotes/releases@1.4.895) (2026-06-05) + +**Note:** Version bump only for package @standardnotes/releases + ## [1.4.894](https://github.com/standardnotes/app/compare/@standardnotes/releases@1.4.893...@standardnotes/releases@1.4.894) (2026-05-27) **Note:** Version bump only for package @standardnotes/releases diff --git a/packages/releases/package.json b/packages/releases/package.json index f81a0c66b..f9e75880e 100644 --- a/packages/releases/package.json +++ b/packages/releases/package.json @@ -1,6 +1,6 @@ { "name": "@standardnotes/releases", - "version": "1.4.894", + "version": "1.4.895", "repository": { "type": "git", "url": "https://github.com/standardnotes/app", diff --git a/packages/ui-services/CHANGELOG.md b/packages/ui-services/CHANGELOG.md index ba71c8e8f..c2fceac63 100644 --- a/packages/ui-services/CHANGELOG.md +++ b/packages/ui-services/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [1.39.5](https://github.com/standardnotes/app/compare/@standardnotes/ui-services@1.39.4...@standardnotes/ui-services@1.39.5) (2026-06-05) + +### Bug Fixes + +* Fixes checkboxes parsed as bullet items when importing Evernote notes ([f488518](https://github.com/standardnotes/app/commit/f4885188ad7a289b16f95eeea56de2bed56dfb95)) +* Fixes empty lines doubled when importing Evernote notes ([b4faa20](https://github.com/standardnotes/app/commit/b4faa20ac1089187d2655dc132f8252893a51a9f)) +* Fixes highlight text style lost when importing Evernote notes ([a62f496](https://github.com/standardnotes/app/commit/a62f496ee6ca588876b616261c479f6499b4ec19)) + ## [1.39.4](https://github.com/standardnotes/app/compare/@standardnotes/ui-services@1.39.3...@standardnotes/ui-services@1.39.4) (2026-04-24) **Note:** Version bump only for package @standardnotes/ui-services diff --git a/packages/ui-services/package.json b/packages/ui-services/package.json index 0c509660b..aa0400c39 100644 --- a/packages/ui-services/package.json +++ b/packages/ui-services/package.json @@ -1,6 +1,6 @@ { "name": "@standardnotes/ui-services", - "version": "1.39.4", + "version": "1.39.5", "engines": { "node": ">=16.0.0 <17.0.0" }, diff --git a/packages/web/CHANGELOG.md b/packages/web/CHANGELOG.md index 065e3aa0f..e986e43af 100644 --- a/packages/web/CHANGELOG.md +++ b/packages/web/CHANGELOG.md @@ -3,6 +3,13 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [3.201.29](https://github.com/standardnotes/app/compare/@standardnotes/web@3.201.28...@standardnotes/web@3.201.29) (2026-06-05) + +### Bug Fixes + +* Fixes checkboxes parsed as bullet items when importing Evernote notes ([f488518](https://github.com/standardnotes/app/commit/f4885188ad7a289b16f95eeea56de2bed56dfb95)) +* Fixes highlight text style lost when importing Evernote notes ([a62f496](https://github.com/standardnotes/app/commit/a62f496ee6ca588876b616261c479f6499b4ec19)) + ## [3.201.28](https://github.com/standardnotes/app/compare/@standardnotes/web@3.201.27...@standardnotes/web@3.201.28) (2026-05-27) ### Bug Fixes diff --git a/packages/web/CHANGELOG.md.json b/packages/web/CHANGELOG.md.json index f7624c957..b99ad0696 100644 --- a/packages/web/CHANGELOG.md.json +++ b/packages/web/CHANGELOG.md.json @@ -1,5 +1,21 @@ { "versions": [ + { + "version": "3.201.29", + "title": "[3.201.29](https://github.com/standardnotes/app/compare/@standardnotes/web@3.201.28...@standardnotes/web@3.201.29) (2026-06-05)", + "date": null, + "body": "### Bug Fixes\n\n* Fixes checkboxes parsed as bullet items when importing Evernote notes ([f488518](https://github.com/standardnotes/app/commit/f4885188ad7a289b16f95eeea56de2bed56dfb95))\n* Fixes highlight text style lost when importing Evernote notes ([a62f496](https://github.com/standardnotes/app/commit/a62f496ee6ca588876b616261c479f6499b4ec19))", + "parsed": { + "_": [ + "Fixes checkboxes parsed as bullet items when importing Evernote notes (f488518)", + "Fixes highlight text style lost when importing Evernote notes (a62f496)" + ], + "Bug Fixes": [ + "Fixes checkboxes parsed as bullet items when importing Evernote notes (f488518)", + "Fixes highlight text style lost when importing Evernote notes (a62f496)" + ] + } + }, { "version": "3.201.28", "title": "[3.201.28](https://github.com/standardnotes/app/compare/@standardnotes/web@3.201.27...@standardnotes/web@3.201.28) (2026-05-27)", diff --git a/packages/web/package.json b/packages/web/package.json index c317b7617..8540628d7 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,6 +1,6 @@ { "name": "@standardnotes/web", - "version": "3.201.28", + "version": "3.201.29", "license": "AGPL-3.0", "main": "dist/app.js", "author": "Standard Notes", From c2bdda2ccb47750300cff01729fd4eaa6ee1fe11 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Sun, 7 Jun 2026 00:06:33 -0300 Subject: [PATCH 37/45] chore: temporarily remove manage sub button for android --- .../Components/ComponentView/NotEntitledBanner.tsx | 8 +++++--- .../Account/Subscription/SubscriptionInformation.tsx | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/web/src/javascripts/Components/ComponentView/NotEntitledBanner.tsx b/packages/web/src/javascripts/Components/ComponentView/NotEntitledBanner.tsx index 42569ca35..f4ad97950 100644 --- a/packages/web/src/javascripts/Components/ComponentView/NotEntitledBanner.tsx +++ b/packages/web/src/javascripts/Components/ComponentView/NotEntitledBanner.tsx @@ -50,9 +50,11 @@ const NotEntitledBanner: FunctionComponent = ({ featureStatus, feature })
- + {application.canShowPurchaseFlow() && ( + + )}
diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Account/Subscription/SubscriptionInformation.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Account/Subscription/SubscriptionInformation.tsx index f945da571..cb4f72041 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Account/Subscription/SubscriptionInformation.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Account/Subscription/SubscriptionInformation.tsx @@ -14,7 +14,7 @@ const SubscriptionInformation = () => { return ( <> - {!isSharedSubscription && ( + {!isSharedSubscription && application.canShowPurchaseFlow() && (
) } diff --git a/packages/web/src/javascripts/Controllers/ItemList/ItemListController.ts b/packages/web/src/javascripts/Controllers/ItemList/ItemListController.ts index e407a8262..41b6f43f7 100644 --- a/packages/web/src/javascripts/Controllers/ItemList/ItemListController.ts +++ b/packages/web/src/javascripts/Controllers/ItemList/ItemListController.ts @@ -227,6 +227,7 @@ export class ItemListController this.searchOptionsController.includeProtectedContents, this.searchOptionsController.includeArchived, this.searchOptionsController.includeTrashed, + this.searchOptionsController.noteTitleOnly, ], () => { this.reloadNotesDisplayOptions() @@ -642,6 +643,7 @@ export class ItemListController searchQuery: { query: searchText, includeProtectedNoteText: this.searchOptionsController.includeProtectedContents, + noteTitleOnly: this.searchOptionsController.noteTitleOnly, }, } diff --git a/packages/web/src/javascripts/Controllers/SearchOptionsController.ts b/packages/web/src/javascripts/Controllers/SearchOptionsController.ts index 0ae8ccf8c..944def1a9 100644 --- a/packages/web/src/javascripts/Controllers/SearchOptionsController.ts +++ b/packages/web/src/javascripts/Controllers/SearchOptionsController.ts @@ -12,6 +12,7 @@ export class SearchOptionsController extends AbstractViewController implements I includeProtectedContents = false includeArchived = false includeTrashed = false + noteTitleOnly = false constructor( private protections: ProtectionsClientInterface, @@ -23,11 +24,13 @@ export class SearchOptionsController extends AbstractViewController implements I includeProtectedContents: observable, includeTrashed: observable, includeArchived: observable, + noteTitleOnly: observable, toggleIncludeArchived: action, toggleIncludeTrashed: action, toggleIncludeProtectedContents: action, refreshIncludeProtectedContents: action, + toggleNoteTitleOnly: action, }) eventBus.addEventHandler(this, ApplicationEvent.UnprotectedSessionBegan) @@ -64,4 +67,8 @@ export class SearchOptionsController extends AbstractViewController implements I }) } } + + toggleNoteTitleOnly = (): void => { + this.noteTitleOnly = !this.noteTitleOnly + } } From 093e76d777841b511c6b420f7026605f9f3485ac Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Mon, 15 Jun 2026 21:22:50 -0300 Subject: [PATCH 39/45] feat: Adds search enhancement feature flag --- .../src/Domain/InternalFeatures/InternalFeature.ts | 1 + packages/web/src/javascripts/Application/DevMode.ts | 1 + .../Components/ContentListView/ContentListView.tsx | 1 + .../src/javascripts/Components/SearchBar/SearchBar.tsx | 10 ++++++++-- .../Components/SearchOptions/SearchOptions.tsx | 7 +++++-- .../src/javascripts/Controllers/FeaturesController.ts | 9 ++++++++- packages/web/src/javascripts/FeatureTrunk.ts | 4 ++++ 7 files changed, 28 insertions(+), 5 deletions(-) diff --git a/packages/services/src/Domain/InternalFeatures/InternalFeature.ts b/packages/services/src/Domain/InternalFeatures/InternalFeature.ts index 1dfa14b22..6b86bd416 100644 --- a/packages/services/src/Domain/InternalFeatures/InternalFeature.ts +++ b/packages/services/src/Domain/InternalFeatures/InternalFeature.ts @@ -1,3 +1,4 @@ export enum InternalFeature { Vaults = 'vaults', + SearchEnhancements = 'search-enhancements', } diff --git a/packages/web/src/javascripts/Application/DevMode.ts b/packages/web/src/javascripts/Application/DevMode.ts index ead3f49c5..1a4ad0466 100644 --- a/packages/web/src/javascripts/Application/DevMode.ts +++ b/packages/web/src/javascripts/Application/DevMode.ts @@ -4,6 +4,7 @@ import { WebApplicationInterface } from '@standardnotes/ui-services' export class DevMode { constructor(private application: WebApplicationInterface) { InternalFeatureService.get().enableFeature(InternalFeature.Vaults) + InternalFeatureService.get().enableFeature(InternalFeature.SearchEnhancements) } /** Valid only when running a mock event publisher on port 3124 */ diff --git a/packages/web/src/javascripts/Components/ContentListView/ContentListView.tsx b/packages/web/src/javascripts/Components/ContentListView/ContentListView.tsx index c66d0e242..e1a118b25 100644 --- a/packages/web/src/javascripts/Components/ContentListView/ContentListView.tsx +++ b/packages/web/src/javascripts/Components/ContentListView/ContentListView.tsx @@ -332,6 +332,7 @@ const ContentListView = forwardRef( itemListController={itemListController} searchOptionsController={searchOptionsController} hideOptions={shouldUseTableView} + showNoteTitleOnlyOption={application.featuresController.isSearchEnhancementsEnabled()} /> )} { +const SearchBar = ({ + itemListController, + searchOptionsController, + hideOptions = false, + showNoteTitleOnlyOption = false, +}: Props) => { const searchBarRef = useRef(null) const searchInputRef = useRef(null) @@ -68,7 +74,7 @@ const SearchBar = ({ itemListController, searchOptionsController, hideOptions = hideOptions ? 'hidden' : !noteFilterText && 'hidden group-focus-within:flex', )} > - +
) diff --git a/packages/web/src/javascripts/Components/SearchOptions/SearchOptions.tsx b/packages/web/src/javascripts/Components/SearchOptions/SearchOptions.tsx index 667267be2..bf4174c05 100644 --- a/packages/web/src/javascripts/Components/SearchOptions/SearchOptions.tsx +++ b/packages/web/src/javascripts/Components/SearchOptions/SearchOptions.tsx @@ -5,9 +5,10 @@ import { SearchOptionsController } from '@/Controllers/SearchOptionsController' type Props = { searchOptions: SearchOptionsController + showNoteTitleOnlyOption?: boolean } -const SearchOptions = ({ searchOptions }: Props) => { +const SearchOptions = ({ searchOptions, showNoteTitleOnlyOption = false }: Props) => { const { includeProtectedContents, includeArchived, includeTrashed, noteTitleOnly } = searchOptions const toggleIncludeProtectedContents = useCallback(async () => { @@ -26,7 +27,9 @@ const SearchOptions = ({ searchOptions }: Props) => { - + {showNoteTitleOnlyOption && ( + + )} ) } diff --git a/packages/web/src/javascripts/Controllers/FeaturesController.ts b/packages/web/src/javascripts/Controllers/FeaturesController.ts index 1ce26bac6..a251d32e2 100644 --- a/packages/web/src/javascripts/Controllers/FeaturesController.ts +++ b/packages/web/src/javascripts/Controllers/FeaturesController.ts @@ -13,7 +13,7 @@ import { import { action, makeObservable, observable, runInAction, when } from 'mobx' import { AbstractViewController } from './Abstract/AbstractViewController' import { CrossControllerEvent } from './CrossControllerEvent' -import { featureTrunkVaultsEnabled } from '@/FeatureTrunk' +import { featureTrunkSearchEnhancementsEnabled, featureTrunkVaultsEnabled } from '@/FeatureTrunk' export class FeaturesController extends AbstractViewController implements InternalEventHandlerInterface { hasFolders: boolean @@ -152,4 +152,11 @@ export class FeaturesController extends AbstractViewController implements Intern return featureTrunkVaultsEnabled() || isEntitledToFeature } + + isSearchEnhancementsEnabled(): boolean { + return ( + featureTrunkSearchEnhancementsEnabled() || + this.features.hasRole(RoleName.create(RoleName.NAMES.InternalTeamUser).getValue()) + ) + } } diff --git a/packages/web/src/javascripts/FeatureTrunk.ts b/packages/web/src/javascripts/FeatureTrunk.ts index b8e6128c1..92875daa7 100644 --- a/packages/web/src/javascripts/FeatureTrunk.ts +++ b/packages/web/src/javascripts/FeatureTrunk.ts @@ -11,3 +11,7 @@ export function featureTrunkEnabled(trunk: FeatureTrunkName): boolean { export function featureTrunkVaultsEnabled(): boolean { return InternalFeatureService.get().isFeatureEnabled(InternalFeature.Vaults) } + +export function featureTrunkSearchEnhancementsEnabled(): boolean { + return InternalFeatureService.get().isFeatureEnabled(InternalFeature.SearchEnhancements) +} From e800bc4710a1563e428f7122c1f3abccb538269d Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Thu, 18 Jun 2026 23:56:45 -0300 Subject: [PATCH 40/45] feat: Improve UX --- .../SearchOptions/SearchOptions.tsx | 59 ++++++++++++++----- .../Controllers/SearchOptionsController.ts | 6 +- 2 files changed, 46 insertions(+), 19 deletions(-) diff --git a/packages/web/src/javascripts/Components/SearchOptions/SearchOptions.tsx b/packages/web/src/javascripts/Components/SearchOptions/SearchOptions.tsx index bf4174c05..b5c7085b4 100644 --- a/packages/web/src/javascripts/Components/SearchOptions/SearchOptions.tsx +++ b/packages/web/src/javascripts/Components/SearchOptions/SearchOptions.tsx @@ -1,7 +1,33 @@ import { observer } from 'mobx-react-lite' import Bubble from '@/Components/Bubble/Bubble' +import Checkbox from '@/Components/Checkbox/Checkbox' import { useCallback } from 'react' import { SearchOptionsController } from '@/Controllers/SearchOptionsController' +import { classNames } from '@standardnotes/snjs' + +type SearchBubblesProps = { + searchOptions: SearchOptionsController +} + +const SearchBubbles = observer(({ searchOptions }: SearchBubblesProps) => { + const { includeProtectedContents, includeArchived, includeTrashed } = searchOptions + + const toggleIncludeProtectedContents = useCallback(async () => { + await searchOptions.toggleIncludeProtectedContents() + }, [searchOptions]) + + return ( + <> + + + + + ) +}) type Props = { searchOptions: SearchOptionsController @@ -9,27 +35,28 @@ type Props = { } const SearchOptions = ({ searchOptions, showNoteTitleOnlyOption = false }: Props) => { - const { includeProtectedContents, includeArchived, includeTrashed, noteTitleOnly } = searchOptions + const { noteTitleOnly } = searchOptions - const toggleIncludeProtectedContents = useCallback(async () => { - await searchOptions.toggleIncludeProtectedContents() - }, [searchOptions]) + if (!showNoteTitleOnlyOption) { + return ( +
e.preventDefault()}> + +
+ ) + } return ( -
e.preventDefault()}> - e.preventDefault()}> + searchOptions.setNoteTitleOnly(event.target.checked)} /> - - - - - {showNoteTitleOnlyOption && ( - - )} +
+ +
) } diff --git a/packages/web/src/javascripts/Controllers/SearchOptionsController.ts b/packages/web/src/javascripts/Controllers/SearchOptionsController.ts index 944def1a9..7be5f9052 100644 --- a/packages/web/src/javascripts/Controllers/SearchOptionsController.ts +++ b/packages/web/src/javascripts/Controllers/SearchOptionsController.ts @@ -30,7 +30,7 @@ export class SearchOptionsController extends AbstractViewController implements I toggleIncludeTrashed: action, toggleIncludeProtectedContents: action, refreshIncludeProtectedContents: action, - toggleNoteTitleOnly: action, + setNoteTitleOnly: action, }) eventBus.addEventHandler(this, ApplicationEvent.UnprotectedSessionBegan) @@ -68,7 +68,7 @@ export class SearchOptionsController extends AbstractViewController implements I } } - toggleNoteTitleOnly = (): void => { - this.noteTitleOnly = !this.noteTitleOnly + setNoteTitleOnly = (value: boolean): void => { + this.noteTitleOnly = value } } From 3f14c9620be41401256254a816dfe87fe2c3e633 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Fri, 19 Jun 2026 00:03:30 -0300 Subject: [PATCH 41/45] fix: Fixes checkboxes cursor --- .../src/javascripts/Components/Checkbox/Checkbox.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/web/src/javascripts/Components/Checkbox/Checkbox.tsx b/packages/web/src/javascripts/Components/Checkbox/Checkbox.tsx index e2ace706a..daa7cbb1d 100644 --- a/packages/web/src/javascripts/Components/Checkbox/Checkbox.tsx +++ b/packages/web/src/javascripts/Components/Checkbox/Checkbox.tsx @@ -1,3 +1,4 @@ +import { classNames } from '@standardnotes/snjs' import { ChangeEventHandler, FunctionComponent } from 'react' type CheckboxProps = { @@ -10,9 +11,15 @@ type CheckboxProps = { const Checkbox: FunctionComponent = ({ name, checked, onChange, disabled, label }) => { return ( -