diff --git a/.github/workflows/ios.testflight.yml b/.github/workflows/ios.testflight.yml index b8fac04b6..fdfc31b4c 100644 --- a/.github/workflows/ios.testflight.yml +++ b/.github/workflows/ios.testflight.yml @@ -15,7 +15,7 @@ jobs: - name: Select Xcode 26 uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: "26.0" + xcode-version: "26" - name: Export version from package.json run: echo "PACKAGE_VERSION=$(grep '"version"' ../web/package.json | cut -d '"' -f 4 | cut -d "-" -f 1)" >> $GITHUB_ENV diff --git a/.github/workflows/mobile.release.prod.yml b/.github/workflows/mobile.release.prod.yml index eb662a7c2..57a683169 100644 --- a/.github/workflows/mobile.release.prod.yml +++ b/.github/workflows/mobile.release.prod.yml @@ -82,7 +82,7 @@ jobs: - name: Select Xcode 26 uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: "26.0" + xcode-version: "26" - name: Export version from package.json run: echo "PACKAGE_VERSION=$(grep '"version"' ../web/package.json | cut -d '"' -f 4 | cut -d "-" -f 1)" >> $GITHUB_ENV diff --git a/.yarn/cache/@lexical-clipboard-npm-0.38.1-d8b1f4e445-d56bd0b5db.zip b/.yarn/cache/@lexical-clipboard-npm-0.38.1-d8b1f4e445-d56bd0b5db.zip deleted file mode 100644 index d7e84e20c..000000000 Binary files a/.yarn/cache/@lexical-clipboard-npm-0.38.1-d8b1f4e445-d56bd0b5db.zip and /dev/null differ diff --git a/.yarn/cache/@lexical-clipboard-npm-0.43.0-ae34976253-9b404ea0e9.zip b/.yarn/cache/@lexical-clipboard-npm-0.43.0-ae34976253-9b404ea0e9.zip new file mode 100644 index 000000000..d27d5364c Binary files /dev/null and b/.yarn/cache/@lexical-clipboard-npm-0.43.0-ae34976253-9b404ea0e9.zip differ diff --git a/.yarn/cache/@lexical-code-core-npm-0.43.0-fa460b0d8f-a63d105158.zip b/.yarn/cache/@lexical-code-core-npm-0.43.0-fa460b0d8f-a63d105158.zip new file mode 100644 index 000000000..85b7c89e1 Binary files /dev/null and b/.yarn/cache/@lexical-code-core-npm-0.43.0-fa460b0d8f-a63d105158.zip differ diff --git a/.yarn/cache/@lexical-code-npm-0.38.1-bcca5107b9-7d2273bb05.zip b/.yarn/cache/@lexical-code-npm-0.38.1-bcca5107b9-7d2273bb05.zip deleted file mode 100644 index 0061b26e2..000000000 Binary files a/.yarn/cache/@lexical-code-npm-0.38.1-bcca5107b9-7d2273bb05.zip and /dev/null differ diff --git a/.yarn/cache/@lexical-code-npm-0.43.0-3412bcee0b-f756ff54db.zip b/.yarn/cache/@lexical-code-npm-0.43.0-3412bcee0b-f756ff54db.zip new file mode 100644 index 000000000..48b39dd3b Binary files /dev/null and b/.yarn/cache/@lexical-code-npm-0.43.0-3412bcee0b-f756ff54db.zip differ diff --git a/.yarn/cache/@lexical-code-prism-npm-0.43.0-60cc0bbea2-c8fcbd2541.zip b/.yarn/cache/@lexical-code-prism-npm-0.43.0-60cc0bbea2-c8fcbd2541.zip new file mode 100644 index 000000000..dcd0cb769 Binary files /dev/null and b/.yarn/cache/@lexical-code-prism-npm-0.43.0-60cc0bbea2-c8fcbd2541.zip differ diff --git a/.yarn/cache/@lexical-devtools-core-npm-0.38.1-5706254649-00ed9e4c9b.zip b/.yarn/cache/@lexical-devtools-core-npm-0.38.1-5706254649-00ed9e4c9b.zip deleted file mode 100644 index 862ac6b01..000000000 Binary files a/.yarn/cache/@lexical-devtools-core-npm-0.38.1-5706254649-00ed9e4c9b.zip and /dev/null differ diff --git a/.yarn/cache/@lexical-devtools-core-npm-0.43.0-f9610c73bb-0728d7bc4e.zip b/.yarn/cache/@lexical-devtools-core-npm-0.43.0-f9610c73bb-0728d7bc4e.zip new file mode 100644 index 000000000..ab0a07f29 Binary files /dev/null and b/.yarn/cache/@lexical-devtools-core-npm-0.43.0-f9610c73bb-0728d7bc4e.zip differ diff --git a/.yarn/cache/@lexical-dragon-npm-0.38.1-483ccfe64f-1771dc711d.zip b/.yarn/cache/@lexical-dragon-npm-0.43.0-a297a8c327-0d1578c21f.zip similarity index 93% rename from .yarn/cache/@lexical-dragon-npm-0.38.1-483ccfe64f-1771dc711d.zip rename to .yarn/cache/@lexical-dragon-npm-0.43.0-a297a8c327-0d1578c21f.zip index 8bc68b6bc..b1316bb56 100644 Binary files a/.yarn/cache/@lexical-dragon-npm-0.38.1-483ccfe64f-1771dc711d.zip and b/.yarn/cache/@lexical-dragon-npm-0.43.0-a297a8c327-0d1578c21f.zip differ diff --git a/.yarn/cache/@lexical-extension-npm-0.38.1-68b14b30a1-36a2d22ee5.zip b/.yarn/cache/@lexical-extension-npm-0.38.1-68b14b30a1-36a2d22ee5.zip deleted file mode 100644 index f93a18bc7..000000000 Binary files a/.yarn/cache/@lexical-extension-npm-0.38.1-68b14b30a1-36a2d22ee5.zip and /dev/null differ diff --git a/.yarn/cache/@lexical-extension-npm-0.43.0-964e94ae06-0097934286.zip b/.yarn/cache/@lexical-extension-npm-0.43.0-964e94ae06-0097934286.zip new file mode 100644 index 000000000..5bb9abeea Binary files /dev/null and b/.yarn/cache/@lexical-extension-npm-0.43.0-964e94ae06-0097934286.zip differ diff --git a/.yarn/cache/@lexical-hashtag-npm-0.38.1-0b8ffe2b43-4976559218.zip b/.yarn/cache/@lexical-hashtag-npm-0.43.0-a065db279d-2762215fd8.zip similarity index 56% rename from .yarn/cache/@lexical-hashtag-npm-0.38.1-0b8ffe2b43-4976559218.zip rename to .yarn/cache/@lexical-hashtag-npm-0.43.0-a065db279d-2762215fd8.zip index 97d8890c3..57cad1813 100644 Binary files a/.yarn/cache/@lexical-hashtag-npm-0.38.1-0b8ffe2b43-4976559218.zip and b/.yarn/cache/@lexical-hashtag-npm-0.43.0-a065db279d-2762215fd8.zip differ diff --git a/.yarn/cache/@lexical-headless-npm-0.38.1-c95092faa6-994a1e3750.zip b/.yarn/cache/@lexical-headless-npm-0.43.0-56c4e37762-87fca41717.zip similarity index 58% rename from .yarn/cache/@lexical-headless-npm-0.38.1-c95092faa6-994a1e3750.zip rename to .yarn/cache/@lexical-headless-npm-0.43.0-56c4e37762-87fca41717.zip index ed595a7a1..620cc0873 100644 Binary files a/.yarn/cache/@lexical-headless-npm-0.38.1-c95092faa6-994a1e3750.zip and b/.yarn/cache/@lexical-headless-npm-0.43.0-56c4e37762-87fca41717.zip differ diff --git a/.yarn/cache/@lexical-history-npm-0.38.1-f57702de2b-12fe92f722.zip b/.yarn/cache/@lexical-history-npm-0.38.1-f57702de2b-12fe92f722.zip deleted file mode 100644 index 5053db0c4..000000000 Binary files a/.yarn/cache/@lexical-history-npm-0.38.1-f57702de2b-12fe92f722.zip and /dev/null differ diff --git a/.yarn/cache/@lexical-history-npm-0.43.0-7ee50eb7ca-13ebbf8ddd.zip b/.yarn/cache/@lexical-history-npm-0.43.0-7ee50eb7ca-13ebbf8ddd.zip new file mode 100644 index 000000000..3339749c7 Binary files /dev/null and b/.yarn/cache/@lexical-history-npm-0.43.0-7ee50eb7ca-13ebbf8ddd.zip differ diff --git a/.yarn/cache/@lexical-html-npm-0.38.1-bcdd5e6b7a-effeae00d2.zip b/.yarn/cache/@lexical-html-npm-0.43.0-48e3433ea7-d62a738fff.zip similarity index 91% rename from .yarn/cache/@lexical-html-npm-0.38.1-bcdd5e6b7a-effeae00d2.zip rename to .yarn/cache/@lexical-html-npm-0.43.0-48e3433ea7-d62a738fff.zip index eeed0a100..d7e6c870d 100644 Binary files a/.yarn/cache/@lexical-html-npm-0.38.1-bcdd5e6b7a-effeae00d2.zip and b/.yarn/cache/@lexical-html-npm-0.43.0-48e3433ea7-d62a738fff.zip differ diff --git a/.yarn/cache/@lexical-link-npm-0.38.1-0dfa5a6ba1-18ffad2fe8.zip b/.yarn/cache/@lexical-link-npm-0.38.1-0dfa5a6ba1-18ffad2fe8.zip deleted file mode 100644 index 2719c4dcc..000000000 Binary files a/.yarn/cache/@lexical-link-npm-0.38.1-0dfa5a6ba1-18ffad2fe8.zip and /dev/null differ diff --git a/.yarn/cache/@lexical-link-npm-0.43.0-23774ea266-ff6db3c073.zip b/.yarn/cache/@lexical-link-npm-0.43.0-23774ea266-ff6db3c073.zip new file mode 100644 index 000000000..61f1fd85f Binary files /dev/null and b/.yarn/cache/@lexical-link-npm-0.43.0-23774ea266-ff6db3c073.zip differ diff --git a/.yarn/cache/@lexical-list-npm-0.38.1-c17b749323-d2b288d3a4.zip b/.yarn/cache/@lexical-list-npm-0.38.1-c17b749323-d2b288d3a4.zip deleted file mode 100644 index f04f5b382..000000000 Binary files a/.yarn/cache/@lexical-list-npm-0.38.1-c17b749323-d2b288d3a4.zip and /dev/null differ diff --git a/.yarn/cache/@lexical-list-npm-0.43.0-be43938741-00193c83c2.zip b/.yarn/cache/@lexical-list-npm-0.43.0-be43938741-00193c83c2.zip new file mode 100644 index 000000000..bc0dc15c7 Binary files /dev/null and b/.yarn/cache/@lexical-list-npm-0.43.0-be43938741-00193c83c2.zip differ diff --git a/.yarn/cache/@lexical-mark-npm-0.38.1-be98b22665-3ec0f37e9a.zip b/.yarn/cache/@lexical-mark-npm-0.38.1-be98b22665-3ec0f37e9a.zip deleted file mode 100644 index d815b507a..000000000 Binary files a/.yarn/cache/@lexical-mark-npm-0.38.1-be98b22665-3ec0f37e9a.zip and /dev/null differ diff --git a/.yarn/cache/@lexical-mark-npm-0.43.0-f0235bef36-d3beaa250f.zip b/.yarn/cache/@lexical-mark-npm-0.43.0-f0235bef36-d3beaa250f.zip new file mode 100644 index 000000000..d3f3e9c6d Binary files /dev/null and b/.yarn/cache/@lexical-mark-npm-0.43.0-f0235bef36-d3beaa250f.zip differ diff --git a/.yarn/cache/@lexical-markdown-npm-0.38.1-56dd7e2c50-9aab4eb6e9.zip b/.yarn/cache/@lexical-markdown-npm-0.38.1-56dd7e2c50-9aab4eb6e9.zip deleted file mode 100644 index 2d3908392..000000000 Binary files a/.yarn/cache/@lexical-markdown-npm-0.38.1-56dd7e2c50-9aab4eb6e9.zip and /dev/null differ diff --git a/.yarn/cache/@lexical-markdown-npm-0.43.0-426a72db47-2dbcbf2989.zip b/.yarn/cache/@lexical-markdown-npm-0.43.0-426a72db47-2dbcbf2989.zip new file mode 100644 index 000000000..d7b7f6580 Binary files /dev/null and b/.yarn/cache/@lexical-markdown-npm-0.43.0-426a72db47-2dbcbf2989.zip differ diff --git a/.yarn/cache/@lexical-offset-npm-0.38.1-e5622a3bc4-bf397bc3db.zip b/.yarn/cache/@lexical-offset-npm-0.43.0-c5b9f6c804-b4a11fd783.zip similarity index 92% rename from .yarn/cache/@lexical-offset-npm-0.38.1-e5622a3bc4-bf397bc3db.zip rename to .yarn/cache/@lexical-offset-npm-0.43.0-c5b9f6c804-b4a11fd783.zip index b507ee231..f774fbee5 100644 Binary files a/.yarn/cache/@lexical-offset-npm-0.38.1-e5622a3bc4-bf397bc3db.zip and b/.yarn/cache/@lexical-offset-npm-0.43.0-c5b9f6c804-b4a11fd783.zip differ diff --git a/.yarn/cache/@lexical-overflow-npm-0.38.1-e0de8c7a60-3812795f83.zip b/.yarn/cache/@lexical-overflow-npm-0.38.1-e0de8c7a60-3812795f83.zip deleted file mode 100644 index 73fbdcc07..000000000 Binary files a/.yarn/cache/@lexical-overflow-npm-0.38.1-e0de8c7a60-3812795f83.zip and /dev/null differ diff --git a/.yarn/cache/@lexical-overflow-npm-0.43.0-5b4b967c61-4ac442c4af.zip b/.yarn/cache/@lexical-overflow-npm-0.43.0-5b4b967c61-4ac442c4af.zip new file mode 100644 index 000000000..7518e6613 Binary files /dev/null and b/.yarn/cache/@lexical-overflow-npm-0.43.0-5b4b967c61-4ac442c4af.zip differ diff --git a/.yarn/cache/@lexical-plain-text-npm-0.38.1-5dd2b5b300-4e2d79ced9.zip b/.yarn/cache/@lexical-plain-text-npm-0.38.1-5dd2b5b300-4e2d79ced9.zip deleted file mode 100644 index b8d3b6323..000000000 Binary files a/.yarn/cache/@lexical-plain-text-npm-0.38.1-5dd2b5b300-4e2d79ced9.zip and /dev/null differ diff --git a/.yarn/cache/@lexical-plain-text-npm-0.43.0-570131dbe1-855460a77a.zip b/.yarn/cache/@lexical-plain-text-npm-0.43.0-570131dbe1-855460a77a.zip new file mode 100644 index 000000000..5c69bb10a Binary files /dev/null and b/.yarn/cache/@lexical-plain-text-npm-0.43.0-570131dbe1-855460a77a.zip differ diff --git a/.yarn/cache/@lexical-react-npm-0.38.1-7926fed74c-a9488dc3f2.zip b/.yarn/cache/@lexical-react-npm-0.38.1-7926fed74c-a9488dc3f2.zip deleted file mode 100644 index e6beb1ec0..000000000 Binary files a/.yarn/cache/@lexical-react-npm-0.38.1-7926fed74c-a9488dc3f2.zip and /dev/null differ diff --git a/.yarn/cache/@lexical-react-npm-0.43.0-560698beb3-ea9fd24fbc.zip b/.yarn/cache/@lexical-react-npm-0.43.0-560698beb3-ea9fd24fbc.zip new file mode 100644 index 000000000..58cce633a Binary files /dev/null and b/.yarn/cache/@lexical-react-npm-0.43.0-560698beb3-ea9fd24fbc.zip differ diff --git a/.yarn/cache/@lexical-rich-text-npm-0.38.1-7afcc3c269-b8d2218d5a.zip b/.yarn/cache/@lexical-rich-text-npm-0.38.1-7afcc3c269-b8d2218d5a.zip deleted file mode 100644 index 9445ccf6c..000000000 Binary files a/.yarn/cache/@lexical-rich-text-npm-0.38.1-7afcc3c269-b8d2218d5a.zip and /dev/null differ diff --git a/.yarn/cache/@lexical-rich-text-npm-0.43.0-b9c388a84c-89b5c3ef33.zip b/.yarn/cache/@lexical-rich-text-npm-0.43.0-b9c388a84c-89b5c3ef33.zip new file mode 100644 index 000000000..486b47190 Binary files /dev/null and b/.yarn/cache/@lexical-rich-text-npm-0.43.0-b9c388a84c-89b5c3ef33.zip differ diff --git a/.yarn/cache/@lexical-selection-npm-0.38.1-1ebdbc83db-7e585e9149.zip b/.yarn/cache/@lexical-selection-npm-0.38.1-1ebdbc83db-7e585e9149.zip deleted file mode 100644 index 2c210b212..000000000 Binary files a/.yarn/cache/@lexical-selection-npm-0.38.1-1ebdbc83db-7e585e9149.zip and /dev/null differ diff --git a/.yarn/cache/@lexical-selection-npm-0.43.0-980812d4c4-20a0896ffe.zip b/.yarn/cache/@lexical-selection-npm-0.43.0-980812d4c4-20a0896ffe.zip new file mode 100644 index 000000000..0c852042e Binary files /dev/null and b/.yarn/cache/@lexical-selection-npm-0.43.0-980812d4c4-20a0896ffe.zip differ diff --git a/.yarn/cache/@lexical-table-npm-0.38.1-6dd61da93e-9ea4a6bee5.zip b/.yarn/cache/@lexical-table-npm-0.38.1-6dd61da93e-9ea4a6bee5.zip deleted file mode 100644 index 2c9e4e22a..000000000 Binary files a/.yarn/cache/@lexical-table-npm-0.38.1-6dd61da93e-9ea4a6bee5.zip and /dev/null differ diff --git a/.yarn/cache/@lexical-table-npm-0.43.0-7dfaa8c3a5-0b5a658978.zip b/.yarn/cache/@lexical-table-npm-0.43.0-7dfaa8c3a5-0b5a658978.zip new file mode 100644 index 000000000..93f34c545 Binary files /dev/null and b/.yarn/cache/@lexical-table-npm-0.43.0-7dfaa8c3a5-0b5a658978.zip differ diff --git a/.yarn/cache/@lexical-text-npm-0.38.1-dfcd229c93-abb9ff9045.zip b/.yarn/cache/@lexical-text-npm-0.38.1-dfcd229c93-abb9ff9045.zip deleted file mode 100644 index d58a88475..000000000 Binary files a/.yarn/cache/@lexical-text-npm-0.38.1-dfcd229c93-abb9ff9045.zip and /dev/null differ diff --git a/.yarn/cache/@lexical-text-npm-0.43.0-129e0e395c-d767454053.zip b/.yarn/cache/@lexical-text-npm-0.43.0-129e0e395c-d767454053.zip new file mode 100644 index 000000000..194b1514a Binary files /dev/null and b/.yarn/cache/@lexical-text-npm-0.43.0-129e0e395c-d767454053.zip differ diff --git a/.yarn/cache/@lexical-utils-npm-0.38.1-71c1450f0f-121c1f2d4a.zip b/.yarn/cache/@lexical-utils-npm-0.38.1-71c1450f0f-121c1f2d4a.zip deleted file mode 100644 index e022fa91c..000000000 Binary files a/.yarn/cache/@lexical-utils-npm-0.38.1-71c1450f0f-121c1f2d4a.zip and /dev/null differ diff --git a/.yarn/cache/@lexical-utils-npm-0.43.0-65a3f77e53-d16c043582.zip b/.yarn/cache/@lexical-utils-npm-0.43.0-65a3f77e53-d16c043582.zip new file mode 100644 index 000000000..d88dadd69 Binary files /dev/null and b/.yarn/cache/@lexical-utils-npm-0.43.0-65a3f77e53-d16c043582.zip differ diff --git a/.yarn/cache/@lexical-yjs-npm-0.38.1-0040cc11a9-fb9ab146ba.zip b/.yarn/cache/@lexical-yjs-npm-0.38.1-0040cc11a9-fb9ab146ba.zip deleted file mode 100644 index 33a278f7a..000000000 Binary files a/.yarn/cache/@lexical-yjs-npm-0.38.1-0040cc11a9-fb9ab146ba.zip and /dev/null differ diff --git a/.yarn/cache/@lexical-yjs-npm-0.43.0-d94172a7ca-b28f15eed2.zip b/.yarn/cache/@lexical-yjs-npm-0.43.0-d94172a7ca-b28f15eed2.zip new file mode 100644 index 000000000..0087c15fc Binary files /dev/null and b/.yarn/cache/@lexical-yjs-npm-0.43.0-d94172a7ca-b28f15eed2.zip differ diff --git a/.yarn/cache/@noble-ciphers-npm-1.3.0-73a7db337f-19722c3547.zip b/.yarn/cache/@noble-ciphers-npm-1.3.0-73a7db337f-19722c3547.zip new file mode 100644 index 000000000..f788f1d69 Binary files /dev/null and b/.yarn/cache/@noble-ciphers-npm-1.3.0-73a7db337f-19722c3547.zip differ diff --git a/.yarn/cache/@noble-hashes-npm-1.8.0-a397449e64-c94e98b941.zip b/.yarn/cache/@noble-hashes-npm-1.8.0-a397449e64-c94e98b941.zip new file mode 100644 index 000000000..43023c476 Binary files /dev/null and b/.yarn/cache/@noble-hashes-npm-1.8.0-a397449e64-c94e98b941.zip differ diff --git a/.yarn/cache/@preact-signals-core-npm-1.12.1-a218932129-55719713dc.zip b/.yarn/cache/@preact-signals-core-npm-1.12.1-a218932129-55719713dc.zip deleted file mode 100644 index 9e9a8dd22..000000000 Binary files a/.yarn/cache/@preact-signals-core-npm-1.12.1-a218932129-55719713dc.zip and /dev/null differ diff --git a/.yarn/cache/@preact-signals-core-npm-1.14.1-5a87932a62-796c718f4d.zip b/.yarn/cache/@preact-signals-core-npm-1.14.1-5a87932a62-796c718f4d.zip new file mode 100644 index 000000000..cf5eaee65 Binary files /dev/null and b/.yarn/cache/@preact-signals-core-npm-1.14.1-5a87932a62-796c718f4d.zip differ diff --git a/.yarn/cache/@react-pdf-fns-npm-3.1.2-5b68a05f5c-4e8cdd32f2.zip b/.yarn/cache/@react-pdf-fns-npm-3.1.2-5b68a05f5c-4e8cdd32f2.zip deleted file mode 100644 index 755e83d5d..000000000 Binary files a/.yarn/cache/@react-pdf-fns-npm-3.1.2-5b68a05f5c-4e8cdd32f2.zip and /dev/null differ diff --git a/.yarn/cache/@react-pdf-fns-npm-3.1.3-c3350c5dc0-0cd27ef6cd.zip b/.yarn/cache/@react-pdf-fns-npm-3.1.3-c3350c5dc0-0cd27ef6cd.zip new file mode 100644 index 000000000..8bc638880 Binary files /dev/null and b/.yarn/cache/@react-pdf-fns-npm-3.1.3-c3350c5dc0-0cd27ef6cd.zip differ diff --git a/.yarn/cache/@react-pdf-font-npm-4.0.3-a46a328086-933e451381.zip b/.yarn/cache/@react-pdf-font-npm-4.0.3-a46a328086-933e451381.zip deleted file mode 100644 index ffd8bc2bc..000000000 Binary files a/.yarn/cache/@react-pdf-font-npm-4.0.3-a46a328086-933e451381.zip and /dev/null differ diff --git a/.yarn/cache/@react-pdf-font-npm-4.0.8-9024a8179e-8343c363ea.zip b/.yarn/cache/@react-pdf-font-npm-4.0.8-9024a8179e-8343c363ea.zip new file mode 100644 index 000000000..752bf76b6 Binary files /dev/null and b/.yarn/cache/@react-pdf-font-npm-4.0.8-9024a8179e-8343c363ea.zip differ diff --git a/.yarn/cache/@react-pdf-image-npm-3.0.3-9bc75136a5-893ebef74d.zip b/.yarn/cache/@react-pdf-image-npm-3.0.3-9bc75136a5-893ebef74d.zip deleted file mode 100644 index 830b611aa..000000000 Binary files a/.yarn/cache/@react-pdf-image-npm-3.0.3-9bc75136a5-893ebef74d.zip and /dev/null differ diff --git a/.yarn/cache/@react-pdf-image-npm-3.1.0-0bd9bba15e-7054b84ada.zip b/.yarn/cache/@react-pdf-image-npm-3.1.0-0bd9bba15e-7054b84ada.zip new file mode 100644 index 000000000..be406e262 Binary files /dev/null and b/.yarn/cache/@react-pdf-image-npm-3.1.0-0bd9bba15e-7054b84ada.zip differ diff --git a/.yarn/cache/@react-pdf-layout-npm-4.4.1-8639686b1e-5d7baa7f92.zip b/.yarn/cache/@react-pdf-layout-npm-4.4.1-8639686b1e-5d7baa7f92.zip deleted file mode 100644 index 6626ca0dd..000000000 Binary files a/.yarn/cache/@react-pdf-layout-npm-4.4.1-8639686b1e-5d7baa7f92.zip and /dev/null differ diff --git a/.yarn/cache/@react-pdf-layout-npm-4.6.1-b12fdb71f4-83ed94e0d0.zip b/.yarn/cache/@react-pdf-layout-npm-4.6.1-b12fdb71f4-83ed94e0d0.zip new file mode 100644 index 000000000..2c8d3e239 Binary files /dev/null and b/.yarn/cache/@react-pdf-layout-npm-4.6.1-b12fdb71f4-83ed94e0d0.zip differ diff --git a/.yarn/cache/@react-pdf-pdfkit-npm-4.0.4-c8d40f5e0b-706cf5bc03.zip b/.yarn/cache/@react-pdf-pdfkit-npm-4.0.4-c8d40f5e0b-706cf5bc03.zip deleted file mode 100644 index 17615810d..000000000 Binary files a/.yarn/cache/@react-pdf-pdfkit-npm-4.0.4-c8d40f5e0b-706cf5bc03.zip and /dev/null differ diff --git a/.yarn/cache/@react-pdf-pdfkit-npm-5.1.1-b05712b991-157af7e765.zip b/.yarn/cache/@react-pdf-pdfkit-npm-5.1.1-b05712b991-157af7e765.zip new file mode 100644 index 000000000..9cd81eeea Binary files /dev/null and b/.yarn/cache/@react-pdf-pdfkit-npm-5.1.1-b05712b991-157af7e765.zip differ diff --git a/.yarn/cache/@react-pdf-png-js-npm-3.0.0-17565fff9e-90c1612d75.zip b/.yarn/cache/@react-pdf-png-js-npm-3.0.0-17565fff9e-90c1612d75.zip deleted file mode 100644 index ffcd4083a..000000000 Binary files a/.yarn/cache/@react-pdf-png-js-npm-3.0.0-17565fff9e-90c1612d75.zip and /dev/null differ diff --git a/.yarn/cache/@react-pdf-primitives-npm-4.1.1-80eed5afd4-adadff1996.zip b/.yarn/cache/@react-pdf-primitives-npm-4.1.1-80eed5afd4-adadff1996.zip deleted file mode 100644 index ee5f1e0a8..000000000 Binary files a/.yarn/cache/@react-pdf-primitives-npm-4.1.1-80eed5afd4-adadff1996.zip and /dev/null differ diff --git a/.yarn/cache/@react-pdf-primitives-npm-4.3.0-75975d0ee7-391f48e09a.zip b/.yarn/cache/@react-pdf-primitives-npm-4.3.0-75975d0ee7-391f48e09a.zip new file mode 100644 index 000000000..f9b0923a8 Binary files /dev/null and b/.yarn/cache/@react-pdf-primitives-npm-4.3.0-75975d0ee7-391f48e09a.zip differ diff --git a/.yarn/cache/@react-pdf-reconciler-npm-1.1.4-0a87628369-d920898a1c.zip b/.yarn/cache/@react-pdf-reconciler-npm-1.1.4-0a87628369-d920898a1c.zip deleted file mode 100644 index cc19ec267..000000000 Binary files a/.yarn/cache/@react-pdf-reconciler-npm-1.1.4-0a87628369-d920898a1c.zip and /dev/null differ diff --git a/.yarn/cache/@react-pdf-reconciler-npm-2.0.0-00957b9834-249df947f0.zip b/.yarn/cache/@react-pdf-reconciler-npm-2.0.0-00957b9834-249df947f0.zip new file mode 100644 index 000000000..19a79bdb5 Binary files /dev/null and b/.yarn/cache/@react-pdf-reconciler-npm-2.0.0-00957b9834-249df947f0.zip differ diff --git a/.yarn/cache/@react-pdf-render-npm-4.3.1-27f48b54c0-2dfea722c1.zip b/.yarn/cache/@react-pdf-render-npm-4.3.1-27f48b54c0-2dfea722c1.zip deleted file mode 100644 index bb655d452..000000000 Binary files a/.yarn/cache/@react-pdf-render-npm-4.3.1-27f48b54c0-2dfea722c1.zip and /dev/null differ diff --git a/.yarn/cache/@react-pdf-render-npm-4.5.1-d592ded218-2ece333774.zip b/.yarn/cache/@react-pdf-render-npm-4.5.1-d592ded218-2ece333774.zip new file mode 100644 index 000000000..a9d2dd6ee Binary files /dev/null and b/.yarn/cache/@react-pdf-render-npm-4.5.1-d592ded218-2ece333774.zip differ diff --git a/.yarn/cache/@react-pdf-renderer-npm-4.3.1-2c58b5a4d3-0e9e2d9b30.zip b/.yarn/cache/@react-pdf-renderer-npm-4.3.1-2c58b5a4d3-0e9e2d9b30.zip deleted file mode 100644 index 77b8cebeb..000000000 Binary files a/.yarn/cache/@react-pdf-renderer-npm-4.3.1-2c58b5a4d3-0e9e2d9b30.zip and /dev/null differ diff --git a/.yarn/cache/@react-pdf-renderer-npm-4.5.1-ab299db753-55046bcba4.zip b/.yarn/cache/@react-pdf-renderer-npm-4.5.1-ab299db753-55046bcba4.zip new file mode 100644 index 000000000..9b43108d1 Binary files /dev/null and b/.yarn/cache/@react-pdf-renderer-npm-4.5.1-ab299db753-55046bcba4.zip differ diff --git a/.yarn/cache/@react-pdf-stylesheet-npm-6.1.1-de82babf5a-129d5e01ec.zip b/.yarn/cache/@react-pdf-stylesheet-npm-6.1.1-de82babf5a-129d5e01ec.zip deleted file mode 100644 index 2dfd21969..000000000 Binary files a/.yarn/cache/@react-pdf-stylesheet-npm-6.1.1-de82babf5a-129d5e01ec.zip and /dev/null differ diff --git a/.yarn/cache/@react-pdf-stylesheet-npm-6.2.1-a76dfb3da3-b5fdedf290.zip b/.yarn/cache/@react-pdf-stylesheet-npm-6.2.1-a76dfb3da3-b5fdedf290.zip new file mode 100644 index 000000000..1d88d544a Binary files /dev/null and b/.yarn/cache/@react-pdf-stylesheet-npm-6.2.1-a76dfb3da3-b5fdedf290.zip differ diff --git a/.yarn/cache/@react-pdf-svg-npm-1.1.0-5a97c07518-23c3052ee0.zip b/.yarn/cache/@react-pdf-svg-npm-1.1.0-5a97c07518-23c3052ee0.zip new file mode 100644 index 000000000..9d28ade8e Binary files /dev/null and b/.yarn/cache/@react-pdf-svg-npm-1.1.0-5a97c07518-23c3052ee0.zip differ diff --git a/.yarn/cache/@react-pdf-textkit-npm-6.0.0-ad8481d0d8-5bc087ad2d.zip b/.yarn/cache/@react-pdf-textkit-npm-6.0.0-ad8481d0d8-5bc087ad2d.zip deleted file mode 100644 index e72524cf5..000000000 Binary files a/.yarn/cache/@react-pdf-textkit-npm-6.0.0-ad8481d0d8-5bc087ad2d.zip and /dev/null differ diff --git a/.yarn/cache/@react-pdf-textkit-npm-6.3.0-1e829976be-d95f16dce9.zip b/.yarn/cache/@react-pdf-textkit-npm-6.3.0-1e829976be-d95f16dce9.zip new file mode 100644 index 000000000..3df5dcb44 Binary files /dev/null and b/.yarn/cache/@react-pdf-textkit-npm-6.3.0-1e829976be-d95f16dce9.zip differ diff --git a/.yarn/cache/@react-pdf-types-npm-2.11.1-8af15ddcd5-54ede09415.zip b/.yarn/cache/@react-pdf-types-npm-2.11.1-8af15ddcd5-54ede09415.zip new file mode 100644 index 000000000..6ae1ecb44 Binary files /dev/null and b/.yarn/cache/@react-pdf-types-npm-2.11.1-8af15ddcd5-54ede09415.zip differ diff --git a/.yarn/cache/@react-pdf-types-npm-2.9.1-67f97a020a-99b8dbc64e.zip b/.yarn/cache/@react-pdf-types-npm-2.9.1-67f97a020a-99b8dbc64e.zip deleted file mode 100644 index 052fe677e..000000000 Binary files a/.yarn/cache/@react-pdf-types-npm-2.9.1-67f97a020a-99b8dbc64e.zip and /dev/null differ diff --git a/.yarn/cache/@types-node-npm-20.19.23-9e5fa45fa9-1f213fa438.zip b/.yarn/cache/@types-node-npm-20.19.23-9e5fa45fa9-1f213fa438.zip deleted file mode 100644 index 6cc73f03c..000000000 Binary files a/.yarn/cache/@types-node-npm-20.19.23-9e5fa45fa9-1f213fa438.zip and /dev/null differ diff --git a/.yarn/cache/@types-node-npm-25.6.0-429799b5e8-98945eb599.zip b/.yarn/cache/@types-node-npm-25.6.0-429799b5e8-98945eb599.zip new file mode 100644 index 000000000..7a68c56c3 Binary files /dev/null and b/.yarn/cache/@types-node-npm-25.6.0-429799b5e8-98945eb599.zip differ diff --git a/.yarn/cache/@types-ws-npm-8.18.1-61dc106ff0-0331b14cde.zip b/.yarn/cache/@types-ws-npm-8.18.1-61dc106ff0-0331b14cde.zip new file mode 100644 index 000000000..bc43fa751 Binary files /dev/null and b/.yarn/cache/@types-ws-npm-8.18.1-61dc106ff0-0331b14cde.zip differ diff --git a/.yarn/cache/color-name-npm-2.1.0-0d5dd61305-eb014f71d8.zip b/.yarn/cache/color-name-npm-2.1.0-0d5dd61305-eb014f71d8.zip new file mode 100644 index 000000000..4cce5c728 Binary files /dev/null and b/.yarn/cache/color-name-npm-2.1.0-0d5dd61305-eb014f71d8.zip differ diff --git a/.yarn/cache/color-string-npm-2.1.4-e5adac6a35-f9caa29d52.zip b/.yarn/cache/color-string-npm-2.1.4-e5adac6a35-f9caa29d52.zip new file mode 100644 index 000000000..f46ba983d Binary files /dev/null and b/.yarn/cache/color-string-npm-2.1.4-e5adac6a35-f9caa29d52.zip differ diff --git a/.yarn/cache/crypto-js-npm-4.2.0-d6f7744e63-f051666dbc.zip b/.yarn/cache/crypto-js-npm-4.2.0-d6f7744e63-f051666dbc.zip deleted file mode 100644 index 87d2ed498..000000000 Binary files a/.yarn/cache/crypto-js-npm-4.2.0-d6f7744e63-f051666dbc.zip and /dev/null differ diff --git a/.yarn/cache/entities-npm-7.0.1-61f8ba3430-5ee49e52e6.zip b/.yarn/cache/entities-npm-7.0.1-61f8ba3430-5ee49e52e6.zip new file mode 100644 index 000000000..ae7941661 Binary files /dev/null and b/.yarn/cache/entities-npm-7.0.1-61f8ba3430-5ee49e52e6.zip differ diff --git a/.yarn/cache/fflate-npm-0.8.3-35acaff861-a2a417e8fb.zip b/.yarn/cache/fflate-npm-0.8.3-35acaff861-a2a417e8fb.zip new file mode 100644 index 000000000..1bc0a8e5d Binary files /dev/null and b/.yarn/cache/fflate-npm-0.8.3-35acaff861-a2a417e8fb.zip differ diff --git a/.yarn/cache/happy-dom-npm-20.0.8-9d375c4358-4d1131542c.zip b/.yarn/cache/happy-dom-npm-20.0.8-9d375c4358-4d1131542c.zip deleted file mode 100644 index 25f3b0193..000000000 Binary files a/.yarn/cache/happy-dom-npm-20.0.8-9d375c4358-4d1131542c.zip and /dev/null differ diff --git a/.yarn/cache/happy-dom-npm-20.9.0-480a4fb09e-6cc02c95c2.zip b/.yarn/cache/happy-dom-npm-20.9.0-480a4fb09e-6cc02c95c2.zip new file mode 100644 index 000000000..adc2f9ba6 Binary files /dev/null and b/.yarn/cache/happy-dom-npm-20.9.0-480a4fb09e-6cc02c95c2.zip differ diff --git a/.yarn/cache/js-md5-npm-0.8.3-c353e27117-b8578232d8.zip b/.yarn/cache/js-md5-npm-0.8.3-c353e27117-b8578232d8.zip new file mode 100644 index 000000000..d4542e3b0 Binary files /dev/null and b/.yarn/cache/js-md5-npm-0.8.3-c353e27117-b8578232d8.zip differ diff --git a/.yarn/cache/lexical-npm-0.38.1-adf999e94e-af080ba244.zip b/.yarn/cache/lexical-npm-0.38.1-adf999e94e-af080ba244.zip deleted file mode 100644 index a0e8b3cf2..000000000 Binary files a/.yarn/cache/lexical-npm-0.38.1-adf999e94e-af080ba244.zip and /dev/null differ diff --git a/.yarn/cache/lexical-npm-0.43.0-dec146ba14-0cb9b22669.zip b/.yarn/cache/lexical-npm-0.43.0-dec146ba14-0cb9b22669.zip new file mode 100644 index 000000000..b620000aa Binary files /dev/null and b/.yarn/cache/lexical-npm-0.43.0-dec146ba14-0cb9b22669.zip differ diff --git a/.yarn/cache/png-js-npm-2.0.0-2a60ae96e2-a880131c0c.zip b/.yarn/cache/png-js-npm-2.0.0-2a60ae96e2-a880131c0c.zip new file mode 100644 index 000000000..c8a611713 Binary files /dev/null and b/.yarn/cache/png-js-npm-2.0.0-2a60ae96e2-a880131c0c.zip differ diff --git a/.yarn/cache/undici-types-npm-7.19.2-93c792b6dd-f721026160.zip b/.yarn/cache/undici-types-npm-7.19.2-93c792b6dd-f721026160.zip new file mode 100644 index 000000000..2c81742da Binary files /dev/null and b/.yarn/cache/undici-types-npm-7.19.2-93c792b6dd-f721026160.zip differ diff --git a/.yarn/cache/ws-npm-8.20.0-b8684d1fe3-2b31d24a53.zip b/.yarn/cache/ws-npm-8.20.0-b8684d1fe3-2b31d24a53.zip new file mode 100644 index 000000000..910664830 Binary files /dev/null and b/.yarn/cache/ws-npm-8.20.0-b8684d1fe3-2b31d24a53.zip differ diff --git a/packages/api/CHANGELOG.md b/packages/api/CHANGELOG.md index 95a48500e..74714671a 100644 --- a/packages/api/CHANGELOG.md +++ b/packages/api/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.26.97](https://github.com/standardnotes/app/compare/@standardnotes/api@1.26.96...@standardnotes/api@1.26.97) (2026-04-24) + +**Note:** Version bump only for package @standardnotes/api + ## [1.26.96](https://github.com/standardnotes/app/compare/@standardnotes/api@1.26.95...@standardnotes/api@1.26.96) (2025-12-18) ### Bug Fixes diff --git a/packages/api/package.json b/packages/api/package.json index 1a2f186cb..05fe68df7 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@standardnotes/api", - "version": "1.26.96", + "version": "1.26.97", "engines": { "node": ">=16.0.0 <17.0.0" }, diff --git a/packages/clipper/CHANGELOG.md b/packages/clipper/CHANGELOG.md index 53a2bea76..da1d3b851 100644 --- a/packages/clipper/CHANGELOG.md +++ b/packages/clipper/CHANGELOG.md @@ -3,6 +3,26 @@ 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 + +## [1.1.586](https://github.com/standardnotes/app/compare/@standardnotes/clipper@1.1.585...@standardnotes/clipper@1.1.586) (2026-04-29) + +**Note:** Version bump only for package @standardnotes/clipper + +## [1.1.585](https://github.com/standardnotes/app/compare/@standardnotes/clipper@1.1.584...@standardnotes/clipper@1.1.585) (2026-04-24) + +**Note:** Version bump only for package @standardnotes/clipper + +## [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..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.583", + "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 2e7e849ee..15f180f46 100644 --- a/packages/desktop/CHANGELOG.md +++ b/packages/desktop/CHANGELOG.md @@ -3,6 +3,26 @@ 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 + +## [3.110.191](https://github.com/standardnotes/app/compare/@standardnotes/desktop@3.201.26...@standardnotes/desktop@3.110.191) (2026-04-29) + +**Note:** Version bump only for package @standardnotes/desktop + +## [3.110.190](https://github.com/standardnotes/app/compare/@standardnotes/desktop@3.201.25...@standardnotes/desktop@3.110.190) (2026-04-24) + +**Note:** Version bump only for package @standardnotes/desktop + +## [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/app/javascripts/Main/Packages/PackageManager.ts b/packages/desktop/app/javascripts/Main/Packages/PackageManager.ts index d38b7aefe..9908dc93c 100644 --- a/packages/desktop/app/javascripts/Main/Packages/PackageManager.ts +++ b/packages/desktop/app/javascripts/Main/Packages/PackageManager.ts @@ -386,8 +386,31 @@ async function installComponent( } } +function validatePackageIdentifier(identifier: string) { + if (!identifier) { + throw new Error('Package identifier must not be empty') + } + + if (identifier.includes('/') || identifier.includes('\\') || identifier === '.' || identifier === '..') { + throw new Error(`Invalid package identifier: ${identifier}`) + } +} + +function assertPathWithinExtensions(absolutePath: string) { + const extensionsRoot = path.resolve(Paths.userDataDir, Paths.extensionsDirRelative) + const resolvedPath = path.resolve(absolutePath) + const relativeToExtensions = path.relative(extensionsRoot, resolvedPath) + + if (relativeToExtensions.startsWith('..') || path.isAbsolute(relativeToExtensions)) { + throw new Error(`Path escapes extensions directory: ${absolutePath}`) + } +} + function pathsForComponent(component: Pick) { - const relativePath = path.join(Paths.extensionsDirRelative, component.content!.package_info.identifier) + const identifier = component.content!.package_info.identifier + validatePackageIdentifier(identifier) + + const relativePath = path.join(Paths.extensionsDirRelative, identifier) const absolutePath = path.join(Paths.userDataDir, relativePath) const downloadPath = path.join(Paths.tempDir, AppName, 'downloads', component.content!.name + '.zip') @@ -404,7 +427,9 @@ async function uninstallComponent(mapping: MappingFileHandler, uuid: string) { /** No mapping for component */ return } - const result = await new FilesManager().deleteDir(path.join(Paths.userDataDir, componentMapping.location)) + const absolutePath = path.join(Paths.userDataDir, componentMapping.location) + assertPathWithinExtensions(absolutePath) + const result = await new FilesManager().deleteDir(absolutePath) if (!result.isFailed()) { mapping.remove(uuid) } diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 9e8234db7..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.188", + "version": "3.110.193", "license": "AGPL-3.0", "author": "Standard Notes.", "private": true, diff --git a/packages/desktop/test/packageManager.spec.ts b/packages/desktop/test/packageManager.spec.ts index 3103129a4..d0cc37460 100644 --- a/packages/desktop/test/packageManager.spec.ts +++ b/packages/desktop/test/packageManager.spec.ts @@ -143,6 +143,71 @@ test("doesn't download anything when two install/uninstall tasks are queued", as t.is(downloadFileCallCount, 1) }) +test('does not uninstall paths outside extensions from poisoned mapping', async (t) => { + await packageManager.syncComponents([fakeComponent()]) + await new Promise((resolve) => setTimeout(resolve, 200)) + + const escapeDir = path.join(tmpDir.path, 'escape') + const markerPath = path.join(escapeDir, 'marker.txt') + await ensureDirectoryExists(escapeDir) + await fs.writeFile(markerPath, 'keep') + + const poisonedLocations = [path.join('Extensions', '..', 'escape'), path.join('..', 'escape')] + + for (const location of poisonedLocations) { + await fs.writeFile( + path.join(contentDir, 'mapping.json'), + JSON.stringify({ + [uuid]: { location, version }, + }), + ) + + await packageManager.syncComponents([fakeComponent({ deleted: true })]) + await new Promise((resolve) => setTimeout(resolve, 200)) + + t.true(await fs.stat(markerPath).then(() => true), `marker preserved for location ${location}`) + t.deepEqual(await readJSONFile(path.join(contentDir, 'mapping.json')), { + [uuid]: { location, version }, + }) + } + + t.true(await fs.stat(path.join(contentDir, identifier)).then(() => true)) +}) + +test('rejects path traversal in package identifier', async (t) => { + const traversalIdentifiers = ['../escape', 'foo/../../escape', '..', '.', 'foo/bar'] + const extensionsParent = path.dirname(contentDir) + + for (const badIdentifier of traversalIdentifiers) { + const before = await fs.readdir(contentDir) + const parentBefore = await fs.readdir(extensionsParent) + + downloadFileCallCount = 0 + await packageManager.syncComponents([ + { + ...fakeComponent({ modifier: badIdentifier }), + content: { + ...fakeComponent().content, + name: `Bad ${badIdentifier}`, + package_info: { + ...fakeComponent().content.package_info, + identifier: badIdentifier, + }, + }, + }, + ]) + await new Promise((resolve) => setTimeout(resolve, 200)) + + t.is(downloadFileCallCount, 0, `should not download for identifier ${badIdentifier}`) + t.deepEqual(await fs.readdir(contentDir), before, `extensions dir unchanged for ${badIdentifier}`) + t.deepEqual( + await fs.readdir(extensionsParent), + parentBefore, + `no directory escape for ${badIdentifier}`, + ) + } +}) + test("Relies on download_url's version field to store the version number", async (t) => { await packageManager.syncComponents([fakeComponent()]) await new Promise((resolve) => setTimeout(resolve, 200)) diff --git a/packages/encryption/CHANGELOG.md b/packages/encryption/CHANGELOG.md index 749f74bf3..3c13924d1 100644 --- a/packages/encryption/CHANGELOG.md +++ b/packages/encryption/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.21.113](https://github.com/standardnotes/app/compare/@standardnotes/encryption@1.21.112...@standardnotes/encryption@1.21.113) (2026-04-24) + +**Note:** Version bump only for package @standardnotes/encryption + ## [1.21.112](https://github.com/standardnotes/app/compare/@standardnotes/encryption@1.21.111...@standardnotes/encryption@1.21.112) (2025-11-06) **Note:** Version bump only for package @standardnotes/encryption diff --git a/packages/encryption/package.json b/packages/encryption/package.json index 88f9d8289..f7c14dd68 100644 --- a/packages/encryption/package.json +++ b/packages/encryption/package.json @@ -1,6 +1,6 @@ { "name": "@standardnotes/encryption", - "version": "1.21.112", + "version": "1.21.113", "engines": { "node": ">=16.0.0 <17.0.0" }, diff --git a/packages/filepicker/CHANGELOG.md b/packages/filepicker/CHANGELOG.md index 3422f5646..0e430bf92 100644 --- a/packages/filepicker/CHANGELOG.md +++ b/packages/filepicker/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.28.134](https://github.com/standardnotes/app/compare/@standardnotes/filepicker@1.28.133...@standardnotes/filepicker@1.28.134) (2026-04-24) + +**Note:** Version bump only for package @standardnotes/filepicker + ## [1.28.133](https://github.com/standardnotes/app/compare/@standardnotes/filepicker@1.28.132...@standardnotes/filepicker@1.28.133) (2025-12-18) **Note:** Version bump only for package @standardnotes/filepicker diff --git a/packages/filepicker/package.json b/packages/filepicker/package.json index b01fb9db4..8aee3b3fd 100644 --- a/packages/filepicker/package.json +++ b/packages/filepicker/package.json @@ -1,6 +1,6 @@ { "name": "@standardnotes/filepicker", - "version": "1.28.133", + "version": "1.28.134", "engines": { "node": ">=16.0.0 <17.0.0" }, diff --git a/packages/files/CHANGELOG.md b/packages/files/CHANGELOG.md index 8e9dddbb1..13b020870 100644 --- a/packages/files/CHANGELOG.md +++ b/packages/files/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.20.17](https://github.com/standardnotes/app/compare/@standardnotes/files@1.20.16...@standardnotes/files@1.20.17) (2026-04-24) + +**Note:** Version bump only for package @standardnotes/files + ## [1.20.16](https://github.com/standardnotes/app/compare/@standardnotes/files@1.20.15...@standardnotes/files@1.20.16) (2025-12-18) ### Bug Fixes diff --git a/packages/files/package.json b/packages/files/package.json index 3b7cd8589..02d5a6bc6 100644 --- a/packages/files/package.json +++ b/packages/files/package.json @@ -1,6 +1,6 @@ { "name": "@standardnotes/files", - "version": "1.20.16", + "version": "1.20.17", "engines": { "node": ">=16.0.0 <17.0.0" }, diff --git a/packages/mobile/CHANGELOG.md b/packages/mobile/CHANGELOG.md index 6f34b7708..bf842dc31 100644 --- a/packages/mobile/CHANGELOG.md +++ b/packages/mobile/CHANGELOG.md @@ -3,6 +3,36 @@ 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 + +## [3.58.253](https://github.com/standardnotes/app/compare/@standardnotes/mobile@3.58.252...@standardnotes/mobile@3.58.253) (2026-04-29) + +**Note:** Version bump only for package @standardnotes/mobile + +## [3.58.252](https://github.com/standardnotes/app/compare/@standardnotes/mobile@3.58.251...@standardnotes/mobile@3.58.252) (2026-04-24) + +**Note:** Version bump only for package @standardnotes/mobile + +## [3.58.251](https://github.com/standardnotes/app/compare/@standardnotes/mobile@3.58.250...@standardnotes/mobile@3.58.251) (2026-04-24) + +**Note:** Version bump only for package @standardnotes/mobile + +## [3.58.250](https://github.com/standardnotes/app/compare/@standardnotes/mobile@3.58.249...@standardnotes/mobile@3.58.250) (2026-04-24) + +**Note:** Version bump only for package @standardnotes/mobile + +## [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/android/app/build.gradle b/packages/mobile/android/app/build.gradle index 814324a19..5fd93616e 100644 --- a/packages/mobile/android/app/build.gradle +++ b/packages/mobile/android/app/build.gradle @@ -73,7 +73,7 @@ def enableProguardInReleaseBuilds = false def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+' def appVersionCode = project.hasProperty('versionCode') ? Integer.valueOf(project.property('versionCode')) : 1 -def appVersionName = project.hasProperty('versionName') ? project.property('versionName') : "1.0" +def appVersionName = project.hasProperty('versionName') ? project.property('versionName') : "1.0.0" android { ndkVersion rootProject.ext.ndkVersion diff --git a/packages/mobile/fastlane/Fastfile b/packages/mobile/fastlane/Fastfile index 97f99aee9..7d5f48aee 100755 --- a/packages/mobile/fastlane/Fastfile +++ b/packages/mobile/fastlane/Fastfile @@ -15,7 +15,6 @@ platform :ios do end def build_ios(scheme) - xcversion(version: "26.0") increment_version_number( version_number: ENV['PACKAGE_VERSION'], xcodeproj: 'ios/StandardNotes.xcodeproj' diff --git a/packages/mobile/package.json b/packages/mobile/package.json index b43d7f380..d0f03866b 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.255", "author": "Standard Notes.", "private": true, "license": "AGPL-3.0", 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/models/CHANGELOG.md b/packages/models/CHANGELOG.md index 1cd4b2dfd..c3ea71912 100644 --- a/packages/models/CHANGELOG.md +++ b/packages/models/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.58.3](https://github.com/standardnotes/app/compare/@standardnotes/models@1.58.2...@standardnotes/models@1.58.3) (2026-04-24) + +**Note:** Version bump only for package @standardnotes/models + ## [1.58.2](https://github.com/standardnotes/app/compare/@standardnotes/models@1.58.1...@standardnotes/models@1.58.2) (2025-11-06) **Note:** Version bump only for package @standardnotes/models diff --git a/packages/models/package.json b/packages/models/package.json index 4107b9b54..52f959e2e 100644 --- a/packages/models/package.json +++ b/packages/models/package.json @@ -1,6 +1,6 @@ { "name": "@standardnotes/models", - "version": "1.58.2", + "version": "1.58.3", "publishConfig": { "access": "public" }, diff --git a/packages/models/src/Domain/Runtime/Display/Search/SearchUtilities.ts b/packages/models/src/Domain/Runtime/Display/Search/SearchUtilities.ts index 0f265ec87..37d64dce7 100644 --- a/packages/models/src/Domain/Runtime/Display/Search/SearchUtilities.ts +++ b/packages/models/src/Domain/Runtime/Display/Search/SearchUtilities.ts @@ -38,18 +38,33 @@ export function itemMatchesQuery( searchQuery: SearchQuery, collection: ReferenceLookupCollection, ): boolean { - const shouldCheckForSomeTagMatches = searchQuery.shouldCheckForSomeTagMatches ?? true - const itemTags = collection.elementsReferencingElement(itemToMatch, ContentType.TYPES.Tag) as SNTag[] - const someTagsMatches = - shouldCheckForSomeTagMatches && - itemTags.some((tag) => matchResultForStringQuery(tag, searchQuery.query) !== MatchResult.None) + const { query, includeProtectedNoteText, shouldCheckForSomeTagMatches = true, noteTitleOnly = false } = searchQuery - if (itemToMatch.protected && !searchQuery.includeProtectedNoteText) { - const match = matchResultForStringQuery(itemToMatch, searchQuery.query) - return match === MatchResult.Title || match === MatchResult.TitleAndText || someTagsMatches + if (query.length === 0) { + return true } - return matchResultForStringQuery(itemToMatch, searchQuery.query) !== MatchResult.None || someTagsMatches + const itemMatch = matchResultForStringQuery(itemToMatch, query) + + if (noteTitleOnly) { + return itemMatch === MatchResult.Title + } + + if (shouldCheckForSomeTagMatches) { + const itemTags = collection.elementsReferencingElement(itemToMatch, ContentType.TYPES.Tag) as SNTag[] + const tagMatches = itemTags.map((tag) => matchResultForStringQuery(tag, query)) + const someTagsMatches = tagMatches.some((match) => match !== MatchResult.None) + + if (someTagsMatches) { + return true + } + } + + if (itemToMatch.protected && !includeProtectedNoteText) { + return itemMatch === MatchResult.Title || itemMatch === MatchResult.TitleAndText + } + + return itemMatch !== MatchResult.None } function matchResultForStringQuery(item: SearchableItem, searchString: string): MatchResult { diff --git a/packages/models/src/Domain/Runtime/Display/Search/Types.ts b/packages/models/src/Domain/Runtime/Display/Search/Types.ts index e574abcaa..fffab2e73 100644 --- a/packages/models/src/Domain/Runtime/Display/Search/Types.ts +++ b/packages/models/src/Domain/Runtime/Display/Search/Types.ts @@ -6,6 +6,7 @@ export type SearchQuery = { query: string includeProtectedNoteText: boolean shouldCheckForSomeTagMatches?: boolean + noteTitleOnly?: boolean } export interface ReferenceLookupCollection { diff --git a/packages/releases/CHANGELOG.md b/packages/releases/CHANGELOG.md index d4642d5ed..49d8b27ab 100644 --- a/packages/releases/CHANGELOG.md +++ b/packages/releases/CHANGELOG.md @@ -3,6 +3,34 @@ 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 + +## [1.4.893](https://github.com/standardnotes/app/compare/@standardnotes/releases@1.4.892...@standardnotes/releases@1.4.893) (2026-04-29) + +**Note:** Version bump only for package @standardnotes/releases + +## [1.4.892](https://github.com/standardnotes/app/compare/@standardnotes/releases@1.4.891...@standardnotes/releases@1.4.892) (2026-04-24) + +**Note:** Version bump only for package @standardnotes/releases + +## [1.4.891](https://github.com/standardnotes/app/compare/@standardnotes/releases@1.4.890...@standardnotes/releases@1.4.891) (2026-04-24) + +**Note:** Version bump only for package @standardnotes/releases + +## [1.4.890](https://github.com/standardnotes/app/compare/@standardnotes/releases@1.4.889...@standardnotes/releases@1.4.890) (2026-04-24) + +**Note:** Version bump only for package @standardnotes/releases + +## [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..f9e75880e 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.895", "repository": { "type": "git", "url": "https://github.com/standardnotes/app", diff --git a/packages/responses/CHANGELOG.md b/packages/responses/CHANGELOG.md index dfc94243c..da9649e05 100644 --- a/packages/responses/CHANGELOG.md +++ b/packages/responses/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. +## [1.13.57](https://github.com/standardnotes/app/compare/@standardnotes/responses@1.13.56...@standardnotes/responses@1.13.57) (2026-04-24) + +### Bug Fixes + +* Shows info on UI and hides Manage button for shared subs ([#3002](https://github.com/standardnotes/app/issues/3002)) ([073db14](https://github.com/standardnotes/app/commit/073db143144bf88007010e4f44003198b327b4da)) + ## [1.13.56](https://github.com/standardnotes/app/compare/@standardnotes/responses@1.13.55...@standardnotes/responses@1.13.56) (2024-04-10) **Note:** Version bump only for package @standardnotes/responses diff --git a/packages/responses/package.json b/packages/responses/package.json index 0c3f78ced..c85d1ea38 100644 --- a/packages/responses/package.json +++ b/packages/responses/package.json @@ -1,6 +1,6 @@ { "name": "@standardnotes/responses", - "version": "1.13.56", + "version": "1.13.57", "engines": { "node": ">=16.0.0 <17.0.0" }, 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/services/CHANGELOG.md b/packages/services/CHANGELOG.md index 2636d2efc..2f655c6f1 100644 --- a/packages/services/CHANGELOG.md +++ b/packages/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.72.3](https://github.com/standardnotes/app/compare/@standardnotes/services@1.72.2...@standardnotes/services@1.72.3) (2026-04-24) + +**Note:** Version bump only for package @standardnotes/services + ## [1.72.2](https://github.com/standardnotes/app/compare/@standardnotes/services@1.72.1...@standardnotes/services@1.72.2) (2025-12-18) ### Bug Fixes diff --git a/packages/services/package.json b/packages/services/package.json index 8806a8fd8..5cce938df 100644 --- a/packages/services/package.json +++ b/packages/services/package.json @@ -1,6 +1,6 @@ { "name": "@standardnotes/services", - "version": "1.72.2", + "version": "1.72.3", "engines": { "node": ">=16.0.0 <17.0.0" }, diff --git a/packages/services/src/Domain/InternalFeatures/InternalFeature.ts b/packages/services/src/Domain/InternalFeatures/InternalFeature.ts index 36a76cd56..861225023 100644 --- a/packages/services/src/Domain/InternalFeatures/InternalFeature.ts +++ b/packages/services/src/Domain/InternalFeatures/InternalFeature.ts @@ -1,4 +1,5 @@ export enum InternalFeature { Vaults = 'vaults', UniversalSearch = 'universal-search', + SearchEnhancements = 'search-enhancements', } diff --git a/packages/snjs/CHANGELOG.md b/packages/snjs/CHANGELOG.md index a7c1dd7a2..703726bd4 100644 --- a/packages/snjs/CHANGELOG.md +++ b/packages/snjs/CHANGELOG.md @@ -3,6 +3,16 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [2.211.7](https://github.com/standardnotes/app/compare/@standardnotes/snjs@2.211.6...@standardnotes/snjs@2.211.7) (2026-04-24) + +**Note:** Version bump only for package @standardnotes/snjs + +## [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/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..f23d3228a 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 skipSessionCooldownTests = urlSearchParams.get('skipSessionCooldownTests') === '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 () { + ;(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({ application: application, email: email, @@ -508,9 +512,12 @@ 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 () { + ;(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, @@ -529,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({ 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 () { diff --git a/packages/snjs/package.json b/packages/snjs/package.json index 7836e924c..5eb2f8968 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.7", "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..c2fceac63 100644 --- a/packages/ui-services/CHANGELOG.md +++ b/packages/ui-services/CHANGELOG.md @@ -3,6 +3,22 @@ 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 + +## [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..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.2", + "version": "1.39.5", "engines": { "node": ">=16.0.0 <17.0.0" }, diff --git a/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.spec.ts b/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.spec.ts index 47d39f9e8..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, enex } 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') @@ -132,6 +167,49 @@ 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 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) + + 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) + + 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 f19fc7b14..4f46633ce 100644 --- a/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.ts +++ b/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.ts @@ -8,6 +8,10 @@ import Base64 from 'crypto-js/enc-base64' import { Converter, UploadFileFn } from '../Converter' import { ConversionResult } from '../ConversionResult' import { getBlobFromBase64 } from '../Utils' +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) @@ -86,14 +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) + this.convertHighlightSpansToMarks(noteElement) } + this.convertEvernoteChecklists(noteElement, canUseSuper) this.removeEmptyAndOrphanListElements(noteElement) this.unwrapTopLevelBreaks(noteElement) @@ -242,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) { @@ -250,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 @@ -298,7 +318,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() } }) } @@ -388,15 +408,155 @@ 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) +} + +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 ffb141189..8d1f6fb30 100644 --- a/packages/ui-services/src/Import/EvernoteConverter/testData.ts +++ b/packages/ui-services/src/Import/EvernoteConverter/testData.ts @@ -36,6 +36,61 @@ export const enex = ` ` +export const highlightEnex = ` + + + + Highlight test + 20210308T051614Z + 20210308T051855Z + + +
Line 1
Line 2
]]> +
+
+
` + +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 = ` + + + + Empty line test + 20210308T051614Z + 20210308T051855Z + + +
line1

line2
]]> +
+
+
` + export function createTestResourceElement( shouldHaveMimeType = true, shouldHaveSourceUrl = false, 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/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/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'] = [] 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/CHANGELOG.md b/packages/web/CHANGELOG.md index 8350995ac..e986e43af 100644 --- a/packages/web/CHANGELOG.md +++ b/packages/web/CHANGELOG.md @@ -3,6 +3,45 @@ 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 + +* Fixes cell padding and bold font within table cells in Super pdf export ([c81589b](https://github.com/standardnotes/app/commit/c81589b945b04b6f2b6842f193495d562c132bc8)) +* Fixes images rendering too small on Super pdf export ([80fa337](https://github.com/standardnotes/app/commit/80fa337afba85bada70b4ddacb6e5ad391978f28)) +* Fixes Super pdf export code block formatting issues ([bd3cf60](https://github.com/standardnotes/app/commit/bd3cf600e7a766c2fd980fbc01924e59664184b8)) +* Fixes Super pdf export headings font sizes and weights ([6daf58c](https://github.com/standardnotes/app/commit/6daf58c92893a6cfa93596a2429ed3e0697222e0)) +* Fixes Super pdf export line height ([e22e6fc](https://github.com/standardnotes/app/commit/e22e6fce7936a9b425f90e7363f274bce3b3b841)) +* Fixes Upgrade button in Settings leading to purchase page ([#3011](https://github.com/standardnotes/app/issues/3011)) ([eda1bfe](https://github.com/standardnotes/app/commit/eda1bfe0bd5b2be762dadf840f49034a2c659816)) + +## [3.201.27](https://github.com/standardnotes/app/compare/@standardnotes/web@3.201.26...@standardnotes/web@3.201.27) (2026-04-29) + +### Bug Fixes + +* Fixes Super search highlights styling ([#3009](https://github.com/standardnotes/app/issues/3009)) ([a39bb82](https://github.com/standardnotes/app/commit/a39bb824f7b6d4f761abb27df9b7c7ba4818ce14)) +* Switches back to note list on mobile when active note is deleted from a different device ([#3008](https://github.com/standardnotes/app/issues/3008)) ([f4a9481](https://github.com/standardnotes/app/commit/f4a94811848ea19550703d5dcd2b446091bf9536)) + +## [3.201.26](https://github.com/standardnotes/app/compare/@standardnotes/web@3.201.25...@standardnotes/web@3.201.26) (2026-04-24) + +### Bug Fixes + +* Shows info on UI and hides Manage button for shared subs ([#3002](https://github.com/standardnotes/app/issues/3002)) ([073db14](https://github.com/standardnotes/app/commit/073db143144bf88007010e4f44003198b327b4da)) + +## [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..b99ad0696 100644 --- a/packages/web/CHANGELOG.md.json +++ b/packages/web/CHANGELOG.md.json @@ -1,5 +1,93 @@ { "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)", + "date": null, + "body": "### Bug Fixes\n\n* Fixes cell padding and bold font within table cells in Super pdf export ([c81589b](https://github.com/standardnotes/app/commit/c81589b945b04b6f2b6842f193495d562c132bc8))\n* Fixes images rendering too small on Super pdf export ([80fa337](https://github.com/standardnotes/app/commit/80fa337afba85bada70b4ddacb6e5ad391978f28))\n* Fixes Super pdf export code block formatting issues ([bd3cf60](https://github.com/standardnotes/app/commit/bd3cf600e7a766c2fd980fbc01924e59664184b8))\n* Fixes Super pdf export headings font sizes and weights ([6daf58c](https://github.com/standardnotes/app/commit/6daf58c92893a6cfa93596a2429ed3e0697222e0))\n* Fixes Super pdf export line height ([e22e6fc](https://github.com/standardnotes/app/commit/e22e6fce7936a9b425f90e7363f274bce3b3b841))\n* Fixes Upgrade button in Settings leading to purchase page ([#3011](https://github.com/standardnotes/app/issues/3011)) ([eda1bfe](https://github.com/standardnotes/app/commit/eda1bfe0bd5b2be762dadf840f49034a2c659816))", + "parsed": { + "_": [ + "Fixes cell padding and bold font within table cells in Super pdf export (c81589b)", + "Fixes images rendering too small on Super pdf export (80fa337)", + "Fixes Super pdf export code block formatting issues (bd3cf60)", + "Fixes Super pdf export headings font sizes and weights (6daf58c)", + "Fixes Super pdf export line height (e22e6fc)", + "Fixes Upgrade button in Settings leading to purchase page (#3011) (eda1bfe)" + ], + "Bug Fixes": [ + "Fixes cell padding and bold font within table cells in Super pdf export (c81589b)", + "Fixes images rendering too small on Super pdf export (80fa337)", + "Fixes Super pdf export code block formatting issues (bd3cf60)", + "Fixes Super pdf export headings font sizes and weights (6daf58c)", + "Fixes Super pdf export line height (e22e6fc)", + "Fixes Upgrade button in Settings leading to purchase page (#3011) (eda1bfe)" + ] + } + }, + { + "version": "3.201.27", + "title": "[3.201.27](https://github.com/standardnotes/app/compare/@standardnotes/web@3.201.26...@standardnotes/web@3.201.27) (2026-04-29)", + "date": null, + "body": "### Bug Fixes\n\n* Fixes Super search highlights styling ([#3009](https://github.com/standardnotes/app/issues/3009)) ([a39bb82](https://github.com/standardnotes/app/commit/a39bb824f7b6d4f761abb27df9b7c7ba4818ce14))\n* Switches back to note list on mobile when active note is deleted from a different device ([#3008](https://github.com/standardnotes/app/issues/3008)) ([f4a9481](https://github.com/standardnotes/app/commit/f4a94811848ea19550703d5dcd2b446091bf9536))", + "parsed": { + "_": [ + "Fixes Super search highlights styling (#3009) (a39bb82)", + "Switches back to note list on mobile when active note is deleted from a different device (#3008) (f4a9481)" + ], + "Bug Fixes": [ + "Fixes Super search highlights styling (#3009) (a39bb82)", + "Switches back to note list on mobile when active note is deleted from a different device (#3008) (f4a9481)" + ] + } + }, + { + "version": "3.201.26", + "title": "[3.201.26](https://github.com/standardnotes/app/compare/@standardnotes/web@3.201.25...@standardnotes/web@3.201.26) (2026-04-24)", + "date": null, + "body": "### Bug Fixes\n\n* Shows info on UI and hides Manage button for shared subs ([#3002](https://github.com/standardnotes/app/issues/3002)) ([073db14](https://github.com/standardnotes/app/commit/073db143144bf88007010e4f44003198b327b4da))", + "parsed": { + "_": [ + "Shows info on UI and hides Manage button for shared subs (#3002) (073db14)" + ], + "Bug Fixes": [ + "Shows info on UI and hides Manage button for shared subs (#3002) (073db14)" + ] + } + }, + { + "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..8540628d7 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.29", "license": "AGPL-3.0", "main": "dist/app.js", "author": "Standard Notes", @@ -108,18 +108,19 @@ }, "dependencies": { "@ariakit/react": "^0.4.18", - "@lexical/clipboard": "0.38.1", - "@lexical/headless": "0.38.1", - "@lexical/link": "0.38.1", - "@lexical/list": "0.38.1", - "@lexical/react": "0.38.1", - "@lexical/rich-text": "0.38.1", - "@lexical/utils": "0.38.1", + "@lexical/clipboard": "0.43.0", + "@lexical/code": "0.43.0", + "@lexical/headless": "0.43.0", + "@lexical/link": "0.43.0", + "@lexical/list": "0.43.0", + "@lexical/react": "0.43.0", + "@lexical/rich-text": "0.43.0", + "@lexical/utils": "0.43.0", "@radix-ui/react-slot": "^1.0.1", - "@react-pdf/renderer": "^4.3.0", + "@react-pdf/renderer": "^4.4.1", "comlink": "^4.4.1", "fast-diff": "^1.3.0", - "lexical": "0.38.1", + "lexical": "0.43.0", "ttag": "^1.8.12", "unicode-script": "^1.2.0" } 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/DevMode.ts b/packages/web/src/javascripts/Application/DevMode.ts index 3b25a6990..652c91a78 100644 --- a/packages/web/src/javascripts/Application/DevMode.ts +++ b/packages/web/src/javascripts/Application/DevMode.ts @@ -5,6 +5,7 @@ export class DevMode { constructor(private application: WebApplicationInterface) { InternalFeatureService.get().enableFeature(InternalFeature.Vaults) InternalFeatureService.get().enableFeature(InternalFeature.UniversalSearch) + InternalFeatureService.get().enableFeature(InternalFeature.SearchEnhancements) } /** Valid only when running a mock event publisher on port 3124 */ 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/AccountMenu/AdvancedOptions.tsx b/packages/web/src/javascripts/Components/AccountMenu/AdvancedOptions.tsx index 0e61145d1..932d7ba7f 100644 --- a/packages/web/src/javascripts/Components/AccountMenu/AdvancedOptions.tsx +++ b/packages/web/src/javascripts/Components/AccountMenu/AdvancedOptions.tsx @@ -6,6 +6,7 @@ import Icon from '@/Components/Icon/Icon' import { useApplication } from '../ApplicationProvider' import ServerPicker from './ServerPicker/ServerPicker' import { DefaultHost } from '@standardnotes/snjs' +import { c } from 'ttag' type Props = { disabled?: boolean @@ -50,7 +51,7 @@ const AdvancedOptions: FunctionComponent = ({ if (!identifier) { if (privateUsername?.length > 0) { - application.alerts.alert('Unable to compute private username.').catch(console.error) + application.alerts.alert(c('Error').t`Unable to compute private username.`).catch(console.error) } return } @@ -112,7 +113,7 @@ const AdvancedOptions: FunctionComponent = ({ onClick={toggleShowAdvanced} >
- Advanced options + {c('Action').t`Advanced options`}
@@ -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`}
= ({ {menuItem.uiFeature.featureIdentifier === NativeFeatureIdentifier.TYPES.SuperEditor && !isSelected(menuItem) && recommendSuper && ( - + Recommended )} 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 ( -
- + {application.canShowPurchaseFlow() && ( + + )}
diff --git a/packages/web/src/javascripts/Components/ConfirmDeleteAccountModal/ConfirmDeleteAccountModal.tsx b/packages/web/src/javascripts/Components/ConfirmDeleteAccountModal/ConfirmDeleteAccountModal.tsx index 3756a0f1a..5d497c28d 100644 --- a/packages/web/src/javascripts/Components/ConfirmDeleteAccountModal/ConfirmDeleteAccountModal.tsx +++ b/packages/web/src/javascripts/Components/ConfirmDeleteAccountModal/ConfirmDeleteAccountModal.tsx @@ -1,5 +1,6 @@ import { observer } from 'mobx-react-lite' import { useCallback, useRef } from 'react' +import { c } from 'ttag' import { STRING_DELETE_ACCOUNT_CONFIRMATION } from '@/Constants/Strings' import Button from '@/Components/Button/Button' import { WebApplication } from '@/Application/WebApplication' @@ -25,22 +26,22 @@ const ConfirmDeleteAccountModal = ({ application }: Props) => { 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/ContentListView/ContentListView.tsx b/packages/web/src/javascripts/Components/ContentListView/ContentListView.tsx index c66d0e242..aaea8c3af 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} + showSearchEnhancements={application.featuresController.isSearchEnhancementsEnabled()} /> )} { 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/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 (
)} @@ -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 6f3f18ad7..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 && } @@ -126,9 +132,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() @@ -151,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}
@@ -192,7 +205,7 @@ const ImportModalFileItem = ({ )} -
or import from:
+
{c('Info').t`or import from:`}
+ {application.canShowPurchaseFlow() && ( + + )}
) } diff --git a/packages/web/src/javascripts/Components/PasswordWizard/FinishStep.tsx b/packages/web/src/javascripts/Components/PasswordWizard/FinishStep.tsx index 344895503..ed3028c5b 100644 --- a/packages/web/src/javascripts/Components/PasswordWizard/FinishStep.tsx +++ b/packages/web/src/javascripts/Components/PasswordWizard/FinishStep.tsx @@ -1,3 +1,4 @@ +import { c } from 'ttag' import { CheckmarkCircle } from '../UIElements/CheckmarkCircle' export const FinishStep = () => { @@ -8,8 +9,11 @@ export const FinishStep = () => {
-
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/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/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.`}
diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Account/SignOutView.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Account/SignOutView.tsx index 53796f663..1a41705c2 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Account/SignOutView.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Account/SignOutView.tsx @@ -8,6 +8,7 @@ import { Subtitle, Title, Text } from '../../PreferencesComponents/Content' import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup' import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment' import ClearSessionDataView from './ClearSessionDataView' +import { c } from 'ttag' type Props = { application: WebApplication @@ -18,27 +19,27 @@ const SignOutView: FunctionComponent = observer(({ application }) => { <> - Sign out - Other devices - Want to sign out on all devices except this one? + {c('Title').t`Sign out`} + {c('Subtitle').t`Other devices`} + {c('Info').t`Want to sign out on all devices except this one?`}
- 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/Account/Subscription/SubscriptionInformation.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Account/Subscription/SubscriptionInformation.tsx index 5e02fc7d7..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 @@ -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 ( <> -
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/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/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/Components/SearchBar/SearchBar.tsx b/packages/web/src/javascripts/Components/SearchBar/SearchBar.tsx index 5eaf25df4..afd8d2234 100644 --- a/packages/web/src/javascripts/Components/SearchBar/SearchBar.tsx +++ b/packages/web/src/javascripts/Components/SearchBar/SearchBar.tsx @@ -1,8 +1,10 @@ import { ItemListController } from '@/Controllers/ItemList/ItemListController' import { KeyboardKey } from '@standardnotes/ui-services' -import { useCallback, KeyboardEventHandler, useRef } from 'react' +import { useCallback, KeyboardEventHandler, useRef, useState } from 'react' import SearchOptions from '@/Components/SearchOptions/SearchOptions' +import SearchFilterSheet from '@/Components/SearchOptions/SearchFilterSheet' import { SearchOptionsController } from '@/Controllers/SearchOptionsController' +import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery' import Icon from '../Icon/Icon' import DecoratedInput from '../Input/DecoratedInput' import { observer } from 'mobx-react-lite' @@ -14,13 +16,49 @@ type Props = { itemListController: ItemListController searchOptionsController: SearchOptionsController hideOptions?: boolean + showSearchEnhancements?: boolean } -const SearchBar = ({ itemListController, searchOptionsController, hideOptions = false }: Props) => { +type SearchFilterButtonProps = { + activeFilterCount: number + isOpen: boolean + onClick: () => void +} + +const SearchFilterButton = ({ activeFilterCount, isOpen, onClick }: SearchFilterButtonProps) => ( + +) + +const SearchBar = ({ + itemListController, + searchOptionsController, + hideOptions = false, + showSearchEnhancements = false, +}: Props) => { const searchBarRef = useRef(null) const searchInputRef = useRef(null) + const [isFilterPanelOpen, setIsFilterPanelOpen] = useState(false) + const isMobileScreen = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm) const { noteFilterText, setNoteFilterText, clearFilterText, onFilterEnter } = itemListController + const { activeSearchFilterCount } = searchOptionsController const onNoteFilterTextChange = useCallback( (text: string) => { @@ -43,6 +81,47 @@ const SearchBar = ({ itemListController, searchOptionsController, hideOptions = searchInputRef.current?.focus() }, [clearFilterText]) + const toggleFilterPanel = useCallback(() => { + setIsFilterPanelOpen((current) => !current) + }, []) + + const closeFilterPanel = useCallback(() => { + setIsFilterPanelOpen(false) + }, []) + + const searchOptionsVisibilityClass = (() => { + if (hideOptions) { + return 'hidden' + } + + if (showSearchEnhancements) { + if (isMobileScreen) { + return 'hidden' + } + + return isFilterPanelOpen ? 'flex' : 'hidden' + } + + if (!noteFilterText) { + return 'hidden group-focus-within:flex' + } + + return undefined + })() + + const rightDecorations = showSearchEnhancements + ? [ +
+ {noteFilterText && } + +
, + ] + : [noteFilterText && ].filter(Boolean) + return (
]} - right={[noteFilterText && ]} + right={rightDecorations} roundedFull /> -
- +
+
+ + {showSearchEnhancements && isMobileScreen && ( + + )}
) } diff --git a/packages/web/src/javascripts/Components/SearchOptions/EnhancedSearchOptionsContent.tsx b/packages/web/src/javascripts/Components/SearchOptions/EnhancedSearchOptionsContent.tsx new file mode 100644 index 000000000..4c695618a --- /dev/null +++ b/packages/web/src/javascripts/Components/SearchOptions/EnhancedSearchOptionsContent.tsx @@ -0,0 +1,53 @@ +import Checkbox from '@/Components/Checkbox/Checkbox' +import { SearchOptionsController } from '@/Controllers/SearchOptionsController' +import { classNames } from '@standardnotes/snjs' +import { observer } from 'mobx-react-lite' +import SearchBubbles from './SearchBubbles' +import SearchOptionsSection, { ClearFilterButton } from './SearchOptionsSection' +import SearchTagFilters from './SearchTagFilters' + +type Props = { + searchOptions: SearchOptionsController + className?: string +} + +const EnhancedSearchOptionsContent = ({ searchOptions, className }: Props) => { + const { noteTitleOnly, tagFilterList, activeSearchFilterCount } = searchOptions + + return ( +
+
event.preventDefault()}> +
+ searchOptions.setNoteTitleOnly(event.target.checked)} + /> + {activeSearchFilterCount > 0 && ( + Clear all filters + )} +
+ + +
+ +
+
+
+ + 0 ? ( + Clear tag filters + ) : undefined + } + > + + +
+ ) +} + +export default observer(EnhancedSearchOptionsContent) diff --git a/packages/web/src/javascripts/Components/SearchOptions/SearchBubbles.tsx b/packages/web/src/javascripts/Components/SearchOptions/SearchBubbles.tsx new file mode 100644 index 000000000..c5ba1cfc5 --- /dev/null +++ b/packages/web/src/javascripts/Components/SearchOptions/SearchBubbles.tsx @@ -0,0 +1,30 @@ +import Bubble from '@/Components/Bubble/Bubble' +import { SearchOptionsController } from '@/Controllers/SearchOptionsController' +import { observer } from 'mobx-react-lite' +import { useCallback } from 'react' + +type Props = { + searchOptions: SearchOptionsController +} + +const SearchBubbles = ({ searchOptions }: Props) => { + const { includeProtectedContents, includeArchived, includeTrashed } = searchOptions + + const toggleIncludeProtectedContents = useCallback(async () => { + await searchOptions.toggleIncludeProtectedContents() + }, [searchOptions]) + + return ( + <> + + + + + ) +} + +export default observer(SearchBubbles) diff --git a/packages/web/src/javascripts/Components/SearchOptions/SearchFilterSheet.tsx b/packages/web/src/javascripts/Components/SearchOptions/SearchFilterSheet.tsx new file mode 100644 index 000000000..8d0474916 --- /dev/null +++ b/packages/web/src/javascripts/Components/SearchOptions/SearchFilterSheet.tsx @@ -0,0 +1,35 @@ +import MobilePopoverContent from '@/Components/Popover/MobilePopoverContent' +import { SearchOptionsController } from '@/Controllers/SearchOptionsController' +import { useAndroidBackHandler } from '@/NativeMobileWeb/useAndroidBackHandler' +import { observer } from 'mobx-react-lite' +import { useEffect } from 'react' +import EnhancedSearchOptionsContent from './EnhancedSearchOptionsContent' + +type Props = { + open: boolean + onClose: () => void + searchOptions: SearchOptionsController +} + +const SearchFilterSheet = ({ open, onClose, searchOptions }: Props) => { + const addAndroidBackHandler = useAndroidBackHandler() + + useEffect(() => { + if (!open) { + return + } + + return addAndroidBackHandler(() => { + onClose() + return true + }) + }, [addAndroidBackHandler, onClose, open]) + + return ( + + + + ) +} + +export default observer(SearchFilterSheet) diff --git a/packages/web/src/javascripts/Components/SearchOptions/SearchOptions.tsx b/packages/web/src/javascripts/Components/SearchOptions/SearchOptions.tsx index 89829a3ea..63e86746d 100644 --- a/packages/web/src/javascripts/Components/SearchOptions/SearchOptions.tsx +++ b/packages/web/src/javascripts/Components/SearchOptions/SearchOptions.tsx @@ -1,32 +1,23 @@ -import { observer } from 'mobx-react-lite' -import Bubble from '@/Components/Bubble/Bubble' -import { useCallback } from 'react' import { SearchOptionsController } from '@/Controllers/SearchOptionsController' +import { observer } from 'mobx-react-lite' +import EnhancedSearchOptionsContent from './EnhancedSearchOptionsContent' +import SearchBubbles from './SearchBubbles' type Props = { searchOptions: SearchOptionsController + showSearchEnhancements?: boolean } -const SearchOptions = ({ searchOptions }: Props) => { - const { includeProtectedContents, includeArchived, includeTrashed } = searchOptions +const SearchOptions = ({ searchOptions, showSearchEnhancements = false }: Props) => { + if (!showSearchEnhancements) { + return ( +
event.preventDefault()}> + +
+ ) + } - const toggleIncludeProtectedContents = useCallback(async () => { - await searchOptions.toggleIncludeProtectedContents() - }, [searchOptions]) - - return ( -
e.preventDefault()}> - - - - - -
- ) + return } export default observer(SearchOptions) diff --git a/packages/web/src/javascripts/Components/SearchOptions/SearchOptionsSection.tsx b/packages/web/src/javascripts/Components/SearchOptions/SearchOptionsSection.tsx new file mode 100644 index 000000000..c8edb58e3 --- /dev/null +++ b/packages/web/src/javascripts/Components/SearchOptions/SearchOptionsSection.tsx @@ -0,0 +1,35 @@ +import { ReactNode } from 'react' + +type ClearFilterButtonProps = { + onClick: () => void + children: ReactNode +} + +export const ClearFilterButton = ({ onClick, children }: ClearFilterButtonProps) => ( + +) + +type SearchOptionsSectionProps = { + label: string + children: ReactNode + action?: ReactNode +} + +const SearchOptionsSection = ({ label, children, action }: SearchOptionsSectionProps) => ( +
+
+
{label}
+ {action} +
+ {children} +
+) + +export default SearchOptionsSection diff --git a/packages/web/src/javascripts/Components/SearchOptions/SearchTagFilters.tsx b/packages/web/src/javascripts/Components/SearchOptions/SearchTagFilters.tsx new file mode 100644 index 000000000..e7d5712ba --- /dev/null +++ b/packages/web/src/javascripts/Components/SearchOptions/SearchTagFilters.tsx @@ -0,0 +1,49 @@ +import ItemSelectionDropdown from '@/Components/ItemSelectionDropdown/ItemSelectionDropdown' +import LinkedItemBubble from '@/Components/LinkedItems/LinkedItemBubble' +import { useApplication } from '@/Components/ApplicationProvider' +import { ElementIds } from '@/Constants/ElementIDs' +import { SearchOptionsController } from '@/Controllers/SearchOptionsController' +import { createLinkFromItem } from '@/Utils/Items/Search/createLinkFromItem' +import { ContentType, SNTag } from '@standardnotes/snjs' +import { observer } from 'mobx-react-lite' + +type Props = { + searchOptions: SearchOptionsController +} + +const SearchTagFilters = ({ searchOptions }: Props) => { + const application = useApplication() + const { tagFilterList } = searchOptions + const selected = application.navigationController.selected + + const excludeUuids = tagFilterList.map((tag) => tag.uuid) + if (selected instanceof SNTag && !excludeUuids.includes(selected.uuid)) { + excludeUuids.push(selected.uuid) + } + + return ( +
+ {tagFilterList.map((tag) => ( + { + searchOptions.removeTagFilter(item as SNTag) + document.getElementById(ElementIds.SearchBar)?.focus() + }} + isBidirectional={false} + inlineFlex={true} + /> + ))} + searchOptions.addTagFilter(item as SNTag)} + placeholder="Add tag..." + contentTypes={[ContentType.TYPES.Tag]} + excludeUuids={excludeUuids} + /> +
+ ) +} + +export default observer(SearchTagFilters) diff --git a/packages/web/src/javascripts/Components/SmartViewBuilder/CompoundPredicateBuilder.tsx b/packages/web/src/javascripts/Components/SmartViewBuilder/CompoundPredicateBuilder.tsx index 549e36d2c..d317950f4 100644 --- a/packages/web/src/javascripts/Components/SmartViewBuilder/CompoundPredicateBuilder.tsx +++ b/packages/web/src/javascripts/Components/SmartViewBuilder/CompoundPredicateBuilder.tsx @@ -59,19 +59,23 @@ const CompoundPredicateBuilder = ({ controller }: Props) => { ))} - + {PredicateKeypathTypes[predicate.keypath as PredicateKeypath] === 'tag' ? ( +
include
+ ) : ( + + )} {predicate.keypath && ( { } } +const toTagsPredicate = (tagTitle: PredicateJsonForm['value']): PredicateJsonForm => ({ + keypath: 'tags', + operator: 'includes', + value: { + keypath: 'title', + operator: '=', + value: tagTitle, + }, +}) + export class CompoundPredicateBuilderController { operator: PredicateCompoundOperator = 'and' predicates: PredicateJsonForm[] = [getEmptyPredicate()] @@ -64,6 +74,9 @@ export class CompoundPredicateBuilderController { case 'date': this.setPredicate(index, { value: '1.days.ago' }) break + case 'tag': + this.setPredicate(index, { value: '' }) + break } } @@ -81,7 +94,12 @@ export class CompoundPredicateBuilderController { toJson(): PredicateJsonForm { return { operator: this.operator, - value: this.predicates, + value: this.predicates.map((predicate) => { + if (predicate.keypath === 'tags') { + return toTagsPredicate(predicate.value) + } + return predicate + }), } } diff --git a/packages/web/src/javascripts/Components/SmartViewBuilder/PredicateKeypaths.ts b/packages/web/src/javascripts/Components/SmartViewBuilder/PredicateKeypaths.ts index 1c22bfb3d..8f624d722 100644 --- a/packages/web/src/javascripts/Components/SmartViewBuilder/PredicateKeypaths.ts +++ b/packages/web/src/javascripts/Components/SmartViewBuilder/PredicateKeypaths.ts @@ -18,6 +18,7 @@ export type PredicateKeypath = | 'starred' | 'hidePreview' | 'spellcheck' + | 'tags' export const PredicateKeypathLabels: { [k in PredicateKeypath]: string } = { title: 'Title', @@ -39,10 +40,11 @@ export const PredicateKeypathLabels: { [k in PredicateKeypath]: string } = { starred: 'Starred', hidePreview: 'Hide Preview', spellcheck: 'Spellcheck', + tags: 'Tags', } as const export const PredicateKeypathTypes: { - [k in PredicateKeypath]: 'string' | 'noteType' | 'editorIdentifier' | 'number' | 'boolean' | 'date' + [k in PredicateKeypath]: 'string' | 'noteType' | 'editorIdentifier' | 'number' | 'boolean' | 'date' | 'tag' } = { title: 'string', 'title.length': 'number', @@ -63,4 +65,5 @@ export const PredicateKeypathTypes: { starred: 'boolean', hidePreview: 'boolean', spellcheck: 'boolean', + tags: 'tag', } as const diff --git a/packages/web/src/javascripts/Components/SmartViewBuilder/PredicateValue.tsx b/packages/web/src/javascripts/Components/SmartViewBuilder/PredicateValue.tsx index 63f652a4a..7a5ef176a 100644 --- a/packages/web/src/javascripts/Components/SmartViewBuilder/PredicateValue.tsx +++ b/packages/web/src/javascripts/Components/SmartViewBuilder/PredicateValue.tsx @@ -1,5 +1,5 @@ import { getDropdownItemsForAllEditors } from '@/Utils/DropdownItemsForEditors' -import { NoteType } from '@standardnotes/snjs' +import { ContentType, naturalSort, NoteType, SNTag } from '@standardnotes/snjs' import { useApplication } from '../ApplicationProvider' import { PredicateKeypath, PredicateKeypathTypes } from './PredicateKeypaths' @@ -14,7 +14,22 @@ const PredicateValue = ({ keypath, value, setValue }: Props) => { const type = PredicateKeypathTypes[keypath] const editorItems = getDropdownItemsForAllEditors(application) - return type === 'noteType' ? ( + return type === 'tag' ? ( + + ) : type === 'noteType' ? (