Compare commits

..
723 Commits
Author SHA1 Message Date
Keshav 1d7c091d55 Desktop, Mobile, Cli: Resolves #16140: Add locked note handling to export and import (#16785) 2026-10-25 10:47:07 +00:00
mrjo118 bad38b7fff Android: Fixes #8099: Fix data share not working after leaving by back button (#16906) 2026-10-24 18:25:18 +02:00
KeshavandLaurent Cozic 75f888ff0d Desktop, Mobile: Resolves #16382: Migrate locked notes when the sync target uses a different note lock key (#16723)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-10-24 18:23:40 +02:00
Ben BalterandClaude Opus 5.5 94168d33d6 Cli: Fixes #16894: Fix quadratic conversion time for large tables (#16901)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:42:49 +02:00
Laurent Cozic 451200c76c Server v3.7.3 2026-10-23 12:33:59 +02:00
Laurent Cozic d865d1434e Chore: Build assets 2026-10-23 12:33:37 +02:00
Laurent Cozic 23c85cdecf Server: Pin Node version to 24.22.1 2026-10-23 12:28:52 +02:00
Joplin Bot 280954e58f Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-10-22 21:22:22 +00:00
Milo Ivir 1dde6e8fb8 Doc: Resolves #16868: Add Croatian website support (#16869) 2026-10-22 22:30:10 +02:00
Ben BalterandClaude Opus 5.5 94ddfda2d6 Desktop, Mobile, Cli: Fixes #16895: Fix quadratic conversion time for elements with many children (#16902)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 22:24:44 +02:00
renovate[bot] 30a017b800 fix(deps): update dependency tar to v7.5.22 (#16922)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-22 22:24:24 +02:00
renovate[bot] 0bce1ae02b chore(deps): update dependency @axe-core/playwright to v4.12.1 (#16913)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-22 20:09:42 +02:00
renovate[bot] 2e45f22d85 chore(deps): update dependency sass to v1.101.7 (#16915)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-22 20:09:21 +02:00
Joplin Bot ac56a7c729 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-10-22 16:51:59 +00:00
Joplin Bot 585ec251b0 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-10-21 11:31:36 +00:00
Laurent Cozic 95375b1f11 Doc: Add information about Joplin Cloud AI and privacy 2026-10-21 12:20:24 +01:00
Aissa BenfoddaandLaurent Cozic 410b5abab9 Desktop: Resolves #16383: Keypresses are sometimes ignored when searching (#16686)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-10-21 10:19:40 +01:00
renovate[bot] 4a74b34e83 chore(deps): update dependency sass to v1.101.6 (#16910)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-21 07:44:58 +00:00
renovate[bot] 5c2601a283 fix(deps): update dependency node-cron to v4.5.0 (#16904)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-20 23:28:26 +01:00
renovate[bot] 7e23fc6446 chore(deps): update dependency globals to v17.7.0 (#16903)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-20 23:27:40 +01:00
Aissa Benfodda e09c8e8527 Desktop: Resolves #16698: OneNote import (*.one / *.onepkg) doesn’t import notes with title longer than about 240 chars (#16836) 2026-10-20 23:27:07 +01:00
mrjo118 4d33866822 Mobile: Support highlighting text in custom colours in the ProseMirror editor, to match TinyMCE behaviour (#16847) 2026-10-20 22:31:54 +01:00
mrjo118 b63bd50b9d Desktop: Show truncated note and notebook title warnings for lossy export formats (#16796) 2026-10-20 22:29:14 +01:00
mrjo118andLaurent Cozic f5b2d654d2 Desktop, Mobile, Cli: Fix sync eligible conflicts not being synced, if the conflict is for a note in a share (#16726)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-10-20 21:00:54 +01:00
Aissa BenfoddaandLaurent Cozic abb4f7e73f Desktop: Resolves #16436: Line return after a bulleted list is stripped when switching away from a note in the Rich Text Editor (#16717)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-10-20 20:54:37 +01:00
KeshavandLaurent Cozic 4571f69389 Desktop, Mobile, Server: Resolves #16336: Handle locked notes inside shared notebooks (#16579)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-10-20 20:40:52 +01:00
renovate[bot] cf977d3839 fix(deps): update dependency tar to v7.5.21 (#16899)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-20 20:35:01 +01:00
Laurent Cozic cf57e59325 Doc: Update canary file 2026-10-19 12:07:36 +01:00
renovate[bot] 7dc1efb1ec chore(deps): update dependency sass to v1.101.3 (#16888)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-19 07:27:05 +00:00
renovate[bot] 1935d47755 fix(deps): update dependency react-native-webview to v13.17.0 (#16884)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-18 22:03:34 +01:00
Laurent Cozic 4724e3c0d8 Server: Only record changes for resource blobs under the .resource/ directory 2026-10-18 22:00:23 +01:00
Laurent Cozic c4c49b6d28 Desktop, Cli: Bind OneDrive OAuth and resource servers to loopback and validate OAuth state 2026-10-18 21:55:10 +01:00
renovate[bot] eba62ede92 fix(deps): update dependency pg to v8.22.0 (#16865)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-18 15:05:06 +01:00
renovate[bot] 95f21039bf fix(deps): update dependency onnxruntime-node to v1.27.0 (#16881)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-18 15:04:44 +01:00
renovate[bot] 1515e8b81c fix(deps): update dependency terminal-kit to v3.1.4 (#16879)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-18 00:24:12 +00:00
renovate[bot] 3dc0e2c0f6 chore(deps): update dependency concurrent-ruby to '< 1.3.9' (#16878)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-18 00:22:50 +00:00
renovate[bot] d0d35c173a chore(deps): update dependency @types/serviceworker to v0.0.200 (#16867)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-17 11:15:58 +00:00
Miguel Lima 71f403d0db All: Translation: Update pt_BR.po (#16810) 2026-10-16 17:32:16 -04:00
Milo Ivir 7c0e5654c6 All: Translation: Update hr_HR.po (#16861) 2026-10-16 17:31:03 -04:00
renovate[bot] c4254c3774 chore(deps): update dependency npm-package-json-lint to v10.4.1 (#16857)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-16 18:25:32 +01:00
renovate[bot] e255ac99be fix(deps): update dependency @react-native-vector-icons/material-design-icons to v13.1.2 (#16859)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-16 18:25:19 +01:00
renovate[bot] cd9f2728bd fix(deps): update dependency @react-native-vector-icons/material-design-icons to v13 (#16856)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-16 14:36:42 +01:00
renovate[bot]andLaurent Cozic 4c0fbaae67 chore(deps): update dependency npm-package-json-lint to v10 (#16855)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-10-16 14:36:31 +01:00
renovate[bot]andLaurent Cozic 44031f1cbb fix(deps): update dependency node-cron to v4.4.1 (#16851)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-10-16 14:36:12 +01:00
renovate[bot] ef61ac2043 fix(deps): update dependency @react-native-vector-icons/get-image to v13 (#16830)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-16 11:20:19 +01:00
Laurent Cozic 3fb835a9bf Chore: Fix flaky Note screen reload test leaking an open act() scope 2026-10-16 10:01:31 +01:00
renovate[bot] a28eb47c1a fix(deps): update dependency @react-native-vector-icons/ionicons to v13.1.2 (#16850)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-16 08:46:11 +01:00
Wei-Lun LiandClaude Opus 5.5 e43be19dbb All: Translation: Update zh_TW.po (#16853)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 14:56:58 -04:00
renovate[bot] 6753a76b99 fix(deps): update dependency @react-native-vector-icons/ionicons to v13 (#16843)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-15 14:29:31 +01:00
renovate[bot] 79eccc2d67 chore(deps): update dependency @playwright/test to v1.61.1 (#16844)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-15 14:29:08 +01:00
mrjo118 057596c23c Desktop, Mobile: Fixes #16734: Upgrade CodeMirror View to v6.43.14 (#16835) 2026-10-14 21:43:24 +01:00
renovate[bot] f3d8e33847 chore(deps): update dependency @playwright/test to v1.61.0 (#16840)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-14 21:04:20 +01:00
renovate[bot] 0d4947bc8e fix(deps): update dependency @react-native-vector-icons/fontawesome5 to v13.1.2 (#16841)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-14 21:04:01 +01:00
renovate[bot] 321dcdbbd5 fix(deps): update dependency @react-native-vector-icons/fontawesome5 to v13 (#16829)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-14 16:30:09 +01:00
renovate[bot]andLaurent Cozic 6b5e3db472 chore(deps): update dependency axios to v1.18.0 (#16828)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-10-14 16:24:08 +01:00
Laurent Cozic 17d3e378d2 Chore: Trying to fix flaky test failure on CI 2026-10-14 12:02:55 +01:00
Laurent Cozic c17f98efb4 Chore: Set published Joplin Server image as latest 2026-10-13 09:59:37 +01:00
Henry Heino 6de5ec074a Desktop: Fix error overlay text has low contast in dark mode (#16827) 2026-10-12 21:26:41 +01:00
mrjo118andLaurent Cozic c87f694dc7 Mobile: Fix issues with modals created via plugins (#16674)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-10-11 16:18:55 +01:00
Laurent Cozic 21d0a8ad71 Revert "Desktop: Resolves #16698: OneNote import (*.one / *.onepkg) doesn’t import notes with title longer than about 240 chars" (#16824) 2026-10-11 16:17:26 +01:00
mrjo118andLaurent Cozic 553ddf6ec1 Mobile: Refresh note when updated by the sync for editor plugins (#16688)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-10-11 16:15:50 +01:00
Aissa Benfodda 7c619e4744 Desktop: Resolves #16698: OneNote import (*.one / *.onepkg) doesn’t import notes with title longer than about 240 chars (#16752) 2026-10-11 16:15:24 +01:00
Keshav 3f7dde0045 Desktop: Fixes #16758: Stop external editing when a note gets locked on another device (#16762) 2026-10-11 16:07:47 +01:00
Henry Heino 052583a8fb Server: Allow syncing with a web app instance hosted on a custom domain (#16760) 2026-10-11 16:02:44 +01:00
Ahmet SezenandClaude Opus 5.5 c51a2cb7e1 Desktop: Fixes #16783: Fix AI chat counting tool results twice towards the size limit (#16784)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 16:02:13 +01:00
Aissa Benfodda 8905795ed1 Server: Resolves #16757: Restore footer (#16763) 2026-10-11 16:01:21 +01:00
Aissa Benfodda 9c820ef8e2 Server: Resolves #16795: Published notebook sidebar has no overlay on small screens (#16798) 2026-10-11 15:58:13 +01:00
renovate[bot] 6bca95dbdc chore(deps): update dependency sass to v1.101.0 (#16813)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-11 11:02:25 +01:00
renovate[bot] 7da9753f71 fix(deps): update dependency tar to v7.5.20 (#16814)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-10 12:22:34 +00:00
renovate[bot] 03c2fa6b07 fix(deps): update dependency dompurify to v3.4.12 (#16811)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-09 22:19:28 +01:00
Laurent Cozic 02f5db5bc8 Doc: More neutral tone in SECURITY.md 2026-10-09 17:46:17 +01:00
renovate[bot] cb2f658b6c fix(deps): update dependency abcjs to v6.6.4 (#16805)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-09 00:23:11 +00:00
renovate[bot] 26ce406e49 chore(deps): update dependency @docusaurus/plugin-sitemap to v3.10.2 (#16801)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-08 22:19:53 +01:00
renovate[bot] 33449150e5 fix(deps): update dependency sharp to v0.35.0 (#16802)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-08 22:19:37 +01:00
Henry HeinoandLaurent Cozic b0affc0a30 Mobile: Implement redesigned screen header (#16345)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-10-08 12:03:38 +01:00
Laurent Cozic 00d62e8700 Desktop: Fix external editor saves being lost immediately after opening a note (#16771) 2026-10-07 08:48:41 +01:00
renovate[bot] 5c7eab0808 chore(deps): update dependency yauzl to v3.4.0 (#16767)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-06 22:57:54 +01:00
renovate[bot] d5a6aeaa75 chore(deps): update dependency @crowdin/cli to v4.14.3 (#16765)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-06 20:24:55 +01:00
mrjo118 bc8bbb85dd Desktop, Mobile, Cli: Fixes #15601: Fix S3 sync failing in the web app with getReader TypeError on some browsers (#16746) 2026-10-06 15:51:39 +01:00
mrjo118 e972fd2397 Desktop, Mobile: Fixes #16759: Fix revision service does not start if the sync on startup fails (#16761) 2026-10-06 15:49:40 +01:00
Henry HeinoandLaurent Cozic ab241bd8d6 Desktop, Mobile: Resolves #16710: Improve E2EE UI when a key is stored in an unknown format (#16711)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-10-06 15:42:30 +01:00
Henry HeinoandLaurent Cozic 9e4ca948be Mobile: Fixes #16589: Upgrade React Native to v0.86, Expo to v57 (#16636)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-10-06 15:37:33 +01:00
9f84bda5db Mobile, Desktop, Cli: Resolves #16130: Encrypt plaintext bodies on ungated saves of locked notes (#16131)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
Co-authored-by: mrjo118 <jo.118@hotmail.com>
2026-10-06 15:31:11 +01:00
renovate[bot] 5dd5842401 fix(deps): update dependency js-yaml to v4.2.0 (#16755)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-06 15:26:27 +01:00
Sriram Varun KumarandLaurent Cozic aaa6f8e3ae Doc: Resolves #16603: Add documentation for conflict auto-merge and desktop resolution UI (#16604)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-10-05 14:09:44 +01:00
renovate[bot] 618f7e4ff7 fix(deps): update dependency prosemirror-dropcursor to v1.8.3 (#16749)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-05 04:24:23 +00:00
renovate[bot] a47a208aa4 chore(deps): update dependency concurrent-ruby to '< 1.3.8' (#16748)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-05 04:22:45 +00:00
renovate[bot] 850030faf6 chore(deps): update dependency @types/nodemailer to v7.0.12 (#16747)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-05 00:22:28 +00:00
Laurent Cozic 5bffa4b728 Chore: Fix flaky test checker 2026-10-04 23:39:06 +01:00
renovate[bot]andLaurent Cozic 6bfd8bb5d9 Update actions/upload-artifact action to v7 (#16735)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-10-04 22:42:51 +01:00
renovate[bot]andLaurent Cozic 7042c2233c Update dependency @react-native-community/datetimepicker to v9 (#16739)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-10-04 22:42:44 +01:00
renovate[bot] 0642345314 chore(deps): update dependency @types/nodemailer to v7 (#16742)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-04 22:42:22 +01:00
James Carroll e8d15e7f92 Chore: Resolves #16119: Remove --no-sandbox checks in installation script (#16738) 2026-10-04 13:04:35 +01:00
Laurent Cozic d32307364c CI: Repeat test files changed by a pull request to detect flaky tests (#16741) 2026-10-04 12:59:25 +01:00
qx100 ca681c57c6 All: Translation: Update zh_CN.po (#16737) 2026-10-03 15:55:58 -04:00
mrjo118 97ab341275 Desktop: Show export warnings on export completion to match mobile behaviour (#16682) 2026-10-03 20:11:32 +01:00
Laurent Cozic cd32d862c4 Chore: Fixed flaky test 2026-10-03 19:21:53 +01:00
renovate[bot]andLaurent Cozic b94994b6e7 Update nvidia/cuda Docker tag to v13.3.0 (#16730)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-10-03 14:12:54 +01:00
renovate[bot] 90d810c0c6 Update actions/create-github-app-token action to v3 (#16732)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-03 14:12:35 +01:00
Laurent Cozic 8db668e22f Chore: Trying to fix flaky test on CI 2026-10-03 14:10:50 +01:00
renovate[bot] b04a5f0489 Update dependency ts-loader to v9.6.2 (#16731)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-03 00:23:02 +00:00
Laurent Cozic 0164f34d8f Chore: Trying to fix flaky iOS UI test by waiting for elements before tapping 2026-10-02 23:51:01 +01:00
renovate[bot] 42cd68983b Update dependency ts-loader to v9.6.0 (#16729)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-02 21:16:25 +01:00
Aissa Benfodda f15506b873 Server: Resolves #16669: Published notebook notes cannot be scrolled down on Android browsers (#16702) 2026-10-02 17:13:30 +01:00
mrjo118 2603333ae5 Desktop: Fixes #15865: Fix toolbar state becoming disabled on a secondary window when the note has been moved (#16620) 2026-10-02 17:09:07 +01:00
Aissa Benfodda 1008705034 iOS: Resolves #16551: "Share" in the note menu does nothing on iOS (promise never settles) (#16664) 2026-10-02 17:08:21 +01:00
Henry Heino c64cfb1943 Desktop: Fix infinite loop in some cases when the notebook ordering is set to "updated date" (#16692) 2026-10-02 16:53:05 +01:00
Sriram Varun Kumar 2a901dea1d Mobile, Desktop, Cli: Resolves #16690: Automerge adds a blank line when one side deleted lines the other edited (#16706) 2026-10-02 16:52:08 +01:00
mrjo118 c199af7ca8 Desktop, Mobile: Fix displayed note updated_time is reverted to the remote updated_time upon auto merging a conflict (#16727) 2026-10-02 16:51:04 +01:00
Henry Heino 5a48b72d43 Chore: Desktop: Refactor warning banner (#16722) 2026-10-02 16:50:03 +01:00
Keshav 7fb04b0a11 Desktop, Mobile: Resolves #16724: Label the note list padlock on desktop and match its colour to the title on mobile (#16725) 2026-10-02 16:49:21 +01:00
renovate[bot] b1c43ccce9 Update dependency webpack-dev-server to v5.2.6 (#16714)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-01 10:27:16 +01:00
renovate[bot] def3e7928f Update eslint (#16715)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-01 10:26:13 +01:00
Joplin Bot 44bd68dbe3 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-10-01 04:36:37 +00:00
Pål Martin 9cbf49b260 All: Translation: Update nb_NO.po (#16705) 2026-09-30 13:38:34 -04:00
Pål Martin cd996c2eff All: Translation: Update eo.po (#16704) 2026-09-30 13:36:40 -04:00
renovate[bot] 06ebfbacde Update dependency terminal-kit to v3.1.3 (#16700)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-29 19:52:24 +02:00
renovate[bot] a653fe877d Update dependency fs-extra to v11.3.6 (#16696)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-29 12:29:23 +00:00
renovate[bot] 1499d4f11f Update dependency adm-zip to v0.5.18 (#16695)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-29 05:33:14 +00:00
renovate[bot] a560f14917 Update dependency @types/serviceworker to v0.0.199 (#16694)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-29 02:52:57 +00:00
renovate[bot] 70665897b5 Update dependency node-forge to v1.4.0 (#16673)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-29 00:27:21 +02:00
renovate[bot] 4c0e2bc0e2 Update dependency @playwright/test to v1.60.0 (#16665)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-27 01:19:02 +02:00
renovate[bot] 3cb9ad062d Update dependency chokidar to v3.6.0 (#16667)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-27 01:18:54 +02:00
renovate[bot] c5f185aacd Update dependency tar to v7.5.19 (#16666)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-26 04:22:27 +00:00
Joplin Bot 80af3696f8 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-09-26 03:57:03 +00:00
renovate[bot] 625f474b6f Update dependency ts-loader to v9.5.7 (#16663)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-25 22:38:00 +00:00
Laurent Cozic d8d9218b76 Merge branch 'release-3.7' into dev 2026-09-25 19:34:37 +01:00
Laurent Cozic e41516e669 Desktop release v3.7.21 2026-09-25 19:32:38 +01:00
Laurent Cozic 97b655510a Update lock files 2026-09-25 19:32:30 +01:00
Joplin Bot c58052c48b Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-09-25 16:48:48 +00:00
Laurent Cozic e84f3614b4 Desktop release v3.7.20 2026-09-25 17:13:32 +01:00
Laurent Cozic a15bead867 Merge remote-tracking branch 'origin/dev' into dev 2026-09-25 17:11:48 +01:00
Laurent Cozic 6ae9353849 Merge branch 'release-3.7' into dev 2026-09-25 17:09:18 +01:00
renovate[bot] a03416829c chore(deps): update dependency python to v3.14.4 (#16656)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-25 16:05:42 +00:00
Laurent Cozic 003f4bf9b7 Releasing sub-packages 2026-09-25 17:04:34 +01:00
Laurent Cozic 6061d506ea Releasing sub-packages 2026-09-25 17:04:34 +01:00
Aissa Benfodda 8b76139278 Desktop: Resolves #16498: AI Chat: Persist conversation history and add a chat switcher (#16657) 2026-09-25 17:02:35 +01:00
Laurent Cozic 83059a860e Doc: Update company registered address 2026-09-25 16:28:24 +01:00
Laurent Cozic fdd4005528 iOS 13.7.6 2026-09-25 12:32:32 +01:00
Laurent Cozic 70eac1b2ec Android 3.7.11 2026-09-25 12:31:22 +01:00
Laurent Cozic 545a448ca9 Desktop release v3.7.19 2026-09-25 12:13:21 +01:00
mrjo118 d9f0b4206a Desktop: Fixes #16215: Increase defaultAutoSelectFamilyAttemptTimeout to 500 ms (#16576) 2026-09-25 12:04:03 +01:00
mrjo118 c32c50fbe9 Desktop: Fixes #16628: Prevent note being selected in the primary window when a secondary window is closed when it's not in focus (#16629) 2026-09-25 12:02:35 +01:00
Laurent Cozic 0a8bfffb96 Desktop, Mobile: Fix table editor losing and mis-rendering inline markdown (#16632) 2026-09-25 12:01:53 +01:00
Henry Heino 4978154dc1 Web: Fixes #16633: Fix sync (#16634) 2026-09-25 12:01:44 +01:00
renovate[bot] 6e28b0a40d chore(deps): update dependency @babel/plugin-transform-export-namespace-from to v7.29.7 (#16639)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-25 11:55:49 +01:00
Henry Heino ef29c43cae Desktop, Mobile, Cli: Fixes #16647: Fix notes and sub-folders are still marked as published after being moved out of a published folder (#16648) 2026-09-25 11:55:30 +01:00
Henry Heino 310b5bbbb6 Desktop: Resolves #16644: Improve how semantic search interacts with item IDs (#16652) 2026-09-25 11:55:00 +01:00
Henry Heino 61eecacf49 Chore: Coding style: Prefer single quotes for SQLite string literals (#16653) 2026-09-25 11:54:44 +01:00
Aissa Benfodda 51560e8084 Desktop: Resolves #16498: AI Chat: Persist conversation history and add a chat switcher (#16583) 2026-09-25 11:53:50 +01:00
renovate[bot] dbdc233bbb fix(deps): update dependency tar to v7.5.17 (#16646)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-25 03:34:38 +00:00
renovate[bot] 32fde22f54 fix(deps): update dependency nanoid to v5.1.16 (#16643)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-23 04:59:45 +00:00
Jonatan Nyberg 1d6beb0443 All: Translation: Update sv.po (#16637)
Signed-off-by: Jonatan Nyberg <84130654+NickWick13@users.noreply.github.com>
2026-09-22 13:42:58 -04:00
renovate[bot] c795a55895 chore(deps): update docker/login-action action to v4 (#16627)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-20 14:35:38 +01:00
renovate[bot] 49b677ea33 chore(deps): update dependency @types/node to v22.19.21 (#16614)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-20 12:16:42 +01:00
mrjo118 7fdcf1eabd Desktop, Mobile: Fix sync status incorrectly calculated for conflict notes (#16621) 2026-09-20 11:04:48 +01:00
mrjo118 227dbdedfc Mobile: Fixes #15892: Prevent the open side menu gesture when in multi selection mode (#16619) 2026-09-20 10:56:38 +01:00
mrjo118 b15d55765a Mobile: Fixes #16597: Avoid overriding the text selection colour (#16618) 2026-09-20 10:56:13 +01:00
Laurent Cozic 56ce951f37 Desktop, Mobile: Handle xlink:href in the note link sanitizer (#16616) 2026-09-20 10:55:42 +01:00
Eric Duarte f2a3569d72 Update Catalan translation of the ca.po file. Correct translation err… (#16571) 2026-09-19 17:17:17 +01:00
Laurent Cozic 12cdce0d43 Desktop, Mobile: Fix AI configuration test failing on reasoning models (#16594) 2026-09-19 17:16:36 +01:00
Jozef Gaal a3d94d4d8e All: Translation: Update sk_SK.po (#16609) 2026-09-19 17:15:45 +01:00
RobertGarciaa d1ac85c0d4 All: Translation: Update ko.po (#16568)
Signed-off-by: Robertgarciaa <chae0218@naver.com>
2026-09-19 17:15:18 +01:00
mrjo118 a7eaddeeee Mobile: Prevent scenario which can cause the note list to become blocked and make the app appear unresponsive (#16612) 2026-09-19 17:14:13 +01:00
Laurent Cozic 18ba88e90d Desktop: Add setting to hide the AI Chat toolbar button (#16607) 2026-09-19 17:13:49 +01:00
mrjo118 56ea34fbe4 Desktop: Fixes #16229: Deselect conflicts or trashed notes when switching to the all notes notebook (#16590) 2026-09-19 17:13:26 +01:00
renovate[bot] c81243852d fix(deps): update dependency react-native-safe-area-context to v5.8.0 (#16577)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-19 17:10:34 +01:00
renovate[bot] 6473be223a fix(deps): update dependency pg to v8.21.0 (#16573)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-19 17:10:10 +01:00
mrjo118 9e4b920cf1 Desktop, Mobile, Cli: Extend the logged basic delta report to include the count of items ignored by the sync (#16567) 2026-09-19 17:10:02 +01:00
mrjo118 552671dde8 Desktop, Mobile, Cli: Support syncing conflict notes (#16439) 2026-09-19 17:06:41 +01:00
Henry Heino 24e5778256 Desktop, Cli: Migrate from node-fetch to Undici (#16566) 2026-09-19 17:04:12 +01:00
Henry Heino cbd3445de9 Linux: Fixes #16457: Fix dropping notes into the whiteboard editor (#16565) 2026-09-19 17:03:56 +01:00
Henry Heino 8cc854c9be Chore: Refactor settings screen logic (#16564) 2026-09-19 17:00:17 +01:00
Keshav 2bddab0d04 Desktop, Mobile: Resolves #16038: Add note history support for locked notes (#16476) 2026-09-19 16:59:53 +01:00
Aissa Benfodda 76ea0093ca Desktop: Fixes #16252: Converting HTML note to Markdown results in new noteID, which breaks any internal Joplin links to that original note (#15588) 2026-09-19 16:43:40 +01:00
renovate[bot] e76fed31df fix(deps): update dependency @adobe/css-tools to v4.5.0 (#16613)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-19 13:59:09 +01:00
renovate[bot] 2b217ce3f9 chore(deps): update dependency sass to v1.100.0 (#16611)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-19 10:55:36 +01:00
renovate[bot] f4ecbed4e6 fix(deps): update dependency nanoid to v5.1.15 (#16606)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-19 01:11:28 +00:00
Henry Heino a859a62506 Server: Accessibility: Associate labels with controls on the "Your Profile" page (#16588) 2026-09-18 19:10:16 +01:00
Henry Heino 32578c761e Web: Fixes #16593: Fix crash when clicking the "upgrade app banner" (#16591) 2026-09-18 19:09:56 +01:00
Henry Heino 27da730f04 Chore: Improve return type of reg.syncTarget() (#16592) 2026-09-18 19:09:36 +01:00
Laurent Cozic 4d36a87296 Desktop: Fix reasoning blocks leaking into AI chat replies (#16595) 2026-09-18 19:05:23 +01:00
thesunshade 0369f2fd84 Doc: Fix grammatical error in front.mustache (#16599) 2026-09-18 18:48:27 +01:00
renovate[bot] b90691d562 fix(deps): update dependency react-native-webview to v13.16.2 (#16600)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-18 13:53:01 +00:00
Joplin Bot 04f53e0c69 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-09-18 10:58:28 +00:00
Laurent Cozic 2c33e1db44 Doc: Update sponsors 2026-09-18 11:47:36 +01:00
Laurent Cozic ef70b72d38 Server: Fix Stripe subscription item being located by position instead of price 2026-09-18 00:07:00 +01:00
Laurent Cozic 36bed147fd Desktop, Cli: Fix ENEX import failing on Evernote's unescaped ampersands (#16580) 2026-09-17 17:33:42 +01:00
renovate[bot] 981a03c5c9 fix(deps): update dependency nanoid to v5.1.14 (#16581)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-16 23:13:49 +01:00
renovate[bot] 4f4494fe1b fix(deps): update dependency dompurify to v3.4.11 (#16572)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-15 23:15:06 +01:00
Jozef Gaal de42dc1e48 All: Translation: Update sk_SK.po (#16562) 2026-09-14 16:33:22 -04:00
Joplin Bot f611833b08 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-09-14 18:36:53 +00:00
Laurent Cozic 6459e6fffd Tools: Fix website build when no Windows arm64 asset exists
downloadUrl() throws when it finds no matching asset, so looking up the
arm64 build unconditionally broke the website build for every release
that predates it. Gate it behind the same flag as the download page,
which is now exported rather than duplicated.
2026-09-14 19:24:53 +01:00
Laurent Cozic fd7f557552 Doc: Add article about AI and privacy in Joplin 2026-09-14 19:22:08 +01:00
Laurent Cozic a2fcac2e08 Chore: CI: Trying to fix randomly failing tests 2026-09-13 22:40:17 +01:00
Laurent Cozic e5ebf70ced Desktop: Resolves #13146: Add Windows ARM64 build (#16555) 2026-09-13 20:52:11 +01:00
Laurent Cozic 7fc4abe2ce Server: Fix account not being created when the Stripe customer has no name (#16557) 2026-09-13 18:42:24 +01:00
mrjo118 468c40d09b Mobile: Add an optional sync button to the editor toolbar (#16268) 2026-09-13 12:01:13 +01:00
Eric DuarteandLaurent Cozic 0342c649bd updating and correcting old strings in the Spanish translation es_ES.po (#16417)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-09-13 11:50:50 +01:00
Arda Kılıçdağı e2036e4d60 Turkish translations updated (#16548) 2026-09-13 11:39:35 +01:00
Henry Heino a661b8897a Chore: Fixes #16503: Fix mobile startup tests not running (#16505) 2026-09-13 11:38:07 +01:00
Laurent Cozic de42c3fba6 Cli: Fixes #16541: Downgrade nanoid to fix ERR_REQUIRE_ESM crash on startup (#16542) 2026-09-13 11:38:00 +01:00
Laurent Cozic 330b86e634 Desktop, Mobile, Cli: Fixes #16504: Fix sync deadlock when Joplin Server rejects an item with HTTP 422 (#16544) 2026-09-13 11:37:43 +01:00
Mirko Kerezovic becfd248f9 Linux: Fixes #16434: Fix AppImage StartupWMClass (#16547) 2026-09-13 11:37:15 +01:00
Liffindra Angga Zaaldian 7c0c50eead All: Translation: Update id_ID.po (#16477) 2026-09-13 11:36:43 +01:00
Henry Heino 70ebecb781 Desktop: Fixes #16539: Fix LMStudio compatibility (#16552) 2026-09-13 11:36:20 +01:00
renovate[bot] 41b5934cb0 chore(deps): update dependency git to v2.53.0 (#16550)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-13 01:20:08 +01:00
Henry Heino 141c451ff9 Chore: Desktop: Add additional networking tests (#16501) 2026-09-12 00:24:41 +01:00
Joplin Bot 1e9f72738a Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-09-11 20:40:59 +00:00
Laurent Cozic dbde4e84b3 Desktop: Fix x-callback-url responses never reaching the calling app (#16497) 2026-09-11 16:00:09 +01:00
Keshav 362d613c3e Chore: Fixes #16494: Fix flaky note menu test and decryption warning (#16499) 2026-09-11 15:59:58 +01:00
renovate[bot] 94084f3677 chore(deps): update dependency nan to v2.27.0 (#16475)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-11 09:31:32 +01:00
renovate[bot]andLaurent Cozic ede6a35169 fix(deps): update dependency react-native-share to v12.3.1 (#16374)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-09-11 09:30:50 +01:00
Henry Heino b1979eda7c Mobile: Joplin Cloud login: Hide "invalid credentials" banner on login screen (#16490) 2026-09-11 09:30:22 +01:00
Laurent Cozic 59ecb92cd2 Server: Do not log invalid requests as errors (#16492) 2026-09-11 09:29:23 +01:00
Laurent Cozic 77a8249388 Server: Fixes sync errors when an upload is retried (#16491) 2026-09-11 09:29:05 +01:00
renovate[bot] 2fd7ac6573 fix(deps): update dependency form-data to v4.0.6 (#16496)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-11 08:23:16 +00:00
Laurent Cozic f97765f332 Server: Do not log failed image downloads as warnings (#16493) 2026-09-11 09:20:37 +01:00
renovate[bot] 1348db9624 chore(deps): update dependency webpack-dev-server to v5.2.5 (#16495)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-11 05:56:21 +00:00
renovate[bot] e356e2b508 fix(deps): update dependency dompurify to v3.4.10 (#16489)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-11 03:42:45 +00:00
Joplin Bot 26bff74d08 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-09-10 20:38:50 +00:00
Laurent Cozic f6b19f80e4 Merge branch 'release-3.7' into dev 2026-09-10 20:16:44 +01:00
Laurent Cozic 7ff01914f9 iOS 13.7.5 2026-09-10 20:16:21 +01:00
Laurent Cozic 57b2f3082f Android 3.7.10 2026-09-10 20:10:22 +01:00
Laurent Cozic ce25482097 Desktop release v3.7.18 2026-09-10 18:40:51 +01:00
Laurent Cozic 29fbc59a85 Desktop, Mobile, Cli: Fixes #16486: Fix WebDAV sync error "Protocol https: not supported" on servers that redirect (#16488) 2026-09-10 18:38:59 +01:00
Henry Heino e55856d74c Mobile: Joplin Cloud login screen: Fix loading indicator is difficult to see in dark mode (#16480) 2026-09-10 18:00:28 +01:00
Henry Heino e0f51d8f43 Server, Web: Applications tab: Fix web app is marked as "unknown" (#16478) 2026-09-10 18:00:06 +01:00
Laurent Cozic 2d27dc4992 Android 3.7.9 2026-09-10 16:57:35 +01:00
Laurent Cozic d242a43b23 Desktop release v3.7.17 2026-09-10 16:03:54 +01:00
Aissa Benfodda 79875c5c8f Desktop: Resolves #16424: Fix notes imported from OneNote are displayed with a very small font size (#16440) 2026-09-10 14:43:28 +01:00
Aissa Benfodda 77ea73d669 Desktop: Resolves #16427: Tag area broken with Japanese language (#16483) 2026-09-10 14:27:48 +01:00
Henry Heino df261c2465 Desktop: Fixes #16454: In-editor Markdown rendering: Fix markup syntax is visible while clicking and dragging the scrollbar (#16461) 2026-09-09 23:55:14 +01:00
Laurent Cozic c3c0d56e35 Desktop: Fixes #16414: Return 202 for MCP notification requests (#16473) 2026-09-09 23:54:07 +01:00
Laurent Cozic f82b0c7850 Desktop: Fixes #16431: Treat private network AI providers as local and make their API key optional (#16472) 2026-09-09 23:54:00 +01:00
Laurent Cozic 9b11d3b595 Desktop, Mobile, Cli: Fixes #16422: Fix WebDAV sync failing with 409 errors after 3.7 upgrade (#16471) 2026-09-09 23:53:47 +01:00
Laurent Cozic 7fb480dc1a Desktop: Fixes #16457: Whiteboard: Add missing "Add note to whiteboard" menu item (#16468) 2026-09-09 23:53:24 +01:00
Laurent Cozic 7ef7b5ee83 Desktop: Whiteboard: Double-click empty canvas to add a note (#16470) 2026-09-09 18:44:50 +01:00
Laurent Cozic 23504fcf7e Server: Add rate limiting to the signup and password reset endpoints (#16467) 2026-09-09 18:44:42 +01:00
Laurent Cozic d9d14ce692 Server: Fix notification read-state being updatable by non-owner (#16466) 2026-09-09 18:44:32 +01:00
Laurent Cozic 7eae6cf3fa Chore: Pin MacOs version on CI to prevent random failures 2026-09-09 17:06:59 +01:00
Laurent Cozic 7ff6bf48b5 Chore: lock file 2026-09-09 15:38:05 +01:00
Laurent Cozic 8a1669c76d Doc: Updated SECURITY.md with new rules for LLM-generated reports 2026-09-09 15:15:05 +01:00
Henry Heino 62f07b4f64 Mobile: Resolves #16442: Remove sync wizard button from config screen (#16458) 2026-09-09 12:07:46 +01:00
Laurent Cozic 6b09f9d378 Chore: Trying to fix flaky Goto Anything and editor plugin saving integration tests (#16455) 2026-09-09 12:07:32 +01:00
Henry Heino 5d733e3268 Linux: Fixes #16434: Fix missing window icon with certain system setups (#16437) 2026-09-09 12:06:16 +01:00
summoner a190a75737 All: Translation: Update hu_HU.po (#16418) 2026-09-09 12:06:07 +01:00
003233a225 Desktop: Fixes #16410: Prevent secondary instance from releasing profile lock (#16411)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
Co-authored-by: mrjo118 <jo.118@hotmail.com>
2026-09-09 12:05:01 +01:00
Henry Heino 6470029203 Mobile: Adjust sidebar appearance (#16389) 2026-09-09 12:04:42 +01:00
Aissa Benfodda 71b0c2027a Mobile, Desktop: Resolves #16066: Add button to toggle blockquotes in Markdown editor (#16385) 2026-09-09 12:03:15 +01:00
renovate[bot] 3a1cd0edc8 chore(deps): update dependency @types/nodemailer to v6.4.24 (#16463)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-09 09:39:26 +00:00
renovate[bot] 076ffc42e8 fix(deps): update dependency multiparty to v4.3.0 (#16460)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-09 00:17:53 +01:00
Laurent Cozic e2ff538613 Desktop: Fixes #16444: Allow OpenAI reasoning models to be used with tools (#16451) 2026-09-08 16:24:14 +01:00
Laurent Cozic 536790b37d Desktop: Fixes #16433: Toggle AI Chat keystroke shortcut does nothing 2026-09-08 12:54:38 +01:00
renovate[bot] 055f92b9f7 fix(deps): update dependency dompurify to v3.4.9 (#16445)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-08 10:43:01 +00:00
Laurent Cozic e2f30308f8 Server v3.7.2 2026-09-07 17:29:53 +01:00
Laurent Cozic 71d4b09d48 Chore: Fixed CI 2026-09-07 13:00:31 +01:00
Laurent Cozic 81f8bfed39 Merge branch 'release-3.7' into dev 2026-09-07 10:20:56 +01:00
Laurent Cozic 425a05ac4b CLI v3.7.1 2026-09-07 10:20:33 +01:00
Laurent Cozic de139fc9c1 Lock file 2026-09-07 10:19:42 +01:00
Laurent Cozic c17c205049 Releasing sub-packages 2026-09-07 10:19:13 +01:00
renovate[bot]andLaurent Cozic 7e73a2a271 fix(deps): update dependency onnxruntime-node to v1.26.0 (#16416)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-09-06 20:31:02 +01:00
renovate[bot]andLaurent Cozic c2d4d37403 fix(deps): update dependency tcp-port-used to v1.0.3 (#16415)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-09-06 20:30:30 +01:00
Henry Heino 63e1f216e1 Desktop, Mobile: Fixes #16407: Fix keyboard navigation issues related to level-1 headings (#16409) 2026-09-06 20:13:05 +01:00
Henry Heino e6a99b978a Chore: Allow the "Web" prefix in pull request titles (#16406) 2026-09-06 20:12:45 +01:00
Henry Heino a263733971 Web: Fix incorrect "Only one copy of Joplin can be open at a time" warning in Safari and Firefox (#16405) 2026-09-06 20:12:36 +01:00
Henry Heino e3e4aebc67 Web: Fixes #16379: Fix menu animation in Safari (#16404) 2026-09-06 20:12:25 +01:00
Aissa Benfodda ac2e04204b Mobile: Fixes #15982: Mobile: Tag list design should be consistent with the note list (#16386) 2026-09-06 12:53:44 +01:00
mrjo118 e611eb4f0c Desktop: Fixes #16363: Fix the editor bar being disabled when opening a conflict or trashed note in a new window (#16369) 2026-09-06 12:50:27 +01:00
mrjo118 5ae6b7b6c6 Desktop, Mobile, Cli: Allow cancelling sync during potentially lengthy start and finish steps (#16362) 2026-09-06 12:50:14 +01:00
Aissa Benfodda 82652953a8 Desktop: Resolves #16209: Unpublish button uses a misleading share icon (#16358) 2026-09-06 12:49:10 +01:00
mrjo118 f4069b5f02 Desktop: Add reveal in notebook option (#16354) 2026-09-06 12:49:03 +01:00
Henry Heino c27de4f34e iOS, Android: Database: Migrate to expo-sqlite (#16347) 2026-09-06 12:47:42 +01:00
mrjo118 b6c2efc567 Android: Improve performance of file system sync (#16321) 2026-09-06 12:41:13 +01:00
Aissa BenfoddaandLaurent Cozic c07d94cc95 Desktop: Fixes #15789: Joplin Desktop external edit feature sometimes causes data loss (#16318)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-09-06 12:40:15 +01:00
Keshav ac5b30aed7 Mobile: Resolves #16002: Add note lock UI (#16069) 2026-09-06 12:38:59 +01:00
Keshav 39a0ce387c Desktop: Resolves #16160: Use lock wording for note lock actions and messages (#16292) 2026-09-06 12:24:27 +01:00
Sriram Varun Kumarandmrjo118 70c2d357b9 Mobile, Desktop, Cli: Resolves #16020: Automatically merge non-overlapping note changes on sync conflicts (#16023)
Co-authored-by: mrjo118 <jo.118@hotmail.com>
2026-09-06 12:23:33 +01:00
Laurent Cozic a92b37e3e7 Merge branch 'release-3.7' into dev 2026-09-06 11:55:55 +01:00
Joplin Bot e67e4d7685 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-09-06 10:30:46 +00:00
Laurent Cozic e3f38c6f1f Desktop release v3.7.16 2026-09-05 20:41:51 +01:00
Laurent Cozic e05c11b17a Chore: Pin macOS M1 build OS to prevent failure on recent macOS image 2026-09-05 20:41:40 +01:00
Joplin Bot 7969c8fce9 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-09-05 14:50:22 +00:00
Laurent Cozic fa25937bc3 Merge branch 'release-3.7' into dev 2026-09-05 11:10:35 +01:00
Laurent Cozic 949d10476f Android 3.7.8 2026-09-05 11:10:15 +01:00
Laurent Cozic c397df5e4c Desktop release v3.7.15 2026-09-05 10:58:57 +01:00
Laurent Cozic 2727aab2da iOS 13.7.4 2026-09-05 09:34:13 +01:00
Laurent Cozic e40b2f50f7 Chore: Update translations 2026-09-05 09:31:17 +01:00
summoner 6da783abe5 All: Translation: Update hu_HU.po (#16408) 2026-09-05 09:28:08 +01:00
renovate[bot] 21a7dd4307 fix(deps): update dependency react-native-zip-archive to v7.1.1 (#16387)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-03 17:25:37 +01:00
renovate[bot] c7dea58402 fix(deps): update dependency @react-native-community/netinfo to v12 (#16388)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-03 17:25:15 +01:00
Laurent Cozic 5df15f3737 Server: Improve logging for shared notebook maintenance (#16400) 2026-09-03 13:45:31 +01:00
ERYpTION 1090c509e4 All: Translation: Update da_DK.po (#16380) 2026-09-02 05:23:52 -04:00
renovate[bot] 518fbb40f8 fix(deps): update dependency node-diff3 to v3.2.1 (#16376)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-02 00:06:42 +01:00
renovate[bot] 517839d99a fix(deps): update dependency dompurify to v3.4.8 (#16373)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-01 22:42:11 +01:00
Joplin Bot c7a4f4c031 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-09-01 11:20:54 +00:00
Henry Heino 845dc16402 Android: Fix additional space present below new note menu on some devices (#16361) 2026-09-01 09:51:35 +01:00
renovate[bot] 46303619c3 chore(deps): update eslint (#16365)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-01 09:36:07 +01:00
Laurent Cozic c048bd9464 Merge branch 'release-3.7' into dev 2026-09-01 01:52:50 +01:00
Laurent Cozic fd3a2fb727 Update translations 2026-09-01 01:52:25 +01:00
Laurent Cozic 7d37bbe29c Update translations 2026-09-01 01:51:16 +01:00
Joplin Bot a0d83c5115 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-08-31 23:09:52 +00:00
Laurent Cozic 5755024a31 Doc: Add release notes 3.7 (#16360) 2026-08-31 23:59:35 +01:00
renovate[bot] 65d0a81a37 fix(deps): update dependency tar to v7.5.16 (#16356)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-30 20:32:08 +01:00
Joplin Bot 77b2ecf567 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-08-30 16:18:26 +00:00
Laurent Cozic 3f23202ec1 Merge branch 'release-3.7' into dev 2026-08-30 12:48:46 +01:00
Laurent Cozic 205c284fb8 Desktop release v3.7.14 2026-08-30 12:48:11 +01:00
Henry Heino a79a2b4de7 Desktop: Resolves #16350: External editor: Add additional logic to avoid overwriting a note with older content (#16352) 2026-08-30 12:47:56 +01:00
mrjo118 6fb816d014 Desktop: Fixes #16059: Stop external editing on note deletion (#16355) 2026-08-30 10:24:03 +01:00
renovate[bot] f49eb1a597 fix(deps): update dependency onnxruntime-node to v1.25.1 (#16338)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-29 22:44:10 +01:00
renovate[bot] d4d9df18d1 fix(deps): update dependency tar-stream to v3.2.0 (#16332)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-29 22:43:49 +01:00
renovate[bot] a903027344 chore(deps): update dependency yauzl to v3.3.2 (#16351)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-29 21:17:08 +01:00
Joplin Bot b7beb0bd06 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-08-28 21:56:10 +00:00
Laurent Cozic 5a4a222972 iOS 13.7.3 2026-08-28 19:17:22 +01:00
Laurent Cozic 6d0f6c023a Android 3.7.7 2026-08-28 19:16:28 +01:00
Laurent Cozic e51093d736 Desktop release v3.7.13 2026-08-28 18:52:49 +01:00
Henry Heino f488dad27a Chore: Tests: Silence expected handleCallbackUrl warnings (#16343) 2026-08-28 18:51:20 +01:00
Aissa Benfodda 44751ef56b Desktop, Cli: Resolves #15223: Add support to import notes from Obsidian (#16136) 2026-08-28 18:43:12 +01:00
renovate[bot] bf69aedf7c chore(deps): update dependency @rollup/plugin-commonjs to v29.0.3 (#16331)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-28 17:42:26 +00:00
Henry Heino 683240968b Desktop: Fixes #15890: Limit maximum size of stored OCR text (#16325) 2026-08-28 09:49:09 +01:00
mrjo118 9bc4af486a Desktop, Mobile: Fixes #16155: Prevent conflicts initially showing alongside the original note in the notebook when created (#16330) 2026-08-28 09:48:55 +01:00
Henry Heino 7fba0fda68 Desktop: Fixes #16317: Markdown editor: Disable table editor when opting out of in-editor rendering (#16333) 2026-08-28 09:47:55 +01:00
Henry Heino 3dc1f5fef6 Chore: Tests: Silence expected warning in AsyncActionQueue tests (#16334) 2026-08-28 09:47:43 +01:00
Henry Heino 8b97347f3a Chore: Tests: Increase timeout in useSearchMarkers test (#16335) 2026-08-28 09:46:57 +01:00
mrjo118 bfe78ca8a0 Desktop: Improve refresh for whiteboards and ensure undo stack is cleared upon note refresh (#16337) 2026-08-28 09:46:48 +01:00
Henry Heino 3899ca9ce5 Chore: Desktop: Refactor end-to-end tests to reduce dependence on a particular default editor state (#16339) 2026-08-28 09:45:41 +01:00
Henry Heino 098b1edaa5 Desktop: Resolves #16120: Increase semantic search minimum relevance (#16340) 2026-08-28 09:45:11 +01:00
mrjo118 4998ec221b Desktop: Fixes #16234: Fix position does not always re-center when switching to a whiteboard (#16341) 2026-08-28 09:43:45 +01:00
mrjo118 3156f23fa2 Desktop: Fixes #16202: Prevent password checking loop when verifying the encryption password (#16267) 2026-08-27 10:01:45 +01:00
Henry Heino 347915c4ba Mobile, Desktop, Cli: Resolves #16251: Sync: Enable new notebook sharing key format (#16305) 2026-08-27 10:01:15 +01:00
Henry Heino 8f98933f14 Desktop: Sharing: Improve error message when public key format is unsupported (#16306) 2026-08-27 10:00:17 +01:00
Henry Heino 9b75d9e4d8 Desktop: Rich Text Editor: Fix error while saving changes (#16307) 2026-08-27 09:59:56 +01:00
Henry Heino dff722d6d8 Desktop: Fixes #12811: Rich Text Editor: Save note changes after changing formatting (#16308) 2026-08-27 09:59:41 +01:00
mrjo118 e6dc8d8222 Desktop: Prevent ability to link to note or set tags for a trashed note after switching to all notes (#16320) 2026-08-27 09:57:40 +01:00
Henry Heino 97fb8d5ec2 Mobile: Fixes #16312: Improve shadow colors in dark mode (#16326) 2026-08-27 09:52:24 +01:00
renovate[bot] 1adc40ab30 fix(deps): update dependency expo to v54.0.35 (#16329)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-27 06:47:03 +01:00
Laurent Cozic 8e9ffe3386 Chore: Add ESLint rule to enforce describe() titles match test file names (#16319) 2026-08-26 21:01:34 +01:00
renovate[bot] b6889d3e7d fix(deps): update dependency dompurify to v3.4.7 (#16313)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-25 20:54:35 +01:00
renovate[bot] c174faf77d fix(deps): update dependency react-native-paper to v5.15.3 (#16302)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-24 18:44:45 +00:00
Ivan 079bf7e48d Server: Fixes #15226: Fix SQLite database getting permanently locked (#16244) 2026-08-24 18:22:01 +01:00
renovate[bot] 0141c60d01 fix(deps): update dependency dompurify to v3.4.6 (#16300)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-24 16:17:52 +01:00
Laurent Cozic fc76a884f3 Desktop: Fixes #15278: Fix video/audio in Markdown viewer continuing to play after hiding it or switching notes (#16299) 2026-08-24 14:20:15 +01:00
Henry Heino 2a83642dbf Desktop, Mobile: Fixes #16285: Editor: Avoid crash when editing certain partially-loaded documents (#16286) 2026-08-24 11:27:24 +01:00
Henry Heino b21a55e073 Desktop, Mobile: Fixes #15770: HTML notes: Fix certain content incorrectly renders as linked text (#16289) 2026-08-24 11:23:24 +01:00
renovate[bot] b8919220a7 fix(deps): update dependency dayjs to v1.11.21 (#16298)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-24 10:31:37 +01:00
Laurent Cozic f034ad244b Desktop: Fix share notebook error message disappearing after half a second 2026-08-23 22:58:28 +01:00
renovate[bot] 73064ec468 fix(deps): update dependency prosemirror-search to v1.1.1 (#16293)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-23 17:31:32 +00:00
Henry Heino 94911a86ff Desktop, Mobile: Markdown editor: Support opening same-note links to elements with id="..." attributes (#16287) 2026-08-22 22:04:24 +01:00
Henry Heino 92eed39ff7 Chore: Mobile: Deduplicate ProseMirror dependencies (#16283) 2026-08-22 19:49:54 +01:00
Joplin Bot 55b1a88b88 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-08-22 18:38:51 +00:00
Keshav 50444adc23 Api, Cli, Desktop: Resolves #16054: Add fail-closed guardrails for locked notes (#16098) 2026-08-22 18:47:02 +01:00
Laurent Cozic 0fa387b463 Plugin Repo CLI v3.7.3 2026-08-22 11:55:21 +01:00
Laurent Cozic 61c58955d3 Tools: Fix plugin-repo-cli to load its self-contained bundle at runtime 2026-08-22 11:54:42 +01:00
Laurent Cozic 14dc994e58 Plugin Repo CLI v3.7.2 2026-08-22 11:50:44 +01:00
Laurent Cozic a8ea99fa59 Tools: Move bundled workspace packages to devDependencies in plugin-repo-cli 2026-08-22 11:50:05 +01:00
Henry HeinoandLaurent Cozic 6845e9d1ee Desktop: Resolves #6088: Highlight global search matches in the Rich Text Editor (#11361)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-08-22 11:46:46 +01:00
Henry Heino 99f0779db5 Chore: Tests: Mobile: Fix "failed to set timeout" warning in note screen tests (#16259) 2026-08-22 11:46:38 +01:00
mrjo118 6a64879e05 Desktop: Fixes #16250: Fix exiting having no visible effect when on the settings screen during a long sync (#16278) 2026-08-22 11:42:07 +01:00
mrjo118 67e6870ec9 Desktop: Fixes #15258: Show a warning icon when a sync completes with errors (#16279) 2026-08-22 11:34:58 +01:00
Laurent Cozic 7fb9ec93e4 Desktop: Strip style elements from whiteboard card content (#16275) 2026-08-22 10:36:36 +01:00
Laurent Cozic 27ae5c0d40 Desktop: Restrict Web Clipper pairing to extension origins (#16276) 2026-08-22 10:36:25 +01:00
Laurent Cozic 8353a82fc0 Desktop: Validate x-callback-url response targets (#16269) 2026-08-22 10:35:38 +01:00
Laurent Cozic 7766eefa11 Server: Scope password-reset tokens to their intended purpose (#16274) 2026-08-21 22:54:19 +01:00
Laurent Cozic 1a0a50afc9 Server: Enforce session and CSRF checks on the application authorisation POST (#16270) 2026-08-21 22:00:03 +01:00
Aissa Benfodda eade70040f Mobile: Fixes #15920: Mobile: When toggling a checkbox which is not at the cursor position, it will scroll away from the checkbox back to the cursor position (#16173) 2026-08-21 14:44:04 +01:00
Henry Heino d36432dd0a Desktop: Improve chat connection check (#16256) 2026-08-21 14:42:37 +01:00
Henry Heino 49756e09f1 Chore: Server: Fix truncateUserDataTables debug command only deletes old changes (#16257) 2026-08-21 14:31:36 +01:00
Henry Heino 7e4bdb64b1 Chore: Embedding tests: Silence expected warnings (#16258) 2026-08-21 14:31:18 +01:00
Joplin Bot dc4e0b464e Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-08-21 01:25:16 +00:00
Laurent Cozic b070bfed1f Server: Display JSB banner on larger Joplin Server instances (#16255) 2026-08-20 20:26:26 +01:00
Laurent Cozic 1aa705b97a Merge branch 'release-3.6' into dev 2026-08-20 20:06:07 +01:00
Laurent Cozic 05ae012ea3 iOS 13.6.11 2026-08-20 19:25:58 +01:00
Laurent Cozic 36ef85f044 Android 3.6.22 2026-08-20 19:25:36 +01:00
Laurent Cozic e1cf9245a5 Desktop release v3.6.16 2026-08-20 19:09:23 +01:00
Henry Heino 011a15374c Desktop, Mobile: Fixes #16070: Fix Markdown editor selection issues (#16126) 2026-08-20 19:06:43 +01:00
Henry Heino ae5855a85f Mobile, Desktop, Cli: Resolves #15753: Support syncing with Joplin v3.7 (#15889) 2026-08-20 19:06:22 +01:00
Laurent Cozic 8e7b1d763b Doc: Improve JSB copy 2026-08-20 17:51:02 +01:00
Laurent Cozic fbd3a423a3 Doc: Improve JSB copy 2026-08-20 17:48:54 +01:00
Laurent Cozic ec583f9c8a Doc: Display JSB equivalent prices per month 2026-08-20 17:46:12 +01:00
Laurent Cozic 941418a271 Doc: Improve CFA on Plans page 2026-08-20 17:37:59 +01:00
Laurent Cozic d22ebdb0a1 Doc: Mention JSB in Joplin Cloud doc 2026-08-20 17:27:09 +01:00
Laurent Cozic c1efee1049 Doc: Mention JSB license in Joplin Server README file 2026-08-20 17:27:09 +01:00
Laurent Cozic f1246530a4 Doc: Replace Tally links with JSB license page 2026-08-20 17:27:08 +01:00
Laurent Cozic 85a61eee2d Desktop: By default, show only the editor when opening the app for the first time (#16254) 2026-08-20 17:24:02 +01:00
Laurent Cozic a349c8e081 Doc: Update URL when toggling between plans in Plans page 2026-08-20 16:35:40 +01:00
Laurent Cozic 66546b587a Doc: Update warrant canary 2026-08-20 13:56:12 +01:00
Laurent Cozic 774aea1a38 Plugin Repo CLI v3.7.1 2026-08-20 13:29:01 +01:00
Aissa Benfodda 7588c2b722 Desktop: Resolves #16156: add support for unpublishing notebooks (#16237) 2026-08-20 11:24:32 +01:00
Henry Heino 09e7e72273 Desktop: Fixes #16015: Fix sync wizard extended options are partially offscreen (#16247) 2026-08-20 11:22:37 +01:00
Henry Heino 9db5a8df36 Desktop, Cli, Mobile: Fixes #16153: Support unpublishing notes published by other users (#16246) 2026-08-20 11:22:24 +01:00
Henry Heino ce36c14098 Chore: Desktop: Add performance logging for the embedding indexer (#16249) 2026-08-20 11:21:37 +01:00
mrjo118 c1013461e0 Desktop, Mobile: Implement readonly state for mobile RTE and disable the MDE interactive table editor for readonly notes (#16231) 2026-08-19 15:27:46 +01:00
Henry Heino cdee3c8901 Desktop: Fixes #16007: Fix toggling the chat panel clears undo/redo history (#16236) 2026-08-19 11:25:08 +01:00
mrjo118 eb800dc54b Desktop: Fixes #15947: Disable link to note button when note is readonly (#16238) 2026-08-19 11:24:53 +01:00
Joplin Bot ce449a5a50 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-08-18 18:48:56 +00:00
Laurent Cozic a36c689473 Android 3.7.6 2026-08-18 17:09:29 +01:00
VortexPandLaurent Cozic 715c1d5514 All: Translation: Update fi_FI.po (#16075)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-08-18 15:17:30 +01:00
mrjo118 d6dcd36c25 Desktop, Mobile, Cli: Fix note screen is not properly refreshed when updated by the sync, when E2EE is enabled (#16179) 2026-08-18 15:15:09 +01:00
mrjo118 3b969d7f65 Mobile: Fix empty navigation when navigating to settings from the conflicts folder after it was removed (#16197) 2026-08-18 15:14:49 +01:00
mrjo118 d73c9ca2dd Desktop, Mobile, Cli: Prevent race which allows changes to be lost, during a long delta step (#16208) 2026-08-18 15:14:12 +01:00
mrjo118 7e66fbff34 Desktop: Fixes #16205: Fix regression which causes text to be lost if switching to another note immediately after typing in the Rich Text editor (#16213) 2026-08-18 15:13:34 +01:00
Henry Heino 561ab70c5e Desktop: Fixes #16219: Fix semantic search availability check (#16220) 2026-08-18 15:13:05 +01:00
renovate[bot] 8cc7c95d67 fix(deps): update dependency follow-redirects to v1.16.0 (#16226)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-18 14:48:49 +01:00
renovate[bot] 9b71467e3b fix(deps): update dependency dompurify to v3.4.5 (#16228)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-18 14:48:07 +01:00
Joplin Bot ca3482a8b3 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-08-18 12:53:45 +00:00
Henry Heino 36b7709072 Desktop: Sync: Apply changes to proxy settings immediately (#16222) 2026-08-18 11:46:46 +01:00
mrjo118 34ded63b0c Desktop, Mobile, Cli: Fixes #16194: Prevent incrementing the master key updated_time when the key has not actually changed (#16223) 2026-08-18 11:45:57 +01:00
renovate[bot] 7fcb0eeadd fix(deps): update dependency dompurify to v3.4.0 (#16218)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-18 11:35:26 +01:00
renovate[bot] 7ec75ea70c fix(deps): update dependency ldapts to v8.1.8 (#16224)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-18 11:35:06 +01:00
renovate[bot] 55afc8622b fix(deps): update dependency react-native-svg to v15.15.5 (#16225)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-18 11:34:57 +01:00
renovate[bot] 629acbf89e chore(deps): update dependency webpack-dev-server to v5.2.4 (#16221)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-18 04:28:02 +00:00
renovate[bot] a310163922 fix(deps): update dependency react-native-quick-crypto to v1.1.4 (#16217)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-18 01:29:21 +00:00
Joplin Bot a43e772c85 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-08-17 18:47:52 +00:00
Laurent Cozic bed73cd7c1 Desktop release v3.7.12 2026-08-17 19:19:17 +01:00
Laurent Cozic d4897e7f73 Desktop release v3.7.11 2026-08-17 19:12:06 +01:00
mrjo118 4afeb59450 Desktop: Prevent the remote version of a note being overwritten if typing while the sync changes the open note (#16206) 2026-08-17 19:08:07 +01:00
mrjo118 21e73968e5 Mobile: Prevent the remote version of a note being overwritten if typing while the sync changes the open note (#16193) 2026-08-17 19:01:04 +01:00
Clarisse Deschamps 7ad86544cf Doc: Fixed french translation in Welcome message (#16195) 2026-08-17 18:55:52 +01:00
Henry Heino d640f36ab1 Desktop: Fixes #16016: Chat panel: Allow closing the chat panel in small windows (#16189) 2026-08-17 18:53:42 +01:00
Henry Heino b46a96e03c Desktop: Chat panel: Fix OpenAI, Anthropic compatibility (#16188) 2026-08-17 18:52:55 +01:00
taw00 0f3d81a983 Doc: Fixes #16186: missing word added (#16187) 2026-08-17 18:52:46 +01:00
Henry Heino 628e6bd6f6 Desktop: Fixes #16167: Fix conflict created when syncing a just-published note (#16182) 2026-08-17 18:52:31 +01:00
Aissa Benfodda 925b83eafe Server: Resolves #16132: Publishing an old note may display an error "Missing property deleted_time" (#16175) 2026-08-17 18:45:34 +01:00
renovate[bot]andLaurent Cozic 82afd4a5f3 fix(deps): update dependency react-native-paper to v5.15.2 (#16158)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-08-17 18:43:01 +01:00
Sriram Varun KumarandLaurent Cozic 6255497969 Desktop: Resolves #16134: Add data loader and word-level diff for Conflict resolution UI (#16135)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-08-17 18:42:22 +01:00
Akshaj Rawat 942d0be0fa Tools: Resolves #16012: Add GitHub authentication and issue submission to publish script (#16022) 2026-08-17 18:39:24 +01:00
Laurent Cozic 5d894d43c0 Doc: Update sponsors 2026-08-17 18:25:55 +01:00
Joplin Bot 2f9c215bf8 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-08-16 06:48:01 +00:00
Joplin Bot 0b07849f4d Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-08-16 01:25:27 +00:00
Jonatan Nyberg 6233b510ff All: Translation: Update sv.po (#16200) 2026-08-14 08:11:16 -04:00
James Carroll 45b7feb683 All: Translation: Update zh_CN.po and ZH_TW.po (#16203) 2026-08-14 08:10:10 -04:00
Joplin Bot 2654b33620 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-08-10 07:49:42 +00:00
Joplin Bot eeb420fbdd Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-08-10 01:55:19 +00:00
renovate[bot] 4942bccd1e chore(deps): update dependency @docusaurus/plugin-sitemap to v3.10.1 (#16161)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-07 23:45:18 +02:00
mrjo118 8b70b5f6e9 Mobile: Make the editor state update, if the current note is updated by the sync (#16150) 2026-08-07 23:44:57 +02:00
Henry HeinoandLaurent Cozic aed0d55398 Desktop: Resolves #16148: Evernote import: Support importing cross-notebook links in some cases (#16151)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-08-07 23:44:38 +02:00
Henry Heino 39169894dd Chore: Website: Fix build (#16164) 2026-08-07 23:44:16 +02:00
renovate[bot] 5d86c83e84 fix(deps): update dependency tar to v7.5.15 (#16159)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-07 01:26:10 +00:00
Laurent Cozic 6cc0b97f64 Merge branch 'mrjo118-fix-long-press-on-web' into dev 2026-08-06 23:02:04 +02:00
Laurent Cozic dd145c39eb Merge branch 'fix-long-press-on-web' of https://github.com/mrjo118/joplin into mrjo118-fix-long-press-on-web 2026-08-06 23:01:41 +02:00
Keshav fbd88155dc Mobile, Desktop, Cli: Fixes #16145: Release the save mutex when a save fails validation (#16147) 2026-08-06 22:56:10 +02:00
Henry Heino 20e55c9937 Chore: Fix generated files precommit check (#16154) 2026-08-06 22:55:31 +02:00
renovate[bot] 5d63d9c77b chore(deps): update dependency @docusaurus/plugin-sitemap to v3.10.0 (#16149)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-06 22:54:33 +02:00
Henry Heino 6e8fc6cd75 Desktop: MCP: Return correct API response type (#16144) 2026-08-06 22:54:09 +02:00
Henry Heino 391cdf7f15 Desktop: Chat panel: Support reading images (#16141) 2026-08-06 22:53:44 +02:00
Henry Heino 1c9ed87941 Desktop: Resolves #15824: Enable semantic search in the sidebar and go-to-anything (#16118) 2026-08-06 22:42:56 +02:00
Aissa BenfoddaandLaurent Cozic 64343897ca Desktop: Resolves #16065: Mark AI features as Beta (#16097)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-08-06 22:40:05 +02:00
mrjo118 b36592577d Mobile: Fixes #15923: Prevent scrolling the cursor into view on the markdown editor when unnecessary to do so (#16091) 2026-08-06 22:36:11 +02:00
Joplin Bot ca36776224 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-08-06 08:44:34 +00:00
Joplin Bot 9fa7278c74 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-08-06 02:41:04 +00:00
Joplin Bot 346a1f1e38 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-08-05 22:43:38 +00:00
Laurent Cozic 150ecb8976 Doc: Update sponsors 2026-08-06 00:32:22 +02:00
mrjo118 a94f981ca9 Add comments 2026-08-04 23:19:33 +01:00
renovate[bot] c153ad8033 fix(deps): update dependency fs-extra to v11.3.5 (#16142)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-04 22:09:57 +00:00
mrjo118 03efac1c17 Fix long press on the note list cancelling multi select immediately after switching on Web 2026-08-04 21:12:45 +01:00
renovate[bot] 747a1876fb fix(deps): update dependency tar to v7.5.14 (#16133)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-04 06:43:09 +00:00
Stephan Paternotte 8d1ba56e2f All: Translation: Update nl_NL.po (#16125) 2026-08-03 09:52:56 -04:00
Henry Heino 45c3cf6fcf Mobile: Implement redesigned new note menu (#16082) 2026-08-03 00:09:03 +02:00
8d8f76fe24 chore(deps): update dependency @playwright/test to v1.59.1 (#16102)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-08-01 19:23:42 +02:00
renovate[bot]andLaurent Cozic c5c23d6a52 chore(deps): update dependency @types/serviceworker to v0.0.197 (#16105)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-08-01 19:23:35 +02:00
renovate[bot] 7270b9f12b chore(deps): update dependency sass to v1.99.0 (#16115)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-01 19:23:23 +02:00
Laurent Cozic 3745d33038 Doc: Added section describing what semantic search is good at 2026-08-01 18:51:26 +02:00
Joplin Bot cb1f473bc6 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-08-01 02:55:33 +00:00
renovate[bot] 03389b59a8 fix(deps): update dependency lodash to v4.18.1 (#16111)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-31 23:33:59 +02:00
Henry Heino 5ab92fe5b7 Chore: Fixes #15841: Fix "makeInlineReplaceExtension" CI failures (#16112) 2026-07-31 21:19:30 +02:00
Henry Heino 1704b5cf7d Web, Android: Resolves #16107: Dialogs: Bold and add margin below titles (#16113) 2026-07-31 20:55:12 +02:00
Keshav 5395a8d02a Desktop: Fixes #16099: Skip the note lock session effect on the first render (#16100) 2026-07-31 20:14:01 +02:00
renovate[bot] 69b5c6ab03 fix(deps): update dependency knex to v3.2.10 (#16108)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-31 16:54:32 +00:00
Joplin Bot 4fdfb5d90e Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-07-31 14:15:50 +00:00
renovate[bot] bba1231448 fix(deps): update dependency dotenv to v17.4.2 (#16106)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-31 15:38:11 +02:00
Laurent Cozic 140ff4552a Desktop: Add support for translating Markdown editor table context menu items 2026-07-31 12:33:11 +02:00
Laurent Cozic f7998114ae iOS 13.7.2 2026-07-31 12:17:58 +02:00
Laurent Cozic 15756bba1a Desktop: Improve rendering of Markdown editor table context menu 2026-07-31 12:17:58 +02:00
Akshaj Rawat 1fba990153 Tools: Resolves #15974: Added new publish plugin command in plugin-repo-cli (#15981) 2026-07-31 11:36:32 +02:00
Henry Heino 1d7c5c0a44 Mobile: Implement redesigned notebook long-press menu (#16027) 2026-07-31 11:31:51 +02:00
Henry Heino d7c29c1d1a Android: Add shadows to the new note and note actions menus (#16067) 2026-07-31 11:31:31 +02:00
Henry Heino d167ae507c Desktop: Plugin API: Fix panels hidden on startup are incorrectly shown in some cases (#16072) 2026-07-31 11:31:14 +02:00
mrjo118 8bb852e639 Mobile: Fixes #15265: Fix blank screen being shown when navigating back after tag changes and other edge cases (#16080) 2026-07-31 11:30:49 +02:00
mrjo118 b0ce2d06d3 Mobile: Make the startup location the first entry in the navigation history instead of 'All Notes' (#16083) 2026-07-31 11:30:32 +02:00
Henry Heino a23e78825d Mobile: Fix inconsistent button order in profile switcher, tag screen, and deletion confirmation dialogs (#16076) 2026-07-31 11:28:26 +02:00
Henry Heino cec9c97ef2 Mobile: Remove single-word titles from error/warning dialogs (#16096) 2026-07-31 11:19:36 +02:00
renovate[bot] 69a7fd19f1 fix(deps): update dependency dotenv to v17.4.0 (#16103)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-31 11:12:39 +02:00
renovate[bot] 6ca3d744ef fix(deps): update dependency nanoid to v5.1.11 (#16101)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-31 00:56:42 +02:00
renovate[bot] 0b8466addd fix(deps): update dependency uuid to v11.1.1 (#16068)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-30 21:04:12 +00:00
renovate[bot] b7031641f3 chore(deps): update dependency @axe-core/playwright to v4.11.3 (#16088)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-30 21:02:19 +00:00
Henry Heino 710f7983b3 Chore: Retry failing Markdown replacement tests in CI (#16077) 2026-07-30 15:16:01 +02:00
ReZa e359f48ccc All: Translation: Update fa.po (#16087) 2026-07-29 11:56:10 -04:00
renovate[bot] 3c313a5b42 chore(deps): update dependency @types/serviceworker to v0.0.196 (#16085)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-29 11:00:24 +00:00
Henry Heino 37feb9a6d0 Chore: Testing: Retry plugin install/uninstall tests (#16079) 2026-07-29 01:53:40 +01:00
Henry Heino f233b8780b Chore: Testing: Fix chat panel test failure (#16078) 2026-07-29 01:53:29 +01:00
Henry Heino ac40404d94 Chore: Fixes #16073: CI: Fix desktop end-to-end test failures caused by missing initial notebook (#16074) 2026-07-29 01:50:25 +01:00
Joplin Bot 4d3dff767b Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-07-28 19:33:09 +00:00
renovate[bot] ac53434542 fix(deps): update dependency tar-stream to v3.1.9 (#16062)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-28 16:54:30 +00:00
renovate[bot] 58460dea63 chore(deps): update dependency axios to v1.14.0 (#16064)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-28 17:52:15 +01:00
renovate[bot] e14393b118 chore(deps): update dependency @crowdin/cli to v4.14.2 (#16063)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-28 17:51:07 +01:00
Joplin Bot 980e1f4b06 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-07-28 14:14:45 +00:00
Laurent Cozic b83c75d9d5 Tools: Ignore test target in iOS deployment target check 2026-07-28 14:06:37 +01:00
Laurent Cozic 28c14d1825 Android 3.7.4 2026-07-28 14:01:46 +01:00
Laurent Cozic 0d5d4c4902 Desktop release v3.7.10 2026-07-28 13:30:03 +01:00
Laurent Cozic d14c25f43f Chore: Fix intermittent makeInlineReplaceExtension test flake on CI 2026-07-28 13:27:54 +01:00
Henry HeinoandJGCarroll 7990ec79db Desktop: Fixes #15030: Upgrade electron-builder (#15043)
Co-authored-by: JGCarroll <JGCarroll@users.noreply.github.com>
2026-07-28 13:21:49 +01:00
Keshav 94955ea573 Desktop: Resolves #15872: Add note lock UI (#15961) 2026-07-28 13:21:37 +01:00
Henry Heino b6eb739680 Chore: Desktop: AI chat panel: Allow enabling existing MCP tools for use in the chat panel (#15999) 2026-07-28 12:57:16 +01:00
Laurent Cozic 6caf347ee0 Desktop: Add getCurrentNote and createNote x-callback-url commands (#16041) 2026-07-28 12:55:14 +01:00
Henry Heino eeef83b081 Mobile: Add shadow to note actions menu (#16053) 2026-07-28 12:53:35 +01:00
Henry Heino 15c78bca60 iOS: Improve drag-to-dismiss behavior for drawer menus (#16055) 2026-07-28 12:53:22 +01:00
mrjo118 aae1a4ceaa Mobile: Consider tags and smart filters when opening the last open location on app start (#16058) 2026-07-28 12:53:12 +01:00
Keshavandmrjo118 b307898b47 Mobile: Resolves #15853: Add note lock configuration section (#15963)
Co-authored-by: mrjo118 <jo.118@hotmail.com>
2026-07-27 11:05:41 +01:00
Laurent Cozic c2c38fb7a0 Chore: Absorb random CI install-network flakes (#16045) 2026-07-26 21:45:35 +01:00
Laurent Cozic c5913bc3d5 Chore: Chore: Add temporary diagnostic for makeInlineReplaceExtension.test.ts flake 2026-07-26 21:45:27 +01:00
mrjo118 bf7689a6c4 Mobile: Fixes #15968: Prevent update to note via the api from refreshing the editor if the change is for a different note (#15985) 2026-07-26 19:06:43 +01:00
renovate[bot] 19c0c808bd fix(deps): update dependency knex to v3.2.9 (#16044)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-26 19:03:10 +01:00
renovate[bot] ee39f12a64 fix(deps): update dependency expo to v54.0.34 (#16043)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-26 17:35:22 +00:00
Aissa Benfodda 51c6d4f809 Desktop: Resolves #15788: Selection sometimes misses markdown starting formatting characters (#15858) 2026-07-26 15:52:56 +01:00
Aissa Benfodda 2fa45a5a05 Mobile: Fixes #15972: New note list design does not include the padding in the touch target to open notes (#15987) 2026-07-26 15:27:00 +01:00
Henry Heino 7d90db0bf6 Desktop: Fixes #15874: AI chat panel: Fix changing the note body causes a cache miss (#15969) 2026-07-26 15:24:42 +01:00
mrjo118 f606699717 Mobile: Fixes #15968: Avoid reverting to the default editor state when the editor reloads the same note (#15986) 2026-07-26 15:21:53 +01:00
mrjo118 995699230a Desktop, Mobile: Fixes #16009: Upgrade CodeMirror View to v6.43.6 (#16017) 2026-07-26 15:20:39 +01:00
Sriram Varun Kumar e855fe6e4a Desktop: Resolves #16039: Add hidden feature flag for conflict resolution (#16040) 2026-07-26 15:17:44 +01:00
renovate[bot] 322219593c fix(deps): update dependency knex to v3.2.7 (#16042)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-26 15:16:55 +01:00
renovate[bot] 4adbae2c88 chore(deps): update dependency moment to v2.30.1 (#16035)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-25 00:20:51 +01:00
Henry Heino 87f82bb8fb Desktop: Chat panel: Fix rate limit handling (#16031) 2026-07-24 08:54:20 +01:00
renovate[bot] 8cd4502472 fix(deps): update dependency abcjs to v6.6.3 (#16028)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-24 01:10:33 +00:00
Henry Heino 1408b87794 Chore: Server: Update home page for users that sync with an external server (#16026) 2026-07-23 20:07:56 +01:00
Henry Heino 966f600d30 Chore: Server: Make the default application name match the built image name (#16025) 2026-07-23 20:07:26 +01:00
renovate[bot] 5df269104e chore(deps): update dependency python to v3.14.3 (#16014)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-22 17:04:35 +01:00
Aissa Benfodda 97e00c6f66 Mobile: Fixes #15973: New note list design does not look good in multi select mode (#15990) 2026-07-22 14:54:36 +01:00
renovate[bot] ed8ce18b0f chore(deps): update dependency @crowdin/cli to v4.14.2 (#16010)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-22 14:53:04 +01:00
renovate[bot] 6f32d26911 fix(deps): update dependency @react-native-documents/picker to v12.0.1 (#16011)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-22 14:52:48 +01:00
Laurent Cozic 09f6a209dd Chore: Retry "Install yarn" CI step on failure 2026-07-22 11:34:47 +01:00
renovate[bot] e983444b96 fix(deps): update dependency @react-native-documents/picker to v12 (#16008)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-22 11:29:33 +01:00
Henry Heino aed692d5c6 Chore: Server: Prevent regenerating database types when there are migrations left over from switching branches (#16006) 2026-07-22 11:29:18 +01:00
Laurent Cozic 96e78ad023 Desktop: Sync Wizard: Make Joplin Cloud the primary sync option (#16004) 2026-07-22 11:29:05 +01:00
Laurent Cozic b598fa2911 Doc: Fixed price of JSB license 2026-07-21 19:22:40 +01:00
Laurent Cozic 248ef10ab2 Doc: enable self-hosting self-service 2026-07-21 18:29:05 +01:00
Henry Heino 9395c9fa87 Desktop: Resolves #15880: Avoid including unused onnxruntime-node files in the built application (#15997) 2026-07-21 17:16:31 +01:00
Henry Heino 151faf9f88 Mobile: Note actions menu: Decrease padding size (#15998) 2026-07-21 10:15:40 +01:00
Henry Heino 4246e09f89 Mobile: Note action menu: Vertically align icons (#15996) 2026-07-21 00:17:53 +01:00
renovate[bot] 75cdbafba9 chore(deps): update nvidia/cuda docker tag to v13.2.1 (#15992)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-20 00:04:48 +01:00
renovate[bot] 341948e0ba fix(deps): update dependency @react-native-vector-icons/material-design-icons to v12.5.0 (#15980)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-19 09:04:26 +01:00
renovate[bot] e3b51d850f fix(deps): update dependency @fortawesome/react-fontawesome to v3.3.1 (#15989)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-19 09:04:12 +01:00
renovate[bot] e6a32029b4 chore(deps): update dependency @types/serviceworker to v0.0.195 (#15988)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-19 09:03:54 +01:00
renovate[bot] 7798a33739 fix(deps): update dependency @react-native-vector-icons/material-icons to v12.5.0 (#15984)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-19 00:15:58 +01:00
mrjo118 f5941b28d4 Desktop, Mobile, Cli: Fixes #15975: Fix WebDAV support for Koofr (#15983) 2026-07-18 21:05:39 +01:00
Laurent Cozic 70846df6b1 Desktop: Make links in the table editor clickable with Ctrl/Cmd+click (#15977) 2026-07-18 20:52:51 +01:00
renovate[bot] 1321b154da fix(deps): update dependency @react-native-vector-icons/ionicons to v12.5.0 (#15979)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-18 20:51:28 +01:00
renovate[bot] 6a21ec6cc1 fix(deps): update dependency @react-native-vector-icons/fontawesome5 to v12.4.0 (#15978)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-18 20:51:24 +01:00
Laurent Cozic b5ed2c944e Desktop, Server: Record which app screen a Joplin Cloud sign-up started from
Adds an optional "source" label to the Joplin Cloud sign-up link (e.g.
"desktop-sync-wizard") so we can see which screens lead people to subscribe.
The label is a fixed string naming the button that was clicked - it carries
no personal or device information. It travels with the checkout as Stripe
metadata and is saved on the subscription record.
2026-07-18 16:05:00 +01:00
mrjo118andLaurent Cozic fa6d4a259b Mobile: Fixes #15914: Fix auto navigate to latest notebook shortly after app start, when the selected notebook was previously deleted (#15950)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-07-18 11:03:52 +01:00
Jonatan NybergandLaurent Cozic 32c29dd2f6 Improve Swedish PO localisation (#15936)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-07-18 11:03:21 +01:00
Stephan Paternotte de93ebed45 Update nl_NL.po (#15921) 2026-07-18 11:02:58 +01:00
Aissa BenfoddaandLaurent Cozic 154f12744d Desktop, Mobile: Resolves #15828:Provide a clear message on what needs to be done when sync fails due to wrong version (#15837)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-07-18 11:01:46 +01:00
Aissa Benfodda 2c11cbbb6b Desktop: Publish a notebook (#15276) 2026-07-18 10:58:57 +01:00
Henry Heino fca026f0d9 Android: Fixes #15928: Fix menu animations and drag-to-dismiss (#15971) 2026-07-18 10:58:16 +01:00
renovate[bot] e51d9f2670 fix(deps): update dependency @fortawesome/react-fontawesome to v3.3.0 (#15976)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-18 10:57:49 +01:00
Henry Heino 4c2c38f43e Android, Web: Fixes #15959: Increase minimum prompt button width (#15966) 2026-07-17 23:52:20 +01:00
Will Hanstedt dafe4cd010 Doc: Fixes 15953: dead link to pandoc deflist documentation (#15967) 2026-07-17 23:51:06 +01:00
Laurent Cozic 6aa4040762 Desktop: Fixes #15918: Keep trailing spaces in table cells while editing (#15964) 2026-07-17 23:50:33 +01:00
Laurent Cozic 22689817c7 Desktop: Whiteboard: Fix edge keeping the blue selection colour after reconnecting 2026-07-17 12:42:27 +01:00
Laurent Cozic 3515c6aee1 Desktop: Whiteboard: Allow selecting which arrow to move when two arrows share an anchor 2026-07-17 12:26:38 +01:00
mrjo118 a180f9151a Mobile: Fixes #15915: Add file extension validation to import actions (#15960) 2026-07-17 11:22:01 +01:00
Laurent Cozic 15207dc931 Desktop: Fix layout divider behaviour in nested layouts and move mode (#15955) 2026-07-17 09:39:35 +01:00
mrjo118 193a17274a Desktop: Fixes #15867: Fix cycling of the active note when other notes in the notebook are moved (#15868) 2026-07-16 23:29:35 +01:00
Keshav b0c977b2b6 Desktop: Resolves #15786: Add note lock configuration screen (#15852) 2026-07-16 23:22:27 +01:00
Henry Heino 45309e1d0a Desktop: AI chat panel: Improve accessibility (#15956) 2026-07-16 23:16:44 +01:00
Laurent Cozic f410aced1e Desktop: Whiteboard: Fixed newline handling within cards 2026-07-16 17:58:59 +01:00
renovate[bot] 6d5444565b chore(deps): update dependency @axe-core/playwright to v4.11.2 (#15952)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-16 16:42:02 +01:00
Henry Heino 54b8e69750 Chore: Fix CI (#15949) 2026-07-16 01:37:55 +01:00
Henry Heino 2e99bd00ae Desktop: AI chat panel: Fix failure when tool responses are invalid JSON (#15886) 2026-07-15 22:33:57 +01:00
Henry Heino 7524c73526 Desktop: AI chat panel: Stop retry loop after several repeated failures (#15944) 2026-07-15 22:29:31 +01:00
Henry Heino 655806e79c Chore: Server: Add Joplin Server Business-related config logic (#15945) 2026-07-15 22:29:15 +01:00
Henry Heino 68ef1623ab Desktop: AI chat panel: Support cancelling in-progress responses (#15946) 2026-07-15 22:28:48 +01:00
renovate[bot] b0f27acaf7 chore(deps): update dependency nan to v2.26.0 (#15938)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-15 18:15:56 +01:00
renovate[bot] ce475ea22e chore(deps): update nvidia/cuda docker tag to v13.2.0 (#15929)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-15 12:55:34 +01:00
Henry Heino 0faa20d827 Chore: Server: Store Stripe period start/end in the database (#15925) 2026-07-14 22:03:54 +01:00
renovate[bot] a7d1e41c09 fix(deps): update dependency nanoid to v5.1.9 (#15926)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-14 20:56:23 +01:00
Joplin Bot 4dbbf2c3f8 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-07-14 13:52:11 +00:00
Henry Heino 24ab23045b Chore: Build: Add build-time LICENSE_REQUIRED config flag to Joplin Server (#15913) 2026-07-14 11:24:48 +01:00
Henry Heino bbc8999a45 Chore: Server: Add logic to customize the home screen for external users (#15912) 2026-07-14 11:24:37 +01:00
Henry Heino 0ef2649edd Chore: Server: Add logic for displaying Joplin Server Business status (#15909) 2026-07-14 11:24:26 +01:00
Aissa Benfodda 974198b3a6 Mobile: Update Search screen (#15736) 2026-07-14 10:26:04 +01:00
Laurent Cozic d58a0bb461 Desktop release v3.7.9 2026-07-14 09:54:16 +01:00
Laurent Cozic 5480a56ff5 Revert "Web: Fixes #15601: Fix S3 sync failing with getReader TypeError (#15851)"
This reverts commit c2b04f26df.

May have caused https://github.com/laurent22/joplin/issues/15916
2026-07-14 09:53:36 +01:00
Joplin Bot 7b2ee03091 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-07-14 02:38:33 +00:00
Laurent Cozic 66cbacca6e Revert "Chore: Set IPHONEOS_DEPLOYMENT_TARGET to 15.6 for tests to match application version"
It appears we need v18.x for the test units

This reverts commit 1f9cec33d8.
2026-07-14 00:12:54 +01:00
Laurent Cozic 6baede235e Chore: Update ignoreDeps in renovate.json5
Added 'node-gyp', 'nitrogen', and 'react-native-nitro-modules' to ignoreDeps in renovate.json5.
2026-07-13 21:39:00 +01:00
Henry Heino 84035e579b Desktop: Fix dependencies used for semantic search (#15891) 2026-07-13 21:33:14 +01:00
Henry Heino ca7cc0fd31 Doc: Resolves #15823: Docker compose template: Don't expose PostgreSQL container port (#15908) 2026-07-13 21:32:41 +01:00
Henry Heino 34ea6c7d8e Chore: Server: Add a utility function for determining the number of enabled non-admin users (#15907) 2026-07-13 20:49:34 +01:00
Joplin Bot ac4d934ebb Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-07-13 19:33:13 +00:00
Laurent Cozic 06a49e6988 iOS 13.7.1 2026-07-13 20:31:46 +01:00
Laurent Cozic 1f9cec33d8 Chore: Set IPHONEOS_DEPLOYMENT_TARGET to 15.6 for tests to match application version 2026-07-13 20:31:00 +01:00
renovate[bot] 39aefb1ad0 fix(deps): update dependency react-native-paper to v5.15.1 (#15905)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-13 20:09:37 +01:00
Laurent Cozic 103ab78b09 Android 3.7.3 2026-07-13 16:07:54 +01:00
Laurent Cozic 674f45d06b Desktop release v3.7.8 2026-07-13 15:52:47 +01:00
Aissa Benfodda 9367ec21f0 Desktop: Resolves #15842: Unable to select text within links (#15871) 2026-07-13 15:52:34 +01:00
Laurent Cozic 5178633558 Desktop: Whiteboard: Place new groups in empty space instead of viewport centre (#15901) 2026-07-13 13:54:29 +01:00
Laurent Cozic 8949fa25f7 Chore: Desktop: Fixed action dispatching error on startup 2026-07-13 00:02:27 +01:00
Laurent Cozic 2ab3b6e00a Desktop release v3.7.7 2026-07-12 23:54:00 +01:00
Laurent Cozic e94a3677f3 Desktop: Surface Joplin Cloud AI degraded state and map error codes (#15849) 2026-07-12 23:53:27 +01:00
Joplin Bot 194ff99911 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-07-12 19:16:00 +00:00
Laurent Cozic 47a3dbb3f3 Doc: Update sponsors 2026-07-12 17:16:21 +01:00
renovate[bot] df770ba38f chore(deps): update dependency lint-staged to v16.4.0 (#15898)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-12 15:41:53 +01:00
Henry Heino 01bcd2b52a Desktop: AI chat panel: Improve reliability of the Joplin Cloud AI provider (#15885) 2026-07-12 15:39:03 +01:00
mrjo118 5de4777ae0 Mobile: Fixes #12962: Improve sidebar swipe gestures and reduce the likelihood of gesture navigation conflicts (#15875) 2026-07-12 15:38:24 +01:00
Henry Heino d7fb11789e Mobile: Implement redesigned note action menu (#15873) 2026-07-12 15:35:25 +01:00
Martino Dell'Ambrogio 526a0037ee Server: Fixes #15857: Fixed LDAP connection leak when login exits early (#15855) 2026-07-12 14:46:27 +01:00
Aissa Benfodda 4efbc6c3fa Desktop: Resolves #15723: Notes issued from an imported .one file show a double title on the published page (#15735) 2026-07-12 14:22:06 +01:00
Joplin Bot fb8628b687 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-07-12 02:54:16 +00:00
Laurent Cozic 71693142df Desktop: Whiteboard: Also color the background of cards and groups 2026-07-10 20:02:55 +01:00
Laurent Cozic 90c1d1ec85 Desktop: Whiteboard: keep wheel scrolling inside scrollable cards 2026-07-10 19:37:18 +01:00
Laurent Cozic 0d4534e546 Desktop: Whiteboard: Improve card content styling 2026-07-10 19:17:07 +01:00
Laurent Cozic affb456b46 Desktop: Whiteboard: Add support for colors 2026-07-10 18:29:57 +01:00
Laurent Cozic d17d84590d Desktop: Whiteboard: Double-click an edge label to edit it 2026-07-10 18:09:10 +01:00
Laurent Cozic a8100e3467 Desktop: Whiteboard: Allow changing the origin of an edge by dragging it 2026-07-10 17:55:58 +01:00
Henry Heino d05a24ef7a Desktop: Resolves #15862: AI chat panel: Change APIs used to expose note edit operations (#15881) 2026-07-10 09:30:35 +01:00
Antonio VargasandLaurent Cozic c2b04f26df Web: Fixes #15601: Fix S3 sync failing with getReader TypeError (#15851)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-07-09 18:22:31 +01:00
renovate[bot] 0f52c0cfb5 chore(deps): update dependency sass to v1.98.0 (#15876)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-09 17:30:46 +01:00
Henry Heino 845a92eae8 Chore: Server: Allow specifying an image name during build and fix license specifiers (#15861) 2026-07-08 22:46:49 +01:00
Henry Heino 081444cd2c Desktop: Resolves #15834: AI chat panel: Render markdown (#15836) 2026-07-08 22:44:13 +01:00
Henry Heino 74b6545689 Chore: Fixes #15864: Fix CI (#15866) 2026-07-07 08:59:16 +01:00
Laurent Cozic 97fff14326 Doc: Add AI Sidebar tools spec 2026-07-06 18:34:23 +01:00
Laurent Cozic 694cbd957d Plugins: Change joplin.ai.chat to return an object with a text property 2026-07-05 00:39:17 +01:00
Keshav 4dad8a7fb6 Mobile, Desktop, Cli: Resolves #15772: Update note lock session state (#15773) 2026-07-04 22:29:06 +01:00
Henry Heino b64d970b5c Chore: Server package refactoring (#15843) 2026-07-04 22:26:02 +01:00
Henry Heino ebaac5c216 Desktop: Fix secondary widnows have visible scrollbars while/after resizing (#15844) 2026-07-04 22:25:52 +01:00
Henry Heino 6fdc78aa1e Desktop: AI chat panel: Make edit operations more likely to succeed (#15845) 2026-07-04 22:25:37 +01:00
Laurent Cozic 52e56deb61 Mobile: Fixes #13901: Fix auto-derived title being cleared after saving a new note (#15846) 2026-07-04 22:25:05 +01:00
Henry Heino 418e8657fe Server: Fixes #15725: Fix LDAP user auto-creation (#15847) 2026-07-04 22:23:28 +01:00
Laurent Cozic f7f7ba10e2 Chore: Desktop: Do no display tooltips over links on desktop 2026-07-03 19:40:41 +01:00
Laurent Cozic 7979cf8702 Mobile: Editor: Display tooltip above links to allow opening the link 2026-07-03 19:19:49 +01:00
Laurent Cozic c6cc90a509 Desktop, Mobile: Editor: Ensure checkbox size is proportional to font size 2026-07-03 18:57:50 +01:00
Laurent Cozic 89884e941e Desktop, Mobile: Editor: Fixed vertical alignement of checkboxes 2026-07-03 18:56:18 +01:00
Laurent Cozic 8d90057453 Server: Allow searching emails by full name or email 2026-07-03 18:50:28 +01:00
Laurent Cozic 8e5d29f058 Desktop: Hide "Sign up to Joplin Cloud" button once the user is logged in 2026-07-03 18:44:31 +01:00
Laurent Cozic 380401c8fc Server: Give focus to MFA field when it is displayed 2026-07-03 18:37:48 +01:00
Laurent Cozic 221b2593ff Server: Silence Katex warnings (#15839) 2026-07-03 14:15:54 +01:00
Laurent Cozic c227b66bb2 Desktop: AI Chat: Added warning in console when structured output schema is not accepted by the model 2026-07-03 12:12:30 +01:00
Henry Heino b3d467839c Desktop: AI chat panel: Make model responses more likely to be valid (#15801) 2026-07-03 12:04:59 +01:00
renovate[bot] 99d51ccecb fix(deps): update dependency pg to v8.20.0 (#15835)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-03 12:04:30 +01:00
renovate[bot] 894b29c9d3 chore(deps): update dependency tesseract.js to v7 (#15838)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-03 12:01:16 +01:00
renovate[bot] 3c6a500889 chore(deps): update dependency turndown to v7.2.4 (#15832)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-02 18:31:53 +00:00
Daniel Nylander 05c7b2f032 All: Translation: Update sv.po (#15833) 2026-07-02 14:29:22 -04:00
Laurent Cozic 7160fbfe20 Doc: Mention that Joplin Cloud AI is beta and added it to the feature list 2026-07-02 16:44:18 +01:00
mrjo118 0c70b1b1eb Mobile: Fixes #15663: Prevent data loss when toggling the checkbox on a todo immediately after changing note data (#15815) 2026-07-02 16:25:41 +01:00
mrjo118 2c71aee90b Desktop, Mobile, Cli: Fixes #15811: Add safeguard to prevent revision chains being broken by revision cleaning, where old revisions still exist unexpectedly (#15812) 2026-07-02 16:23:05 +01:00
mrjo118 2fa1ecbd98 Mobile: Fixes #11479: Harden share to Joplin logic, to prevent overwriting existing notes in certain cases (#15794) 2026-07-02 16:21:28 +01:00
Henry Heino 5cd3445cae Desktop: Markdown editor: Fix certain types of note edits can't be undone with ctrl-z (#15806) 2026-07-02 16:19:11 +01:00
Henry Heino e67b50764f Desktop: AI chat panel: Support multiple windows (#15803) 2026-07-02 16:19:03 +01:00
Aissa Benfodda 2c3fac7b12 Desktop: Resolves #15763: Reveal markup on mouseup instead of mousedown (#15778) 2026-07-02 16:18:17 +01:00
renovate[bot] c5390d4f80 chore(deps): update dependency turndown to v7.2.3 (#15829)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-02 14:59:56 +01:00
Aissa Benfodda f7675413ec Desktop: Resolves #15716: Published note issued from an imported .one file breaks UI (#15734) 2026-07-02 14:57:23 +01:00
Henry Heino e63b96be5c Mobile: Update appearance of dialogs (#15706) 2026-07-02 14:55:51 +01:00
Laurent Cozic d97a5091a3 Server: Validate transcribe job ID to prevent path traversal in proxy endpoint 2026-07-01 22:39:35 +01:00
Laurent Cozic dff533ccbd Server: Prevent pending share recipients from writing items into shared folders before accepting the invitation 2026-07-01 22:31:25 +01:00
renovate[bot] a5d7dbdd17 chore(deps): update actions/upload-artifact action to v6 (#15809)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-01 15:16:18 +01:00
Laurent Cozic c7841670cd Server: Fixes #15802: Allow SVG resources to be served inline (#15814) 2026-07-01 12:28:51 +01:00
renovate[bot] 5101b72a3c fix(deps): update dependency slugify to v1.6.9 (#15817)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-01 12:28:41 +01:00
Joplin Bot 658e741029 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-07-01 04:03:54 +00:00
renovate[bot] 9884e7f0ca chore(deps): update dependency esbuild to v0.27.5 (#15816)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-01 03:12:32 +00:00
Laurent Cozic 1a2501e85f Desktop: Fixes #4238: Make the editor absorb space on window resize, regardless of layout position (#15774) 2026-06-30 18:18:48 +01:00
renovate[bot] 600c8d0f8f fix(deps): update dependency adm-zip to v0.5.17 (#15805)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-30 18:05:24 +01:00
Laurent Cozic a67f1bf1fe Desktop: Add joplin.ai.getEmbeddings() plugin API for raw vector access (#15799) 2026-06-30 09:58:46 +01:00
Laurent Cozic 952ae75ef4 Desktop: Add joplin.ai.getIndexStatus() plugin API (#15800) 2026-06-30 00:14:46 +01:00
Laurent Cozic 0065760e88 Chore: Mobile: Add perf logging for NoteBodyViewer WebView load and renderer RPC (#15796) 2026-06-29 15:43:53 +01:00
Laurent Cozic 31be269339 Chore: Desktop: Check that vectors are all numeric before saving them (#15792) 2026-06-29 09:26:27 +01:00
Laurent Cozic 1f612b398e Chore: Update lockfile 2026-06-28 23:44:26 +01:00
renovate[bot] 4e4d48e28e chore(deps): update dependency lint-staged to v16.3.4 (#15791)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-28 17:56:25 +00:00
renovate[bot] 1449b5faf5 chore(deps): update dependency lint-staged to v16.3.0 (#15790)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-28 15:03:05 +01:00
Laurent Cozic 920cd8f463 Server: Harden user-content responses against XSS via uploaded resources (#15787) 2026-06-28 10:11:17 +01:00
Joplin Bot 1cd7af30e0 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-06-27 19:30:20 +00:00
Laurent Cozic ee10ebf76b Chore: Desktop: Added helper functions to check AI availability (#15785) 2026-06-27 19:08:32 +01:00
Laurent Cozic 2887e3f29e Doc: Update sponsors 2026-06-27 18:07:32 +01:00
Laurent Cozic cd75b3e9c8 Desktop: Add AI chat panel for the active note (#15771) 2026-06-27 15:05:10 +01:00
renovate[bot] 2be2c152af fix(deps): update dependency @react-native-vector-icons/ionicons to v12.4.3 (#15782)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-27 12:46:39 +00:00
Laurent Cozic befc5686dc Desktop: Resolves #1449: Improve usability of Link to note command 2026-06-27 12:56:45 +01:00
renovate[bot] 24e5354001 fix(deps): update dependency @react-native-vector-icons/ionicons to v12.4.1 (#15777)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-27 11:46:55 +01:00
Laurent Cozic 22d0894b08 Desktop: Fixes #15779: Update GotoAnything selection on mouse hover 2026-06-27 11:45:55 +01:00
Laurent Cozic 4742fdfc8d Desktop: Fixes #15780: Fix "Link to note" command not working from command palette 2026-06-27 10:37:45 +01:00
Laurent Cozic 34b5e8cbe1 Desktop: Fix crash on startup when layout references a component that no longer exists 2026-06-27 10:28:43 +01:00
Laurent Cozic 7fa42e06f3 Desktop: Fix issue with Chat API not working with max_completion_tokens parameter of OpenAI 2026-06-26 15:40:08 +01:00
Henry Heino c444bfd4f4 Desktop, Mobile: Slightly adjust button colors and add new theme variable (#15764) 2026-06-26 12:22:59 +01:00
Henry Heino aa39146662 Chore: Server: Update Stripe config to match new backend products (#15769) 2026-06-26 12:19:19 +01:00
renovate[bot] 7c0f6ba93f fix(deps): update dependency pg to v8.19.0 (#15768)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-25 20:46:20 +01:00
Henry Heino 6396caaa4c Chore: Server: Retry Stripe events interrupted by server shutdown (#15755) 2026-06-25 11:09:24 +01:00
Henry Heino 24c6fa7194 Server: Hardening: Disable the __proto__ property (#15765) 2026-06-25 11:00:04 +01:00
renovate[bot] b9ca2a2ffd fix(deps): update dependency react-native-safe-area-context to v5.7.0 (#15766)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-25 10:33:20 +01:00
Aissa Benfodda 92b46ad541 Desktop: Resolves #15720: Tests in lib package print OneNote related warnings (#15767) 2026-06-25 10:30:12 +01:00
Joplin Bot a114f7355a Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-06-25 03:43:39 +00:00
Laurent Cozic 3eb2260b60 Desktop release v3.7.6 2026-06-24 21:05:11 +01:00
Laurent Cozic 6668375ae5 Desktop: Speed up embedding indexer during initial scan 2026-06-24 21:02:07 +01:00
Laurent Cozic 4a6da2c0d2 Desktop: Wipe orphan vec rows before insert in NoteEmbedding.saveChunks 2026-06-24 20:53:08 +01:00
Laurent Cozic 109d1b1114 Desktop: Cap ONNX embedding batch size to prevent renderer OOM 2026-06-24 20:51:58 +01:00
Laurent Cozic 9de4a11e35 Desktop: Fix sqlite-vec extension path inside packaged Electron app 2026-06-24 20:50:33 +01:00
Laurent Cozic 821ebbbb78 Desktop release v3.7.5 2026-06-24 18:02:41 +01:00
Keshav a8291bb4c2 Chore: Mobile, Desktop, Cli: Resolves #15737: Exclude locked notes from search indexes (#15756) 2026-06-24 16:52:13 +01:00
Laurent Cozic 7f1d39b75a Linux: Fixed loading of note embeddings 2026-06-24 16:44:06 +01:00
Joplin Bot be41fecb23 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-06-24 14:39:48 +00:00
Laurent Cozic cd9d5c1255 Desktop release v3.7.4 2026-06-24 11:52:39 +01:00
Laurent Cozic f6d97e4871 Desktop: Fixes #15761: Fix renderer crash loop when sqlite-vec is unavailable (#15762) 2026-06-24 11:52:19 +01:00
Henry Heino c1d60df234 Server: Fix "password has not been changed" warning not shown in some cases where it should be (#15747) 2026-06-23 15:16:03 +01:00
mrjo118 1346d29e25 Mobile: Fixes #15618: Fix sharing to Joplin when opening in edit mode with the RTE enabled (#15746) 2026-06-23 15:15:18 +01:00
Henry Heino e56c997554 Mobile: Adjust UI padding to match the mobile app redesign (#15741) 2026-06-23 15:13:36 +01:00
Keshav 86e07b64e3 Mobile, Desktop, Cli: Resolves #15714: Index locked note resources (#15717) 2026-06-23 15:10:21 +01:00
Keshav bdc1b64f48 Mobile: Fixes #15729: Hide private feature flags (#15730) 2026-06-23 14:47:40 +01:00
renovate[bot] df740e68ff fix(deps): update dependency @react-native-documents/picker to v11.0.4 (#15754)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-23 11:39:33 +01:00
Joplin Bot 578e7fae8e Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-06-23 09:48:02 +00:00
renovate[bot] e92af6ac23 fix(deps): update dependency @react-native-documents/picker to v11 (#15752)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-23 09:53:54 +01:00
Henry Heino 25a93ff554 Cli: Fixes #15738: Fix version command when published to NPM (#15740) 2026-06-23 06:33:05 +01:00
Laurent Cozic 49c083f995 Chore: Removed unused module short-uuid 2026-06-23 06:13:49 +01:00
Laurent Cozic f18ef2f0c1 Chore: Improved date handling when signing canary 2026-06-23 05:50:13 +01:00
Laurent Cozic 483e63474e Doc: Fix warrant canary signature by restoring valid-until date
The date had been hand-edited from 18 to 19 August after signing,
which invalidated the PGP signature. Restore the original signed
value so the signature verifies.
2026-06-23 05:19:02 +01:00
renovate[bot] 9ba9665476 Update dependency react-native-localize to v3.7.0 (#15742)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-23 01:33:46 +01:00
renovate[bot] 60c9c4d275 Update dependency nitrogen to v0.33.9 (#15743)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-23 01:33:37 +01:00
Joplin Bot bc1d468c81 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-06-22 20:58:25 +00:00
Laurent Cozic a0bed69a77 Android 3.7.2 2026-06-22 21:31:27 +01:00
Laurent Cozic 08bdff221f Desktop release v3.7.3 2026-06-22 21:13:05 +01:00
renovate[bot] cce0dd864a fix(deps): update dependency tar to v7.5.13 (#15733)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-21 23:11:20 +01:00
Laurent Cozic dc21be248d Desktop: Add tooltip on note title in note list (#15732) 2026-06-21 18:36:53 +01:00
Laurent Cozic d85f4305b6 Desktop, Cli, Mobile: Fixed crash when the profile database is updated 2026-06-21 15:57:29 +01:00
Joplin Bot 945de8d17b Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-06-21 04:23:29 +00:00
Laurent Cozic a0b6f2a937 Mobile, Desktop, Cli: Preserve customised note history settings when syncing across clients (#15728) 2026-06-20 22:36:33 +01:00
Joplin Bot c3a71ba203 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-06-20 14:14:00 +00:00
1051 changed files with 233307 additions and 97338 deletions

No files matched your search

+2
View File
@@ -17,4 +17,6 @@ packages/server/db-*.sqlite
packages/server/dist/
packages/server/logs/
packages/server/temp/
packages/server/assets/license/signingKey.private.*.pem
packages/server/readme/
packages/transcribe/.env
+9
View File
@@ -0,0 +1,9 @@
# Environment variables injected by Yarn into every install/build-script run.
# See the injectEnvironmentFiles setting: https://yarnpkg.com/configuration/yarnrc#injectEnvironmentFiles
# onnxruntime-node bundles the CPU runtime we use, but its postinstall also
# downloads the CUDA/GPU execution provider from nuget.org on Linux x64. Joplin
# only does CPU inference, so that download is unused - and it randomly times
# out and fails the whole `yarn install`. Skip it. This is a no-op on
# macOS/Windows, where onnxruntime-node downloads nothing anyway.
ONNXRUNTIME_NODE_INSTALL=skip
+1
View File
@@ -26,6 +26,7 @@ Valid prefixes:
- `Mobile` — the mobile app (both Android and iOS)
- `Android` — only the Android app
- `iOS` — only the iOS app
- `Web` — only the web app
- `Windows` — Windows-specific changes
- `Linux` — Linux-specific changes
- `macOS` — macOS-specific changes
+37 -1
View File
@@ -8,6 +8,22 @@ function Install-WindowsDeps {
$env:BUILD_SEQUENCIAL = '1'
$env:IS_CONTINUOUS_INTEGRATION = '1'
if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') {
# Deliberately not setting npm_config_build_from_source to force the
# sqlite3 source build: sharp treats that variable as a boolean and
# would then also build libvips from source, which fails. sqlite3
# installs with --fallback-to-build, so it compiles by itself when the
# win32-arm64 prebuilt is missing.
# Deliberately not installing the VSSetup PowerShell module here: with it
# present, node-gyp probes Visual Studio via `Get-VSSetupInstance |
# ConvertTo-Json`, whose output exceeds the default 1MB child process
# buffer on the VS 2026 image. That fails with
# ERR_CHILD_PROCESS_STDIO_MAXBUFFER and node-gyp then reports the
# install as version "undefined". Without the module it falls through to
# its own Find-VisualStudio.cs helper, which prints far less.
}
$attempts = 3
for ($i = 1; $i -le $attempts; $i++) {
yarn install
@@ -20,12 +36,32 @@ function Install-WindowsDeps {
# Install dependencies and run `yarn dist` for the Windows desktop app.
# Extra args are forwarded to `yarn dist` (e.g. --publish=never).
#
# Each runner builds only its own arch: cross compiling arm64 from x64 silently
# ships x64 .node files, as @electron/rebuild doesn't resolve native modules
# hoisted to the workspace root.
# https://github.com/electron-userland/electron-builder/issues/10187
function Build-WindowsApp {
param([string[]]$DistArgs = @())
Install-WindowsDeps
if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') {
# NSIS gets an `-arm64` suffix on its own, but the portable name is fixed
# and the update metadata is `latest.yml` for every Windows arch. Both
# would clobber the x64 runner's artifacts.
# https://github.com/electron-userland/electron-builder/issues/6372
$archArgs = @(
'--arm64'
'-c.portable.artifactName=${productName}Portable-arm64.${ext}'
'-c.publish.provider=github'
'-c.publish.channel=latest-win-arm64'
)
} else {
$archArgs = @('--x64', '--ia32')
}
cd packages/app-desktop
yarn dist @DistArgs
yarn dist @archArgs @DistArgs
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
}
+1 -1
View File
@@ -25,7 +25,7 @@ module.exports = async ({ github, context, core }) => {
// the changelog generator. Non-product prefixes (Tools, Chore, CI, Doc,
// Plugin Repo) are administrative categories for changes that do not
// ship to users and so do not appear in the changelog.
const prefix = '(Desktop|Mobile|Android|iOS|Windows|Linux|macOS|Cli|Clipper|Server|Transcribe|Plugins|Api|Cloud|Tools|Chore|CI|Doc)';
const prefix = '(Desktop|Mobile|Android|iOS|Web|Windows|Linux|macOS|Cli|Clipper|Server|Transcribe|Plugins|Api|Cloud|Tools|Chore|CI|Doc)';
const prefixList = `${prefix}(,\\s*${prefix})*`;
const strictRegex = new RegExp(`^${prefixList}: (Fixes|Resolves) #[0-9]+: .+`);
const softRegex = new RegExp(`^${prefixList}: ((Fixes|Resolves) #[0-9]+: )?.+`);
@@ -32,3 +32,12 @@ docker manifest annotate $SERVER_REPOSITORY:$VERSION $SERVER_REPOSITORY:arm64-$V
docker manifest annotate $SERVER_REPOSITORY:$VERSION $SERVER_REPOSITORY:amd64-$VERSION --arch amd64
docker manifest push $SERVER_REPOSITORY:$VERSION
docker manifest create $SERVER_REPOSITORY:latest \
$SERVER_REPOSITORY:arm64-$VERSION \
$SERVER_REPOSITORY:amd64-$VERSION
docker manifest annotate $SERVER_REPOSITORY:latest $SERVER_REPOSITORY:arm64-$VERSION --arch arm64
docker manifest annotate $SERVER_REPOSITORY:latest $SERVER_REPOSITORY:amd64-$VERSION --arch amd64
docker manifest push $SERVER_REPOSITORY:latest
+73
View File
@@ -0,0 +1,73 @@
#!/bin/bash
# Run the test files changed by a pull request several times, to catch flaky
# tests before they are merged.
#
# Usage: repeat_changed_tests.sh <base_ref> [repeat_count]
set -u
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
ROOT_DIR="$SCRIPT_DIR/../.."
BASE_REF="${1:-}"
REPEAT_COUNT="${2:-5}"
cd "$ROOT_DIR"
# The CI checkout is shallow, so the base branch is not present locally. Always
# fetch it rather than using a local branch of the same name, which may be
# outdated.
if git fetch --no-tags --depth=1 origin "$BASE_REF" 2> /dev/null; then
BASE_REF=FETCH_HEAD
elif ! git rev-parse --verify --quiet "$BASE_REF" > /dev/null; then
echo "Could not resolve base ref \"$BASE_REF\""
exit 1
fi
# `integration-tests` holds the Playwright specs (see app-desktop/playwright.config.ts),
# which Jest cannot run. Other `.spec.*` files are Jest tests, so they are kept.
testFiles=$(git diff --name-only --diff-filter=d "$BASE_REF" HEAD | grep -E '^packages/[^/]+/.*\.(test|spec)\.(ts|tsx|js|jsx)$' | grep -v -E '^packages/[^/]+/integration-tests/' || true)
if [ -z "$testFiles" ]; then
echo "No test files were changed - skipping flaky test detection"
exit 0
fi
echo "Changed test files:"
echo "$testFiles" | sed 's/^/ /'
# Each package has its own Jest config and must be run from its own directory.
packages=$(echo "$testFiles" | sed -E 's|^packages/([^/]+)/.*|\1|' | sort -u)
for package in $packages; do
packageDir="$ROOT_DIR/packages/$package"
# Some packages have test files but no test runner configured.
if ! node -e "const s = require('$packageDir/package.json').scripts; process.exit(s && s['test-ci'] ? 0 : 1)" 2> /dev/null; then
echo "Package $package has no test-ci script - skipping"
continue
fi
# See packages/app-mobile/tools/runTestsConditionally.js
if [ "$package" == "app-mobile" ] && [ "$RUNNER_OS" == "macOS" ]; then
echo "Skipping app-mobile tests on macOS"
continue
fi
# Pass the file names without their extension: some packages run the
# TypeScript sources while others run the compiled files, and Jest matches
# these patterns against the full path either way.
packageTestNames=$(echo "$testFiles" | grep -E "^packages/$package/" | xargs -n 1 basename | sed -E 's#\.[a-z]+$##')
cd "$packageDir"
for i in $(seq 1 "$REPEAT_COUNT"); do
echo "Running $package tests - attempt $i/$REPEAT_COUNT..."
# shellcheck disable=SC2086
if ! yarn jest $packageTestNames --forceExit; then
echo "Tests failed on attempt $i/$REPEAT_COUNT in $package - they are flaky if earlier attempts passed"
exit 1
fi
done
cd "$ROOT_DIR"
done
+12
View File
@@ -165,6 +165,18 @@ if [ "$RUN_TESTS" == "1" ]; then
if [ $testResult -ne 0 ]; then
exit $testResult
fi
# Run the test files changed by the pull request several times, to catch
# flaky tests before they are merged.
if [ "$IS_PULL_REQUEST" == "1" ]; then
echo "Step: Checking changed tests for flakiness..."
"$SCRIPT_DIR/repeat_changed_tests.sh" "$GITHUB_BASE_REF"
testResult=$?
if [ $testResult -ne 0 ]; then
exit $testResult
fi
fi
fi
# =============================================================================
+4 -4
View File
@@ -42,12 +42,12 @@ jobs:
node-version: '24'
cache: 'yarn'
- name: Install Yarn
run: |
corepack enable
- uses: ./.github/workflows/shared/install-yarn
- name: Install
run: yarn install
run: |
source "${GITHUB_WORKSPACE}/.github/scripts/retry.sh"
retry yarn install
env:
SKIP_ONENOTE_CONVERTER_BUILD: 1
+4 -6
View File
@@ -5,7 +5,7 @@ jobs:
Main:
# We always process desktop release tags, because they also publish the release
if: github.repository == 'laurent22/joplin'
runs-on: macos-latest
runs-on: macos-15
steps:
- uses: actions/checkout@v6
@@ -15,10 +15,7 @@ jobs:
node-version: '24'
cache: 'yarn'
- name: Install Yarn
run: |
# https://yarnpkg.com/getting-started/install
corepack enable
- uses: ./.github/workflows/shared/install-yarn
- name: Install macOs dependencies
if: runner.os == 'macOS'
@@ -56,7 +53,8 @@ jobs:
run: |
export npm_config_arch=arm64
export npm_config_target_arch=arm64
yarn install
source "${GITHUB_WORKSPACE}/.github/scripts/retry.sh"
retry yarn install
cd packages/app-desktop
npm pkg set 'build.mac.artifactName'='${productName}-${version}-${arch}.${ext}'
@@ -35,7 +35,7 @@ jobs:
- name: Generate GitHub App token
if: steps.check-secrets.outputs.available == 'true'
id: app-token
uses: actions/create-github-app-token@v2
uses: actions/create-github-app-token@v3
with:
app-id: ${{ secrets.CODERABBIT_CLEANUP_APP_ID }}
private-key: ${{ secrets.CODERABBIT_CLEANUP_APP_PRIVATE_KEY }}
+12 -9
View File
@@ -10,7 +10,12 @@ jobs:
matrix:
# Do not use unbuntu-latest because it causes `The operation was canceled` failures:
# https://github.com/actions/runner-images/issues/6709
os: [macos-15-intel, ubuntu-22.04, windows-2025, ubuntu-22.04-arm]
#
# windows-11-vs2026-arm is pinned because windows-11-arm migrates to
# Visual Studio 2026 in late September 2026, and that job compiles
# sqlite3 from source.
# https://github.com/actions/runner-images/issues/14602
os: [macos-15-intel, ubuntu-22.04, windows-2025, ubuntu-22.04-arm, windows-11-vs2026-arm]
steps:
- uses: actions/checkout@v6
@@ -45,7 +50,7 @@ jobs:
# Login to Docker only if we're on a server release tag. If we run this on
# a pull request it will fail because the PR doesn't have access to
# secrets
- uses: docker/login-action@v3
- uses: docker/login-action@v4
if: runner.os == 'Linux' && (startsWith(github.ref, 'refs/tags/server-v') || startsWith(github.ref, 'refs/tags/transcribe-v'))
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
@@ -164,10 +169,7 @@ jobs:
docker builder prune -af || true
sudo rm -rf /var/lib/docker/tmp/* || true
- name: Install Yarn
run: |
# https://yarnpkg.com/getting-started/install
corepack enable
- uses: ./.github/workflows/shared/install-yarn
- name: Build Docker Image
env:
@@ -181,17 +183,18 @@ jobs:
echo "RUNNER_ARCH=$RUNNER_ARCH"
echo "DOCKER_IMAGE_PLATFORM=$DOCKER_IMAGE_PLATFORM"
yarn install
source "${GITHUB_WORKSPACE}/.github/scripts/retry.sh"
retry yarn install
yarn buildServerDocker --docker-file Dockerfile.server --platform $DOCKER_IMAGE_PLATFORM --tag-name server-v0.0.0 --repository joplin/server
# Basic test to ensure that the created build is valid. It should exit with
# code 0 if it works.
docker run joplin/server:$(dpkg --print-architecture)-0.0.0 node dist/app.js migrate list
docker run joplin/server:$(dpkg --print-architecture)-0.0.0 node dist/index.js migrate list
- name: Check HTTP request
run: |
# Need to pass environment variables:
docker run --env MAX_TIME_DRIFT=0 --publish 22300:22300 joplin/server:$(dpkg --print-architecture)-0.0.0 node dist/app.js --env dev &
docker run --env MAX_TIME_DRIFT=0 --publish 22300:22300 joplin/server:$(dpkg --print-architecture)-0.0.0 node dist/index.js --env dev &
# Wait for server to start
sleep 120
@@ -0,0 +1,22 @@
name: 'Install Yarn'
description: 'Enable Corepack and install the pinned Yarn version, with retries'
runs:
using: 'composite'
steps:
- name: Install Yarn
shell: bash
# https://yarnpkg.com/getting-started/install
#
# `corepack enable` only sets up the shims - Corepack downloads the
# pinned Yarn version from repo.yarnpkg.com on first use. That fetch has
# a short timeout and no built-in retry, so a transient network blip on
# CI fails the whole job. Prepare it eagerly with a retry loop.
env:
COREPACK_NETWORK_TIMEOUT: '60000'
run: |
corepack enable
for i in 1 2 3; do
corepack prepare --activate && break
echo "corepack prepare failed (attempt $i), retrying in 15s..."
sleep 15
done
@@ -60,11 +60,7 @@ runs:
# See https://github.com/laurent22/joplin/commit/47d0d3eb9e89153a609fb5441344da10904c6308#commitcomment-159577783.
# cache: ${{ (!contains(runner.os, 'arm') && 'yarn') || '' }}
- name: Install Yarn
shell: bash
run: |
# https://yarnpkg.com/getting-started/install
corepack enable
- uses: ./.github/workflows/shared/install-yarn
# macos-latest ships with Python 3.12 by default, but this removes a
# utility that's used by electron-builder (distutils) so we need to pin
+8 -4
View File
@@ -25,7 +25,9 @@ jobs:
Install-WindowsDeps
- name: Build
if: runner.os != 'Windows'
run: yarn install
run: |
source "${GITHUB_WORKSPACE}/.github/scripts/retry.sh"
retry yarn install
env:
# The onenote-converter package uses Rust, which isn't installed on all CI
# runners. Since the onenote-converter doesn't have UI tests, it can be excluded
@@ -37,7 +39,7 @@ jobs:
cd ${GITHUB_WORKSPACE}/packages/app-desktop/
bash ./integration-tests/run-ci.sh
# See https://playwright.dev/docs/ci-intro#setting-up-github-actions
- uses: actions/upload-artifact@v5
- uses: actions/upload-artifact@v7
if: ${{ !cancelled() }}
with:
name: playwright-report-${{ matrix.os }}
@@ -45,13 +47,15 @@ jobs:
retention-days: 7
Mobile:
if: github.repository == 'laurent22/joplin'
runs-on: macos-latest
runs-on: macos-26
steps:
- uses: actions/checkout@v6
- name: Setup build environment
uses: ./.github/workflows/shared/setup-build-environment
- name: Build
run: yarn install
run: |
source "${GITHUB_WORKSPACE}/.github/scripts/retry.sh"
retry yarn install
env:
SKIP_ONENOTE_CONVERTER_BUILD: 1
- name: Run mobile UI tests
+170 -21
View File
@@ -71,6 +71,8 @@ docs/**/*.mustache
!.yarn/sdks
!.yarn/versions
packages/generator-joplin/generators/app/templates/script/publish/dist/
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
packages/app-cli/app/LinkSelector.js
packages/app-cli/app/ResourceServer.js
@@ -83,6 +85,7 @@ packages/app-cli/app/cli-utils.js
packages/app-cli/app/command-apidoc.js
packages/app-cli/app/command-attach.js
packages/app-cli/app/command-batch.js
packages/app-cli/app/command-cat.test.js
packages/app-cli/app/command-cat.js
packages/app-cli/app/command-clear.js
packages/app-cli/app/command-config.js
@@ -91,6 +94,7 @@ packages/app-cli/app/command-done.test.js
packages/app-cli/app/command-done.js
packages/app-cli/app/command-dump.js
packages/app-cli/app/command-e2ee.js
packages/app-cli/app/command-edit.test.js
packages/app-cli/app/command-edit.js
packages/app-cli/app/command-exit.js
packages/app-cli/app/command-export-sync-status.js
@@ -115,6 +119,7 @@ packages/app-cli/app/command-rmnote.test.js
packages/app-cli/app/command-rmnote.js
packages/app-cli/app/command-search.js
packages/app-cli/app/command-server.js
packages/app-cli/app/command-set.test.js
packages/app-cli/app/command-set.js
packages/app-cli/app/command-settingschema.js
packages/app-cli/app/command-share.test.js
@@ -159,6 +164,7 @@ packages/app-desktop/ElectronAppWrapper.js
packages/app-desktop/InteropServiceHelper.js
packages/app-desktop/app.reducer.test.js
packages/app-desktop/app.reducer.js
packages/app-desktop/app.test.js
packages/app-desktop/app.js
packages/app-desktop/bridge.js
packages/app-desktop/checkForUpdates.js
@@ -177,6 +183,7 @@ packages/app-desktop/commands/openNoteInNewWindow.js
packages/app-desktop/commands/openPrimaryAppInstance.js
packages/app-desktop/commands/openProfileDirectory.js
packages/app-desktop/commands/openSecondaryAppInstance.js
packages/app-desktop/commands/openSyncWizard.js
packages/app-desktop/commands/replaceMisspelling.js
packages/app-desktop/commands/restoreNoteRevision.js
packages/app-desktop/commands/showProfileEditor.js
@@ -189,17 +196,25 @@ packages/app-desktop/commands/switchProfile3.js
packages/app-desktop/commands/toggleExternalEditing.js
packages/app-desktop/commands/toggleSafeMode.js
packages/app-desktop/commands/toggleTabMovesFocus.js
packages/app-desktop/gui/AiDegradedNotice.js
packages/app-desktop/gui/Button/Button.js
packages/app-desktop/gui/ChatPanel/ChatHistory.js
packages/app-desktop/gui/ChatPanel/ChatMessageItem.js
packages/app-desktop/gui/ChatPanel/ChatPanel.test.js
packages/app-desktop/gui/ChatPanel/ChatPanel.js
packages/app-desktop/gui/ClipperConfigScreen.js
packages/app-desktop/gui/ConfigScreen/ButtonBar.js
packages/app-desktop/gui/ConfigScreen/ConfigScreen.js
packages/app-desktop/gui/ConfigScreen/Sidebar.js
packages/app-desktop/gui/ConfigScreen/configSearch.js
packages/app-desktop/gui/ConfigScreen/controls/AiIndexStatus.js
packages/app-desktop/gui/ConfigScreen/controls/AiStatus.js
packages/app-desktop/gui/ConfigScreen/controls/FontSearch.js
packages/app-desktop/gui/ConfigScreen/controls/GlobalHotkeyInput.test.js
packages/app-desktop/gui/ConfigScreen/controls/GlobalHotkeyInput.js
packages/app-desktop/gui/ConfigScreen/controls/MissingPasswordHelpLink.js
packages/app-desktop/gui/ConfigScreen/controls/NoteLockSettings.js
packages/app-desktop/gui/ConfigScreen/controls/SettingButton.js
packages/app-desktop/gui/ConfigScreen/controls/SettingComponent.js
packages/app-desktop/gui/ConfigScreen/controls/SettingDescription.js
packages/app-desktop/gui/ConfigScreen/controls/SettingHeader.js
@@ -208,8 +223,10 @@ packages/app-desktop/gui/ConfigScreen/controls/ToggleAdvancedSettingsButton.js
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js
packages/app-desktop/gui/ConfigScreen/controls/plugins/SearchPlugins.js
packages/app-desktop/gui/ConfigScreen/controls/useNoteLockMode.js
packages/app-desktop/gui/ConfigScreen/searchHighlight.test.js
packages/app-desktop/gui/ConfigScreen/searchHighlight.js
packages/app-desktop/gui/ConfigScreen/types.js
packages/app-desktop/gui/DialogButtonRow.js
packages/app-desktop/gui/DialogButtonRow/useKeyboardHandler.js
packages/app-desktop/gui/DialogTitle.js
@@ -228,6 +245,7 @@ packages/app-desktop/gui/HelpButton.js
packages/app-desktop/gui/IconButton.js
packages/app-desktop/gui/ImportScreen.js
packages/app-desktop/gui/InlineCombobox.js
packages/app-desktop/gui/InlineMarkdownDisplay.js
packages/app-desktop/gui/ItemList.js
packages/app-desktop/gui/JoplinCloudConfigScreen.js
packages/app-desktop/gui/JoplinCloudLoginScreen.js
@@ -238,7 +256,10 @@ packages/app-desktop/gui/KeymapConfig/styles/index.js
packages/app-desktop/gui/KeymapConfig/utils/getLabel.js
packages/app-desktop/gui/KeymapConfig/utils/useCommandStatus.js
packages/app-desktop/gui/KeymapConfig/utils/useKeymap.js
packages/app-desktop/gui/MainLayoutPane.js
packages/app-desktop/gui/MainScreen.js
packages/app-desktop/gui/MainScreen/handleCallbackUrl.test.js
packages/app-desktop/gui/MainScreen/handleCallbackUrl.js
packages/app-desktop/gui/MasterPasswordDialog/Dialog.js
packages/app-desktop/gui/MenuBar.js
packages/app-desktop/gui/MultiNoteActions.js
@@ -291,6 +312,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useCursorPositioning.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useEditDialog.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useEditDialogEventListeners.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useHighlightedSearchTerms.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useKeyboardRefocusHandler.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useLinkTooltips.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js
@@ -307,9 +329,13 @@ packages/app-desktop/gui/NoteEditor/NoteBody/WhiteboardEditor/nodes/GroupNode.js
packages/app-desktop/gui/NoteEditor/NoteBody/WhiteboardEditor/nodes/LinkNode.js
packages/app-desktop/gui/NoteEditor/NoteBody/WhiteboardEditor/nodes/TextNode.js
packages/app-desktop/gui/NoteEditor/NoteBody/WhiteboardEditor/nodes/handlePositions.js
packages/app-desktop/gui/NoteEditor/NoteBody/WhiteboardEditor/nodes/stripUnsafeStyles.test.js
packages/app-desktop/gui/NoteEditor/NoteBody/WhiteboardEditor/nodes/stripUnsafeStyles.js
packages/app-desktop/gui/NoteEditor/NoteBody/WhiteboardEditor/nodes/useCardWheel.js
packages/app-desktop/gui/NoteEditor/NoteBody/WhiteboardEditor/useCheckboxToggle.test.js
packages/app-desktop/gui/NoteEditor/NoteBody/WhiteboardEditor/useCheckboxToggle.js
packages/app-desktop/gui/NoteEditor/NoteEditor.js
packages/app-desktop/gui/NoteEditor/NoteLockPanel/NoteLockPanel.js
packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.js
packages/app-desktop/gui/NoteEditor/StatusBar.js
packages/app-desktop/gui/NoteEditor/WarningBanner/BannerContent.js
@@ -352,6 +378,7 @@ packages/app-desktop/gui/NoteEditor/utils/usePluginServiceRegistration.js
packages/app-desktop/gui/NoteEditor/utils/useResourceUnwatcher.js
packages/app-desktop/gui/NoteEditor/utils/useScheduleSaveCallbacks.js
packages/app-desktop/gui/NoteEditor/utils/useScrollWhenReadyOptions.js
packages/app-desktop/gui/NoteEditor/utils/useSearchMarkers.test.js
packages/app-desktop/gui/NoteEditor/utils/useSearchMarkers.js
packages/app-desktop/gui/NoteEditor/utils/useWindowCommandHandler.js
packages/app-desktop/gui/NoteList/NoteList2.js
@@ -388,6 +415,7 @@ packages/app-desktop/gui/NoteListHeader/utils/validateColumns.js
packages/app-desktop/gui/NoteListItem/NoteListItem.js
packages/app-desktop/gui/NoteListItem/utils/getNoteElementIdFromJoplinId.js
packages/app-desktop/gui/NoteListItem/utils/getNoteTitleHtml.js
packages/app-desktop/gui/NoteListItem/utils/noteTitleTooltip.js
packages/app-desktop/gui/NoteListItem/utils/prepareViewProps.test.js
packages/app-desktop/gui/NoteListItem/utils/prepareViewProps.js
packages/app-desktop/gui/NoteListItem/utils/types.js
@@ -398,6 +426,9 @@ packages/app-desktop/gui/NoteListItem/utils/useRenderedNote.js
packages/app-desktop/gui/NoteListItem/utils/useRootElement.test.js
packages/app-desktop/gui/NoteListItem/utils/useRootElement.js
packages/app-desktop/gui/NoteListWrapper/NoteListWrapper.js
packages/app-desktop/gui/NoteLockMigrationDialog/Dialog.test.js
packages/app-desktop/gui/NoteLockMigrationDialog/Dialog.js
packages/app-desktop/gui/NoteLockUnlockDialog/Dialog.js
packages/app-desktop/gui/NotePropertiesDialog.js
packages/app-desktop/gui/NoteRevisionViewer.js
packages/app-desktop/gui/NoteSearchBar.js
@@ -417,6 +448,7 @@ packages/app-desktop/gui/PopupNotification/types.js
packages/app-desktop/gui/ProfileEditor.js
packages/app-desktop/gui/PromptDialog.test.js
packages/app-desktop/gui/PromptDialog.js
packages/app-desktop/gui/PublishFolderDialog.js
packages/app-desktop/gui/QuitSyncDialog.js
packages/app-desktop/gui/ResizableLayout/LayoutItemContainer.js
packages/app-desktop/gui/ResizableLayout/MoveButtons.js
@@ -431,6 +463,8 @@ packages/app-desktop/gui/ResizableLayout/utils/persist.test.js
packages/app-desktop/gui/ResizableLayout/utils/persist.js
packages/app-desktop/gui/ResizableLayout/utils/removeItem.js
packages/app-desktop/gui/ResizableLayout/utils/removeKeylessItems.js
packages/app-desktop/gui/ResizableLayout/utils/resizeLogic.test.js
packages/app-desktop/gui/ResizableLayout/utils/resizeLogic.js
packages/app-desktop/gui/ResizableLayout/utils/setLayoutItemProps.js
packages/app-desktop/gui/ResizableLayout/utils/style.js
packages/app-desktop/gui/ResizableLayout/utils/types.js
@@ -482,6 +516,7 @@ packages/app-desktop/gui/ToolbarSpace.js
packages/app-desktop/gui/TrashNotification/TrashNotification.js
packages/app-desktop/gui/TrashNotification/TrashNotificationMessage.js
packages/app-desktop/gui/UpdateNotification/UpdateNotification.js
packages/app-desktop/gui/WarningBanner.js
packages/app-desktop/gui/WindowCommandsAndDialogs/AppDialogs.js
packages/app-desktop/gui/WindowCommandsAndDialogs/ModalMessageOverlay.js
packages/app-desktop/gui/WindowCommandsAndDialogs/PluginDialogs.js
@@ -490,8 +525,10 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/addNoteToWhiteboard.j
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/addProfile.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/commandPalette.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/deleteFolder.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/disableNoteEncryption.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/duplicateNote.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/editAlarm.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/enableNoteEncryption.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/exportPdf.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/globalRedo.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/globalUndo.js
@@ -500,6 +537,7 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/hideModalMessage.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/importFrom.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/index.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/linkToNote.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/lockEncryptedNotes.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/moveToFolder.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newFolder.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newNote.test.js
@@ -507,6 +545,7 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newNote.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newSubFolder.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newTodo.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newWhiteboard.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openAiChatConversation.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openFolder.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openFolderDialog.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openItem.js
@@ -519,6 +558,7 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/renameTag.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/resetLayout.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/restoreFolder.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/restoreNote.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/revealInNotebook.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/revealResourceFile.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/search.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/setTags.js
@@ -526,10 +566,12 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showModalMessage.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showNoteContentProperties.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showNoteProperties.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showPrompt.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showPublishFolderDialog.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showShareFolderDialog.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showShareNoteDialog.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showSpellCheckerMenu.test.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showSpellCheckerMenu.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleAiChat.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleEditors.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleLayoutMoveMode.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleMenuBar.js
@@ -569,20 +611,30 @@ packages/app-desktop/gui/utils/NoteListUtils.js
packages/app-desktop/gui/utils/announceForAccessibility.js
packages/app-desktop/gui/utils/convertToScreenCoordinates.js
packages/app-desktop/gui/utils/dragAndDrop.js
packages/app-desktop/gui/utils/hasNoteLockKey.js
packages/app-desktop/gui/utils/loadScript.js
packages/app-desktop/gulpfile.js
packages/app-desktop/integration-tests/chatPanel.spec.js
packages/app-desktop/integration-tests/goToAnything.spec.js
packages/app-desktop/integration-tests/main.spec.js
packages/app-desktop/integration-tests/markdownEditor.spec.js
packages/app-desktop/integration-tests/models/ChangeAppLayoutScreen.js
packages/app-desktop/integration-tests/models/EditorCodeDialog.js
packages/app-desktop/integration-tests/models/ChatPanel.js
packages/app-desktop/integration-tests/models/GoToAnything.js
packages/app-desktop/integration-tests/models/JoplinCloudLoginScreen.js
packages/app-desktop/integration-tests/models/MainScreen.js
packages/app-desktop/integration-tests/models/NoteEditorScreen.js
packages/app-desktop/integration-tests/models/NoteEditor/BaseEditor.js
packages/app-desktop/integration-tests/models/NoteEditor/EditorCodeDialog.js
packages/app-desktop/integration-tests/models/NoteEditor/MarkdownEditor.js
packages/app-desktop/integration-tests/models/NoteEditor/NoteEditorScreen.js
packages/app-desktop/integration-tests/models/NoteEditor/NoteViewer.js
packages/app-desktop/integration-tests/models/NoteEditor/RichTextEditor.js
packages/app-desktop/integration-tests/models/NoteList.js
packages/app-desktop/integration-tests/models/SettingsScreen.js
packages/app-desktop/integration-tests/models/SettingsScreen/SettingsScreen.js
packages/app-desktop/integration-tests/models/SettingsScreen/SyncTab.js
packages/app-desktop/integration-tests/models/Sidebar.js
packages/app-desktop/integration-tests/multiWindow.spec.js
packages/app-desktop/integration-tests/networking.spec.js
packages/app-desktop/integration-tests/noteList.spec.js
packages/app-desktop/integration-tests/pluginApi.spec.js
packages/app-desktop/integration-tests/resizableLayout.spec.js
@@ -611,6 +663,8 @@ packages/app-desktop/main-html.js
packages/app-desktop/main.js
packages/app-desktop/playwright.config.js
packages/app-desktop/plugins/GotoAnything.js
packages/app-desktop/services/aiStatusBridge.test.js
packages/app-desktop/services/aiStatusBridge.js
packages/app-desktop/services/autoUpdater/AutoUpdaterService.test.js
packages/app-desktop/services/autoUpdater/AutoUpdaterService.js
packages/app-desktop/services/bridge.js
@@ -636,6 +690,7 @@ packages/app-desktop/services/plugins/types.js
packages/app-desktop/services/restart.js
packages/app-desktop/services/spellChecker/SpellCheckerServiceDriverNative.js
packages/app-desktop/tools/bundleJs.js
packages/app-desktop/tools/cleanOnnxRuntime.js
packages/app-desktop/tools/copy7Zip.js
packages/app-desktop/tools/generateLatestArm64Yml.js
packages/app-desktop/tools/githubReleasesUtils.js
@@ -655,8 +710,11 @@ packages/app-desktop/utils/customProtocols/registerCustomProtocols.js
packages/app-desktop/utils/getAssetPath.js
packages/app-desktop/utils/initReact.js
packages/app-desktop/utils/initializeCommandService.js
packages/app-desktop/utils/initializeNetworkConnectionAttemptTimeout.js
packages/app-desktop/utils/isSafeToOpen.test.js
packages/app-desktop/utils/isSafeToOpen.js
packages/app-desktop/utils/layout/layoutKeyToLabel.js
packages/app-desktop/utils/noteLockPrompts.js
packages/app-desktop/utils/restartInSafeModeFromMain.test.js
packages/app-desktop/utils/restartInSafeModeFromMain.js
packages/app-desktop/utils/sourceMapSetup.js
@@ -674,6 +732,7 @@ packages/app-mobile/commands/util/goToNote.js
packages/app-mobile/commands/util/showResource.js
packages/app-mobile/components/BetaChip.js
packages/app-mobile/components/BottomDrawer.js
packages/app-mobile/components/BottomDrawerMenu.js
packages/app-mobile/components/CameraView/ActionButtons.js
packages/app-mobile/components/CameraView/Camera/index.jest.js
packages/app-mobile/components/CameraView/Camera/index.js
@@ -771,6 +830,11 @@ packages/app-mobile/components/ScreenHeader/index.js
packages/app-mobile/components/SearchInput.js
packages/app-mobile/components/SelectDateTimeDialog.js
packages/app-mobile/components/SideMenu.js
packages/app-mobile/components/SideMenuContent/BottomPanelActions.js
packages/app-mobile/components/SideMenuContent/FolderItem.js
packages/app-mobile/components/SideMenuContent/SideMenuContent.js
packages/app-mobile/components/SideMenuContent/SideMenuItem.js
packages/app-mobile/components/SideMenuContent/SidebarIcon.js
packages/app-mobile/components/SideMenuContentNote.js
packages/app-mobile/components/SyncWizard/JoplinCloudIcon.js
packages/app-mobile/components/SyncWizard/SyncWizard.js
@@ -803,6 +867,7 @@ packages/app-mobile/components/plugins/PluginNotification.js
packages/app-mobile/components/plugins/PluginRunner.js
packages/app-mobile/components/plugins/PluginRunnerWebView.test.js
packages/app-mobile/components/plugins/PluginRunnerWebView.js
packages/app-mobile/components/plugins/backgroundPage/initializeDialogWebView.test.js
packages/app-mobile/components/plugins/backgroundPage/initializeDialogWebView.js
packages/app-mobile/components/plugins/backgroundPage/initializePluginBackgroundIframe.js
packages/app-mobile/components/plugins/backgroundPage/pluginRunnerBackgroundPage.js
@@ -835,6 +900,8 @@ packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/expo
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportDebugReport.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportProfile.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/makeImportExportCacheDirectory.js
packages/app-mobile/components/screens/ConfigScreen/NoteLockConfig.test.js
packages/app-mobile/components/screens/ConfigScreen/NoteLockConfig.js
packages/app-mobile/components/screens/ConfigScreen/SectionDescription.js
packages/app-mobile/components/screens/ConfigScreen/SectionHeader.js
packages/app-mobile/components/screens/ConfigScreen/SectionSelector/SectionTab.js
@@ -878,6 +945,7 @@ packages/app-mobile/components/screens/JoplinCloudLoginScreen.js
packages/app-mobile/components/screens/LogScreen.js
packages/app-mobile/components/screens/Note/Note.test.js
packages/app-mobile/components/screens/Note/Note.js
packages/app-mobile/components/screens/Note/NoteLockPanel.js
packages/app-mobile/components/screens/Note/commands/attachFile.js
packages/app-mobile/components/screens/Note/commands/hideKeyboard.js
packages/app-mobile/components/screens/Note/commands/index.js
@@ -885,6 +953,8 @@ packages/app-mobile/components/screens/Note/commands/insertDateTime.js
packages/app-mobile/components/screens/Note/commands/setTags.js
packages/app-mobile/components/screens/Note/commands/toggleVisiblePanes.js
packages/app-mobile/components/screens/Note/types.js
packages/app-mobile/components/screens/NoteLockMigrationScreen.test.js
packages/app-mobile/components/screens/NoteLockMigrationScreen.js
packages/app-mobile/components/screens/NoteRevisionViewer.test.js
packages/app-mobile/components/screens/NoteRevisionViewer.js
packages/app-mobile/components/screens/NoteTagsDialog.js
@@ -913,7 +983,6 @@ packages/app-mobile/components/screens/resourceScreenUtils.test.js
packages/app-mobile/components/screens/resourceScreenUtils.js
packages/app-mobile/components/screens/status.js
packages/app-mobile/components/screens/tags.js
packages/app-mobile/components/side-menu-content.js
packages/app-mobile/components/testing/TestProviderStack.js
packages/app-mobile/components/voiceTyping/AudioRecordingBanner.js
packages/app-mobile/components/voiceTyping/RecordingControls.js
@@ -995,6 +1064,7 @@ packages/app-mobile/utils/database-driver-react-native.web.js
packages/app-mobile/utils/debounce.js
packages/app-mobile/utils/focusView.js
packages/app-mobile/utils/fs-driver/constants.js
packages/app-mobile/utils/fs-driver/fs-driver-rn.test.js
packages/app-mobile/utils/fs-driver/fs-driver-rn.js
packages/app-mobile/utils/fs-driver/fs-driver-rn.web.js
packages/app-mobile/utils/fs-driver/fs-driver-rn.web.worker.js
@@ -1090,8 +1160,6 @@ packages/editor/CodeMirror/extensions/links/ctrlClickLinksExtension.js
packages/editor/CodeMirror/extensions/links/followLinkTooltipExtension.test.js
packages/editor/CodeMirror/extensions/links/followLinkTooltipExtension.js
packages/editor/CodeMirror/extensions/links/referenceLinksStateField.js
packages/editor/CodeMirror/extensions/links/utils/findLineMatchingLink.test.js
packages/editor/CodeMirror/extensions/links/utils/findLineMatchingLink.js
packages/editor/CodeMirror/extensions/links/utils/getUrlAtPosition.js
packages/editor/CodeMirror/extensions/links/utils/openLink.js
packages/editor/CodeMirror/extensions/markdownDecorationExtension.test.js
@@ -1122,6 +1190,8 @@ packages/editor/CodeMirror/extensions/rendering/replaceLinks.test.js
packages/editor/CodeMirror/extensions/rendering/replaceLinks.js
packages/editor/CodeMirror/extensions/rendering/types.js
packages/editor/CodeMirror/extensions/rendering/utils/makeBlockReplaceExtension.js
packages/editor/CodeMirror/extensions/rendering/utils/makeHideReplaceExtension.js
packages/editor/CodeMirror/extensions/rendering/utils/makeInlineReplaceExtension.test.js
packages/editor/CodeMirror/extensions/rendering/utils/makeInlineReplaceExtension.js
packages/editor/CodeMirror/extensions/rendering/utils/nodeIntersectsSelection.js
packages/editor/CodeMirror/extensions/searchExtension.test.js
@@ -1133,6 +1203,7 @@ packages/editor/CodeMirror/pluginApi/PluginLoader.js
packages/editor/CodeMirror/pluginApi/codeMirrorRequire.js
packages/editor/CodeMirror/pluginApi/customEditorCompletion.test.js
packages/editor/CodeMirror/pluginApi/customEditorCompletion.js
packages/editor/CodeMirror/testing/backspaceOnce.js
packages/editor/CodeMirror/testing/createEditorControl.js
packages/editor/CodeMirror/testing/createTestEditor.js
packages/editor/CodeMirror/testing/findNodesWithName.js
@@ -1140,7 +1211,12 @@ packages/editor/CodeMirror/testing/forceFullParse.js
packages/editor/CodeMirror/testing/loadLanguages.js
packages/editor/CodeMirror/testing/pressReleaseKey.js
packages/editor/CodeMirror/testing/typeText.js
packages/editor/CodeMirror/testing/visibleEditorText.js
packages/editor/CodeMirror/theme.js
packages/editor/CodeMirror/utils/clampSelectionToDocument.test.js
packages/editor/CodeMirror/utils/clampSelectionToDocument.js
packages/editor/CodeMirror/utils/findPositionMatchingLink.test.js
packages/editor/CodeMirror/utils/findPositionMatchingLink.js
packages/editor/CodeMirror/utils/formatting/RegionSpec.js
packages/editor/CodeMirror/utils/formatting/computeSelectionFormatting.test.js
packages/editor/CodeMirror/utils/formatting/computeSelectionFormatting.js
@@ -1269,9 +1345,6 @@ packages/generator-joplin/generators/app/templates/api/index.js
packages/generator-joplin/generators/app/templates/api/noteListType.js
packages/generator-joplin/generators/app/templates/api/types.js
packages/generator-joplin/generators/app/templates/api_index.js
packages/generator-joplin/generators/app/templates/script/publish/steps/verifyBuild.js
packages/generator-joplin/generators/app/templates/script/publish/steps/verifyGitState.js
packages/generator-joplin/generators/app/templates/script/publish/utils/logger.js
packages/generator-joplin/generators/app/templates/src/index.js
packages/generator-joplin/tools/updateCategories.js
packages/htmlpack/index.test.js
@@ -1289,6 +1362,7 @@ packages/lib/BaseModel.test.js
packages/lib/BaseModel.js
packages/lib/BaseSyncTarget.js
packages/lib/Cache.js
packages/lib/ClipperServer.test.js
packages/lib/ClipperServer.js
packages/lib/CssUtils.js
packages/lib/EventDispatcher.test.js
@@ -1320,6 +1394,7 @@ packages/lib/SyncTargetWebDAV.js
packages/lib/Synchronizer.js
packages/lib/TaskQueue.test.js
packages/lib/TaskQueue.js
packages/lib/WebDavApi.test.js
packages/lib/WebDavApi.js
packages/lib/WelcomeUtils.test.js
packages/lib/WelcomeUtils.js
@@ -1354,15 +1429,18 @@ packages/lib/components/shared/NoteEditor/WarningBanner/useEditorTypeMigrationBa
packages/lib/components/shared/NoteList/getEmptyFolderMessage.js
packages/lib/components/shared/NoteRevisionViewer/getHelpMessage.js
packages/lib/components/shared/NoteRevisionViewer/useDeleteHistoryClick.js
packages/lib/components/shared/PublishFolderDialog/useOnPublishFolderLinkClick.js
packages/lib/components/shared/SamlShared.js
packages/lib/components/shared/ShareNoteDialog/onUnshareNoteClick.js
packages/lib/components/shared/ShareNoteDialog/types.js
packages/lib/components/shared/ShareNoteDialog/useEncryptionWarningMessage.js
packages/lib/components/shared/ShareNoteDialog/useIsPublished.js
packages/lib/components/shared/ShareNoteDialog/useOnShareLinkClick.js
packages/lib/components/shared/ShareNoteDialog/useShareStatusMessage.js
packages/lib/components/shared/SsoScreenShared.js
packages/lib/components/shared/config/config-search-text.js
packages/lib/components/shared/config/config-search.test.js
packages/lib/components/shared/config/config-shared.test.js
packages/lib/components/shared/config/config-shared.js
packages/lib/components/shared/config/plugins/types.js
packages/lib/components/shared/config/plugins/useOnDeleteHandler.js
@@ -1371,6 +1449,7 @@ packages/lib/components/shared/config/plugins/useOnInstallHandler.js
packages/lib/components/shared/config/shouldShowMissingPasswordWarning.test.js
packages/lib/components/shared/config/shouldShowMissingPasswordWarning.js
packages/lib/components/shared/dropbox-login-shared.js
packages/lib/components/shared/note-screen-shared.test.js
packages/lib/components/shared/note-screen-shared.js
packages/lib/components/shared/reduxSharedMiddleware.js
packages/lib/components/shared/side-menu-shared.test.js
@@ -1389,6 +1468,7 @@ packages/lib/errorUtils.js
packages/lib/errors.js
packages/lib/eventManager.test.js
packages/lib/eventManager.js
packages/lib/file-api-driver-joplinServer.test.js
packages/lib/file-api-driver-joplinServer.js
packages/lib/file-api-driver-local.js
packages/lib/file-api-driver-memory.js
@@ -1445,7 +1525,11 @@ packages/lib/mimeUtils.test.js
packages/lib/models/Alarm.js
packages/lib/models/BaseItem.test.js
packages/lib/models/BaseItem.js
packages/lib/models/ChatConversation.test.js
packages/lib/models/ChatConversation.js
packages/lib/models/ChatMessage.js
packages/lib/models/ConflictNoteState.js
packages/lib/models/Folder.publishing.test.js
packages/lib/models/Folder.sharing.test.js
packages/lib/models/Folder.test.js
packages/lib/models/Folder.js
@@ -1543,6 +1627,7 @@ packages/lib/services/ResourceEditWatcher/reducer.js
packages/lib/services/ResourceFetcher.js
packages/lib/services/ResourceService.test.js
packages/lib/services/ResourceService.js
packages/lib/services/RevisionService.noteLock.test.js
packages/lib/services/RevisionService.test.js
packages/lib/services/RevisionService.js
packages/lib/services/SettingUtils.js
@@ -1563,16 +1648,48 @@ packages/lib/services/ai/SearchService.js
packages/lib/services/ai/SqliteVec.test.js
packages/lib/services/ai/aiSettingsTransition.test.js
packages/lib/services/ai/aiSettingsTransition.js
packages/lib/services/ai/availability.test.js
packages/lib/services/ai/availability.js
packages/lib/services/ai/chunker.test.js
packages/lib/services/ai/chunker.js
packages/lib/services/ai/classification.js
packages/lib/services/ai/noteChat.test.js
packages/lib/services/ai/noteChat.tools.test.js
packages/lib/services/ai/noteChat.js
packages/lib/services/ai/providers/Anthropic.test.js
packages/lib/services/ai/providers/Anthropic.js
packages/lib/services/ai/providers/ChatProviderBase.js
packages/lib/services/ai/providers/JoplinCloud.test.js
packages/lib/services/ai/providers/JoplinCloud.js
packages/lib/services/ai/providers/OpenAiCompatible.test.js
packages/lib/services/ai/providers/OpenAiCompatible.js
packages/lib/services/ai/providers/TestProvider.js
packages/lib/services/ai/testing/TestEmbeddingProvider.test.js
packages/lib/services/ai/testing/TestEmbeddingProvider.js
packages/lib/services/ai/tools/ToolIndex.js
packages/lib/services/ai/tools/buildEditorTools.js
packages/lib/services/ai/tools/global/createNote.js
packages/lib/services/ai/tools/global/createNotebook.js
packages/lib/services/ai/tools/global/deleteNote.js
packages/lib/services/ai/tools/global/index.js
packages/lib/services/ai/tools/global/listNotebooks.js
packages/lib/services/ai/tools/global/listTags.js
packages/lib/services/ai/tools/global/manageTags.js
packages/lib/services/ai/tools/global/readImage.js
packages/lib/services/ai/tools/global/readNote.js
packages/lib/services/ai/tools/global/searchNotes.js
packages/lib/services/ai/tools/global/semanticSearchNotes.js
packages/lib/services/ai/tools/global/updateNote.js
packages/lib/services/ai/tools/types.js
packages/lib/services/ai/tools/utils/applyNoteEdits.test.js
packages/lib/services/ai/tools/utils/applyNoteEdits.js
packages/lib/services/ai/tools/utils/buildTool.js
packages/lib/services/ai/tools/utils/serializeToolOutput.js
packages/lib/services/ai/tools/utils/settings.js
packages/lib/services/ai/types.js
packages/lib/services/ai/utils/extractReasoning.test.js
packages/lib/services/ai/utils/extractReasoning.js
packages/lib/services/ai/utils/findFencedBlock.js
packages/lib/services/commands/MenuUtils.js
packages/lib/services/commands/ToolbarButtonUtils.test.js
packages/lib/services/commands/ToolbarButtonUtils.js
@@ -1582,6 +1699,19 @@ packages/lib/services/commands/isEditorCommand.js
packages/lib/services/commands/propsHaveChanged.js
packages/lib/services/commands/stateToWhenClauseContext.test.js
packages/lib/services/commands/stateToWhenClauseContext.js
packages/lib/services/conflict/autoMergeNote.test.js
packages/lib/services/conflict/autoMergeNote.js
packages/lib/services/conflict/boundedDiff3.test.js
packages/lib/services/conflict/boundedDiff3.js
packages/lib/services/conflict/decryptNoteInMemory.js
packages/lib/services/conflict/diffNotes.test.js
packages/lib/services/conflict/diffNotes.js
packages/lib/services/conflict/isAutoMergeEnabled.js
packages/lib/services/conflict/isConflictResolutionEnabled.js
packages/lib/services/conflict/loadConflictData.test.js
packages/lib/services/conflict/loadConflictData.js
packages/lib/services/conflict/wordDiff.test.js
packages/lib/services/conflict/wordDiff.js
packages/lib/services/contextkey/contextkey.js
packages/lib/services/database/addMigrationFile.js
packages/lib/services/database/isSqliteSyntaxError.js
@@ -1597,6 +1727,7 @@ packages/lib/services/database/migrations/50.js
packages/lib/services/database/migrations/51.js
packages/lib/services/database/migrations/52.js
packages/lib/services/database/migrations/53.js
packages/lib/services/database/migrations/54.js
packages/lib/services/database/migrations/index.js
packages/lib/services/database/sqlStringToLines.js
packages/lib/services/database/types.js
@@ -1619,6 +1750,7 @@ packages/lib/services/e2ee/ppk/webCrypto/buildRsaCryptoProvider.js
packages/lib/services/e2ee/types.js
packages/lib/services/e2ee/utils.test.js
packages/lib/services/e2ee/utils.js
packages/lib/services/interop/InteropService.noteLock.test.js
packages/lib/services/interop/InteropService.test.js
packages/lib/services/interop/InteropService.js
packages/lib/services/interop/InteropService_Exporter_Base.js
@@ -1634,12 +1766,15 @@ packages/lib/services/interop/InteropService_Exporter_Raw.js
packages/lib/services/interop/InteropService_Importer_Base.js
packages/lib/services/interop/InteropService_Importer_Custom.js
packages/lib/services/interop/InteropService_Importer_EnexToHtml.js
packages/lib/services/interop/InteropService_Importer_EnexToMd.test.js
packages/lib/services/interop/InteropService_Importer_EnexToMd.js
packages/lib/services/interop/InteropService_Importer_Jex.js
packages/lib/services/interop/InteropService_Importer_Md.test.js
packages/lib/services/interop/InteropService_Importer_Md.js
packages/lib/services/interop/InteropService_Importer_Md_frontmatter.test.js
packages/lib/services/interop/InteropService_Importer_Md_frontmatter.js
packages/lib/services/interop/InteropService_Importer_Obsidian.test.js
packages/lib/services/interop/InteropService_Importer_Obsidian.js
packages/lib/services/interop/InteropService_Importer_OneNote.postprocessHtml.test.js
packages/lib/services/interop/InteropService_Importer_OneNote.test.js
packages/lib/services/interop/InteropService_Importer_OneNote.js
@@ -1661,33 +1796,33 @@ packages/lib/services/keychain/KeychainServiceDriver.node.js
packages/lib/services/keychain/KeychainServiceDriverBase.js
packages/lib/services/mcp/McpServer.test.js
packages/lib/services/mcp/McpServer.js
packages/lib/services/mcp/registry.js
packages/lib/services/mcp/tools/createNote.js
packages/lib/services/mcp/tools/createNotebook.js
packages/lib/services/mcp/tools/deleteNote.js
packages/lib/services/mcp/tools/listNotebooks.js
packages/lib/services/mcp/tools/listTags.js
packages/lib/services/mcp/tools/manageTags.js
packages/lib/services/mcp/tools/readNote.js
packages/lib/services/mcp/tools/searchNotes.js
packages/lib/services/mcp/tools/semanticSearchNotes.js
packages/lib/services/mcp/tools/updateNote.js
packages/lib/services/mcp/types.js
packages/lib/services/noteList/checkboxPieCss.js
packages/lib/services/noteList/defaultLeftToRightListRenderer.js
packages/lib/services/noteList/defaultListRenderer.js
packages/lib/services/noteList/defaultMultiColumnsRenderer.js
packages/lib/services/noteList/depNameToNoteProp.js
packages/lib/services/noteList/isSyncDisabledConflict.test.js
packages/lib/services/noteList/isSyncDisabledConflict.js
packages/lib/services/noteList/renderTemplate.test.js
packages/lib/services/noteList/renderTemplate.js
packages/lib/services/noteList/renderViewProps.test.js
packages/lib/services/noteList/renderViewProps.js
packages/lib/services/noteList/renderers.js
packages/lib/services/noteLock/NoteLockKey.js
packages/lib/services/noteLock/NoteLockKeyMigration.test.js
packages/lib/services/noteLock/NoteLockKeyMigration.js
packages/lib/services/noteLock/NoteLockNote.js
packages/lib/services/noteLock/NoteLockService.test.js
packages/lib/services/noteLock/NoteLockService.js
packages/lib/services/noteLock/NoteLockSession.test.js
packages/lib/services/noteLock/NoteLockSession.js
packages/lib/services/noteLock/hasLockedNoteWhileSessionLocked.js
packages/lib/services/noteLock/isNoteLockEnabled.js
packages/lib/services/noteLock/noteLockPrompts.test.js
packages/lib/services/noteLock/noteLockPrompts.js
packages/lib/services/noteLock/setNoteLockState.test.js
packages/lib/services/noteLock/setNoteLockState.js
packages/lib/services/ocr/OcrDriverBase.js
packages/lib/services/ocr/OcrService.test.js
packages/lib/services/ocr/OcrService.js
@@ -1785,11 +1920,13 @@ packages/lib/services/rest/routes/events.js
packages/lib/services/rest/routes/folders.test.js
packages/lib/services/rest/routes/folders.js
packages/lib/services/rest/routes/master_keys.js
packages/lib/services/rest/routes/mcp.test.js
packages/lib/services/rest/routes/mcp.js
packages/lib/services/rest/routes/notes.test.js
packages/lib/services/rest/routes/notes.js
packages/lib/services/rest/routes/ping.js
packages/lib/services/rest/routes/resources.js
packages/lib/services/rest/routes/revisions.test.js
packages/lib/services/rest/routes/revisions.js
packages/lib/services/rest/routes/search.js
packages/lib/services/rest/routes/tags.js
@@ -1803,6 +1940,7 @@ packages/lib/services/rest/utils/readonlyProperties.js
packages/lib/services/rest/utils/requestFields.js
packages/lib/services/rest/utils/requestPaginationOptions.js
packages/lib/services/search/SearchEngine.resources.test.js
packages/lib/services/search/SearchEngine.semantic.test.js
packages/lib/services/search/SearchEngine.test.js
packages/lib/services/search/SearchEngine.js
packages/lib/services/search/SearchEngineUtils.test.js
@@ -1832,10 +1970,12 @@ packages/lib/services/synchronizer/ItemUploader.test.js
packages/lib/services/synchronizer/ItemUploader.js
packages/lib/services/synchronizer/LockHandler.js
packages/lib/services/synchronizer/MigrationHandler.js
packages/lib/services/synchronizer/Synchronizer.autoMerge.test.js
packages/lib/services/synchronizer/Synchronizer.baseFields.test.js
packages/lib/services/synchronizer/Synchronizer.basics.test.js
packages/lib/services/synchronizer/Synchronizer.conflicts.test.js
packages/lib/services/synchronizer/Synchronizer.e2ee.test.js
packages/lib/services/synchronizer/Synchronizer.noteLock.test.js
packages/lib/services/synchronizer/Synchronizer.ppk.test.js
packages/lib/services/synchronizer/Synchronizer.resources.test.js
packages/lib/services/synchronizer/Synchronizer.revisions.test.js
@@ -1869,9 +2009,12 @@ packages/lib/services/trash/permanentlyDeleteOldItems.test.js
packages/lib/services/trash/permanentlyDeleteOldItems.js
packages/lib/services/trash/restoreItems.test.js
packages/lib/services/trash/restoreItems.js
packages/lib/services/whiteboard/findEmptySpot.test.js
packages/lib/services/whiteboard/findEmptySpot.js
packages/lib/services/whiteboard/generateId.js
packages/lib/services/whiteboard/jsoncanvas.js
packages/lib/services/whiteboard/parse.js
packages/lib/services/whiteboard/presetColors.js
packages/lib/services/whiteboard/resolveRef.js
packages/lib/services/whiteboard/serialize.js
packages/lib/services/whiteboard/whiteboard.test.js
@@ -1880,6 +2023,8 @@ packages/lib/shim-init-node.js
packages/lib/shim.js
packages/lib/string-utils.test.js
packages/lib/string-utils.js
packages/lib/testing/ai/semanticSearch.js
packages/lib/testing/createLocalhostServer.js
packages/lib/testing/dom-test-environment.js
packages/lib/testing/plugins/createTestPlugin.js
packages/lib/testing/share/makeMockShareInvitation.js
@@ -1932,6 +2077,8 @@ packages/lib/utils/replaceUnsupportedCharacters.test.js
packages/lib/utils/replaceUnsupportedCharacters.js
packages/lib/utils/resolvePathWithinDir.test.js
packages/lib/utils/resolvePathWithinDir.js
packages/lib/utils/tls/setExtraRootCertificates.test.js
packages/lib/utils/tls/setExtraRootCertificates.js
packages/lib/utils/types/pdfJs.js
packages/lib/utils/userFetcher.js
packages/lib/utils/webDAVUtils.test.js
@@ -1999,6 +2146,7 @@ packages/renderer/MdToHtml/rules/frontmatter.js
packages/renderer/MdToHtml/rules/highlight_keywords.js
packages/renderer/MdToHtml/rules/html_image.js
packages/renderer/MdToHtml/rules/image.js
packages/renderer/MdToHtml/rules/katex.test.js
packages/renderer/MdToHtml/rules/katex.js
packages/renderer/MdToHtml/rules/link_close.js
packages/renderer/MdToHtml/rules/link_open.js
@@ -2034,6 +2182,7 @@ packages/tools/checkIgnoredFiles.js
packages/tools/checkLibPaths.test.js
packages/tools/checkLibPaths.js
packages/tools/convertThemesToCss.js
packages/tools/eslint-rules/describeFilename.test.js
packages/tools/fuzzer/ActionRunner.js
packages/tools/fuzzer/Fuzzer.js
packages/tools/fuzzer/cli.js
@@ -2111,8 +2260,8 @@ packages/tools/updateMarkdownDoc.js
packages/tools/utils/discourse.test.js
packages/tools/utils/discourse.js
packages/tools/utils/loadSponsors.js
packages/tools/utils/parsePluralLocalizationForm.test.js
packages/tools/utils/parsePluralLocalizationForm.js
packages/tools/utils/parsePlurallLocalizationForm.test.js
packages/tools/utils/translation.js
packages/tools/validateFilenames.js
packages/tools/website/build.js
+170 -21
View File
@@ -8,6 +8,7 @@ _releases/
**/node_modules/
**/abcjs-basic-min.js
packages/generator-joplin/generators/app/templates/api/
packages/generator-joplin/generators/app/templates/script/publish/dist/
Assets/
docs/
highlight.pack.js
@@ -82,6 +83,7 @@ packages/lib/welcomeAssets.js
packages/editor/*/vendor/
packages/plugins/**/api
packages/plugins/**/dist
packages/plugin-repo-cli/dist/
packages/server/dist/
packages/utils/dist/
packages/transcribe/dist/
@@ -109,6 +111,7 @@ packages/app-cli/app/cli-utils.js
packages/app-cli/app/command-apidoc.js
packages/app-cli/app/command-attach.js
packages/app-cli/app/command-batch.js
packages/app-cli/app/command-cat.test.js
packages/app-cli/app/command-cat.js
packages/app-cli/app/command-clear.js
packages/app-cli/app/command-config.js
@@ -117,6 +120,7 @@ packages/app-cli/app/command-done.test.js
packages/app-cli/app/command-done.js
packages/app-cli/app/command-dump.js
packages/app-cli/app/command-e2ee.js
packages/app-cli/app/command-edit.test.js
packages/app-cli/app/command-edit.js
packages/app-cli/app/command-exit.js
packages/app-cli/app/command-export-sync-status.js
@@ -141,6 +145,7 @@ packages/app-cli/app/command-rmnote.test.js
packages/app-cli/app/command-rmnote.js
packages/app-cli/app/command-search.js
packages/app-cli/app/command-server.js
packages/app-cli/app/command-set.test.js
packages/app-cli/app/command-set.js
packages/app-cli/app/command-settingschema.js
packages/app-cli/app/command-share.test.js
@@ -185,6 +190,7 @@ packages/app-desktop/ElectronAppWrapper.js
packages/app-desktop/InteropServiceHelper.js
packages/app-desktop/app.reducer.test.js
packages/app-desktop/app.reducer.js
packages/app-desktop/app.test.js
packages/app-desktop/app.js
packages/app-desktop/bridge.js
packages/app-desktop/checkForUpdates.js
@@ -203,6 +209,7 @@ packages/app-desktop/commands/openNoteInNewWindow.js
packages/app-desktop/commands/openPrimaryAppInstance.js
packages/app-desktop/commands/openProfileDirectory.js
packages/app-desktop/commands/openSecondaryAppInstance.js
packages/app-desktop/commands/openSyncWizard.js
packages/app-desktop/commands/replaceMisspelling.js
packages/app-desktop/commands/restoreNoteRevision.js
packages/app-desktop/commands/showProfileEditor.js
@@ -215,17 +222,25 @@ packages/app-desktop/commands/switchProfile3.js
packages/app-desktop/commands/toggleExternalEditing.js
packages/app-desktop/commands/toggleSafeMode.js
packages/app-desktop/commands/toggleTabMovesFocus.js
packages/app-desktop/gui/AiDegradedNotice.js
packages/app-desktop/gui/Button/Button.js
packages/app-desktop/gui/ChatPanel/ChatHistory.js
packages/app-desktop/gui/ChatPanel/ChatMessageItem.js
packages/app-desktop/gui/ChatPanel/ChatPanel.test.js
packages/app-desktop/gui/ChatPanel/ChatPanel.js
packages/app-desktop/gui/ClipperConfigScreen.js
packages/app-desktop/gui/ConfigScreen/ButtonBar.js
packages/app-desktop/gui/ConfigScreen/ConfigScreen.js
packages/app-desktop/gui/ConfigScreen/Sidebar.js
packages/app-desktop/gui/ConfigScreen/configSearch.js
packages/app-desktop/gui/ConfigScreen/controls/AiIndexStatus.js
packages/app-desktop/gui/ConfigScreen/controls/AiStatus.js
packages/app-desktop/gui/ConfigScreen/controls/FontSearch.js
packages/app-desktop/gui/ConfigScreen/controls/GlobalHotkeyInput.test.js
packages/app-desktop/gui/ConfigScreen/controls/GlobalHotkeyInput.js
packages/app-desktop/gui/ConfigScreen/controls/MissingPasswordHelpLink.js
packages/app-desktop/gui/ConfigScreen/controls/NoteLockSettings.js
packages/app-desktop/gui/ConfigScreen/controls/SettingButton.js
packages/app-desktop/gui/ConfigScreen/controls/SettingComponent.js
packages/app-desktop/gui/ConfigScreen/controls/SettingDescription.js
packages/app-desktop/gui/ConfigScreen/controls/SettingHeader.js
@@ -234,8 +249,10 @@ packages/app-desktop/gui/ConfigScreen/controls/ToggleAdvancedSettingsButton.js
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js
packages/app-desktop/gui/ConfigScreen/controls/plugins/SearchPlugins.js
packages/app-desktop/gui/ConfigScreen/controls/useNoteLockMode.js
packages/app-desktop/gui/ConfigScreen/searchHighlight.test.js
packages/app-desktop/gui/ConfigScreen/searchHighlight.js
packages/app-desktop/gui/ConfigScreen/types.js
packages/app-desktop/gui/DialogButtonRow.js
packages/app-desktop/gui/DialogButtonRow/useKeyboardHandler.js
packages/app-desktop/gui/DialogTitle.js
@@ -254,6 +271,7 @@ packages/app-desktop/gui/HelpButton.js
packages/app-desktop/gui/IconButton.js
packages/app-desktop/gui/ImportScreen.js
packages/app-desktop/gui/InlineCombobox.js
packages/app-desktop/gui/InlineMarkdownDisplay.js
packages/app-desktop/gui/ItemList.js
packages/app-desktop/gui/JoplinCloudConfigScreen.js
packages/app-desktop/gui/JoplinCloudLoginScreen.js
@@ -264,7 +282,10 @@ packages/app-desktop/gui/KeymapConfig/styles/index.js
packages/app-desktop/gui/KeymapConfig/utils/getLabel.js
packages/app-desktop/gui/KeymapConfig/utils/useCommandStatus.js
packages/app-desktop/gui/KeymapConfig/utils/useKeymap.js
packages/app-desktop/gui/MainLayoutPane.js
packages/app-desktop/gui/MainScreen.js
packages/app-desktop/gui/MainScreen/handleCallbackUrl.test.js
packages/app-desktop/gui/MainScreen/handleCallbackUrl.js
packages/app-desktop/gui/MasterPasswordDialog/Dialog.js
packages/app-desktop/gui/MenuBar.js
packages/app-desktop/gui/MultiNoteActions.js
@@ -317,6 +338,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useCursorPositioning.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useEditDialog.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useEditDialogEventListeners.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useHighlightedSearchTerms.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useKeyboardRefocusHandler.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useLinkTooltips.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js
@@ -333,9 +355,13 @@ packages/app-desktop/gui/NoteEditor/NoteBody/WhiteboardEditor/nodes/GroupNode.js
packages/app-desktop/gui/NoteEditor/NoteBody/WhiteboardEditor/nodes/LinkNode.js
packages/app-desktop/gui/NoteEditor/NoteBody/WhiteboardEditor/nodes/TextNode.js
packages/app-desktop/gui/NoteEditor/NoteBody/WhiteboardEditor/nodes/handlePositions.js
packages/app-desktop/gui/NoteEditor/NoteBody/WhiteboardEditor/nodes/stripUnsafeStyles.test.js
packages/app-desktop/gui/NoteEditor/NoteBody/WhiteboardEditor/nodes/stripUnsafeStyles.js
packages/app-desktop/gui/NoteEditor/NoteBody/WhiteboardEditor/nodes/useCardWheel.js
packages/app-desktop/gui/NoteEditor/NoteBody/WhiteboardEditor/useCheckboxToggle.test.js
packages/app-desktop/gui/NoteEditor/NoteBody/WhiteboardEditor/useCheckboxToggle.js
packages/app-desktop/gui/NoteEditor/NoteEditor.js
packages/app-desktop/gui/NoteEditor/NoteLockPanel/NoteLockPanel.js
packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.js
packages/app-desktop/gui/NoteEditor/StatusBar.js
packages/app-desktop/gui/NoteEditor/WarningBanner/BannerContent.js
@@ -378,6 +404,7 @@ packages/app-desktop/gui/NoteEditor/utils/usePluginServiceRegistration.js
packages/app-desktop/gui/NoteEditor/utils/useResourceUnwatcher.js
packages/app-desktop/gui/NoteEditor/utils/useScheduleSaveCallbacks.js
packages/app-desktop/gui/NoteEditor/utils/useScrollWhenReadyOptions.js
packages/app-desktop/gui/NoteEditor/utils/useSearchMarkers.test.js
packages/app-desktop/gui/NoteEditor/utils/useSearchMarkers.js
packages/app-desktop/gui/NoteEditor/utils/useWindowCommandHandler.js
packages/app-desktop/gui/NoteList/NoteList2.js
@@ -414,6 +441,7 @@ packages/app-desktop/gui/NoteListHeader/utils/validateColumns.js
packages/app-desktop/gui/NoteListItem/NoteListItem.js
packages/app-desktop/gui/NoteListItem/utils/getNoteElementIdFromJoplinId.js
packages/app-desktop/gui/NoteListItem/utils/getNoteTitleHtml.js
packages/app-desktop/gui/NoteListItem/utils/noteTitleTooltip.js
packages/app-desktop/gui/NoteListItem/utils/prepareViewProps.test.js
packages/app-desktop/gui/NoteListItem/utils/prepareViewProps.js
packages/app-desktop/gui/NoteListItem/utils/types.js
@@ -424,6 +452,9 @@ packages/app-desktop/gui/NoteListItem/utils/useRenderedNote.js
packages/app-desktop/gui/NoteListItem/utils/useRootElement.test.js
packages/app-desktop/gui/NoteListItem/utils/useRootElement.js
packages/app-desktop/gui/NoteListWrapper/NoteListWrapper.js
packages/app-desktop/gui/NoteLockMigrationDialog/Dialog.test.js
packages/app-desktop/gui/NoteLockMigrationDialog/Dialog.js
packages/app-desktop/gui/NoteLockUnlockDialog/Dialog.js
packages/app-desktop/gui/NotePropertiesDialog.js
packages/app-desktop/gui/NoteRevisionViewer.js
packages/app-desktop/gui/NoteSearchBar.js
@@ -443,6 +474,7 @@ packages/app-desktop/gui/PopupNotification/types.js
packages/app-desktop/gui/ProfileEditor.js
packages/app-desktop/gui/PromptDialog.test.js
packages/app-desktop/gui/PromptDialog.js
packages/app-desktop/gui/PublishFolderDialog.js
packages/app-desktop/gui/QuitSyncDialog.js
packages/app-desktop/gui/ResizableLayout/LayoutItemContainer.js
packages/app-desktop/gui/ResizableLayout/MoveButtons.js
@@ -457,6 +489,8 @@ packages/app-desktop/gui/ResizableLayout/utils/persist.test.js
packages/app-desktop/gui/ResizableLayout/utils/persist.js
packages/app-desktop/gui/ResizableLayout/utils/removeItem.js
packages/app-desktop/gui/ResizableLayout/utils/removeKeylessItems.js
packages/app-desktop/gui/ResizableLayout/utils/resizeLogic.test.js
packages/app-desktop/gui/ResizableLayout/utils/resizeLogic.js
packages/app-desktop/gui/ResizableLayout/utils/setLayoutItemProps.js
packages/app-desktop/gui/ResizableLayout/utils/style.js
packages/app-desktop/gui/ResizableLayout/utils/types.js
@@ -508,6 +542,7 @@ packages/app-desktop/gui/ToolbarSpace.js
packages/app-desktop/gui/TrashNotification/TrashNotification.js
packages/app-desktop/gui/TrashNotification/TrashNotificationMessage.js
packages/app-desktop/gui/UpdateNotification/UpdateNotification.js
packages/app-desktop/gui/WarningBanner.js
packages/app-desktop/gui/WindowCommandsAndDialogs/AppDialogs.js
packages/app-desktop/gui/WindowCommandsAndDialogs/ModalMessageOverlay.js
packages/app-desktop/gui/WindowCommandsAndDialogs/PluginDialogs.js
@@ -516,8 +551,10 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/addNoteToWhiteboard.j
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/addProfile.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/commandPalette.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/deleteFolder.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/disableNoteEncryption.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/duplicateNote.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/editAlarm.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/enableNoteEncryption.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/exportPdf.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/globalRedo.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/globalUndo.js
@@ -526,6 +563,7 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/hideModalMessage.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/importFrom.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/index.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/linkToNote.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/lockEncryptedNotes.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/moveToFolder.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newFolder.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newNote.test.js
@@ -533,6 +571,7 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newNote.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newSubFolder.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newTodo.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newWhiteboard.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openAiChatConversation.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openFolder.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openFolderDialog.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openItem.js
@@ -545,6 +584,7 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/renameTag.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/resetLayout.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/restoreFolder.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/restoreNote.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/revealInNotebook.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/revealResourceFile.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/search.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/setTags.js
@@ -552,10 +592,12 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showModalMessage.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showNoteContentProperties.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showNoteProperties.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showPrompt.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showPublishFolderDialog.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showShareFolderDialog.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showShareNoteDialog.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showSpellCheckerMenu.test.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showSpellCheckerMenu.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleAiChat.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleEditors.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleLayoutMoveMode.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleMenuBar.js
@@ -595,20 +637,30 @@ packages/app-desktop/gui/utils/NoteListUtils.js
packages/app-desktop/gui/utils/announceForAccessibility.js
packages/app-desktop/gui/utils/convertToScreenCoordinates.js
packages/app-desktop/gui/utils/dragAndDrop.js
packages/app-desktop/gui/utils/hasNoteLockKey.js
packages/app-desktop/gui/utils/loadScript.js
packages/app-desktop/gulpfile.js
packages/app-desktop/integration-tests/chatPanel.spec.js
packages/app-desktop/integration-tests/goToAnything.spec.js
packages/app-desktop/integration-tests/main.spec.js
packages/app-desktop/integration-tests/markdownEditor.spec.js
packages/app-desktop/integration-tests/models/ChangeAppLayoutScreen.js
packages/app-desktop/integration-tests/models/EditorCodeDialog.js
packages/app-desktop/integration-tests/models/ChatPanel.js
packages/app-desktop/integration-tests/models/GoToAnything.js
packages/app-desktop/integration-tests/models/JoplinCloudLoginScreen.js
packages/app-desktop/integration-tests/models/MainScreen.js
packages/app-desktop/integration-tests/models/NoteEditorScreen.js
packages/app-desktop/integration-tests/models/NoteEditor/BaseEditor.js
packages/app-desktop/integration-tests/models/NoteEditor/EditorCodeDialog.js
packages/app-desktop/integration-tests/models/NoteEditor/MarkdownEditor.js
packages/app-desktop/integration-tests/models/NoteEditor/NoteEditorScreen.js
packages/app-desktop/integration-tests/models/NoteEditor/NoteViewer.js
packages/app-desktop/integration-tests/models/NoteEditor/RichTextEditor.js
packages/app-desktop/integration-tests/models/NoteList.js
packages/app-desktop/integration-tests/models/SettingsScreen.js
packages/app-desktop/integration-tests/models/SettingsScreen/SettingsScreen.js
packages/app-desktop/integration-tests/models/SettingsScreen/SyncTab.js
packages/app-desktop/integration-tests/models/Sidebar.js
packages/app-desktop/integration-tests/multiWindow.spec.js
packages/app-desktop/integration-tests/networking.spec.js
packages/app-desktop/integration-tests/noteList.spec.js
packages/app-desktop/integration-tests/pluginApi.spec.js
packages/app-desktop/integration-tests/resizableLayout.spec.js
@@ -637,6 +689,8 @@ packages/app-desktop/main-html.js
packages/app-desktop/main.js
packages/app-desktop/playwright.config.js
packages/app-desktop/plugins/GotoAnything.js
packages/app-desktop/services/aiStatusBridge.test.js
packages/app-desktop/services/aiStatusBridge.js
packages/app-desktop/services/autoUpdater/AutoUpdaterService.test.js
packages/app-desktop/services/autoUpdater/AutoUpdaterService.js
packages/app-desktop/services/bridge.js
@@ -662,6 +716,7 @@ packages/app-desktop/services/plugins/types.js
packages/app-desktop/services/restart.js
packages/app-desktop/services/spellChecker/SpellCheckerServiceDriverNative.js
packages/app-desktop/tools/bundleJs.js
packages/app-desktop/tools/cleanOnnxRuntime.js
packages/app-desktop/tools/copy7Zip.js
packages/app-desktop/tools/generateLatestArm64Yml.js
packages/app-desktop/tools/githubReleasesUtils.js
@@ -681,8 +736,11 @@ packages/app-desktop/utils/customProtocols/registerCustomProtocols.js
packages/app-desktop/utils/getAssetPath.js
packages/app-desktop/utils/initReact.js
packages/app-desktop/utils/initializeCommandService.js
packages/app-desktop/utils/initializeNetworkConnectionAttemptTimeout.js
packages/app-desktop/utils/isSafeToOpen.test.js
packages/app-desktop/utils/isSafeToOpen.js
packages/app-desktop/utils/layout/layoutKeyToLabel.js
packages/app-desktop/utils/noteLockPrompts.js
packages/app-desktop/utils/restartInSafeModeFromMain.test.js
packages/app-desktop/utils/restartInSafeModeFromMain.js
packages/app-desktop/utils/sourceMapSetup.js
@@ -700,6 +758,7 @@ packages/app-mobile/commands/util/goToNote.js
packages/app-mobile/commands/util/showResource.js
packages/app-mobile/components/BetaChip.js
packages/app-mobile/components/BottomDrawer.js
packages/app-mobile/components/BottomDrawerMenu.js
packages/app-mobile/components/CameraView/ActionButtons.js
packages/app-mobile/components/CameraView/Camera/index.jest.js
packages/app-mobile/components/CameraView/Camera/index.js
@@ -797,6 +856,11 @@ packages/app-mobile/components/ScreenHeader/index.js
packages/app-mobile/components/SearchInput.js
packages/app-mobile/components/SelectDateTimeDialog.js
packages/app-mobile/components/SideMenu.js
packages/app-mobile/components/SideMenuContent/BottomPanelActions.js
packages/app-mobile/components/SideMenuContent/FolderItem.js
packages/app-mobile/components/SideMenuContent/SideMenuContent.js
packages/app-mobile/components/SideMenuContent/SideMenuItem.js
packages/app-mobile/components/SideMenuContent/SidebarIcon.js
packages/app-mobile/components/SideMenuContentNote.js
packages/app-mobile/components/SyncWizard/JoplinCloudIcon.js
packages/app-mobile/components/SyncWizard/SyncWizard.js
@@ -829,6 +893,7 @@ packages/app-mobile/components/plugins/PluginNotification.js
packages/app-mobile/components/plugins/PluginRunner.js
packages/app-mobile/components/plugins/PluginRunnerWebView.test.js
packages/app-mobile/components/plugins/PluginRunnerWebView.js
packages/app-mobile/components/plugins/backgroundPage/initializeDialogWebView.test.js
packages/app-mobile/components/plugins/backgroundPage/initializeDialogWebView.js
packages/app-mobile/components/plugins/backgroundPage/initializePluginBackgroundIframe.js
packages/app-mobile/components/plugins/backgroundPage/pluginRunnerBackgroundPage.js
@@ -861,6 +926,8 @@ packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/expo
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportDebugReport.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportProfile.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/makeImportExportCacheDirectory.js
packages/app-mobile/components/screens/ConfigScreen/NoteLockConfig.test.js
packages/app-mobile/components/screens/ConfigScreen/NoteLockConfig.js
packages/app-mobile/components/screens/ConfigScreen/SectionDescription.js
packages/app-mobile/components/screens/ConfigScreen/SectionHeader.js
packages/app-mobile/components/screens/ConfigScreen/SectionSelector/SectionTab.js
@@ -904,6 +971,7 @@ packages/app-mobile/components/screens/JoplinCloudLoginScreen.js
packages/app-mobile/components/screens/LogScreen.js
packages/app-mobile/components/screens/Note/Note.test.js
packages/app-mobile/components/screens/Note/Note.js
packages/app-mobile/components/screens/Note/NoteLockPanel.js
packages/app-mobile/components/screens/Note/commands/attachFile.js
packages/app-mobile/components/screens/Note/commands/hideKeyboard.js
packages/app-mobile/components/screens/Note/commands/index.js
@@ -911,6 +979,8 @@ packages/app-mobile/components/screens/Note/commands/insertDateTime.js
packages/app-mobile/components/screens/Note/commands/setTags.js
packages/app-mobile/components/screens/Note/commands/toggleVisiblePanes.js
packages/app-mobile/components/screens/Note/types.js
packages/app-mobile/components/screens/NoteLockMigrationScreen.test.js
packages/app-mobile/components/screens/NoteLockMigrationScreen.js
packages/app-mobile/components/screens/NoteRevisionViewer.test.js
packages/app-mobile/components/screens/NoteRevisionViewer.js
packages/app-mobile/components/screens/NoteTagsDialog.js
@@ -939,7 +1009,6 @@ packages/app-mobile/components/screens/resourceScreenUtils.test.js
packages/app-mobile/components/screens/resourceScreenUtils.js
packages/app-mobile/components/screens/status.js
packages/app-mobile/components/screens/tags.js
packages/app-mobile/components/side-menu-content.js
packages/app-mobile/components/testing/TestProviderStack.js
packages/app-mobile/components/voiceTyping/AudioRecordingBanner.js
packages/app-mobile/components/voiceTyping/RecordingControls.js
@@ -1021,6 +1090,7 @@ packages/app-mobile/utils/database-driver-react-native.web.js
packages/app-mobile/utils/debounce.js
packages/app-mobile/utils/focusView.js
packages/app-mobile/utils/fs-driver/constants.js
packages/app-mobile/utils/fs-driver/fs-driver-rn.test.js
packages/app-mobile/utils/fs-driver/fs-driver-rn.js
packages/app-mobile/utils/fs-driver/fs-driver-rn.web.js
packages/app-mobile/utils/fs-driver/fs-driver-rn.web.worker.js
@@ -1116,8 +1186,6 @@ packages/editor/CodeMirror/extensions/links/ctrlClickLinksExtension.js
packages/editor/CodeMirror/extensions/links/followLinkTooltipExtension.test.js
packages/editor/CodeMirror/extensions/links/followLinkTooltipExtension.js
packages/editor/CodeMirror/extensions/links/referenceLinksStateField.js
packages/editor/CodeMirror/extensions/links/utils/findLineMatchingLink.test.js
packages/editor/CodeMirror/extensions/links/utils/findLineMatchingLink.js
packages/editor/CodeMirror/extensions/links/utils/getUrlAtPosition.js
packages/editor/CodeMirror/extensions/links/utils/openLink.js
packages/editor/CodeMirror/extensions/markdownDecorationExtension.test.js
@@ -1148,6 +1216,8 @@ packages/editor/CodeMirror/extensions/rendering/replaceLinks.test.js
packages/editor/CodeMirror/extensions/rendering/replaceLinks.js
packages/editor/CodeMirror/extensions/rendering/types.js
packages/editor/CodeMirror/extensions/rendering/utils/makeBlockReplaceExtension.js
packages/editor/CodeMirror/extensions/rendering/utils/makeHideReplaceExtension.js
packages/editor/CodeMirror/extensions/rendering/utils/makeInlineReplaceExtension.test.js
packages/editor/CodeMirror/extensions/rendering/utils/makeInlineReplaceExtension.js
packages/editor/CodeMirror/extensions/rendering/utils/nodeIntersectsSelection.js
packages/editor/CodeMirror/extensions/searchExtension.test.js
@@ -1159,6 +1229,7 @@ packages/editor/CodeMirror/pluginApi/PluginLoader.js
packages/editor/CodeMirror/pluginApi/codeMirrorRequire.js
packages/editor/CodeMirror/pluginApi/customEditorCompletion.test.js
packages/editor/CodeMirror/pluginApi/customEditorCompletion.js
packages/editor/CodeMirror/testing/backspaceOnce.js
packages/editor/CodeMirror/testing/createEditorControl.js
packages/editor/CodeMirror/testing/createTestEditor.js
packages/editor/CodeMirror/testing/findNodesWithName.js
@@ -1166,7 +1237,12 @@ packages/editor/CodeMirror/testing/forceFullParse.js
packages/editor/CodeMirror/testing/loadLanguages.js
packages/editor/CodeMirror/testing/pressReleaseKey.js
packages/editor/CodeMirror/testing/typeText.js
packages/editor/CodeMirror/testing/visibleEditorText.js
packages/editor/CodeMirror/theme.js
packages/editor/CodeMirror/utils/clampSelectionToDocument.test.js
packages/editor/CodeMirror/utils/clampSelectionToDocument.js
packages/editor/CodeMirror/utils/findPositionMatchingLink.test.js
packages/editor/CodeMirror/utils/findPositionMatchingLink.js
packages/editor/CodeMirror/utils/formatting/RegionSpec.js
packages/editor/CodeMirror/utils/formatting/computeSelectionFormatting.test.js
packages/editor/CodeMirror/utils/formatting/computeSelectionFormatting.js
@@ -1295,9 +1371,6 @@ packages/generator-joplin/generators/app/templates/api/index.js
packages/generator-joplin/generators/app/templates/api/noteListType.js
packages/generator-joplin/generators/app/templates/api/types.js
packages/generator-joplin/generators/app/templates/api_index.js
packages/generator-joplin/generators/app/templates/script/publish/steps/verifyBuild.js
packages/generator-joplin/generators/app/templates/script/publish/steps/verifyGitState.js
packages/generator-joplin/generators/app/templates/script/publish/utils/logger.js
packages/generator-joplin/generators/app/templates/src/index.js
packages/generator-joplin/tools/updateCategories.js
packages/htmlpack/index.test.js
@@ -1315,6 +1388,7 @@ packages/lib/BaseModel.test.js
packages/lib/BaseModel.js
packages/lib/BaseSyncTarget.js
packages/lib/Cache.js
packages/lib/ClipperServer.test.js
packages/lib/ClipperServer.js
packages/lib/CssUtils.js
packages/lib/EventDispatcher.test.js
@@ -1346,6 +1420,7 @@ packages/lib/SyncTargetWebDAV.js
packages/lib/Synchronizer.js
packages/lib/TaskQueue.test.js
packages/lib/TaskQueue.js
packages/lib/WebDavApi.test.js
packages/lib/WebDavApi.js
packages/lib/WelcomeUtils.test.js
packages/lib/WelcomeUtils.js
@@ -1380,15 +1455,18 @@ packages/lib/components/shared/NoteEditor/WarningBanner/useEditorTypeMigrationBa
packages/lib/components/shared/NoteList/getEmptyFolderMessage.js
packages/lib/components/shared/NoteRevisionViewer/getHelpMessage.js
packages/lib/components/shared/NoteRevisionViewer/useDeleteHistoryClick.js
packages/lib/components/shared/PublishFolderDialog/useOnPublishFolderLinkClick.js
packages/lib/components/shared/SamlShared.js
packages/lib/components/shared/ShareNoteDialog/onUnshareNoteClick.js
packages/lib/components/shared/ShareNoteDialog/types.js
packages/lib/components/shared/ShareNoteDialog/useEncryptionWarningMessage.js
packages/lib/components/shared/ShareNoteDialog/useIsPublished.js
packages/lib/components/shared/ShareNoteDialog/useOnShareLinkClick.js
packages/lib/components/shared/ShareNoteDialog/useShareStatusMessage.js
packages/lib/components/shared/SsoScreenShared.js
packages/lib/components/shared/config/config-search-text.js
packages/lib/components/shared/config/config-search.test.js
packages/lib/components/shared/config/config-shared.test.js
packages/lib/components/shared/config/config-shared.js
packages/lib/components/shared/config/plugins/types.js
packages/lib/components/shared/config/plugins/useOnDeleteHandler.js
@@ -1397,6 +1475,7 @@ packages/lib/components/shared/config/plugins/useOnInstallHandler.js
packages/lib/components/shared/config/shouldShowMissingPasswordWarning.test.js
packages/lib/components/shared/config/shouldShowMissingPasswordWarning.js
packages/lib/components/shared/dropbox-login-shared.js
packages/lib/components/shared/note-screen-shared.test.js
packages/lib/components/shared/note-screen-shared.js
packages/lib/components/shared/reduxSharedMiddleware.js
packages/lib/components/shared/side-menu-shared.test.js
@@ -1415,6 +1494,7 @@ packages/lib/errorUtils.js
packages/lib/errors.js
packages/lib/eventManager.test.js
packages/lib/eventManager.js
packages/lib/file-api-driver-joplinServer.test.js
packages/lib/file-api-driver-joplinServer.js
packages/lib/file-api-driver-local.js
packages/lib/file-api-driver-memory.js
@@ -1471,7 +1551,11 @@ packages/lib/mimeUtils.test.js
packages/lib/models/Alarm.js
packages/lib/models/BaseItem.test.js
packages/lib/models/BaseItem.js
packages/lib/models/ChatConversation.test.js
packages/lib/models/ChatConversation.js
packages/lib/models/ChatMessage.js
packages/lib/models/ConflictNoteState.js
packages/lib/models/Folder.publishing.test.js
packages/lib/models/Folder.sharing.test.js
packages/lib/models/Folder.test.js
packages/lib/models/Folder.js
@@ -1569,6 +1653,7 @@ packages/lib/services/ResourceEditWatcher/reducer.js
packages/lib/services/ResourceFetcher.js
packages/lib/services/ResourceService.test.js
packages/lib/services/ResourceService.js
packages/lib/services/RevisionService.noteLock.test.js
packages/lib/services/RevisionService.test.js
packages/lib/services/RevisionService.js
packages/lib/services/SettingUtils.js
@@ -1589,16 +1674,48 @@ packages/lib/services/ai/SearchService.js
packages/lib/services/ai/SqliteVec.test.js
packages/lib/services/ai/aiSettingsTransition.test.js
packages/lib/services/ai/aiSettingsTransition.js
packages/lib/services/ai/availability.test.js
packages/lib/services/ai/availability.js
packages/lib/services/ai/chunker.test.js
packages/lib/services/ai/chunker.js
packages/lib/services/ai/classification.js
packages/lib/services/ai/noteChat.test.js
packages/lib/services/ai/noteChat.tools.test.js
packages/lib/services/ai/noteChat.js
packages/lib/services/ai/providers/Anthropic.test.js
packages/lib/services/ai/providers/Anthropic.js
packages/lib/services/ai/providers/ChatProviderBase.js
packages/lib/services/ai/providers/JoplinCloud.test.js
packages/lib/services/ai/providers/JoplinCloud.js
packages/lib/services/ai/providers/OpenAiCompatible.test.js
packages/lib/services/ai/providers/OpenAiCompatible.js
packages/lib/services/ai/providers/TestProvider.js
packages/lib/services/ai/testing/TestEmbeddingProvider.test.js
packages/lib/services/ai/testing/TestEmbeddingProvider.js
packages/lib/services/ai/tools/ToolIndex.js
packages/lib/services/ai/tools/buildEditorTools.js
packages/lib/services/ai/tools/global/createNote.js
packages/lib/services/ai/tools/global/createNotebook.js
packages/lib/services/ai/tools/global/deleteNote.js
packages/lib/services/ai/tools/global/index.js
packages/lib/services/ai/tools/global/listNotebooks.js
packages/lib/services/ai/tools/global/listTags.js
packages/lib/services/ai/tools/global/manageTags.js
packages/lib/services/ai/tools/global/readImage.js
packages/lib/services/ai/tools/global/readNote.js
packages/lib/services/ai/tools/global/searchNotes.js
packages/lib/services/ai/tools/global/semanticSearchNotes.js
packages/lib/services/ai/tools/global/updateNote.js
packages/lib/services/ai/tools/types.js
packages/lib/services/ai/tools/utils/applyNoteEdits.test.js
packages/lib/services/ai/tools/utils/applyNoteEdits.js
packages/lib/services/ai/tools/utils/buildTool.js
packages/lib/services/ai/tools/utils/serializeToolOutput.js
packages/lib/services/ai/tools/utils/settings.js
packages/lib/services/ai/types.js
packages/lib/services/ai/utils/extractReasoning.test.js
packages/lib/services/ai/utils/extractReasoning.js
packages/lib/services/ai/utils/findFencedBlock.js
packages/lib/services/commands/MenuUtils.js
packages/lib/services/commands/ToolbarButtonUtils.test.js
packages/lib/services/commands/ToolbarButtonUtils.js
@@ -1608,6 +1725,19 @@ packages/lib/services/commands/isEditorCommand.js
packages/lib/services/commands/propsHaveChanged.js
packages/lib/services/commands/stateToWhenClauseContext.test.js
packages/lib/services/commands/stateToWhenClauseContext.js
packages/lib/services/conflict/autoMergeNote.test.js
packages/lib/services/conflict/autoMergeNote.js
packages/lib/services/conflict/boundedDiff3.test.js
packages/lib/services/conflict/boundedDiff3.js
packages/lib/services/conflict/decryptNoteInMemory.js
packages/lib/services/conflict/diffNotes.test.js
packages/lib/services/conflict/diffNotes.js
packages/lib/services/conflict/isAutoMergeEnabled.js
packages/lib/services/conflict/isConflictResolutionEnabled.js
packages/lib/services/conflict/loadConflictData.test.js
packages/lib/services/conflict/loadConflictData.js
packages/lib/services/conflict/wordDiff.test.js
packages/lib/services/conflict/wordDiff.js
packages/lib/services/contextkey/contextkey.js
packages/lib/services/database/addMigrationFile.js
packages/lib/services/database/isSqliteSyntaxError.js
@@ -1623,6 +1753,7 @@ packages/lib/services/database/migrations/50.js
packages/lib/services/database/migrations/51.js
packages/lib/services/database/migrations/52.js
packages/lib/services/database/migrations/53.js
packages/lib/services/database/migrations/54.js
packages/lib/services/database/migrations/index.js
packages/lib/services/database/sqlStringToLines.js
packages/lib/services/database/types.js
@@ -1645,6 +1776,7 @@ packages/lib/services/e2ee/ppk/webCrypto/buildRsaCryptoProvider.js
packages/lib/services/e2ee/types.js
packages/lib/services/e2ee/utils.test.js
packages/lib/services/e2ee/utils.js
packages/lib/services/interop/InteropService.noteLock.test.js
packages/lib/services/interop/InteropService.test.js
packages/lib/services/interop/InteropService.js
packages/lib/services/interop/InteropService_Exporter_Base.js
@@ -1660,12 +1792,15 @@ packages/lib/services/interop/InteropService_Exporter_Raw.js
packages/lib/services/interop/InteropService_Importer_Base.js
packages/lib/services/interop/InteropService_Importer_Custom.js
packages/lib/services/interop/InteropService_Importer_EnexToHtml.js
packages/lib/services/interop/InteropService_Importer_EnexToMd.test.js
packages/lib/services/interop/InteropService_Importer_EnexToMd.js
packages/lib/services/interop/InteropService_Importer_Jex.js
packages/lib/services/interop/InteropService_Importer_Md.test.js
packages/lib/services/interop/InteropService_Importer_Md.js
packages/lib/services/interop/InteropService_Importer_Md_frontmatter.test.js
packages/lib/services/interop/InteropService_Importer_Md_frontmatter.js
packages/lib/services/interop/InteropService_Importer_Obsidian.test.js
packages/lib/services/interop/InteropService_Importer_Obsidian.js
packages/lib/services/interop/InteropService_Importer_OneNote.postprocessHtml.test.js
packages/lib/services/interop/InteropService_Importer_OneNote.test.js
packages/lib/services/interop/InteropService_Importer_OneNote.js
@@ -1687,33 +1822,33 @@ packages/lib/services/keychain/KeychainServiceDriver.node.js
packages/lib/services/keychain/KeychainServiceDriverBase.js
packages/lib/services/mcp/McpServer.test.js
packages/lib/services/mcp/McpServer.js
packages/lib/services/mcp/registry.js
packages/lib/services/mcp/tools/createNote.js
packages/lib/services/mcp/tools/createNotebook.js
packages/lib/services/mcp/tools/deleteNote.js
packages/lib/services/mcp/tools/listNotebooks.js
packages/lib/services/mcp/tools/listTags.js
packages/lib/services/mcp/tools/manageTags.js
packages/lib/services/mcp/tools/readNote.js
packages/lib/services/mcp/tools/searchNotes.js
packages/lib/services/mcp/tools/semanticSearchNotes.js
packages/lib/services/mcp/tools/updateNote.js
packages/lib/services/mcp/types.js
packages/lib/services/noteList/checkboxPieCss.js
packages/lib/services/noteList/defaultLeftToRightListRenderer.js
packages/lib/services/noteList/defaultListRenderer.js
packages/lib/services/noteList/defaultMultiColumnsRenderer.js
packages/lib/services/noteList/depNameToNoteProp.js
packages/lib/services/noteList/isSyncDisabledConflict.test.js
packages/lib/services/noteList/isSyncDisabledConflict.js
packages/lib/services/noteList/renderTemplate.test.js
packages/lib/services/noteList/renderTemplate.js
packages/lib/services/noteList/renderViewProps.test.js
packages/lib/services/noteList/renderViewProps.js
packages/lib/services/noteList/renderers.js
packages/lib/services/noteLock/NoteLockKey.js
packages/lib/services/noteLock/NoteLockKeyMigration.test.js
packages/lib/services/noteLock/NoteLockKeyMigration.js
packages/lib/services/noteLock/NoteLockNote.js
packages/lib/services/noteLock/NoteLockService.test.js
packages/lib/services/noteLock/NoteLockService.js
packages/lib/services/noteLock/NoteLockSession.test.js
packages/lib/services/noteLock/NoteLockSession.js
packages/lib/services/noteLock/hasLockedNoteWhileSessionLocked.js
packages/lib/services/noteLock/isNoteLockEnabled.js
packages/lib/services/noteLock/noteLockPrompts.test.js
packages/lib/services/noteLock/noteLockPrompts.js
packages/lib/services/noteLock/setNoteLockState.test.js
packages/lib/services/noteLock/setNoteLockState.js
packages/lib/services/ocr/OcrDriverBase.js
packages/lib/services/ocr/OcrService.test.js
packages/lib/services/ocr/OcrService.js
@@ -1811,11 +1946,13 @@ packages/lib/services/rest/routes/events.js
packages/lib/services/rest/routes/folders.test.js
packages/lib/services/rest/routes/folders.js
packages/lib/services/rest/routes/master_keys.js
packages/lib/services/rest/routes/mcp.test.js
packages/lib/services/rest/routes/mcp.js
packages/lib/services/rest/routes/notes.test.js
packages/lib/services/rest/routes/notes.js
packages/lib/services/rest/routes/ping.js
packages/lib/services/rest/routes/resources.js
packages/lib/services/rest/routes/revisions.test.js
packages/lib/services/rest/routes/revisions.js
packages/lib/services/rest/routes/search.js
packages/lib/services/rest/routes/tags.js
@@ -1829,6 +1966,7 @@ packages/lib/services/rest/utils/readonlyProperties.js
packages/lib/services/rest/utils/requestFields.js
packages/lib/services/rest/utils/requestPaginationOptions.js
packages/lib/services/search/SearchEngine.resources.test.js
packages/lib/services/search/SearchEngine.semantic.test.js
packages/lib/services/search/SearchEngine.test.js
packages/lib/services/search/SearchEngine.js
packages/lib/services/search/SearchEngineUtils.test.js
@@ -1858,10 +1996,12 @@ packages/lib/services/synchronizer/ItemUploader.test.js
packages/lib/services/synchronizer/ItemUploader.js
packages/lib/services/synchronizer/LockHandler.js
packages/lib/services/synchronizer/MigrationHandler.js
packages/lib/services/synchronizer/Synchronizer.autoMerge.test.js
packages/lib/services/synchronizer/Synchronizer.baseFields.test.js
packages/lib/services/synchronizer/Synchronizer.basics.test.js
packages/lib/services/synchronizer/Synchronizer.conflicts.test.js
packages/lib/services/synchronizer/Synchronizer.e2ee.test.js
packages/lib/services/synchronizer/Synchronizer.noteLock.test.js
packages/lib/services/synchronizer/Synchronizer.ppk.test.js
packages/lib/services/synchronizer/Synchronizer.resources.test.js
packages/lib/services/synchronizer/Synchronizer.revisions.test.js
@@ -1895,9 +2035,12 @@ packages/lib/services/trash/permanentlyDeleteOldItems.test.js
packages/lib/services/trash/permanentlyDeleteOldItems.js
packages/lib/services/trash/restoreItems.test.js
packages/lib/services/trash/restoreItems.js
packages/lib/services/whiteboard/findEmptySpot.test.js
packages/lib/services/whiteboard/findEmptySpot.js
packages/lib/services/whiteboard/generateId.js
packages/lib/services/whiteboard/jsoncanvas.js
packages/lib/services/whiteboard/parse.js
packages/lib/services/whiteboard/presetColors.js
packages/lib/services/whiteboard/resolveRef.js
packages/lib/services/whiteboard/serialize.js
packages/lib/services/whiteboard/whiteboard.test.js
@@ -1906,6 +2049,8 @@ packages/lib/shim-init-node.js
packages/lib/shim.js
packages/lib/string-utils.test.js
packages/lib/string-utils.js
packages/lib/testing/ai/semanticSearch.js
packages/lib/testing/createLocalhostServer.js
packages/lib/testing/dom-test-environment.js
packages/lib/testing/plugins/createTestPlugin.js
packages/lib/testing/share/makeMockShareInvitation.js
@@ -1958,6 +2103,8 @@ packages/lib/utils/replaceUnsupportedCharacters.test.js
packages/lib/utils/replaceUnsupportedCharacters.js
packages/lib/utils/resolvePathWithinDir.test.js
packages/lib/utils/resolvePathWithinDir.js
packages/lib/utils/tls/setExtraRootCertificates.test.js
packages/lib/utils/tls/setExtraRootCertificates.js
packages/lib/utils/types/pdfJs.js
packages/lib/utils/userFetcher.js
packages/lib/utils/webDAVUtils.test.js
@@ -2025,6 +2172,7 @@ packages/renderer/MdToHtml/rules/frontmatter.js
packages/renderer/MdToHtml/rules/highlight_keywords.js
packages/renderer/MdToHtml/rules/html_image.js
packages/renderer/MdToHtml/rules/image.js
packages/renderer/MdToHtml/rules/katex.test.js
packages/renderer/MdToHtml/rules/katex.js
packages/renderer/MdToHtml/rules/link_close.js
packages/renderer/MdToHtml/rules/link_open.js
@@ -2060,6 +2208,7 @@ packages/tools/checkIgnoredFiles.js
packages/tools/checkLibPaths.test.js
packages/tools/checkLibPaths.js
packages/tools/convertThemesToCss.js
packages/tools/eslint-rules/describeFilename.test.js
packages/tools/fuzzer/ActionRunner.js
packages/tools/fuzzer/Fuzzer.js
packages/tools/fuzzer/cli.js
@@ -2137,8 +2286,8 @@ packages/tools/updateMarkdownDoc.js
packages/tools/utils/discourse.test.js
packages/tools/utils/discourse.js
packages/tools/utils/loadSponsors.js
packages/tools/utils/parsePluralLocalizationForm.test.js
packages/tools/utils/parsePluralLocalizationForm.js
packages/tools/utils/parsePlurallLocalizationForm.test.js
packages/tools/utils/translation.js
packages/tools/validateFilenames.js
packages/tools/website/build.js
@@ -1,90 +0,0 @@
# This patch's goal is to work around an issue in the NSIS uninstaller on Windows:
# - For future uninstallers, this patch backports an upstream commit that changes how
# running copies of the app are found.
# - See https://github.com/electron-userland/electron-builder/pull/8133
# - If an existing uninstaller fails, gives an option to continue with the installation
# despite the failure.
# - Updates "uninstall failed" error messages to state that uninstallation failed (rather
# than incorrectly stating that the issue was with closing the app).
#
# See https://github.com/laurent22/joplin/pull/11541
diff --git a/templates/nsis/include/allowOnlyOneInstallerInstance.nsh b/templates/nsis/include/allowOnlyOneInstallerInstance.nsh
index fe5d45c730f36c9fe8d8cfea12e242e501b67139..97b27fce6798e30e3e631221435f09b3579e77c3 100644
--- a/templates/nsis/include/allowOnlyOneInstallerInstance.nsh
+++ b/templates/nsis/include/allowOnlyOneInstallerInstance.nsh
@@ -42,7 +42,7 @@
${nsProcess::FindProcess} "${_FILE}" ${_ERR}
!else
# find process owned by current user
- nsExec::Exec `%SYSTEMROOT%\System32\cmd.exe /c tasklist /FI "USERNAME eq %USERNAME%" /FI "IMAGENAME eq ${_FILE}" /FO csv | %SYSTEMROOT%\System32\find.exe "${_FILE}"`
+ nsExec::Exec `"$SYSDIR\cmd.exe" /c tasklist /FI "USERNAME eq %USERNAME%" /FI "IMAGENAME eq ${_FILE}" /FO csv | "$SYSDIR\find.exe" "${_FILE}"`
Pop ${_ERR}
!endif
!macroend
@@ -73,7 +73,7 @@
!ifdef INSTALL_MODE_PER_ALL_USERS
nsExec::Exec `taskkill /im "${APP_EXECUTABLE_FILENAME}" /fi "PID ne $pid"`
!else
- nsExec::Exec `%SYSTEMROOT%\System32\cmd.exe /c taskkill /im "${APP_EXECUTABLE_FILENAME}" /fi "PID ne $pid" /fi "USERNAME eq %USERNAME%"`
+ nsExec::Exec `"$SYSDIR\cmd.exe" /c taskkill /im "${APP_EXECUTABLE_FILENAME}" /fi "PID ne $pid" /fi "USERNAME eq %USERNAME%"`
!endif
# to ensure that files are not "in-use"
Sleep 300
@@ -91,7 +91,7 @@
!ifdef INSTALL_MODE_PER_ALL_USERS
nsExec::Exec `taskkill /f /im "${APP_EXECUTABLE_FILENAME}" /fi "PID ne $pid"`
!else
- nsExec::Exec `%SYSTEMROOT%\System32\cmd.exe /c taskkill /f /im "${APP_EXECUTABLE_FILENAME}" /fi "PID ne $pid" /fi "USERNAME eq %USERNAME%"`
+ nsExec::Exec `"$SYSDIR\cmd.exe" /c taskkill /f /im "${APP_EXECUTABLE_FILENAME}" /fi "PID ne $pid" /fi "USERNAME eq %USERNAME%"`
!endif
!insertmacro FIND_PROCESS "${APP_EXECUTABLE_FILENAME}" $R0
${If} $R0 == 0
diff --git a/templates/nsis/include/installUtil.nsh b/templates/nsis/include/installUtil.nsh
index 47367741632726ba0886ac516461dbe98b7aea58..675965762375925a505ca6d8bbb67507ef696c2e 100644
--- a/templates/nsis/include/installUtil.nsh
+++ b/templates/nsis/include/installUtil.nsh
@@ -126,10 +126,11 @@ Function handleUninstallResult
Return
${if} $R0 != 0
- MessageBox MB_OK|MB_ICONEXCLAMATION "$(uninstallFailed): $R0"
+ # MessageBox MB_OK|MB_ICONEXCLAMATION "$(uninstallFailed): $R0"
DetailPrint `Uninstall was not successful. Uninstaller error code: $R0.`
- SetErrorLevel 2
- Quit
+ DetailPrint `Continuing anyway. See https://github.com/laurent22/joplin/pull/11612.`
+ # SetErrorLevel 2
+ # Quit
${endif}
FunctionEnd
@@ -216,11 +217,13 @@ Function uninstallOldVersion
IntOp $R5 $R5 + 1
${if} $R5 > 5
- MessageBox MB_RETRYCANCEL|MB_ICONEXCLAMATION "$(appCannotBeClosed)" /SD IDCANCEL IDRETRY OneMoreAttempt
- Return
+ MessageBox MB_RETRYCANCEL|MB_ICONEXCLAMATION "$(appCannotBeUninstalled)" /SD IDCANCEL IDRETRY ContinueWithoutUninstall
+ Abort ; Exit early
+ ContinueWithoutUninstall:
+ Return
${endIf}
- OneMoreAttempt:
+# OneMoreAttempt: ; Commented out because unused
ExecWait '"$uninstallerFileNameTemp" /S /KEEP_APP_DATA $0 _?=$installationDir' $R0
ifErrors TryInPlace CheckResult
diff --git a/templates/nsis/messages.yml b/templates/nsis/messages.yml
index a1c2847fa48d79f835b30b48e999ccaf3c818657..6884c18d1e77dbd6be114401d23cf5caf3e0dd94 100644
--- a/templates/nsis/messages.yml
+++ b/templates/nsis/messages.yml
@@ -235,3 +235,8 @@ uninstallFailed:
sv: Det gick inte att avinstallera gamla programfiler. Försök att köra installationsprogrammet igen.
uk: Не вдалось видалити старі файли застосунку. Будь ласка, спробуйте запустити встановлювач знов.
zh_TW: 無法俺安裝舊的應用程式檔案。 請嘗試再次執行安裝程式。
+
+
+appCannotBeUninstalled:
+ en: "The old version of ${PRODUCT_NAME} could not be removed. \nClick Retry to skip this step."
+
@@ -0,0 +1,48 @@
# Patched to work around a Windows install issue: On Windows, if an existing uninstaller fails,
# this change gives an option to continue with the installation despite the failure
# (https://github.com/laurent22/joplin/pull/11541).
diff --git a/templates/nsis/include/installUtil.nsh b/templates/nsis/include/installUtil.nsh
index 47367741632726ba0886ac516461dbe98b7aea58..c40a0907688cd8b1b8d09047cb0b380e42110ea1 100644
--- a/templates/nsis/include/installUtil.nsh
+++ b/templates/nsis/include/installUtil.nsh
@@ -128,8 +128,9 @@ Function handleUninstallResult
${if} $R0 != 0
- MessageBox MB_OK|MB_ICONEXCLAMATION "$(uninstallFailed): $R0"
+ # MessageBox MB_OK|MB_ICONEXCLAMATION "$(uninstallFailed): $R0"
DetailPrint `Uninstall was not successful. Uninstaller error code: $R0.`
- SetErrorLevel 2
- Quit
+ DetailPrint `Continuing anyway. See https://github.com/laurent22/joplin/pull/11612.`
+ # SetErrorLevel 2
+ # Quit
${endif}
FunctionEnd
@@ -216,11 +217,13 @@ Function uninstallOldVersion
IntOp $R5 $R5 + 1
${if} $R5 > 5
- MessageBox MB_RETRYCANCEL|MB_ICONEXCLAMATION "$(appCannotBeClosed)" /SD IDCANCEL IDRETRY OneMoreAttempt
- Return
+ MessageBox MB_RETRYCANCEL|MB_ICONEXCLAMATION "$(appCannotBeUninstalled)" /SD IDCANCEL IDRETRY ContinueWithoutUninstall
+ Abort ; Exit early
+ ContinueWithoutUninstall:
+ Return
${endIf}
- OneMoreAttempt:
+# OneMoreAttempt: ; OneMoreAttempt is unused
ExecWait '"$uninstallerFileNameTemp" /S /KEEP_APP_DATA $0 _?=$installationDir' $R0
ifErrors TryInPlace CheckResult
diff --git a/templates/nsis/messages.yml b/templates/nsis/messages.yml
index 982e1b09e424c4f07ce7c1152fe5ce8b703f5f57..20dfa5743d18f8775ce88658014092cf977ad055 100644
--- a/templates/nsis/messages.yml
+++ b/templates/nsis/messages.yml
@@ -238,3 +238,5 @@ uninstallFailed:
appClosing:
en: "Closing running ${PRODUCT_NAME}..."
cs: "Ukončuji aplikaci ${PRODUCT_NAME}..."
+appCannotBeUninstalled:
+ en: "The old version of ${PRODUCT_NAME} could not be removed. \nClick Retry to skip this step."
\ No newline at end of file
@@ -0,0 +1,17 @@
diff --git a/types/react-native-web.d.ts b/types/react-native-web.d.ts
index d6159045aa4b1f6864037cfc1c001a68605b416d..5950b82015f82f7840cf3bd2fc88e81dd0ff789c 100644
--- a/types/react-native-web.d.ts
+++ b/types/react-native-web.d.ts
@@ -3,7 +3,11 @@
import { StyleProp } from 'react-native';
import * as RN from 'react-native';
-declare module 'react-native' {
+// Expo's extended react-native types are out-of-sync with upstream.
+// This causes TypeScript errors when compiling. These errors only occur
+// when strict null checks is disabled, and so do not occur with the default React Native
+// and Expo TypeScript config.
+declare module 'react-native-disabled' {
/**
* View
*/
@@ -0,0 +1,56 @@
# This patch makes react-native-quick-actions compatible with the iOS UIKit
# scene-based lifecycle. Requires calling `setInitialAction` on app load when
# started from a quick action.
diff --git a/RNQuickAction/RNQuickAction/RNQuickActionManager.h b/RNQuickAction/RNQuickAction/RNQuickActionManager.h
index ad4baad075513115892bd120a278f1ba664480b7..7a366391a48e77d7b08eb2741d2a499a267304b8 100644
--- a/RNQuickAction/RNQuickAction/RNQuickActionManager.h
+++ b/RNQuickAction/RNQuickAction/RNQuickActionManager.h
@@ -10,4 +10,5 @@
@interface RNQuickActionManager : NSObject <RCTBridgeModule>
+(void) onQuickActionPress:(UIApplicationShortcutItem *) shortcutItem completionHandler:(void (^)(BOOL succeeded)) completionHandler;
++(void) setInitialAction:(UIApplicationShortcutItem *) shortcutItem;
@end
diff --git a/RNQuickAction/RNQuickAction/RNQuickActionManager.m b/RNQuickAction/RNQuickAction/RNQuickActionManager.m
index 8db8cab947349763a9c3d9d6004868b675b9c7ef..3217a35634ad457e33756b895b32d80f3fa11178 100644
--- a/RNQuickAction/RNQuickAction/RNQuickActionManager.m
+++ b/RNQuickAction/RNQuickAction/RNQuickActionManager.m
@@ -31,6 +31,7 @@ @implementation RNQuickActionManager
RCT_EXPORT_MODULE();
@synthesize bridge = _bridge;
+UIApplicationShortcutItem *lastInitialAction_ = NULL;
- (instancetype)init
{
@@ -39,6 +40,7 @@ - (instancetype)init
selector:@selector(handleQuickActionPress:)
name:RCTShortcutItemClicked
object:nil];
+ _initialAction = lastInitialAction_;
}
return self;
}
@@ -59,7 +61,9 @@ - (void)dealloc
- (void)setBridge:(RCTBridge *)bridge
{
_bridge = bridge;
- _initialAction = [bridge.launchOptions[UIApplicationLaunchOptionsShortcutItemKey] copy];
+ // Patch: The initial action must now be set through `setInitialAction`:
+ // _initialAction = NULL;
+ // _initialAction = [bridge.launchOptions[UIApplicationLaunchOptionsShortcutItemKey] copy];
}
// Map user passed array of UIApplicationShortcutItem
@@ -155,6 +159,11 @@ + (void)onQuickActionPress:(UIApplicationShortcutItem *) shortcutItem completion
completionHandler(YES);
}
++ (void)setInitialAction:(UIApplicationShortcutItem *) shortcutItem
+{
+ lastInitialAction_ = [shortcutItem copy];
+}
+
- (void)handleQuickActionPress:(NSNotification *) notification
{
[_bridge.eventDispatcher sendDeviceEventWithName:@"quickActionShortcut"
@@ -0,0 +1,47 @@
# Fixes the following error:
# A problem occurred evaluating project ':react-native-rsa-native'.
# > Could not find method jcenter() for arguments [] on repository container of type org.gradle.api.internal.artifacts.dsl.DefaultRepositoryHandler.
#
# See https://github.com/amitaymolko/react-native-rsa-native/issues/175
diff --git a/android/bin/build.gradle b/android/bin/build.gradle
index b5f54a631f4bed61973c77c2a6b27c5df0b26d17..28f38ca5966097f798f3f3607ded55dd4fe9b6d1 100644
--- a/android/bin/build.gradle
+++ b/android/bin/build.gradle
@@ -1,7 +1,6 @@
buildscript {
repositories {
maven { url "https://maven.google.com" }
- jcenter()
google()
maven {
url 'https://maven.google.com/'
@@ -41,7 +40,7 @@ allprojects {
repositories {
mavenLocal()
maven { url "https://maven.google.com" }
- jcenter()
+ mavenCentral()
google()
maven {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
diff --git a/android/build.gradle b/android/build.gradle
index 82f60d3d862258e50023076b0db6e792bd2a2503..9e4606903f2dcf4ee7bb49dc878a080023ed314c 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -9,7 +9,6 @@ buildscript {
if (project == rootProject) {
repositories {
google()
- jcenter()
maven {
url "https://maven.fabric.io/public"
}
@@ -45,7 +44,7 @@ allprojects {
repositories {
mavenLocal()
google()
- jcenter()
+ mavenCentral()
maven {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
url "$rootDir/../node_modules/react-native/android"
@@ -1,37 +0,0 @@
diff --git a/platforms/android/src/main/java/org/pgsqlite/SQLitePlugin.java b/platforms/android/src/main/java/org/pgsqlite/SQLitePlugin.java
index 4f2391b..f7cc433 100644
--- a/platforms/android/src/main/java/org/pgsqlite/SQLitePlugin.java
+++ b/platforms/android/src/main/java/org/pgsqlite/SQLitePlugin.java
@@ -8,11 +8,14 @@
package org.pgsqlite;
import android.annotation.SuppressLint;
+import android.database.AbstractWindowedCursor;
import android.database.Cursor;
+import android.database.CursorWindow;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteStatement;
import android.content.Context;
+import android.os.Build;
import android.util.Base64;
import java.io.Closeable;
@@ -808,6 +811,17 @@ public class SQLitePlugin extends ReactContextBaseJavaModule {
throw ex;
}
+ // To try to fix the error "Row too big to fit into CursorWindow" when using sqlite binary bundled with the device
+ // https://github.com/andpor/react-native-sqlite-storage/issues/364#issuecomment-526423153
+ // https://github.com/laurent22/joplin/issues/1767#issuecomment-515617991
+
+ if (cur != null && Build.VERSION.SDK_INT >= 28) {
+ CursorWindow cw = new CursorWindow(null, 50 * 1024 * 1024);
+ AbstractWindowedCursor ac = (AbstractWindowedCursor) cur;
+ ac.setWindow(cw);
+ cur = ac;
+ }
+
// If query result has rows
if (cur != null && cur.moveToFirst()) {
WritableArray rowsArrayResult = Arguments.createArray();
@@ -1,6 +1,25 @@
# Fixes sync issues caused by locale-sensitive lowercasing
# of HTTP headers.
# See https://github.com/laurent22/joplin/issues/10681
# Fixes the following errors:
# - Could not find method jcenter() for arguments [] on repository container of type org.gradle.api.internal.artifacts.dsl.DefaultRepositoryHandler.
# - Sync issues caused by locale-sensitive lowercasing of HTTP headers (see https://github.com/laurent22/joplin/issues/10681).
diff --git a/android/build.gradle b/android/build.gradle
index a4ca7a421bff66bcc378c1092474bac6b7a82738..d2527c7e695d09dac3069fecd14931eae33a03e7 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -6,13 +6,12 @@ def safeExtGet(prop, fallback) {
repositories {
mavenCentral()
- jcenter()
google()
}
buildscript {
repositories {
- jcenter()
+ mavenCentral()
google()
}
dependencies {
diff --git a/android/src/main/java/com/RNFetchBlob/RNFetchBlobConfig.java b/android/src/main/java/com/RNFetchBlob/RNFetchBlobConfig.java
index 8ac9e7a855162cefbf99024eb013c8a3b11de1ec..1c639cf9d84821b6ffc132960e2d1c044bedbd48 100644
--- a/android/src/main/java/com/RNFetchBlob/RNFetchBlobConfig.java
@@ -0,0 +1,15 @@
diff --git a/binary.js b/binary.js
index f91bdbd2e4b9bfb3a6376849598a9352f8678584..f42f96aef91c483c88fdd47ff82473069ce3456d 100644
--- a/binary.js
+++ b/binary.js
@@ -9,7 +9,9 @@ const getPlatform = () => {
const arch = os.arch();
// https://github.com/nodejs/node/blob/c3664227a83cf009e9a2e1ddeadbd09c14ae466f/deps/uv/src/win/util.c#L1566-L1573
- if ((type === "Windows_NT" || type.startsWith("MINGW32_NT-")) && arch === "x64") {
+ // arm64 uses the x64 binary, which runs under emulation - as for Darwin
+ // below. Upstream publishes no aarch64-pc-windows-msvc build.
+ if ((type === "Windows_NT" || type.startsWith("MINGW32_NT-")) && (arch === "x64" || arch === "arm64")) {
return windows;
}
if (type === "Linux" && arch === "x64") {
+16 -5
View File
@@ -1212,6 +1212,13 @@ footer .bottom-links-row p {
margin-top: 2em;
}
.price-container p.pricing-table-billed-annually {
font-size: 14px;
font-style: italic;
opacity: 0.7;
margin-top: 0.5em;
}
.price-row {
display: flex;
flex-direction: row;
@@ -1304,6 +1311,15 @@ footer .bottom-links-row p {
opacity: 0.4;
}
.cfa-button {
margin-top: 10px;
}
.cfa-description {
font-style: italic;
text-align: center;
}
@media (max-width: 1400px) {
.price-row .plan-type {
font-size: 16px;
@@ -1392,8 +1408,3 @@ footer .bottom-links-row p {
:lang(zh-cn) #plans-section .faq {
display: none;
}
.cfa-button {
margin-top: 10px;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 700 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 575 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

+25 -3
View File
@@ -1,3 +1,6 @@
// Set to true once a Windows ARM64 build is published in a release.
const windowsArm64Enabled = false;
async function getOs() {
// The macOS release is available for Intel and Apple silicon processors,
@@ -25,7 +28,21 @@ async function getOs() {
}
}
if (navigator.appVersion.indexOf("Win")!=-1) return "windows";
// Windows on ARM has the same appVersion as x64, so the architecture has to
// come from getHighEntropyValues, as for Apple Silicon above. Falling back
// to the x64 build is fine - it runs on ARM under emulation.
if (navigator.appVersion.indexOf("Win")!=-1) {
if (windowsArm64Enabled) {
try {
const platformInfo = await navigator.userAgentData.getHighEntropyValues(['architecture']);
console.info('Got platform info:', platformInfo);
if (platformInfo.architecture === 'arm') return "windowsArm64";
} catch (error) {
console.warn('Failed getting Windows architecture:', error);
}
}
return "windows";
}
if (navigator.appVersion.indexOf("X11")!=-1) return "linux";
if (navigator.appVersion.indexOf("Linux")!=-1) return "linux";
return null;
@@ -78,7 +95,12 @@ async function setupDownloadPage() {
$('.page-download .get-it-desktop a').each(function() {
const href = $(this).attr('href');
if (href.indexOf('-Setup') > 0) downloadLinks['windows'] = href;
// The arm64 installer is also named "-Setup", so it must be excluded
// from the x64 link even when disabled, or it would overwrite it.
if (href.indexOf('-arm64.exe') > 0) {
if (windowsArm64Enabled) downloadLinks['windowsArm64'] = href;
}
else if (href.indexOf('-Setup') > 0) downloadLinks['windows'] = href;
if (href.indexOf('.dmg') > 0) downloadLinks['macOs'] = href;
if (href.indexOf('arm64.DMG') > 0) downloadLinks['macOsM1'] = href;
if (href.indexOf('.AppImage') > 0) downloadLinks['linux'] = href;
@@ -131,7 +153,7 @@ const localePathOverrides = {
};
// List of supported language codes
const supportedLanguages = ['fr', 'de', 'zh'];
const supportedLanguages = ['fr', 'de', 'hr', 'zh'];
function getLocalePath(langCode) {
const pathPrefix = localePathOverrides[langCode] || langCode;
+81 -100
View File
@@ -1,4 +1,83 @@
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Joplin]]></title><description><![CDATA[Joplin, the open source note-taking application]]></description><link>https://joplinapp.org</link><generator>RSS for Node</generator><lastBuildDate>Tue, 05 May 2026 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Tue, 05 May 2026 00:00:00 GMT</pubDate><item><title><![CDATA[What's new in Joplin 3.6]]></title><description><![CDATA[<p>Joplin 3.6 brings a more polished editing experience, a much improved OneNote importer, and a clearer view of what's happening when the app syncs. Mobile also gains a number of features that previously were only available on desktop.</p>
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Joplin]]></title><description><![CDATA[Joplin, the open source note-taking application]]></description><link>https://joplinapp.org</link><generator>RSS for Node</generator><lastBuildDate>Wed, 21 Oct 2026 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><item><title><![CDATA[How we protect your privacy with Joplin Cloud AI]]></title><description><![CDATA[<p>Joplin Cloud AI features work on your notes. Notes are often personal, so we've set up the AI service so that your content goes to as few places as possible and stays there for as short a time as possible.</p>
<p><strong>A European AI provider, processing in Europe.</strong> Joplin Cloud AI uses models from Mistral AI, a French company. We send requests to Mistral's EU regional endpoint, which processes data only in data centres in the European Union and EFTA countries. Your notes are not sent to the United States for processing.</p>
<p><strong>Zero Data Retention.</strong> Mistral has enabled Zero Data Retention on our account. Once a response has been generated, Mistral doesn't store or log the content of the request or the response. Only basic billing information, such as how many tokens were used, is kept.</p>
<p><strong>No training on your notes.</strong> Your content isn't used to train or improve AI models. Mistral's commercial terms rule this out for our account, and training is switched off in our account settings.</p>
<p><strong>GDPR.</strong> Our use of Mistral is covered by their Data Processing Addendum, which sets out how they may handle personal data on our behalf under the GDPR.</p>
<p><strong>What Joplin Cloud stores.</strong> Our own servers don't keep the text you send to the AI or the replies you get back. For each request we only record who made it, which model was used, how many tokens it used and whether it succeeded. We need this to apply usage limits and keep track of costs.</p>
]]></description><link>https://joplinapp.org/news/20261021-joplin-cloud-ai-privacy</link><guid isPermaLink="false">20261021-joplin-cloud-ai-privacy</guid><pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[Your notes and AI: privacy first]]></title><description><![CDATA[<p>AI can be useful for summarising, rewriting and asking questions about your notes. But your notes are private, so you should always know where your data is going.</p>
<p><strong>Joplin does not send your notes to an AI service by default.</strong> AI features are disabled by default, and you have to explicitly enable them and choose an AI provider. <a href="https://joplinapp.org/help/apps/ai_chat">Learn more about AI Chat</a>.</p>
<h2>Keep your notes on your own device<a name="keep-your-notes-on-your-own-device" href="#keep-your-notes-on-your-own-device" class="heading-anchor">🔗</a></h2>
<p>You can connect Joplin to a local AI model, such as Ollama or LM Studio. In this case, your notes stay within your own device or private network and are not sent to an external service.</p>
<p>Joplin also blocks remote AI providers by default, as an additional safeguard.</p>
<h2>You control what AI can access<a name="you-control-what-ai-can-access" href="#you-control-what-ai-can-access" class="heading-anchor">🔗</a></h2>
<p>Joplin lets you choose <strong>which tools are available to AI</strong>. This applies both to AI Chat and to the Model Context Protocol (MCP) integration.</p>
<p>For example, you can allow an AI assistant to search your notes without giving it permission to modify them. You can enable or disable individual tools depending on what you are comfortable with.</p>
<p>This means that connecting an AI service to Joplin does not automatically give it unrestricted access to your data or to all of Joplin's capabilities. <a href="https://joplinapp.org/help/apps/ai_mcp">Learn more about MCP</a>.</p>
<h2>If you use a cloud AI provider<a name="if-you-use-a-cloud-ai-provider" href="#if-you-use-a-cloud-ai-provider" class="heading-anchor">🔗</a></h2>
<p>You can connect Joplin to cloud AI services such as Joplin Cloud AI, OpenAI or Anthropic. When you do, the relevant data is sent to the provider you have selected.</p>
<p>For AI Chat, Joplin only sends the note content required for your request — such as the currently open note or selected text — rather than automatically sending your entire notebook. <a href="https://joplinapp.org/help/apps/ai_chat_panel">Learn more about the AI Chat panel</a>.</p>
<h2>You are in control<a name="you-are-in-control" href="#you-are-in-control" class="heading-anchor">🔗</a></h2>
<p>Joplin's approach is simple: <strong>AI should be opt-in, transparent and configurable.</strong></p>
<p>You choose whether to use AI, which provider to use, whether to use a local or remote model, and which tools that AI is allowed to access.</p>
<p>If you want maximum privacy, use a local AI model and only enable the tools you actually need.</p>
]]></description><link>https://joplinapp.org/news/20260914-ai-privacy</link><guid isPermaLink="false">20260914-ai-privacy</guid><pubDate>Mon, 14 Sep 2026 00:00:00 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[What's new in Joplin 3.7]]></title><description><![CDATA[<p>Joplin 3.7 is the release where AI arrives in the app. A new chat panel lets you ask questions about the note you're working on, semantic search finds notes by meaning rather than by keyword, and a built-in MCP server allows external tools to work with your notes.</p>
<p>Alongside this, the desktop app gains whiteboards - a new way to lay out notes and ideas visually - while mobile continues its interface redesign. Both platforms benefit from a more capable Markdown table editor and a number of sync reliability improvements.</p>
<h2>Improvements across desktop and mobile<a name="improvements-across-desktop-and-mobile" href="#improvements-across-desktop-and-mobile" class="heading-anchor">🔗</a></h2>
<h3>A real table editor in the Markdown editor<a name="a-real-table-editor-in-the-markdown-editor" href="#a-real-table-editor-in-the-markdown-editor" class="heading-anchor">🔗</a></h3>
<p>Editing Markdown tables has always been the least pleasant part of writing in plain text. Joplin 3.7 adds an interactive table editor to the Markdown editor, with a context menu to add and delete rows and columns.</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20260831-table-editor.png" alt="The Joplin Markdown editor showing a table with the table context menu open, listing the commands to add and delete rows and columns."></p>
<p>Because the editor is shared between platforms, this is available on mobile as well.</p>
<h3>More reliable syncing<a name="more-reliable-syncing" href="#more-reliable-syncing" class="heading-anchor">🔗</a></h3>
<p>Syncing is faster and safer this cycle. Deleting a lot of notes at once on Joplin Server and Joplin Cloud no longer takes as long, and if you happen to be typing in a note just as it's being updated from another device, your changes are no longer at risk of being overwritten.</p>
<h3>Faster startup<a name="faster-startup" href="#faster-startup" class="heading-anchor">🔗</a></h3>
<p>Joplin no longer processes plugin files that don't need processing at startup, which noticeably reduces launch time for users with several plugins installed.</p>
<h2>Desktop-specific improvements<a name="desktop-specific-improvements" href="#desktop-specific-improvements" class="heading-anchor">🔗</a></h2>
<h3>AI features (Beta)<a name="ai-features-beta" href="#ai-features-beta" class="heading-anchor">🔗</a></h3>
<p>The headline addition of this release is a set of AI features built into the app.</p>
<p>The <strong>chat panel</strong> sits alongside the current note and can answer questions about it, summarise it, or make edits to it on your behalf, as well as read images in your notes.</p>
<p>To use the AI features you need a provider, and this release introduces <strong>Joplin Cloud AI</strong> to make that as simple as possible. If you already sync with Joplin Cloud, you can simply turn it on and start using the chat panel right away - there's no API key to obtain, and nothing to configure.</p>
<p>If you would rather use your own provider, that remains possible: the chat panel also works with your own OpenAI or Anthropic key. It can equally be pointed at a local model running on your own machine, in which case your notes never leave your computer at all.</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20260831-ai-chat.png" alt="The Joplin desktop app with the AI chat panel open beside a note, showing a question about the note and the reply from the model."></p>
<p><strong>Semantic search</strong> is now available in the sidebar and in Go To Anything. Unlike the regular search, it matches on meaning rather than exact words, so a search for &quot;holiday plans&quot; can surface a note titled &quot;Trip to Portugal&quot;.</p>
<p>This is powered by a new note embeddings indexer, which runs in the background and indexes both note titles and content.</p>
<p>Finally, all of this is available to plugin authors too. Plugins can now hold conversations with a model, run semantic searches, and work with the note index directly - which opens the door to a whole new class of plugins, from automatic tagging and summarising to tools that surface related notes as you write.</p>
<p>All the AI features are marked as <strong>Beta</strong>: they work, but they are new, and we expect to refine them based on how people actually use them.</p>
<h3>MCP server<a name="mcp-server" href="#mcp-server" class="heading-anchor">🔗</a></h3>
<p>Joplin now includes an <a href="https://modelcontextprotocol.io/">MCP server</a>, which means external AI tools and assistants can read from and write to your notes through a standard protocol. If you already use an MCP-capable app such as Claude, it can now work directly with your Joplin data.</p>
<h3>Whiteboards<a name="whiteboards" href="#whiteboards" class="heading-anchor">🔗</a></h3>
<p>A brand new feature on desktop: whiteboards let you arrange notes, files and free-form cards on an infinite canvas, and connect them with labelled arrows.</p>
<p>Notebooks and tags are good at filing things away, but they say little about how your notes relate to one another. A whiteboard gives you that missing view: you can map out a project, plan a piece of writing, or lay out the pieces of a problem and see the connections between them at a glance.</p>
<p>Cards and groups can be coloured, and files can be dragged and dropped straight onto the canvas.</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20260831-whiteboard.png" alt="A Joplin whiteboard with several coloured cards linked to notes, arranged on the canvas and connected by labelled arrows."></p>
<h3>Importing from other apps<a name="importing-from-other-apps" href="#importing-from-other-apps" class="heading-anchor">🔗</a></h3>
<p>Joplin 3.7 adds support for importing an Obsidian vault, on both desktop and the CLI, making it straightforward to move over from Obsidian while keeping your notes and their structure.</p>
<p>The Evernote importer now resolves cross-notebook links in many cases where they were previously lost.</p>
<p>The OneNote importer also received another round of fixes, in particular around note order, printouts and handwriting.</p>
<h3>Publishing and sharing<a name="publishing-and-sharing" href="#publishing-and-sharing" class="heading-anchor">🔗</a></h3>
<p>Joplin Cloud users could already publish a note to the internet as a public page. It is now possible to publish a <strong>whole notebook</strong> at once.</p>
<p>The notes are published together, with their links between each other preserved, which effectively turns a notebook into a small website: a piece of documentation, a recipe collection, a set of course notes, or anything else you want to share as a whole rather than one page at a time. Anyone with the link can read it, and it stays up to date as you edit the notes in Joplin.</p>
<p>And when sharing a notebook, email addresses are now auto-completed, which makes sharing with the same people repeatedly much quicker.</p>
<h3>Settings search<a name="settings-search" href="#settings-search" class="heading-anchor">🔗</a></h3>
<p>Joplin has accumulated a lot of settings over the years, and it is not always obvious which section holds the one you need. The configuration screen now has a search box: type what you are after, and the matching settings appear, wherever they happen to live.</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20260831-setting-search.png" alt="The Joplin configuration screen with a term typed into the new search box and the matching settings listed below it."></p>
<h3>Post-quantum encryption for connections<a name="post-quantum-encryption-for-connections" href="#post-quantum-encryption-for-connections" class="heading-anchor">🔗</a></h3>
<p>Joplin desktop now supports post-quantum cryptography (PQC) for TLS connections. This protects your synchronisation traffic against &quot;harvest now, decrypt later&quot; attacks, where encrypted data captured today could be decrypted by a quantum computer in the future.</p>
<h3>Easier installation on Linux<a name="easier-installation-on-linux" href="#easier-installation-on-linux" class="heading-anchor">🔗</a></h3>
<p>The Linux AppImage no longer depends on the deprecated <code>libfuse2</code> library. On many distributions, installing Joplin used to mean first tracking down and installing that dependency by hand - it should now simply run out of the box.</p>
<h2>Mobile-specific improvements<a name="mobile-specific-improvements" href="#mobile-specific-improvements" class="heading-anchor">🔗</a></h2>
<h3>The interface redesign continues<a name="the-interface-redesign-continues" href="#the-interface-redesign-continues" class="heading-anchor">🔗</a></h3>
<p>The mobile redesign that started in previous releases carries on through 3.7, this time focusing on the menus you reach for constantly - creating a note, acting on one, or long-pressing a notebook. They are now easier to read at a glance, and their options are laid out more sensibly, so common actions take less hunting. The search screen has had the same treatment.</p>
<p>Dialogs have been tidied up too, with buttons always appearing in the same order throughout the app - a small thing, but it means you stop having to check which side &quot;Cancel&quot; is on before tapping.</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20260831-mobile-new-note.png" alt="The redesigned new note menu on Joplin mobile."></p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20260831-mobile-note-menu.png" alt="The redesigned note actions menu on Joplin mobile."></p>
<h3>Faster encryption<a name="faster-encryption" href="#faster-encryption" class="heading-anchor">🔗</a></h3>
<p>The native crypto module has been refactored, which makes encryption and decryption substantially faster on mobile. This is most noticeable when first setting up end-to-end encryption or syncing a large collection for the first time.</p>
<h3>Notebook sort options<a name="notebook-sort-options" href="#notebook-sort-options" class="heading-anchor">🔗</a></h3>
<p>You can now choose how the notebook list itself is ordered, from the configuration screen: alphabetically, or by which notebook was most recently updated - and in either direction. Sorting by recent activity is particularly handy on mobile, as it keeps the notebooks you are actually working in at the top of the sidebar.</p>
<h2>Bug fixes and security fixes<a name="bug-fixes-and-security-fixes" href="#bug-fixes-and-security-fixes" class="heading-anchor">🔗</a></h2>
<p>Beyond the new features, a lot of this release went into making the apps more reliable and more secure: Joplin 3.7 includes <strong>over 100 bug fixes</strong> and <strong>eight security fixes</strong> across desktop and mobile.</p>
<p>For the full list of changes, see <a href="https://joplinapp.org/help/about/changelog/desktop/">the desktop changelog</a> and <a href="https://joplinapp.org/help/about/changelog/android/">the mobile changelog</a>.</p>
]]></description><link>https://joplinapp.org/news/20260831-release-3-7</link><guid isPermaLink="false">20260831-release-3-7</guid><pubDate>Mon, 31 Aug 2026 00:00:00 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[What's new in Joplin 3.6]]></title><description><![CDATA[<p>Joplin 3.6 brings a more polished editing experience, a much improved OneNote importer, and a clearer view of what's happening when the app syncs. Mobile also gains a number of features that previously were only available on desktop.</p>
<h2>Improvements across desktop and mobile<a name="improvements-across-desktop-and-mobile" href="#improvements-across-desktop-and-mobile" class="heading-anchor">🔗</a></h2>
<h3>A more visual Markdown editor<a name="a-more-visual-markdown-editor" href="#a-more-visual-markdown-editor" class="heading-anchor">🔗</a></h3>
<p>The Markdown editor now renders formatting directly in the editor by default, which means headings, bold and italic text, links, images and other elements now appear styled as you type, instead of as raw Markdown. This brings the editing experience much closer to the final rendered note, while keeping all the speed and predictability of plain Markdown.</p>
@@ -485,102 +564,4 @@ sys 0m38.013s</p>
<li><a href="https://ryotak.net/">RyotaK</a></li>
<li><a href="https://twitter.com/YNizry">Yaniv Nizry</a></li>
</ul>
]]></description><link>https://joplinapp.org/news/20231023-white-hat-hackers</link><guid isPermaLink="false">20231023-white-hat-hackers</guid><pubDate>Mon, 23 Oct 2023 00:00:00 GMT</pubDate><twitter-text>Working in the shadows with white-hat hackers</twitter-text></item><item><title><![CDATA[What's new in Joplin 2.12]]></title><description><![CDATA[<h2>Desktop<a name="desktop" href="#desktop" class="heading-anchor">🔗</a></h2>
<h3>Support for Apple Silicon<a name="support-for-apple-silicon" href="#support-for-apple-silicon" class="heading-anchor">🔗</a></h3>
<p>A new release is now available for Apple Silicon, which provides improve performances on this architecture.</p>
<h3>Rich Text editor<a name="rich-text-editor" href="#rich-text-editor" class="heading-anchor">🔗</a></h3>
<p>In this release, we've undertaken numerous enhancements and addressed various bugs in the Rich Text editor. Notably, we've introduced support for plugin toolbar icons. Additionally, we've refined the editor's ability to manage text that's copied from applications like Word, Office, and LibreOffice, thereby enhancing cross-application compatibility. Among the minor yet impactful improvements, we've fine-tuned the handling of newlines and paragraphs—a highly requested feature. Another notable update is the improved automatic switching between light and dark modes.</p>
<p>Altogether, this release encompasses around 12 significant changes for the Rich Text editor, and rest assured, there's more to come in the future!</p>
<h3>Share permissions<a name="share-permissions" href="#share-permissions" class="heading-anchor">🔗</a></h3>
<p>Using Joplin Cloud Teams, you now have the ability to customize the read and write permissions for the notebooks you share. You have the option to grant other users permission to edit the notes or share them as read-only. This ensures that you can confidently share a notebook without worrying about unintentional modifications by your friends or colleagues.</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230825-share-permissions.png" alt=""></p>
<h3>Email to Note<a name="email-to-note" href="#email-to-note" class="heading-anchor">🔗</a></h3>
<p>Joplin Cloud Pro and Teams also now include the Email to Note feature, allowing you to conveniently store your emails within Joplin Cloud. By simply forwarding your emails to your Joplin Cloud address, you can transform them into notes. The email's subject will serve as the note title, while the body of the email will be the note's content. These notes will be organized within a notebook named &quot;Inbox.&quot;</p>
<p>More information in the <a href="https://joplinapp.org/help/apps/email_to_note">Email to Note documentation</a>.</p>
<h3>Choose to resize an image or not<a name="choose-to-resize-an-image-or-not" href="#choose-to-resize-an-image-or-not" class="heading-anchor">🔗</a></h3>
<p>By default, when you add a large image, Joplin will ask you if you would like to shrink it down or not. With this new release, you now have the option to always ask, to always resize, or to never resize the image, giving you more flexibility and reducing the number of prompts in the app.</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230825-resize-note.png" alt=""></p>
<h3>Rotating log<a name="rotating-log" href="#rotating-log" class="heading-anchor">🔗</a></h3>
<p>Up until now, the logs could grow to become very large, and if you wanted to shrink them down you would have to manually delete them. With this new release, we now automatically handle logs by rotating them - once the log becomes large enough it is moved to an archive. And once the archive is older than a number of days, it is deleted.</p>
<h3>Mobile<a name="mobile" href="#mobile" class="heading-anchor">🔗</a></h3>
<p>Like the desktop app, the mobile apps benefits from the Share Permissions update, as well as the Email to Note feature for Joplin Cloud. There are also plenty of other improvements - all in all there's 23 bug fixes, enhancements and security fixes in this release.</p>
<h2>Full changelog<a name="full-changelog" href="#full-changelog" class="heading-anchor">🔗</a></h2>
<p>This is just an overview of the main features. The full changelog is available there:</p>
<ul>
<li>Desktop: <a href="https://joplinapp.org/help/about/changelog/desktop">https://joplinapp.org/help/about/changelog/desktop</a></li>
<li>Android: <a href="https://joplinapp.org/help/about/changelog/android">https://joplinapp.org/help/about/changelog/android</a></li>
<li>iOS: <a href="https://joplinapp.org/help/about/changelog/ios">https://joplinapp.org/help/about/changelog/ios</a></li>
</ul>
]]></description><link>https://joplinapp.org/news/20230508-release-2-12</link><guid isPermaLink="false">20230508-release-2-12</guid><pubDate>Wed, 06 Sep 2023 12:00:00 GMT</pubDate><twitter-text>What&apos;s new in Joplin 2.12</twitter-text></item><item><title><![CDATA[What's new in Joplin 2.11]]></title><description><![CDATA[<h2>Desktop<a name="desktop" href="#desktop" class="heading-anchor">🔗</a></h2>
<h3>Add support for plugin user data<a name="add-support-for-plugin-user-data" href="#add-support-for-plugin-user-data" class="heading-anchor">🔗</a></h3>
<p>Developers of plugins now have the ability to associate additional information with notes, notebooks, and tags. This data is then synchronized across multiple devices. For instance, it is possible to envision attaching OCR data to an image or incorporating plugin-specific parameters to a note, which can subsequently be synchronized across all devices.</p>
<h3>Improved end-to-end encryption<a name="improved-end-to-end-encryption" href="#improved-end-to-end-encryption" class="heading-anchor">🔗</a></h3>
<p>End-to-end encryption (E2EE) plays a vital role in safeguarding your data privacy. Our aim is to make it effortlessly user-friendly, seamlessly integrated, and possibly enabled by default.</p>
<p>To achieve this objective, we have implemented several enhancements in this version. These improvements encompass more streamlined management of encryption keys and simplified setup processes for both new and existing devices utilising end-to-end encryption.</p>
<p>Moreover, we have upgraded the encryption algorithm from AES-128 to AES-256, ensuring heightened security for your data in the long run.</p>
<h3>Export a Mermaid graph<a name="export-a-mermaid-graph" href="#export-a-mermaid-graph" class="heading-anchor">🔗</a></h3>
<p>There is now a context menu on Mermaid graphs which you can use to export your graph as PNG, SVG, or by copying the vector data to the clipboard.</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230711-mermaid-export.png" alt=""></p>
<h3>Auto-detect language on startup<a name="auto-detect-language-on-startup" href="#auto-detect-language-on-startup" class="heading-anchor">🔗</a></h3>
<p>A small but useful change - the application language is now automatically detected startup. Of course, if needed you can later change it in the configuration screen.</p>
<h2>Android<a name="android" href="#android" class="heading-anchor">🔗</a></h2>
<p>As always most of these changes also apply to the mobile applications, such as the improved end-to-end encryption support, language auto-detection, and various bug fixes and improvements. But besides those, we also have several mobile-specific updates:</p>
<h3>Support for offline voice typing<a name="support-for-offline-voice-typing" href="#support-for-offline-voice-typing" class="heading-anchor">🔗</a></h3>
<p>The Android mobile app now support offline voice typing - it means your voice recording does not need to be uploaded to Google or Samsung server - it all takes place on your device for added security. To make use of it, open any note, tap on the three dots menu, then &quot;Voice typing&quot;.</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230711-voice-typing-1.png" alt=""></p>
<p>The first time, the app will need to download the language files to your device:</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230711-voice-typing-2.png" alt=""></p>
<p>Once they are there, simply record your message and it will appear in the note:</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230711-voice-typing-3.png" alt=""></p>
<h3>Create and move sub-notebooks<a name="create-and-move-sub-notebooks" href="#create-and-move-sub-notebooks" class="heading-anchor">🔗</a></h3>
<p>@jcgurango implemented support for creating sub-notebooks, and moving notebooks around in the mobile app. To do so, long press on any notebook and press &quot;Edit&quot;. You can now choose to move it under any other notebook using the provided dropdown list.</p>
<h2>Full changelog<a name="full-changelog" href="#full-changelog" class="heading-anchor">🔗</a></h2>
<p>This is just an overview of the main features. The full changelog is available there:</p>
<ul>
<li>Desktop: <a href="https://joplinapp.org/help/about/changelog/desktop">https://joplinapp.org/help/about/changelog/desktop</a></li>
<li>Android: <a href="https://joplinapp.org/help/about/changelog/android">https://joplinapp.org/help/about/changelog/android</a></li>
<li>iOS: <a href="https://joplinapp.org/help/about/changelog/ios">https://joplinapp.org/help/about/changelog/ios</a></li>
</ul>
]]></description><link>https://joplinapp.org/news/20230508-release-2-11</link><guid isPermaLink="false">20230508-release-2-11</guid><pubDate>Fri, 14 Jul 2023 12:00:00 GMT</pubDate><twitter-text>What&apos;s new in Joplin 2.11</twitter-text></item><item><title><![CDATA[What's new in Joplin 2.10]]></title><description><![CDATA[<p>Great news! Joplin 2.10 is here and we've made some amazing improvements and bug fixes, with a focus on the mobile app.</p>
<h2>New design for &quot;New note&quot; and &quot;New to-do&quot; buttons<a name="new-design-for-new-note-and-new-to-do-buttons" href="#new-design-for-new-note-and-new-to-do-buttons" class="heading-anchor">🔗</a></h2>
<p>We're excited to announce that we've made it even easier to create new notes and to-do lists with new designs for the &quot;New note&quot; and &quot;New to-do&quot; buttons.</p>
<p>If there is enough space, the button labels will be shown in full:</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230508-new-note-1.png" alt=""></p>
<p>While for those who prefer a more narrow note list, only the button icons will be shown:</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230508-new-note-2.png" alt=""></p>
<p>It's a small improvement, but we're confident it will make the app even more intuitive for new users.</p>
<h2>Fixes and improvements<a name="fixes-and-improvements" href="#fixes-and-improvements" class="heading-anchor">🔗</a></h2>
<p>This version includes 30 bug fixes and 16 general improvements. Let's dive into some of the highlights:</p>
<ul>
<li>
<p>Self Not Found and Pedro have been working tirelessly on improving the pasting of plain text in the application, and we're happy to say that there is now a brand new &quot;Paste as text&quot; option in the Edit menu.</p>
</li>
<li>
<p>For our amazing plugin developers out there, we have added a few new APIs and fixed a bug that was preventing certain plugins from starting. You can now get even more creative with your plugins!</p>
</li>
<li>
<p>Tao Klerks, has been hard at work fixing and improving the custom sort order of the note list. No more notes in the wrong position when you drop them! Plus, custom sort order is now synchronised too.</p>
</li>
<li>
<p>We've also fixed a few bugs with our Web Clipper, including an issue where certain pages wouldn't import their images. A new Web Clipper has also been released, so you can clip to your heart's content!</p>
</li>
</ul>
<p>Last but not least, we have modernised both the desktop and mobile application modules, just <a href="https://joplinapp.org/news/20221115-renovate/">as we previously announced</a>. Although these changes may not be visible to you, they required a lot of work! But the result is that our applications are now more stable and secure, and it will be easier to maintain them in the long run. We're using a tool called <a href="https://www.mend.io/free-developer-tools/renovate/">Renovate</a>, which will automatically propose package updates that we will review. In total, we've updated a whopping 633 packages so far!</p>
<h2>Android app is available in the Play Store<a name="android-app-is-available-in-the-play-store" href="#android-app-is-available-in-the-play-store" class="heading-anchor">🔗</a></h2>
<p>Our latest version, 2.10, is now back in the Play Store and ready for download! Although we had to skip 2.9 due to some of Google's requirements, we worked hard to ensure that our app complies with their standards, and we are excited to announce that we are back and better than ever! Our iOS version is also available, so you can continue to enjoy the app regardless of the platform you use.</p>
<h3>Biometrics support<a name="biometrics-support" href="#biometrics-support" class="heading-anchor">🔗</a></h3>
<p>To make your experience even more secure, our Android and iOS apps now support biometric unlock! With just a quick scan of your fingerprint or Face ID, you can unlock your app in no time. To enable this beta feature, just head over to the settings and click on &quot;Use biometrics to secure access to the app&quot;.</p>
<p>We've tested this feature thoroughly during prerelease, and have already fixed all known issues. However we still consider it as a beta feature for now, so if you run into any issues please let us know.</p>
<h3>Support for multiple profile<a name="support-for-multiple-profile" href="#support-for-multiple-profile" class="heading-anchor">🔗</a></h3>
<p>We're thrilled to announce that multiple profiles are now supported in our mobile app! To create a new profile, simply go to the Configuration screen and click on &quot;Manage profiles&quot; under the Tools section.</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230508-biometrics-1.png" alt=""></p>
<p>From there, you can easily add or remove profiles as needed.</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230508-biometrics-2.png" alt=""></p>
<p>Once multiple profiles are setup, you will see a new option in the sidebar to quickly toggle from one profile to another:</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230508-biometrics-3.png" alt=""></p>
<p>Once you've set up multiple profiles, you can easily toggle between them using the new option in the sidebar. This feature is perfect for separating your personal and work notes into independent collections.</p>
<h3>Support for realtime search<a name="support-for-realtime-search" href="#support-for-realtime-search" class="heading-anchor">🔗</a></h3>
<p>Our mobile app now has an improved search function that performs text searches in real time! No more waiting for the search results to load, they'll appear instantly as you type.</p>
<h3>Improved filesystem sync performance<a name="improved-filesystem-sync-performance" href="#improved-filesystem-sync-performance" class="heading-anchor">🔗</a></h3>
<p>Thanks to the hard work of jd1378, the sync no longer freezes during filesystem synchronisation. We know how frustrating that can be, and we're thrilled to have solved this issue. Getting filesystem sync to work on Android is never easy due to the restrictions put in place by Google, especially since they frequently change, but we're committed to delivering the best possible experience for our Android users.</p>
]]></description><link>https://joplinapp.org/news/20230508-release-2-10</link><guid isPermaLink="false">20230508-release-2-10</guid><pubDate>Wed, 10 May 2023 12:00:00 GMT</pubDate><twitter-text>What&apos;s new in Joplin 2.10</twitter-text></item></channel></rss>
]]></description><link>https://joplinapp.org/news/20231023-white-hat-hackers</link><guid isPermaLink="false">20231023-white-hat-hackers</guid><pubDate>Mon, 23 Oct 2023 00:00:00 GMT</pubDate><twitter-text>Working in the shadows with white-hat hackers</twitter-text></item></channel></rss>
@@ -300,7 +300,7 @@
A <span class="frame-bg frame-bg-yellow-lg">French</span> Alternative
</h2>
<p translate>
Joplin Cloud is based in France. This means your data is protected by strict European Union privacy laws. In addition, Joplin Cloud implements strong end-to-end encryption so that not even us can have access to your data.
Joplin Cloud is based in France. This means your data is protected by strict European Union privacy laws. In addition, Joplin Cloud implements strong end-to-end encryption so that not even we can access your data.
</p>
<br />
<br />
@@ -40,12 +40,16 @@
{{#pricingTable}}
<strong translate>Pricing</strong>
<table class="table">
<thead>
<tr><th translate>Users</th><th translate>Price</th></tr>
</thead>
<tbody>
{{#rows}}
<tr><td>{{condition}}</td><td>{{priceYearly}}</td></tr>
<tr><td>{{condition}}</td><td>{{priceMonthly}}</td></tr>
{{/rows}}
</tbody>
</table>
<p class="pricing-table-billed-annually">{{billedAnnually}}</p>
{{/pricingTable}}
<p class="text-center subscribe-wrapper">
@@ -54,6 +58,10 @@
{{#learnMoreUrl}}
<a id="learnMore-{{name}}" href="{{learnMoreUrl}}" class="button-link btn-white learnMoreButton cfa-button">Learn more</a>
{{/learnMoreUrl}}
{{#cfaDescription}}
<p class="cfa-description">{{cfaDescription}}</p>
{{/cfaDescription}}
</p>
{{#footnote}}<sub>(*) {{.}}</sub>{{/footnote}}
+14 -3
View File
@@ -88,7 +88,7 @@
Joplin Server Business
</h1>
<p translate class="text-center sub-title">
Joplin Server Business is a synchronisation server that you can install on your own infrastructure, so that your data remains private and secure within your business.
Joplin Server Business is the self-hosted solution for businesses and organisations. Install it on your own infrastructure, keeping your data private and secure within your organisation while retaining full control over where it is stored.
</p>
</div>
</div>
@@ -216,6 +216,11 @@
applyPeriod(period);
});
// Keep the ?source= label so it can be sent to Stripe at checkout.
// Only overwrite when the param is present, to preserve an earlier one.
const sourceParam = urlQuery.get('source');
if (sourceParam) localStorage.setItem('source', sourceParam);
setupBetaHandling(urlQuery);
if (urlQuery.get('period') === 'monthly') {
// Nothing - this is the default
@@ -232,7 +237,7 @@
});
});
const setHostingType = (type) => {
const setHostingType = (type, updateUrl = true) => {
const other = type === 'managed' ? 'self' : 'managed';
$('.toggle-button-' + type).addClass('active');
$('.toggle-button-' + type).removeClass('inactive');
@@ -241,6 +246,12 @@
$('.hosting-type-' + type).show();
$('.hosting-type-' + other).hide();
if (updateUrl) {
const url = new URL(location.href);
url.searchParams.set('hosting', type);
history.replaceState(null, '', url);
}
}
$('.toggle-button-managed').click((event) => {
@@ -256,6 +267,6 @@
});
const initialHostingType = urlQuery.get('hosting') ? urlQuery.get('hosting') : 'managed';
setHostingType(initialHostingType);
setHostingType(initialHostingType, false);
</script>
</div>
+12 -3
View File
@@ -2,7 +2,7 @@
# Build stage
# =============================================================================
FROM node:24 AS builder
FROM node:24.22.1 AS builder
RUN apt-get update \
&& apt-get install -y \
@@ -40,6 +40,9 @@ COPY packages/server ./packages/server
# We don't want to build onenote-converter since it is not used by the server
RUN sed --in-place '/onenote-converter/d' ./packages/lib/package.json
# Joplin Server -- and the Joplin packages, when included with it -- are licensed under their own license.
RUN sed --in-place 's/"license": "AGPL-3.0-or-later"/"license": "UNLICENSED"/' ./package.json ./packages/*/package.json
# For some reason there's both a .yarn/cache and .yarn/berry/cache that are
# being generated, and both have the same content. Not clear why it does this
# but we can delete it anyway. We can delete the cache because we use
@@ -63,7 +66,7 @@ RUN --mount=type=cache,target=/build/.yarn/cache --mount=type=cache,target=/buil
# from a smaller base image.
# =============================================================================
FROM node:24-slim
FROM node:24.22.1-slim
ARG user=joplin
RUN useradd --create-home --shell /bin/bash $user
@@ -86,6 +89,9 @@ RUN pm2 install https://registry.npmjs.org/pm2-logrotate/-/pm2-logrotate-3.0.0.t
&& pm2 set pm2-logrotate:retain 5 \
&& pm2 set pm2-logrotate:compress true
ARG LICENSE_REQUIRED="0"
ENV LICENSE_REQUIRED=${LICENSE_REQUIRED}
ENV NODE_ENV=production
ENV RUNNING_IN_DOCKER=1
EXPOSE ${APP_PORT}
@@ -96,6 +102,9 @@ WORKDIR /home/$user/packages/server
ENTRYPOINT ["tini", "--"]
CMD ["yarn", "start-prod"]
ARG IMAGE_NAME="Joplin Server"
ENV APP_NAME=${IMAGE_NAME}
# Build-time metadata
# https://github.com/opencontainers/image-spec/blob/master/annotations.md
ARG BUILD_DATE
@@ -103,7 +112,7 @@ ARG REVISION
ARG VERSION
ARG SOURCE
LABEL org.opencontainers.image.created="$BUILD_DATE" \
org.opencontainers.image.title="Joplin Server" \
org.opencontainers.image.title="$IMAGE_NAME" \
org.opencontainers.image.description="Docker image for Joplin Server" \
org.opencontainers.image.url="https://joplinapp.org/" \
org.opencontainers.image.revision="$REVISION" \
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:24-bookworm
FROM node:24.22.1-bookworm
RUN apt-get update \
&& apt-get install -y \
+1 -1
View File
@@ -23,7 +23,7 @@
FROM ghcr.io/ggml-org/llama.cpp:full-cuda-b5449 AS llama-cuda
# Stage 2: our runtime image.
FROM nvidia/cuda:13.1.1-cudnn-runtime-ubuntu22.04
FROM nvidia/cuda:13.3.0-cudnn-runtime-ubuntu22.04
RUN apt-get update \
&& apt-get install -y \
+23 -64
View File
@@ -172,24 +172,6 @@ elif [[ $ARCHITECTURE =~ .*i386.*|.*i686.* ]]; then
fi
#-----------------------------------------------------
print "Checking dependencies..."
## Check if libfuse2 is present.
if [[ $(command -v ldconfig) ]]; then
LIBFUSE=$(ldconfig -p | grep "libfuse.so.2" || echo '')
fi
if [[ $LIBFUSE == "" ]]; then
LIBFUSE=$(find /lib /usr/lib /lib64 /usr/lib64 /usr/local/lib -name "libfuse.so.2" 2>/dev/null | grep "libfuse.so.2" || echo '')
fi
if [[ $LIBFUSE == "" ]]; then
print "${COLOR_RED}Error: Can't get libfuse2 on system, please install libfuse2${COLOR_RESET}"
print "See https://joplinapp.org/help/faq/#desktop-application-will-not-launch-on-linux and https://github.com/AppImage/AppImageKit/wiki/FUSE for more information"
exit 1
fi
#-----------------------------------------------------
# Download Joplin
#-----------------------------------------------------
# Get the latest version to download
if [[ "$INCLUDE_PRE_RELEASE" == true ]]; then
RELEASE_VERSION=$($DL - "https://api.github.com/repos/laurent22/joplin/releases" | grep -Po '"tag_name": ?"v\K.*?(?=")' | sort -rV | head -1)
@@ -197,6 +179,27 @@ else
RELEASE_VERSION=$($DL - "https://api.github.com/repos/laurent22/joplin/releases/latest" | grep -Po '"tag_name": ?"v\K.*?(?=")')
fi
#-----------------------------------------------------
print "Checking dependencies..."
## Check for libfuse2 for Joplin versions lesser than 3.7.10, which transitioned to a new AppImage runtime without libfuse2 dependency
if [[ $(compareVersions "$RELEASE_VERSION" "3.7.10") -lt 0 ]]; then
if [[ $(command -v ldconfig) ]]; then
LIBFUSE=$(ldconfig -p | grep "libfuse.so.2" || echo '')
fi
if [[ $LIBFUSE == "" ]]; then
LIBFUSE=$(find /lib /usr/lib /lib64 /usr/lib64 /usr/local/lib -name "libfuse.so.2" 2>/dev/null | grep "libfuse.so.2" || echo '')
fi
if [[ $LIBFUSE == "" ]]; then
print "${COLOR_RED}Error: Can't get libfuse2 on system, please install libfuse2${COLOR_RESET}"
print "See https://joplinapp.org/help/faq/#desktop-application-will-not-launch-on-linux and https://github.com/AppImage/AppImageKit/wiki/FUSE for more information"
exit 1
fi
fi
#-----------------------------------------------------
# Download Joplin
#-----------------------------------------------------
# Check if it's in the latest version
if [[ -e "${INSTALL_DIR}/VERSION" ]]; then
CURRENT_VERSION=$(< "${INSTALL_DIR}/VERSION")
@@ -259,42 +262,6 @@ DESKTOP=${DESKTOP,,} # convert to lower case
echo 'Create Desktop icon...'
# Detect distribution environment, and apply --no-sandbox fix
SANDBOXPARAM=""
# lsb_release isn't available on some platforms (e.g. opensuse)
# The equivalent of lsb_release in OpenSuse is the file /usr/lib/os-release
if command -v lsb_release &> /dev/null; then
DISTVER=$(lsb_release -is) && DISTVER=$DISTVER$(lsb_release -rs)
DISTCODENAME=$(lsb_release -cs)
DISTMAJOR=$(lsb_release -rs|cut -d. -f1)
#-----------------------------------------------------
# Check for "The SUID sandbox helper binary was found, but is not configured correctly" problem.
# It is present in Debian 1X. A (temporary) patch will be applied at .desktop file
# Linux Mint 4 Debbie is based on Debian 10 and requires the same param handling.
#
# TODO: Remove: This is likely no longer an issue. See https://issues.chromium.org/issues/40462640.
BAD_HELPER_BINARY=false
if [[ $DISTVER =~ Debian1. || ( "$DISTVER" = "Linuxmint4" && "$DISTCODENAME" = "debbie" ) || ( "$DISTVER" = "CentOS" && "$DISTMAJOR" =~ 6|7 ) ]]; then
BAD_HELPER_BINARY=true
fi
# Work around Ubuntu 23.10+'s restrictions on unprivileged user namespaces. Electron
# uses these to sandbox processes. Unfortunately, it doesn't look like we can get around this
# without writing the AppImage to a non-user-writable location (without invalidating other security
# controls). See https://discourse.joplinapp.org/t/possible-future-requirement-for-no-sandbox-flag-for-ubuntu-23-10/.
HAS_USERNS_RESTRICTIONS=false
if [[ "$DISTVER" =~ ^(Ubuntu|Tuxedo) && $DISTMAJOR -ge 23 ]]; then
HAS_USERNS_RESTRICTIONS=true
fi
if [[ $HAS_USERNS_RESTRICTIONS = true || $BAD_HELPER_BINARY = true ]]; then
SANDBOXPARAM="--no-sandbox"
print "${COLOR_YELLOW}WARNING${COLOR_RESET} Electron sandboxing disabled."
print " See https://discourse.joplinapp.org/t/32160/5 for details."
fi
fi
# Initially only desktop environments that were confirmed to use desktop files stored in
# `.local/share/desktop` had a desktop file created.
# However some environments don't return a desktop BUT still support these desktop files
@@ -307,13 +274,7 @@ if [[ $DESKTOP =~ .*gnome.*|.*kde.*|.*xfce.*|.*mate.*|.*lxqt.*|.*unity.*|.*x-cin
DATA_HOME=${XDG_DATA_HOME:-~/.local/share}
DESKTOP_FILE_LOCATION="$DATA_HOME/applications"
# Only later versions of Joplin default to Wayland
IS_WAYLAND_BY_DEFAULT=$(compareVersions "$RELEASE_VERSION" "3.5.6")
# Joplin has a different startup WM class on Wayland and X11:
STARTUP_WM_CLASS=Joplin
if [[ $XDG_SESSION_TYPE != "x11" && $IS_WAYLAND_BY_DEFAULT == "1" ]]; then
STARTUP_WM_CLASS=@joplin/app-desktop
fi
STARTUP_WM_CLASS=appimagekit-joplin
# Only delete the desktop file if it will be replaced
rm -f "$DESKTOP_FILE_LOCATION/appimagekit-joplin.desktop"
@@ -327,10 +288,8 @@ if [[ $DESKTOP =~ .*gnome.*|.*kde.*|.*xfce.*|.*mate.*|.*lxqt.*|.*unity.*|.*x-cin
Encoding=UTF-8
Name=Joplin
Comment=Joplin for Desktop
Exec=env APPIMAGELAUNCHER_DISABLE=TRUE "${INSTALL_DIR}/Joplin.AppImage" ${SANDBOXPARAM} %u
Exec=env APPIMAGELAUNCHER_DISABLE=TRUE "${INSTALL_DIR}/Joplin.AppImage" %u
Icon=joplin
# This will be different between Wayland and X11. On Wayland, the startup
# WM class is "@joplin/app-desktop". On X11, it's "Joplin".
StartupWMClass=${STARTUP_WM_CLASS}
Type=Application
Categories=Office;
+1 -1
View File
@@ -31,7 +31,7 @@ Please see the [donation page](https://github.com/laurent22/joplin/blob/dev/read
# Sponsors
<!-- SPONSORS-ORG -->
<a href="https://seirei.ne.jp"><img title="Serei Network" width="256" src="https://joplinapp.org/images/sponsors/SeireiNetwork.png"/></a> <a href="https://citricsheep.com"><img title="Citric Sheep" width="256" src="https://joplinapp.org/images/sponsors/CitricSheep.png"/></a> <a href="https://sorted.travel/?utm_source=joplinapp"><img title="Sorted Travel" width="256" src="https://joplinapp.org/images/sponsors/SortedTravel.png"/></a> <a href="https://celebian.com"><img title="Celebian" width="256" src="https://joplinapp.org/images/sponsors/Celebian.png"/></a> <a href="https://bestkru.com"><img title="BestKru" width="256" src="https://joplinapp.org/images/sponsors/BestKru.png"/></a> <a href="https://www.socialfollowers.uk/buy-tiktok-followers/"><img title="Social Followers" width="256" src="https://joplinapp.org/images/sponsors/SocialFollowers.png"/></a> <a href="https://stormlikes.com/"><img title="Stormlikes" width="256" src="https://joplinapp.org/images/sponsors/Stormlikes.png"/></a> <a href="https://route4me.com"><img title="Route4Me" width="256" src="https://joplinapp.org/images/sponsors/Route4Me.png"/></a> <a href="https://topagency.webflow.io"><img title="WebDesignAgency" width="256" src="https://joplinapp.org/images/sponsors/WebDesignAgency.png" alt="topagency"/></a> <a href="https://essayshark.com"><img title="EssayShark - essay writers for hire" width="256" src="https://joplinapp.org/images/sponsors/EssayShark.png" alt="EssayShark - essay writers for hire"/></a> <a href="https://pokieslab1.com/real-money-pokies/"><img title="Australian Real Money Pokies" width="256" src="https://joplinapp.org/images/sponsors/PokiesLab.png" alt="Australian Real Money Pokies"/></a> <a href="https://pokiesman1.net/real-money-pokies/"><img title="Australian Real Money Pokies" width="256" src="https://joplinapp.org/images/sponsors/Pokiesman.png" alt="Australian Real Money Pokies"/></a> <a href="https://socialkings.online"><img title="Boost your reach and buy real followers" width="256" src="https://joplinapp.org/images/sponsors/SocialKings.png" alt="Boost your reach and buy real followers"/></a> <a href="https://essayservice.com/"><img title="For those in need of immediate academic assistance, EssayService offers a fast and reliable service to write my essay for me now, ensuring high-quality results within tight deadlines" width="256" src="https://joplinapp.org/images/sponsors/EssayService.png" alt="For those in need of immediate academic assistance, EssayService offers a fast and reliable service to write my essay for me now, ensuring high-quality results within tight deadlines"/></a> <a href="https://thenationonlineng.net/casino-en-ligne/casino-en-ligne-payant-au-canada/"><img title="casino en ligne le plus payant" width="256" src="https://joplinapp.org/images/sponsors/TheNationOnline.jpg" alt="casino en ligne le plus payant"/></a>
<a href="https://seirei.ne.jp"><img title="Serei Network" width="256" src="https://joplinapp.org/images/sponsors/SeireiNetwork.png"/></a> <a href="https://citricsheep.com"><img title="Citric Sheep" width="256" src="https://joplinapp.org/images/sponsors/CitricSheep.png"/></a> <a href="https://sorted.travel/?utm_source=joplinapp"><img title="Sorted Travel" width="256" src="https://joplinapp.org/images/sponsors/SortedTravel.png"/></a> <a href="https://celebian.com"><img title="Celebian" width="256" src="https://joplinapp.org/images/sponsors/Celebian.png"/></a> <a href="https://bestkru.com"><img title="BestKru" width="256" src="https://joplinapp.org/images/sponsors/BestKru.png"/></a> <a href="https://route4me.com"><img title="Route4Me" width="256" src="https://joplinapp.org/images/sponsors/Route4Me.png"/></a> <a href="https://socialboom.io/"><img title="Buy YouTube, Instagram, TikTok &amp; Facebook Growth Services at Socialboom" width="256" src="https://joplinapp.org/images/sponsors/Socialboom.png" alt="Buy YouTube, Instagram, TikTok &amp; Facebook Growth Services at Socialboom"/></a> <a href="https://essaypro.com/"><img title="write my essay EssayPro" width="256" src="https://joplinapp.org/images/sponsors/EssayPro.png" alt="write my essay EssayPro"/></a> <a href="https://writepaper.com/"><img title="write my paper" width="256" src="https://joplinapp.org/images/sponsors/WritePaper.png" alt="write my paper"/></a> <a href="https://mypaperhelp.com/"><img title="MyPaperHelp" width="256" src="https://joplinapp.org/images/sponsors/MyPaperHelp.png" alt="MyPaperHelp"/></a> <a href="https://essaypay.com/"><img title="EssayPay" width="256" src="https://joplinapp.org/images/sponsors/EssayPay.png" alt="EssayPay"/></a> <a href="https://essayservice.com/nursing-essay-writing-service"><img title="write my nursing paper today" width="256" src="https://joplinapp.org/images/sponsors/EssayService.png" alt="write my nursing paper today"/></a>
<!-- SPONSORS-ORG -->
* * *
+11
View File
@@ -1,5 +1,8 @@
# Security Policy
> [!IMPORTANT]
> Before submitting a report, please read the [Rules for reporting](#rules-for-reporting).
## Supported Versions
Only the latest version is supported with security updates.
@@ -10,6 +13,14 @@ Please report vulnerabilities [through private vulnerability reporting](https://
For general opinions on what makes an app more or less secure, please use the forum.
## Rules for reporting
Joplin is maintained by a small team, and we receive a high volume of reports that have not been verified by the person submitting them. Reviewing and reproducing an unverified finding takes us as long as a real one, so we need a few rules to keep the process workable for everyone.
- **Two active reports per person.** If you have more than two open at once, we may close them all, and we may block the account.
- **Verify your report before submitting it.** Confirm that the issue is still present in the latest version, and that you have actually carried out the steps your conclusion relies on. Repeatedly submitting findings that do not hold up may result in a permanent block.
## Areas outside Joplin's Threat Model
Note: we're mostly linking to Chrome's documentation since our reasoning for these exclusions is the same.
+2 -2
View File
@@ -11,13 +11,13 @@
},
"nodejs": "24.12.0",
"pkg-config": "latest",
"python": "3.14.2",
"python": "3.14.4",
"bat": "latest",
"electron": {
"version": "latest",
"excluded_platforms": ["aarch64-darwin", "x86_64-darwin"],
},
"git": "2.52.0",
"git": "2.53.0",
},
"shell": {
"init_hook": [
+2 -2
View File
@@ -30,8 +30,8 @@ services:
- ./data/postgres:/var/lib/postgresql/data
networks:
- app-network
ports:
- "5432:5432"
# (Optional): Allow accessing the database using an external tool (e.g. `psql`) from the host:
# ports: [ "127.0.0.1:5432:5432" ]
restart: unless-stopped
environment:
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
+9
View File
@@ -14,6 +14,8 @@ const github = require('eslint-plugin-github');
const { fixupPluginRules, includeIgnoreFile } = require('@eslint/compat');
const js = require('@eslint/js');
const joplin = require('./packages/tools/eslint-rules');
const { join } = require('path');
// As of ESLint v9, .eslintignore is no longer supported and the presence of
@@ -246,6 +248,7 @@ module.exports = defineConfig([{
promise,
jest,
github: fixupPluginRules(github),
joplin,
},
}, {
files: [
@@ -257,6 +260,12 @@ module.exports = defineConfig([{
rules: {
'no-console': 'off',
},
}, {
files: ['**/*.test.ts', '**/*.test.tsx', '**/*.test.js', '**/*.test.jsx'],
rules: {
'joplin/describe-filename': ['error'],
},
}, {
// .jsx files should be linted with the default rules
files: ['**/*.jsx'],
+30
View File
@@ -33,3 +33,33 @@ if (typeof document !== 'undefined') {
return range;
};
}
if (typeof window !== 'undefined') {
const { Buffer, Blob, File } = require('node:buffer');
const { TextDecoder, TextEncoder } = require('node:util');
const { URLSearchParams, URL } = require('node:url');
const { ReadableStream } = require('node:stream/web');
const { MessageChannel, MessagePort } = require('node:worker_threads');
const { setTimeout, setInterval, clearTimeout, clearInterval, setImmediate, clearImmediate } = require('node:timers');
const { performance } = require('node:perf_hooks');
window.performance.markResourceTiming = performance.markResourceTiming;
// Override some of the JSDom globals. Some libraries (e.g. Undici) require the
// original Node.js versions of these globals:
globalThis.Buffer = Buffer;
globalThis.Blob = Blob;
globalThis.File = File;
globalThis.URLSearchParams = URLSearchParams;
globalThis.URL = URL;
globalThis.TextDecoder = TextDecoder;
globalThis.TextEncoder = TextEncoder;
globalThis.ReadableStream = ReadableStream;
globalThis.MessageChannel = MessageChannel;
globalThis.MessagePort = MessagePort;
globalThis.setTimeout = setTimeout;
globalThis.setInterval = setInterval;
globalThis.clearTimeout = clearTimeout;
globalThis.clearInterval = clearInterval;
globalThis.setImmediate = setImmediate;
globalThis.clearImmediate = clearImmediate;
}
+27 -23
View File
@@ -9,8 +9,8 @@
"url": "git+https://github.com/laurent22/joplin.git"
},
"engines": {
"node": ">=22.12",
"yarn": "4.12.0"
"node": ">=22.19",
"yarn": "4.16.0"
},
"scripts": {
"buildApiDoc": "yarn workspace joplin start apidoc ../../readme/api/references/rest_api.md",
@@ -77,52 +77,50 @@
"@seiyab/eslint-plugin-react-hooks": "4.5.1-beta.0",
"@stylistic/eslint-plugin": "5.10.0",
"@types/jest": "29.5.14",
"@typescript-eslint/eslint-plugin": "8.60.0",
"@typescript-eslint/parser": "8.60.0",
"@typescript-eslint/eslint-plugin": "8.60.1",
"@typescript-eslint/parser": "8.60.1",
"cspell": "5.21.2",
"eslint": "9.39.4",
"eslint-interactive": "14.0.0",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-jest": "29.15.2",
"eslint-plugin-jest": "29.15.4",
"eslint-plugin-promise": "7.3.0",
"eslint-plugin-react": "7.37.5",
"execa": "5.1.1",
"fs-extra": "11.3.4",
"fs-extra": "11.3.6",
"glob": "11.1.0",
"globals": "17.6.0",
"globals": "17.7.0",
"gulp": "4.0.2",
"husky": "9.1.7",
"lerna": "3.22.1",
"lint-staged": "16.2.7",
"lint-staged": "16.4.0",
"madge": "8.0.0",
"npm-package-json-lint": "9.1.0",
"npm-package-json-lint": "10.4.1",
"typescript": "5.9.3"
},
"dependencies": {
"@types/fs-extra": "11.0.4",
"eslint-plugin-github": "4.10.2",
"http-server": "14.1.1",
"node-gyp": "11.5.0",
"node-gyp": "12.4.0",
"nodemon": "3.1.14"
},
"packageManager": "yarn@4.16.0",
"resolutions": {
"@codemirror/view": "6.42.1",
"@codemirror/state": "6.6.0",
"@codemirror/view": "6.43.14",
"@codemirror/state": "6.7.1",
"@codemirror/language": "6.12.3",
"@codemirror/lint": "6.9.5",
"@codemirror/autocomplete": "6.20.1",
"onnxruntime-node": "1.27.0",
"eslint": "patch:eslint@9.39.4#./.yarn/patches/eslint-npm-9.39.4-81a84865c0.patch",
"yauzl": "3.3.1",
"yauzl": "3.4.0",
"react-native-camera@4.2.1": "patch:react-native-camera@npm%3A4.2.1#./.yarn/patches/react-native-camera-npm-4.2.1-24b2600a7e.patch",
"app-builder-lib@24.4.0": "patch:app-builder-lib@npm%3A24.4.0#./.yarn/patches/app-builder-lib-npm-24.4.0-05322ff057.patch",
"nanoid": "patch:nanoid@npm%3A3.3.7#./.yarn/patches/nanoid-npm-3.3.7-98824ba130.patch",
"pdfjs-dist": "patch:pdfjs-dist@npm%3A3.11.174#./.yarn/patches/pdfjs-dist-npm-3.11.174-67f2fee6d6.patch",
"chokidar@^2.0.0": "3.5.3",
"chokidar@^2.0.0": "3.6.0",
"rn-fetch-blob@0.12.0": "patch:rn-fetch-blob@npm%3A0.12.0#./.yarn/patches/rn-fetch-blob-npm-0.12.0-cf02e3c544.patch",
"app-builder-lib@26.0.0-alpha.7": "patch:app-builder-lib@npm%3A26.0.0-alpha.7#./.yarn/patches/app-builder-lib-npm-26.0.0-alpha.7-e1b3dca119.patch",
"app-builder-lib@24.13.3": "patch:app-builder-lib@npm%3A24.13.3#./.yarn/patches/app-builder-lib-npm-24.13.3-86a66c0bf3.patch",
"react-native-sqlite-storage@6.0.1": "patch:react-native-sqlite-storage@npm%3A6.0.1#./.yarn/patches/react-native-sqlite-storage-npm-6.0.1-8369d747bd.patch",
"react-native-paper@5.13.1": "patch:react-native-paper@npm%3A5.13.1#./.yarn/patches/react-native-paper-npm-5.13.1-f153e542e2.patch",
"react-native-popup-menu@0.17.0": "patch:react-native-popup-menu@npm%3A0.17.0#./.yarn/patches/react-native-popup-menu-npm-0.17.0-8b745d88dd.patch",
"react-native@0.79.2": "patch:react-native@npm%3A0.79.2#./.yarn/patches/react-native-npm-0.79.2-9db13eddfe.patch",
@@ -130,11 +128,15 @@
"pdfjs-dist@*": "patch:pdfjs-dist@npm%3A3.11.174#./.yarn/patches/pdfjs-dist-npm-3.11.174-67f2fee6d6.patch",
"pdfjs-dist@3.11.174": "patch:pdfjs-dist@npm%3A3.11.174#./.yarn/patches/pdfjs-dist-npm-3.11.174-67f2fee6d6.patch",
"canvas@npm:^2.11.2": "link:./.yarn/joplin-empty-package/",
"@xenova/transformers/sharp": "link:./.yarn/joplin-empty-package/",
"node-gyp@npm:^9.0.0": "11.2.0",
"node-forge@npm:^1": "1.3.2",
"node-forge@npm:^1.3.0": "1.3.2",
"moment@npm:^2.8.4": "2.29.4",
"@huggingface/transformers/sharp": "link:./.yarn/joplin-empty-package/",
"node-gyp@npm:^9.0.0": "12.4.0",
"node-gyp@npm:11.2.0": "12.4.0",
"node-gyp@npm:11.5.0": "12.4.0",
"node-gyp@npm:latest": "12.4.0",
"node-gyp@npm:8.x": "12.4.0",
"node-forge@npm:^1": "1.4.0",
"node-forge@npm:^1.3.0": "1.4.0",
"moment@npm:^2.8.4": "2.30.1",
"depd@npm:^2.0.0": "patch:depd@npm%3A2.0.0#~/.yarn/patches/depd-npm-2.0.0-b6c51a4b43.patch",
"depd@npm:~2.0.0": "patch:depd@npm%3A2.0.0#~/.yarn/patches/depd-npm-2.0.0-b6c51a4b43.patch",
"depd@npm:~1.1.2": "patch:depd@npm%3A2.0.0#~/.yarn/patches/depd-npm-2.0.0-b6c51a4b43.patch",
@@ -146,6 +148,8 @@
"@react-native-vector-icons/fontawesome5@npm:12.3.0": "patch:@react-native-vector-icons/fontawesome5@npm%3A12.3.0#~/.yarn/patches/@react-native-vector-icons-fontawesome5-npm-12.3.0-a1ca46610f.patch",
"@react-native-vector-icons/ionicons@npm:12.3.0": "patch:@react-native-vector-icons/ionicons@npm%3A12.3.0#~/.yarn/patches/@react-native-vector-icons-ionicons-npm-12.3.0-9bd4746f3f.patch",
"@react-native-vector-icons/material-design-icons@npm:12.4.0": "patch:@react-native-vector-icons/material-design-icons@npm%3A12.4.0#~/.yarn/patches/@react-native-vector-icons-material-design-icons-npm-12.4.0-890f7f618b.patch",
"@react-native-vector-icons/material-icons@npm:12.4.0": "patch:@react-native-vector-icons/material-icons@npm%3A12.4.0#~/.yarn/patches/@react-native-vector-icons-material-icons-npm-12.4.0-94138e627b.patch"
"@react-native-vector-icons/material-icons@npm:12.4.0": "patch:@react-native-vector-icons/material-icons@npm%3A12.4.0#~/.yarn/patches/@react-native-vector-icons-material-icons-npm-12.4.0-94138e627b.patch",
"app-builder-lib@npm:26.15.6": "patch:app-builder-lib@npm%3A26.15.6#~/.yarn/patches/app-builder-lib-npm-26.15.6-010811df92.patch",
"wasm-pack@npm:0.13.1": "patch:wasm-pack@npm%3A0.13.1#~/.yarn/patches/wasm-pack-npm-0.13.1-2a81da84b4.patch"
}
}
+1 -1
View File
@@ -84,7 +84,7 @@ class ResourceServer {
this.logger().error('Resource server:', error);
});
this.server_.listen(this.port_);
this.server_.listen(this.port_, '127.0.0.1');
enableServerDestroy(this.server_);
+4 -1
View File
@@ -1,5 +1,7 @@
import { _ } from '@joplin/lib/locale';
import { reg } from '@joplin/lib/registry.js';
import isNoteLockEnabled from '@joplin/lib/services/noteLock/isNoteLockEnabled';
import NoteLockNote from '@joplin/lib/services/noteLock/NoteLockNote';
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Stdout can be called with formatted strings or arbitrary values
type StdoutFn = (text: any)=> void;
@@ -18,8 +20,9 @@ export default class BaseCommand {
throw new Error('Usage not defined');
}
public encryptionCheck(item: { encryption_applied?: number } | null) {
public encryptionCheck(item: { encryption_applied?: number; is_locked?: number } | null) {
if (item && item.encryption_applied) throw new Error(_('Cannot change encrypted item'));
if (isNoteLockEnabled() && NoteLockNote.isLocked(item)) throw new Error(_('Cannot change a locked note'));
}
public description(): string {
@@ -14,7 +14,7 @@ import { loadKeychainServiceAndSettings } from '@joplin/lib/services/SettingUtil
import shimInitCli from './utils/shimInitCli';
const baseDir = `${dirname(__dirname)}/tests/cli-integration`;
const joplinAppPath = `${__dirname}/main.js`;
const joplinAppPath = `${__dirname}/build/main.js`;
shimInitCli({ nodeSqlite, appVersion: () => require('../package.json').version, keytar: null, sharp: null, React: null, electronBridge: null, pdfJs: null });
require('@joplin/lib/testing/test-utils');
+2 -2
View File
@@ -2,7 +2,7 @@ import { _ } from '@joplin/lib/locale';
import time from '@joplin/lib/time';
import Logger, { TargetType } from '@joplin/utils/Logger';
import * as readline from 'readline';
import { Writable } from 'stream';
import { Stream, Writable } from 'stream';
import BaseCommand from './base-command';
import yargParser = require('yargs-parser');
const stringPadding = require('string-padding');
@@ -236,7 +236,7 @@ cliUtils.prompt = function(_initialText = '', promptString = ':', options: Promp
if (!this.muted) process.stdout.write(chunk, encoding);
callback();
},
}) as MutableStdout;
} as Stream.WritableOptions<Writable>) as MutableStdout;
const rl = readline.createInterface({
input: process.stdin,
+26
View File
@@ -0,0 +1,26 @@
import Note from '@joplin/lib/models/Note';
import Setting from '@joplin/lib/models/Setting';
import { setupDatabaseAndSynchronizer, switchClient, noteLockCipherTextStandIn } from '@joplin/lib/testing/test-utils';
import { setupCommandForTesting, setupApplication } from './utils/testUtils';
const Command = require('./command-cat');
describe('command-cat', () => {
beforeEach(async () => {
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
await setupApplication();
});
it('should refuse to display a locked note', async () => {
Setting.setValue('featureFlag.noteLock', true);
const note = await Note.save({ title: 'hello', body: noteLockCipherTextStandIn(), is_locked: 1, parent_id: '' });
let output = '';
const command = setupCommandForTesting(Command, (text: string) => { output += text; });
await expect(command.action({ note: note.id, options: {} })).rejects.toThrow('locked note');
expect(output).toBe('');
});
});
+3
View File
@@ -4,6 +4,8 @@ import { _ } from '@joplin/lib/locale';
import { ModelType } from '@joplin/lib/BaseModel';
import BaseItem from '@joplin/lib/models/BaseItem';
import Note from '@joplin/lib/models/Note';
import isNoteLockEnabled from '@joplin/lib/services/noteLock/isNoteLockEnabled';
import NoteLockNote from '@joplin/lib/services/noteLock/NoteLockNote';
class Command extends BaseCommand {
public override usage() {
@@ -23,6 +25,7 @@ class Command extends BaseCommand {
const item = await app().loadItem(ModelType.Note, title, { parent: app().currentFolder() });
if (!item) throw new Error(_('Cannot find "%s".', title));
if (isNoteLockEnabled() && NoteLockNote.isLocked(item)) throw new Error(_('Cannot display a locked note'));
let content = '';
+1 -1
View File
@@ -58,7 +58,7 @@ class Command extends BaseCommand {
}
});
inputStream.on('error', (error) => {
inputStream.on('error', (error: Error) => {
if (!isSettled) {
isSettled = true;
reject(error);
+29
View File
@@ -0,0 +1,29 @@
import * as fs from 'fs-extra';
import Note from '@joplin/lib/models/Note';
import Setting from '@joplin/lib/models/Setting';
import { setupDatabaseAndSynchronizer, switchClient, noteLockCipherTextStandIn } from '@joplin/lib/testing/test-utils';
import { setupCommandForTesting, setupApplication } from './utils/testUtils';
const Command = require('./command-edit');
describe('command-edit', () => {
beforeEach(async () => {
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
await setupApplication();
});
it('should refuse to edit a locked note without writing a temp file', async () => {
Setting.setValue('featureFlag.noteLock', true);
const note = await Note.save({ title: 'hello', body: noteLockCipherTextStandIn(), is_locked: 1, parent_id: '' });
const tempDir = Setting.value('tempDir');
await fs.ensureDir(tempDir);
const filesBefore = await fs.readdir(tempDir);
const command = setupCommandForTesting(Command);
await expect(command.action({ note: note.id })).rejects.toThrow('locked note');
expect(await fs.readdir(tempDir)).toEqual(filesBefore);
});
});
+44
View File
@@ -0,0 +1,44 @@
import Note from '@joplin/lib/models/Note';
import Setting from '@joplin/lib/models/Setting';
import { setupDatabaseAndSynchronizer, switchClient, noteLockCipherTextStandIn } from '@joplin/lib/testing/test-utils';
import { setupCommandForTesting, setupApplication } from './utils/testUtils';
const Command = require('./command-set');
describe('command-set', () => {
beforeEach(async () => {
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
await setupApplication();
});
// This guard lives in BaseCommand.encryptionCheck, so it also covers attach, done, edit, ren and todo.
it.each([
{ label: 'refuse to change a locked note', flagEnabled: true, expectedError: 'Cannot change a locked note', expectedTitle: 'hello' },
{ label: 'change a locked note when note lock is disabled', flagEnabled: false, expectedError: null, expectedTitle: 'renamed' },
])('should $label', async ({ flagEnabled, expectedError, expectedTitle }) => {
Setting.setValue('featureFlag.noteLock', flagEnabled);
const note = await Note.save({ title: 'hello', body: noteLockCipherTextStandIn(), is_locked: 1, parent_id: '' });
const command = setupCommandForTesting(Command);
let error: string|null = null;
try {
await command.action({ note: note.id, name: 'title', value: 'renamed' });
} catch (e) {
error = (e as Error).message;
}
expect(error).toBe(expectedError);
expect((await Note.load(note.id)).title).toBe(expectedTitle);
});
it('should block changing the lock state of a plain note', async () => {
Setting.setValue('featureFlag.noteLock', true);
const note = await Note.save({ title: 'hello', body: 'plain', parent_id: '' });
const command = setupCommandForTesting(Command);
await expect(command.action({ note: note.id, name: 'is_locked', value: '1' })).rejects.toThrow('The note lock state cannot be changed from the command line');
expect((await Note.load(note.id)).is_locked).toBe(0);
});
});
+6
View File
@@ -4,6 +4,7 @@ import { _ } from '@joplin/lib/locale';
import { ModelType } from '@joplin/lib/BaseModel';
import Database from '@joplin/lib/database';
import Note from '@joplin/lib/models/Note';
import isNoteLockEnabled from '@joplin/lib/services/noteLock/isNoteLockEnabled';
class Command extends BaseCommand {
public override usage() {
@@ -34,6 +35,11 @@ class Command extends BaseCommand {
for (let i = 0; i < notes.length; i++) {
this.encryptionCheck(notes[i]);
// Enabling the lock here would only fail deep inside the gated save, so refuse it outright.
if (isNoteLockEnabled() && propName === 'is_locked') {
throw new Error(_('The note lock state cannot be changed from the command line'));
}
const timestamp = Date.now();
const newNote: Record<string, unknown> = {
+4 -2
View File
@@ -18,6 +18,8 @@ import { checkIfLoginWasSuccessful, generateApplicationConfirmUrl } from '@jopli
import Logger from '@joplin/utils/Logger';
import { uuidgen } from '@joplin/lib/uuid';
import ShareService from '@joplin/lib/services/share/ShareService';
import SyncTargetOneDrive from '@joplin/lib/SyncTargetOneDrive';
import SyncTargetDropbox from '@joplin/lib/SyncTargetDropbox';
const logger = Logger.create('command-sync');
@@ -57,7 +59,7 @@ class Command extends BaseCommand {
if (this.syncTargetId_ === 3 || this.syncTargetId_ === 4) {
// OneDrive
this.oneDriveApiUtils_ = new OneDriveApiNodeUtils(syncTarget.api());
this.oneDriveApiUtils_ = new OneDriveApiNodeUtils((syncTarget as SyncTargetOneDrive).api());
const auth = await this.oneDriveApiUtils_.oauthDance({
log: (s: string) => {
return this.stdout(s);
@@ -74,7 +76,7 @@ class Command extends BaseCommand {
return true;
} else if (syncTargetMd.name === 'dropbox') {
// Dropbox
const api = await syncTarget.api();
const api = await (syncTarget as SyncTargetDropbox).api();
const loginUrl = api.loginUrl();
this.stdout(_('To allow Joplin to synchronise with Dropbox, please follow the steps below:'));
this.stdout(_('Step 1: Open this URL in your browser to authorise the application:'));
+3 -1
View File
@@ -12,7 +12,9 @@ class Command extends BaseCommand {
}
public override async action() {
this.stdout(versionInfo(require('../package.json'), {}).message);
// The relative path to package.json is from the built version of this command
// in app-cli/build/command-version.js (see https://github.com/laurent22/joplin/issues/15738):
this.stdout(versionInfo(require('./package.json'), {}).message);
}
}
+7 -7
View File
@@ -35,10 +35,10 @@
],
"owner": "Laurent Cozic"
},
"version": "3.7.0",
"version": "3.7.1",
"bin": "./main.js",
"engines": {
"node": ">=10.0.0"
"node": ">=22.19.0"
},
"dependencies": {
"@joplin/lib": "~3.7",
@@ -48,7 +48,7 @@
"chalk": "4.1.2",
"compare-version": "0.1.2",
"file-type": "16.5.4",
"fs-extra": "11.3.4",
"fs-extra": "11.3.6",
"html-entities": "1.4.0",
"keytar": "7.9.0",
"md5": "2.3.0",
@@ -57,13 +57,13 @@
"proper-lockfile": "4.1.2",
"redux": "4.2.1",
"server-destroy": "1.0.1",
"sharp": "0.34.5",
"sharp": "0.35.0",
"sprintf-js": "1.1.3",
"sqlite3": "5.1.6",
"string-padding": "1.0.2",
"strip-ansi": "6.0.1",
"tcp-port-used": "1.0.2",
"terminal-kit": "3.1.2",
"tcp-port-used": "1.0.3",
"terminal-kit": "3.1.4",
"tkwidgets": "0.5.27",
"url-parse": "1.5.10",
"word-wrap": "1.2.5",
@@ -73,7 +73,7 @@
"@joplin/tools": "~3.7",
"@types/fs-extra": "11.0.4",
"@types/jest": "29.5.14",
"@types/node": "18.19.130",
"@types/node": "22.19.21",
"@types/proper-lockfile": "^4.1.2",
"gulp": "4.0.2",
"jest": "29.7.0",
+52
View File
@@ -2,7 +2,9 @@ import shim from '@joplin/lib/shim';
import * as os from 'os';
import { readFile } from 'fs/promises';
import { filename } from '@joplin/lib/path-utils';
import { dirname } from 'path';
import HtmlToMd, { ParseOptions } from '@joplin/lib/HtmlToMd';
import MdToHtml from '@joplin/renderer/MdToHtml';
describe('HtmlToMd', () => {
@@ -129,4 +131,54 @@ describe('HtmlToMd', () => {
expect(collapsedResult).toContain('\n\n');
});
it('should not re-read the rows of a table for every row', async () => {
// Under domino, each access to `table.rows` walks the whole table, so reading it once per row made
// converting large tables quadratic: https://github.com/laurent22/joplin/issues/16894
const resolveFrom = (moduleName: string, fromPath: string) => require.resolve(moduleName, { paths: [dirname(fromPath)] });
const domino: { createDocument: (html: string)=> Document } = require(resolveFrom('@mixmark-io/domino', resolveFrom('@joplin/turndown', require.resolve('@joplin/lib/HtmlToMd'))));
const rowCount = 100;
const rows = Array.from({ length: rowCount }, (_, i) => `<tr><td>${i}</td><td>b</td><td>c</td></tr>`).join('');
const root = domino.createDocument(`<div><table><thead><tr><th>A</th><th>B</th><th>C</th></tr></thead><tbody>${rows}</tbody></table></div>`).querySelector('div');
// Turndown converts a clone of the element it's given, which wouldn't have the counter below
Object.defineProperty(root, 'cloneNode', { value: () => root });
const table = root.querySelector('table');
const getRows = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(table), 'rows').get;
let rowsReadCount = 0;
Object.defineProperty(table, 'rows', { get: () => {
rowsReadCount++;
return getRows.call(table);
} });
const markdown = new HtmlToMd().parse(root);
expect(markdown.split('\n')).toHaveLength(rowCount + 2);
expect(rowsReadCount).toBeLessThan(rowCount);
});
// Covers how newlines are carried between children when joining them, which was rewritten to fix
// https://github.com/laurent22/joplin/issues/16895
it.each([
['<ul><li>a</li><li></li><li>b</li></ul>', '- a\n\n- b'],
['<span><p>a</p></span>b', 'a\n\nb'],
['<pre><code><span>line 1</span>\n\n\n<span>line 2</span></code></pre>', '```\nline 1\n\n\nline 2\n```'],
])('should join the Markdown of child elements: %j', (html, expected) => {
expect(new HtmlToMd().parse(html)).toBe(expected);
});
it.each([
['inline code ending with a space at the end of a heading', '<h2><code>npm</code> install&nbsp;<code>a&nbsp;</code></h2>', 'a\u00A0'],
['a heading that only contains inline code with a space', '<h2><code>&nbsp;</code></h2>', '\u00A0'],
])('should not change %s when the note is saved again', async (_title, html, codeText) => {
const htmlToMd = new HtmlToMd();
const mdToHtml = new MdToHtml();
const firstSave = htmlToMd.parse(html);
const reopened = await mdToHtml.render(firstSave, null, { bodyOnly: true });
const secondSave = htmlToMd.parse(reopened.html);
expect(reopened.html).toContain(`>${codeText}</code>`);
expect(secondSave).toBe(firstSave);
});
});
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE en-export SYSTEM "http://xml.evernote.com/pub/evernote-export4.dtd">
<en-export export-date="20260805T200517Z" application="Evernote" version="11.28.2">
<note>
<title>Note 2</title>
<created>20260805T185318Z</created>
<updated>20260805T200333Z</updated>
<note-attributes>
<author>author</author>
</note-attributes>
<content>
<![CDATA[<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd"><en-note><div style="display:none;"> </div><div>This is a test.</div><div><br/></div><div><span style="--en-highlight:red;background-color: #ffe2d5;">Link to a note in another notebook</span>: <a type="inline-richlink" rev="{&quot;isCached&quot;:false}" href="https://share.evernote.com/note/3784df9c-5e9f-dd33-c246-a6e9616db99b">Test</a></div><div>Link to a note in the same notebook: <a type="inline-richlink" rev="{&quot;isCached&quot;:false}" href="https://share.evernote.com/note/471e9dd8-9436-276a-0594-6d3f5a006084">Example note</a></div></en-note> ]]>
</content>
</note>
<note>
<title>Example note</title>
<created>20260805T200313Z</created>
<updated>20260805T200320Z</updated>
<note-attributes>
<author>author</author>
</note-attributes>
<content>
<![CDATA[<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd"><en-note><div style="display:none;"> </div><div>Testing!</div></en-note> ]]>
</content>
</note>
</en-export>
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE en-export SYSTEM "http://xml.evernote.com/pub/evernote-export4.dtd">
<en-export export-date="20260805T200508Z" application="Evernote" version="11.28.2">
<note>
<title>Test</title>
<created>20260805T184526Z</created>
<updated>20260805T200439Z</updated>
<note-attributes>
<author>author</author>
</note-attributes>
<content>
<![CDATA[<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd"><en-note><div style="display:none;"> </div><div><br/></div><div><a type="inline-richlink" rev="{&quot;isCached&quot;:false}" href="evernote:///view/312260475/s710/0c61536b-4d58-e7ee-1070-7a1a50d94852/e312cdf9-8314-440f-91a6-4bfca42da635">Test note</a>. <a type="inline-richlink" rev="{&quot;isCached&quot;:false}" href="https://share.evernote.com/note/471e9dd8-9436-276a-0594-6d3f5a006084">Example note</a></div></en-note> ]]>
</content>
</note>
<note>
<title>Testing</title>
<created>20260805T185202Z</created>
<updated>20260805T200413Z</updated>
<note-attributes>
<author>author</author>
</note-attributes>
<content>
<![CDATA[<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd"><en-note><div style="display:none;"> </div><div><a rev="en_rl_none" href="https://share.evernote.com/note/3784df9c-5e9f-dd33-c246-a6e9616db99b">Test</a>...</div><div><br/></div><div><br/></div></en-note> ]]>
</content>
</note>
<note>
<title>Test note</title>
<created>20260805T184449Z</created>
<updated>20260805T184526Z</updated>
<note-attributes>
<author>author</author>
</note-attributes>
<content>
<![CDATA[<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd"><en-note><div style="display:none;"> </div><div>This is a test note!</div><ul><li><div>Test</div></li></ul><div><br/></div><div><br/></div></en-note> ]]>
</content>
</note>
</en-export>
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE en-export SYSTEM "http://xml.evernote.com/pub/evernote-export4.dtd">
<en-export export-date="20260914T100000Z" application="Evernote" version="10.0">
<note>
<title>Unescaped ampersand</title>
<content>
<![CDATA[<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd">
<en-note><div>Mobilize &amp; Measure</div></en-note>
]]>
</content>
<created>20260914T100000Z</created>
<updated>20260914T100000Z</updated>
<note-attributes>
<application-data key="evernote.caption">Phase 1: Mobilize & Measure, which involves forming an advocacy group.</application-data>
</note-attributes>
</note>
<note>
<title>Note after the bad one</title>
<content>
<![CDATA[<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd">
<en-note><div>This note must still be imported</div></en-note>
]]>
</content>
<created>20260914T100000Z</created>
<updated>20260914T100000Z</updated>
</note>
</en-export>
@@ -1 +1 @@
<a data-from-md href='#'>test</a>
<a>test</a>
@@ -0,0 +1,5 @@
<p>Headings with nonbreaking spaces should be preserved:</p>
<h2>&nbsp;</h2>
<h2>&nbsp; &nbsp;</h2>
<h3>&nbsp;Leading space</h3>
<h2><code>a&nbsp; b</code></h2>
@@ -0,0 +1,9 @@
Headings with nonbreaking spaces should be preserved:
## &nbsp;
## &nbsp; &nbsp;
### &nbsp;Leading space
## `a  b`
@@ -47,7 +47,8 @@ joplin.plugins.register({
// must reach .chat from `joplin` in a single chain and call it
// immediately. Storing `joplin.ai` or `joplin.ai.chat` first and
// then invoking it later corrupts the path tracking.
summary = await (joplin as any).ai.chat(messages);
const result = await (joplin as any).ai.chat(messages);
summary = result.text;
} catch (error) {
alert(`AI call failed: ${error.message}`);
return;
+58 -3
View File
@@ -8,8 +8,12 @@ import { ExportModule } from '@joplin/lib/services/interop/Module';
import { _ } from '@joplin/lib/locale';
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
import bridge from './services/bridge';
import isNoteLockEnabled from '@joplin/lib/services/noteLock/isNoteLockEnabled';
import { unlockNoteLockSession } from '@joplin/lib/services/noteLock/noteLockPrompts';
import noteLockPrompts from './utils/noteLockPrompts';
import Setting from '@joplin/lib/models/Setting';
import Note from '@joplin/lib/models/Note';
import Folder from '@joplin/lib/models/Folder';
import { friendlySafeFilename } from '@joplin/lib/path-utils';
import time from '@joplin/lib/time';
import { BrowserWindow, BrowserWindowConstructorOptions } from 'electron';
@@ -36,6 +40,8 @@ export default class InteropServiceHelper {
format: ExportModuleOutputFormat.Html,
target: FileSystemItem.File,
sourceNoteIds: [noteId],
// The PDF export and printing ask first.
decryptLockedNotes: true,
customCss: '', ...exportOptions };
const service = InteropService.instance();
@@ -193,7 +199,30 @@ export default class InteropServiceHelper {
return `${filename}.${fileExtension}`;
}
public static async export(_dispatch: Dispatch, module: ExportModule, options: ExportNoteOptions = null) {
// Decrypting exports write locked notes out as plain text, so the user is asked even with the session unlocked,
// which only skips the password.
public static async confirmLockedNoteExport(dispatch: Dispatch, noteIds: string[], folderIds: string[] = null) {
if (!isNoteLockEnabled()) return 'export';
let hasLockedNotes = false;
if (noteIds?.length) {
hasLockedNotes = (await Note.byIds(noteIds, { fields: ['is_locked'] })).some(note => !!note.is_locked);
} else if (folderIds?.length) {
hasLockedNotes = await Note.hasLockedNoteInFolderTrees(folderIds);
} else {
// Same scope as the export of everything: notebooks not in the trash.
hasLockedNotes = await Note.hasLockedNoteInFolderTrees(await Folder.childrenIds(''));
}
if (!hasLockedNotes) return 'export';
const answer = bridge().showMessageBox(_('Some notes are locked and this export format requires them to be unlocked. Would you like to export these notes unprotected, or export without these notes?'), {
buttons: [_('Export unprotected'), _('Export without locked notes'), _('Cancel')],
});
if (answer === 1) return 'skipLocked';
if (answer === 0 && await unlockNoteLockSession(noteLockPrompts(dispatch))) return 'export';
return 'cancel';
}
public static async export(dispatch: Dispatch, module: ExportModule, options: ExportNoteOptions = null) {
if (!options) options = {};
let path = null;
@@ -214,6 +243,18 @@ export default class InteropServiceHelper {
if (Array.isArray(path)) path = path[0];
// Backups keep locked notes encrypted, so only the decrypting formats ask.
const isBackup = module.format === ExportModuleOutputFormat.Raw || module.format === ExportModuleOutputFormat.Jex;
const lockedNoteChoice = isBackup ? 'export' : await this.confirmLockedNoteExport(dispatch, options.sourceNoteIds, options.sourceFolderIds);
if (lockedNoteChoice === 'cancel') return;
let sourceNoteIds = options.sourceNoteIds;
// A selection left with no note would make a single-file export fail on the file it never wrote.
if (sourceNoteIds?.length && lockedNoteChoice === 'skipLocked') {
sourceNoteIds = (await Note.byIds(sourceNoteIds, { fields: ['id', 'is_locked'] })).filter(note => !note.is_locked).map(note => note.id);
if (!sourceNoteIds.length) return;
}
void CommandService.instance().execute('showModalMessage', _('Exporting to "%s" as "%s" format. Please wait...', path, module.format));
const exportOptions: ExportOptions = {};
@@ -225,20 +266,34 @@ export default class InteropServiceHelper {
exportOptions.target = module.target;
exportOptions.includeConflicts = !!options.includeConflicts;
if (options.sourceFolderIds) exportOptions.sourceFolderIds = options.sourceFolderIds;
if (options.sourceNoteIds) exportOptions.sourceNoteIds = options.sourceNoteIds;
if (sourceNoteIds) exportOptions.sourceNoteIds = sourceNoteIds;
exportOptions.skipLockedNotes = lockedNoteChoice === 'skipLocked';
exportOptions.decryptLockedNotes = lockedNoteChoice === 'export';
const service = InteropService.instance();
let keepModalOpen = false;
try {
const result = await service.export(exportOptions);
// eslint-disable-next-line no-console
console.info('Export result: ', result);
if (result.warnings.length) {
keepModalOpen = true;
const message = [
_('Completed with warnings:'),
'',
_('If any files are reported missing, download all relevant attachments to this device and try again.'),
'',
...result.warnings,
].join('\n');
void CommandService.instance().execute('showModalMessage', message, true);
}
} catch (error) {
console.error(error);
bridge().showErrorMessageBox(_('Could not export notes: %s', error.message));
}
void CommandService.instance().execute('hideModalMessage');
if (!keepModalOpen) void CommandService.instance().execute('hideModalMessage');
}
}
+106 -1
View File
@@ -1,7 +1,25 @@
import { AppState, createAppDefaultWindowState } from './app.reducer';
import { ChatRole, ChatToolCall, ChatToolMessage } from '@joplin/lib/services/ai/types';
import { AiChatMessage, AppState, createAppDefaultWindowState } from './app.reducer';
import appReducer, { createAppDefaultState } from './app.reducer';
describe('app.reducer', () => {
it('should clear a deleted conversation from matching windows and keep other conversations', () => {
const messages: AiChatMessage[] = [{ id: 'question', createdTime: 1, role: 'user', text: 'Question', raw: [], noteId: '', noteTitle: '' }];
const state = createAppDefaultState({});
state.aiChatConversationId = 'deleted';
state.aiChatMessages = messages;
state.backgroundWindows = {
second: { ...createAppDefaultWindowState(), windowId: 'second', aiChatConversationId: 'deleted', aiChatMessages: messages },
third: { ...createAppDefaultWindowState(), windowId: 'third', aiChatConversationId: 'other', aiChatMessages: messages },
};
const result = appReducer(state, { type: 'AI_CHAT_DELETE', conversationId: 'deleted' });
expect(result.aiChatConversationId).toBeNull();
expect(result.aiChatMessages).toEqual([]);
expect(result.backgroundWindows.second.aiChatConversationId).toBeNull();
expect(result.backgroundWindows.second.aiChatMessages).toEqual([]);
expect(result.backgroundWindows.third.aiChatConversationId).toBe('other');
expect(result.backgroundWindows.third.aiChatMessages).toEqual(messages);
});
it('should handle DIALOG_OPEN', async () => {
const state: AppState = createAppDefaultState({});
@@ -47,6 +65,37 @@ describe('app.reducer', () => {
]);
});
it('aI_STATUS_UPDATE merges a partial payload without clobbering unrelated fields', () => {
const state: AppState = {
...createAppDefaultState({}),
aiStatus: { degraded: true, tokensUsed: 500, tokensBudget: 1000, lastToastShownAt: 12345 },
};
const afterUsage = appReducer(state, {
type: 'AI_STATUS_UPDATE',
payload: { tokensUsed: 600 },
});
expect(afterUsage.aiStatus).toEqual({
degraded: true,
tokensUsed: 600,
tokensBudget: 1000,
lastToastShownAt: 12345,
});
const afterToast = appReducer(afterUsage, {
type: 'AI_STATUS_UPDATE',
payload: { lastToastShownAt: 67890 },
});
expect(afterToast.aiStatus).toEqual({
degraded: true,
tokensUsed: 600,
tokensBudget: 1000,
lastToastShownAt: 67890,
});
});
it('showing a dialog in one window should hide dialogs with the same ID in background windows', () => {
const state: AppState = {
...createAppDefaultState({}),
@@ -71,4 +120,60 @@ describe('app.reducer', () => {
expect(newState.visibleDialogs).toEqual({ testDialog: true });
});
it('should build AI chat history', () => {
let state: AppState = {
...createAppDefaultState({}),
aiChatConversationId: 'chat-1',
};
let idCounter = 0;
const buildMessage = (role: ChatRole.User | ChatRole.Assistant, content: string, toolCalls: ChatToolCall[]) => ({
id: `id-${idCounter++}`,
createdTime: 1,
noteId: 'note-a',
noteTitle: 'A',
role,
text: content,
raw: [
{
role,
content: content,
...(toolCalls.length ? { toolCalls } : {}),
},
],
} satisfies AiChatMessage);
state = appReducer(state, {
type: 'AI_CHAT_APPEND',
conversationId: 'chat-1',
message: buildMessage(ChatRole.User, 'Test', []),
});
state = appReducer(state, {
type: 'AI_CHAT_APPEND',
conversationId: 'chat-1',
message: buildMessage(
ChatRole.Assistant,
'Testing',
[{ toolName: 'testTool', callId: 'call-1', arguments: { arg: 1 }, parseError: null }],
),
});
state = appReducer(state, {
type: 'AI_CHAT_ADD_TOOL_RESULT',
conversationId: 'chat-1',
toolCall: ({
role: ChatRole.Tool,
toolName: 'testTool',
toolCallId: 'call-1',
userDescription: '',
isError: false,
isEdit: true,
content: 'Result',
} satisfies ChatToolMessage),
});
expect(state.aiChatMessages).toMatchObject([
{ id: 'id-0', role: 'user', text: 'Test', raw: [{ role: 'user' }] },
{ id: 'id-1', role: 'assistant', text: 'Testing' },
]);
});
});
+171 -42
View File
@@ -1,13 +1,44 @@
import { produce } from 'immer';
import Setting from '@joplin/lib/models/Setting';
import { defaultState, defaultWindowState, State, WindowState } from '@joplin/lib/reducer';
import { defaultState, defaultWindowId, defaultWindowState, State, stateUtils, WindowState } from '@joplin/lib/reducer';
import iterateItems from './gui/ResizableLayout/utils/iterateItems';
import { LayoutItem } from './gui/ResizableLayout/utils/types';
import validateLayout from './gui/ResizableLayout/utils/validateLayout';
import Logger from '@joplin/utils/Logger';
import { ChatMessage, ChatRole } from '@joplin/lib/services/ai/types';
const logger = Logger.create('app.reducer');
export interface AiChatMessage {
id: string;
createdTime: number;
noteId: string;
noteTitle: string;
role: 'user' | 'assistant' | 'error' | 'separator';
text: string;
hide?: boolean;
// The raw message(s) corresponding to this event
raw: ChatMessage[];
}
// Joplin Cloud degradation / budget snapshot. Populated from the provider's
// InternalChatResult after each chat() call, and persisted to Setting so it
// survives restarts and reflects plugin-driven calls even when no UI was open.
export interface AiStatus {
degraded: boolean;
tokensUsed: number;
tokensBudget: number;
lastToastShownAt: number | null;
}
export const defaultAiStatus = (): AiStatus => ({
degraded: false,
tokensUsed: 0,
tokensBudget: 0,
lastToastShownAt: null,
});
export interface AppStateRoute {
type: string;
routeName: string;
@@ -47,6 +78,12 @@ export interface AppWindowState extends WindowState {
// the toolbar to show the editor toggle button. (We can't compute this
// from the redux note list because `body` isn't in the preview fields.)
activeNoteIsWhiteboard: boolean;
// In window state so the conversation survives panel hide/show (the
// layout container can swap component types and unmount the panel).
aiChatMessages: AiChatMessage[];
aiChatConversationId: string|null;
// Layout for secondary windows
secondaryWindowLayout: LayoutItem|null;
}
interface BackgroundWindowStates {
@@ -63,10 +100,12 @@ export interface AppState extends State, AppWindowState {
layoutMoveMode: boolean;
startupPluginsLoaded: boolean;
modalOverlayMessage: string|null;
modalOverlayHasCloseButton: boolean;
// Extra reducer keys go here
mainLayout: LayoutItem;
isResettingLayout: boolean;
aiStatus: AiStatus;
}
export const createAppDefaultWindowState = (): AppWindowState => {
@@ -80,6 +119,9 @@ export const createAppDefaultWindowState = (): AppWindowState => {
watchedResources: {},
whiteboardForceMarkdown: {},
activeNoteIsWhiteboard: false,
aiChatMessages: [],
aiChatConversationId: null,
secondaryWindowLayout: null,
};
};
@@ -102,6 +144,8 @@ export function createAppDefaultState(resourceEditWatcherDefaultState: Partial<A
startupPluginsLoaded: false,
isResettingLayout: false,
modalOverlayMessage: null,
modalOverlayHasCloseButton: false,
aiStatus: defaultAiStatus(),
...resourceEditWatcherDefaultState,
};
}
@@ -115,6 +159,42 @@ const hideBackgroundDialogsWithId = produce((state: AppState, id: string) => {
}
});
const withWindowStateUpdated = <Key extends keyof AppWindowState> (
state: AppState, windowId: string, stateKey: Key, value: (oldValue: AppWindowState[Key])=> AppWindowState[Key],
) => {
return produce((state: AppState) => {
const windowState = stateUtils.windowStateById(state, windowId);
windowState[stateKey] = value(windowState[stateKey]);
})(state);
};
interface SetLayoutPropOptions {
key: string;
prop: string;
value: string;
}
const setLayoutProp = (layout: LayoutItem, { key, prop, value }: SetLayoutPropOptions) => {
let newLayout = produce(layout, (draftLayout: LayoutItem) => {
iterateItems(draftLayout, (_itemIndex: number, item: LayoutItem, _parent: LayoutItem) => {
if (!item) {
logger.warn('MAIN_LAYOUT_SET_ITEM_PROP: Found an empty item in layout: ', JSON.stringify(layout));
} else {
if (item.key === key) {
(item as unknown as Record<string, unknown>)[prop] = value;
return false;
}
}
return true;
});
});
if (newLayout !== layout) newLayout = validateLayout(newLayout);
return newLayout;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Redux actions are heterogeneous; typing this would require an action-type union and many narrowing casts inside the switch
export default function(state: AppState, action: any) {
let newState = state;
@@ -226,58 +306,107 @@ export default function(state: AppState, action: any) {
}
case 'WHITEBOARD_ACTIVE_NOTE_SET':
newState = {
...state,
activeNoteIsWhiteboard: !!action.value,
};
newState = withWindowStateUpdated(
state, action.windowId, 'activeNoteIsWhiteboard', () => !!action.value,
);
break;
case 'MAIN_LAYOUT_SET':
newState = {
...state,
mainLayout: action.value,
};
break;
case 'MAIN_LAYOUT_SET_ITEM_PROP':
{
if (!state.mainLayout) {
logger.warn('MAIN_LAYOUT_SET_ITEM_PROP: Trying to set an item prop on the layout, but layout is empty: ', JSON.stringify(action));
} else {
let newLayout = produce(state.mainLayout, (draftLayout: LayoutItem) => {
iterateItems(draftLayout, (_itemIndex: number, item: LayoutItem, _parent: LayoutItem) => {
if (!item) {
logger.warn('MAIN_LAYOUT_SET_ITEM_PROP: Found an empty item in layout: ', JSON.stringify(state.mainLayout));
} else {
if (item.key === action.itemKey) {
(item as unknown as Record<string, unknown>)[action.propName] = action.propValue;
return false;
}
}
return true;
});
});
if (newLayout !== state.mainLayout) newLayout = validateLayout(newLayout);
newState = {
...state,
mainLayout: newLayout,
};
case 'AI_CHAT_DELETE':
newState = produce(state, draft => {
for (const windowState of stateUtils.allWindowStates(draft)) {
if (windowState.aiChatConversationId !== action.conversationId) continue;
windowState.aiChatConversationId = null;
windowState.aiChatMessages = [];
}
});
break;
case 'AI_CHAT_OPEN':
newState = produce(state, draft => {
const openConversation = action.conversationId && stateUtils.allWindowStates(state)
.find(windowState => windowState.aiChatConversationId === action.conversationId);
const windowState = stateUtils.windowStateById(draft, action.windowId);
windowState.aiChatConversationId = action.conversationId;
windowState.aiChatMessages = openConversation ? openConversation.aiChatMessages : action.messages;
});
break;
case 'AI_CHAT_APPEND':
case 'AI_CHAT_ADD_TOOL_RESULT':
case 'AI_CHAT_REMOVE':
newState = produce(state, draft => {
for (const windowState of stateUtils.allWindowStates(draft)) {
if (windowState.aiChatConversationId !== action.conversationId) continue;
if (action.type === 'AI_CHAT_APPEND') {
windowState.aiChatMessages.push(action.message);
} else if (action.type === 'AI_CHAT_REMOVE') {
windowState.aiChatMessages = windowState.aiChatMessages.filter(message => message.id !== action.id);
} else {
const message = windowState.aiChatMessages.find(message => message.raw.some(entry =>
entry.role === ChatRole.Assistant && entry.toolCalls?.some(call => call.callId === action.toolCall.toolCallId),
));
message?.raw.push(action.toolCall);
}
}
});
break;
case 'AI_STATUS_UPDATE':
// Partial merge — callers can bump `lastToastShownAt` alone after
// firing the toast without clobbering the degraded/usage numbers.
newState = {
...state,
aiStatus: {
...(state.aiStatus ?? defaultAiStatus()),
...(action.payload as Partial<AiStatus>),
},
};
break;
case 'WINDOW_LAYOUT_SET':
case 'MAIN_LAYOUT_SET':
if ((action.windowId ?? defaultWindowId) === defaultWindowId) {
newState = {
...state,
mainLayout: action.value,
};
} else {
newState = withWindowStateUpdated(
state, action.windowId, 'secondaryWindowLayout', () => action.value,
);
}
break;
case 'WINDOW_LAYOUT_SET_ITEM_PROP':
case 'MAIN_LAYOUT_SET_ITEM_PROP': {
const updateOption = { key: action.itemKey, prop: action.propName, value: action.propValue };
if ((action.windowId ?? defaultWindowId) !== defaultWindowId) {
newState = withWindowStateUpdated(
state, action.windowId, 'secondaryWindowLayout', oldLayout => (
setLayoutProp(oldLayout, updateOption)
),
);
} else {
newState = {
...state,
mainLayout: setLayoutProp(state.mainLayout, updateOption),
};
}
break;
}
case 'SHOW_MODAL_MESSAGE':
newState = { ...newState, modalOverlayMessage: action.message };
newState = {
...newState,
modalOverlayMessage: action.message,
modalOverlayHasCloseButton: !!action.hasCloseButton,
};
break;
case 'HIDE_MODAL_MESSAGE':
newState = { ...newState, modalOverlayMessage: null };
newState = { ...newState, modalOverlayMessage: null, modalOverlayHasCloseButton: false };
break;
case 'NOTE_FILE_WATCHER_ADD':
+306
View File
@@ -0,0 +1,306 @@
import BaseApplication, { shouldPreserveSelectedNoteOnSmartFilterSelect } from '@joplin/lib/BaseApplication';
import ItemChange from '@joplin/lib/models/ItemChange';
import Note from '@joplin/lib/models/Note';
import reducer, { defaultState, defaultWindowId, State } from '@joplin/lib/reducer';
import { ALL_NOTES_FILTER_ID } from '@joplin/lib/reserved-ids';
import { NoteEntity } from '@joplin/lib/services/database/types';
import ExternalEditWatcher from '@joplin/lib/services/ExternalEditWatcher';
import { setEncryptionEnabled } from '@joplin/lib/services/synchronizer/syncInfoUtils';
import { loadEncryptionMasterKey, setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
import waitFor from '@joplin/lib/testing/waitFor';
import AsyncActionQueue from '@joplin/lib/AsyncActionQueue';
import { appendFile, readFile } from 'fs/promises';
import { TextEncoder as NodeTextEncoder } from 'util';
import app from './app';
type NoteUpdateAction = {
changeSource: number;
changedFields: string[];
note: NoteEntity;
[key: string]: unknown;
};
const mockBaseMiddleware = () => {
return jest.spyOn(BaseApplication.prototype as unknown as {
generalMiddleware: (store: unknown, next: (action: unknown)=> unknown, action: unknown)=> Promise<unknown>;
}, 'generalMiddleware').mockImplementation(async (_store, next, action) => next(action));
};
const createDesktopActionHandler = (watchedNoteFiles: string[] = []) => {
const middleware = app().generalMiddlewareFn()({ getState: () => ({ watchedNoteFiles }) });
return middleware(jest.fn());
};
describe('app', () => {
const watcher = ExternalEditWatcher.instance();
const originalTextEncoder = globalThis.TextEncoder;
const originalNoteDispatch = Note.dispatch;
let baseMiddlewareMock: jest.SpyInstance;
test.each([
['regular', { deleted_time: 0, is_conflict: 0 }, true],
['trashed', { deleted_time: 1, is_conflict: 0 }, false],
['conflict', { deleted_time: 0, is_conflict: 1 }, false],
])('should determine whether to preserve a %s note when selecting all notes', (_noteType, noteProperties, expected) => {
watcher.initialize(jest.fn(), jest.fn());
const note = { id: 'note-id', ...noteProperties } as NoteEntity;
const state = { ...defaultState, notes: [note], selectedNoteIds: [note.id] };
expect(shouldPreserveSelectedNoteOnSmartFilterSelect(state, ALL_NOTES_FILTER_ID)).toBe(expected);
});
test('should not apply a note refresh after the active window changes', async () => {
const secondaryState = {
...defaultState,
windowId: 'secondary-window',
notesParentType: 'Folder',
selectedFolderId: 'secondary-folder',
selectedNoteIds: ['secondary-note'],
} as State;
let activeState = secondaryState;
const dispatch = jest.fn();
const storeMock = jest.spyOn(app(), 'store').mockReturnValue({
dispatch,
getState: () => activeState,
} as unknown as ReturnType<ReturnType<typeof app>['store']>);
const previewsMock = jest.spyOn(Note, 'previews').mockImplementation(async () => {
activeState = { ...defaultState, windowId: defaultWindowId } as State;
return [{ id: 'secondary-note' }] as NoteEntity[];
});
try {
await app().refreshNotes(secondaryState, true);
expect(dispatch).not.toHaveBeenCalled();
} finally {
previewsMock.mockRestore();
storeMock.mockRestore();
}
});
test('should not apply a note refresh after a newer refresh has started', async () => {
const state = { ...defaultState, notesParentType: 'Folder', selectedFolderId: 'folder' } as State;
const dispatch = jest.fn();
const storeMock = jest.spyOn(app(), 'store').mockReturnValue({
dispatch,
getState: () => state,
} as unknown as ReturnType<ReturnType<typeof app>['store']>);
let resolveSlowPreviews: (notes: NoteEntity[])=> void = null;
const previewsMock = jest.spyOn(Note, 'previews')
.mockImplementationOnce(() => new Promise(resolve => { resolveSlowPreviews = resolve; }))
.mockResolvedValueOnce([{ id: 'fast-note' }]);
try {
const slowRefresh = app().refreshNotes(state);
const fastRefresh = (async () => {
await app().refreshNotes(state);
resolveSlowPreviews([{ id: 'slow-note' }]);
})();
await Promise.all([slowRefresh, fastRefresh]);
const noteUpdates = dispatch.mock.calls.map(call => call[0]).filter(action => action.type === 'NOTE_UPDATE_ALL');
expect(noteUpdates.map(action => action.notes)).toEqual([[{ id: 'fast-note' }]]);
} finally {
previewsMock.mockRestore();
storeMock.mockRestore();
}
});
test('should not let a refresh for an inactive window cancel the active window refresh', async () => {
const state = { ...defaultState, notesParentType: 'Folder', selectedFolderId: 'folder' } as State;
const dispatch = jest.fn();
const storeMock = jest.spyOn(app(), 'store').mockReturnValue({
dispatch,
getState: () => state,
} as unknown as ReturnType<ReturnType<typeof app>['store']>);
const previewsMock = jest.spyOn(Note, 'previews').mockResolvedValue([]);
try {
await Promise.all([app().refreshNotes(state), app().refreshNotes({ ...state, windowId: 'inactive-window' })]);
expect(dispatch).toHaveBeenCalledWith(expect.objectContaining({ type: 'NOTE_UPDATE_ALL' }));
} finally {
previewsMock.mockRestore();
storeMock.mockRestore();
}
});
test.each([
['the active window', true, [[expect.objectContaining({ windowId: defaultWindowId }), false, '']]],
['a background window', false, []],
])('should only refresh notes when the active window is closed (closing %s)', async (_name, focusSecondaryWindow, expectedRefreshCalls) => {
let state = reducer(defaultState, { type: 'WINDOW_OPEN', windowId: 'secondary-window', folderId: 'folder', noteId: 'note' });
if (focusSecondaryWindow) state = reducer(state, { type: 'WINDOW_FOCUS', windowId: 'secondary-window' });
const store = { getState: () => state, dispatch: jest.fn() };
const next = (action: unknown) => { state = reducer(state, action); };
const refreshNotesMock = jest.spyOn(app(), 'refreshNotes').mockResolvedValue(undefined);
baseMiddlewareMock.mockRestore();
try {
await (BaseApplication.prototype as unknown as {
generalMiddleware: (store: unknown, next: (action: unknown)=> unknown, action: unknown)=> Promise<unknown>;
}).generalMiddleware.call(app(), store, next, { type: 'WINDOW_CLOSE', windowId: 'secondary-window' });
expect(refreshNotesMock.mock.calls).toEqual(expectedRefreshCalls);
} finally {
refreshNotesMock.mockRestore();
}
});
beforeEach(async () => {
await setupDatabaseAndSynchronizer(0);
await switchClient(0);
baseMiddlewareMock = mockBaseMiddleware();
});
afterEach(async () => {
Note.dispatch = originalNoteDispatch;
baseMiddlewareMock.mockRestore();
setEncryptionEnabled(false);
Object.defineProperty(globalThis, 'TextEncoder', { configurable: true, value: originalTextEncoder });
await watcher.stopWatchingAll();
});
const startExternalEditing = async () => {
const openItem = jest.fn();
watcher.initialize(jest.fn(() => ({
openItem,
})), jest.fn());
const originalNote = await Note.save({
title: 'Test note',
body: 'Original body',
});
await watcher.openAndWatch(originalNote);
return {
originalNote,
externalFilePath: openItem.mock.calls[0][0] as string,
};
};
test('should update the external edit file after a synced note changes', async () => {
const { originalNote, externalFilePath } = await startExternalEditing();
const syncedNote = await Note.save({
...originalNote,
body: 'Changed on another device',
}, { changeSource: ItemChange.SOURCE_SYNC });
const syncedUpdateAction = {
type: 'NOTE_UPDATE_ONE',
changeSource: ItemChange.SOURCE_SYNC,
changedFields: ['body'],
note: syncedNote,
};
const handleDesktopAction = createDesktopActionHandler();
await handleDesktopAction(syncedUpdateAction);
expect(await readFile(externalFilePath, 'utf8')).toBe(await Note.serializeForEdit(syncedNote));
});
test('should not update the external edit file when the note is not watched', async () => {
const note = await Note.save({
title: 'Test note',
body: 'Changed on another device',
}, { changeSource: ItemChange.SOURCE_SYNC });
const updateNoteFile = jest.spyOn(watcher, 'updateNoteFile');
const handleDesktopAction = createDesktopActionHandler();
await handleDesktopAction({
type: 'NOTE_UPDATE_ONE',
changeSource: ItemChange.SOURCE_SYNC,
changedFields: ['body'],
note,
});
expect(watcher.noteIsWatched(note)).toBe(false);
expect(updateNoteFile).not.toHaveBeenCalled();
updateNoteFile.mockRestore();
});
test('should not update the external edit file when the synced note is locked', async () => {
const { originalNote } = await startExternalEditing();
const updateNoteFile = jest.spyOn(watcher, 'updateNoteFile');
const handleDesktopAction = createDesktopActionHandler();
await handleDesktopAction({
type: 'NOTE_UPDATE_ONE',
changeSource: ItemChange.SOURCE_SYNC,
changedFields: ['body', 'is_locked'],
note: { ...originalNote, body: 'Locked note ciphertext', is_locked: 1 },
});
expect(updateNoteFile).not.toHaveBeenCalled();
updateNoteFile.mockRestore();
});
test('should drop pending external edits when a synced note arrives locked', async () => {
const { originalNote, externalFilePath } = await startExternalEditing();
const lockedNote = await Note.save({
...originalNote,
body: 'Locked note ciphertext',
is_locked: 1,
}, { changeSource: ItemChange.SOURCE_SYNC });
await appendFile(externalFilePath, ' (plaintext edit)');
const changeEventQueue = (watcher as unknown as { changeEventQueue_: AsyncActionQueue<unknown> }).changeEventQueue_;
await waitFor(() => {
expect(changeEventQueue.isEmpty).toBe(false);
});
const handleDesktopAction = createDesktopActionHandler([originalNote.id]);
await handleDesktopAction({
type: 'NOTE_UPDATE_ONE',
changeSource: ItemChange.SOURCE_SYNC,
changedFields: ['body', 'is_locked'],
note: lockedNote,
});
await changeEventQueue.waitForAllDone();
expect(watcher.noteIsWatched(originalNote)).toBe(false);
expect((await Note.load(originalNote.id)).body).toBe('Locked note ciphertext');
});
test('should wait for a synced encrypted note to be decrypted before updating the external edit file', async () => {
const { originalNote, externalFilePath } = await startExternalEditing();
const noteActions = jest.fn();
Note.dispatch = noteActions;
Object.defineProperty(globalThis, 'TextEncoder', { configurable: true, value: NodeTextEncoder });
setEncryptionEnabled(true);
await loadEncryptionMasterKey();
const encryptedSyncPayload = await Note.serializeForSync({
...originalNote,
body: 'Changed on another device',
} as NoteEntity);
const encryptedNote = Note.filter(await Note.unserialize(encryptedSyncPayload));
const savedEncryptedNote = await Note.save(encryptedNote, {
autoTimestamp: false,
changeSource: ItemChange.SOURCE_SYNC,
oldItem: originalNote as unknown as Record<string, unknown>,
});
const encryptedSyncAction = noteActions.mock.calls[0][0] as NoteUpdateAction;
expect(encryptedSyncAction.note.encryption_applied).toBe(1);
const handleDesktopAction = createDesktopActionHandler();
await handleDesktopAction(encryptedSyncAction);
expect(await readFile(externalFilePath, 'utf8')).toBe(await Note.serializeForEdit(originalNote));
noteActions.mockClear();
const decryptedNote = await Note.decrypt(savedEncryptedNote);
const decryptedUpdateActions = noteActions.mock.calls
.map(call => call[0] as NoteUpdateAction)
.filter(action => action.type === 'NOTE_UPDATE_ONE');
expect(decryptedUpdateActions).toHaveLength(1);
const decryptedAction = decryptedUpdateActions[0];
expect(decryptedAction.note).toEqual(decryptedNote);
expect(decryptedAction.note.body).toBe('Changed on another device');
expect(decryptedAction.note.encryption_applied).toBe(0);
expect(decryptedAction.changedFields).toEqual(expect.arrayContaining(['body', 'encryption_applied']));
await handleDesktopAction(decryptedAction);
expect(await readFile(externalFilePath, 'utf8')).toBe(await Note.serializeForEdit(decryptedNote));
});
});
+41 -1
View File
@@ -68,6 +68,8 @@ import Note from '@joplin/lib/models/Note';
import Resource from '@joplin/lib/models/Resource';
import AiService from '@joplin/lib/services/ai/AiService';
import LocalEmbeddingProvider from '@joplin/lib/services/ai/LocalEmbeddingProvider';
import { installAiStatusBridge, AiStatusStore } from './services/aiStatusBridge';
import ItemChange from '@joplin/lib/models/ItemChange';
const perfLogger = PerformanceLogger.create();
@@ -161,6 +163,17 @@ class Application extends BaseApplication {
await AlarmService.updateNoteNotification(action.id, action.type === 'NOTE_DELETE');
}
if (action.type === 'NOTE_DELETE' && store.getState().watchedNoteFiles.includes(action.id)) {
await ExternalEditWatcher.instance().stopWatching(action.id);
}
// Like a lock made on this device (see enableNoteEncryption), a lock that arrives with a sync ends the note's
// external editing, which the UI cannot stop for a locked note. The lock is already saved here, so pending
// plaintext edits are dropped rather than saved over the ciphertext.
if (action.type === 'NOTE_UPDATE_ONE' && action.note.is_locked && store.getState().watchedNoteFiles.includes(action.note.id)) {
await ExternalEditWatcher.instance().stopWatching(action.note.id, false);
}
if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'featureFlag.autoUpdaterServiceEnabled' || action.type === 'SETTING_UPDATE_ALL') {
if (Setting.value('featureFlag.autoUpdaterServiceEnabled')) this.setupAutoUpdaterService();
}
@@ -168,6 +181,19 @@ class Application extends BaseApplication {
const result = await super.generalMiddleware(store, next, action);
const newState = store.getState();
if (
action.type === 'NOTE_UPDATE_ONE' &&
[ItemChange.SOURCE_SYNC, ItemChange.SOURCE_DECRYPTION].includes(action.changeSource) &&
!action.note.encryption_applied &&
!action.note.is_locked &&
action.changedFields.some((field: string) => ['title', 'body'].includes(field))
) {
const externalEditWatcher = ExternalEditWatcher.instance();
if (externalEditWatcher.noteIsWatched(action.note)) {
await externalEditWatcher.updateNoteFile(action.note);
}
}
if (['NOTE_VISIBLE_PANES_TOGGLE', 'NOTE_VISIBLE_PANES_SET'].indexOf(action.type) >= 0) {
Setting.setValue('noteVisiblePanes', newState.noteVisiblePanes);
}
@@ -506,6 +532,11 @@ class Application extends BaseApplication {
this.initRedux();
// BaseApplication.store() is typed against the shared State; the
// runtime store carries AppState. The bridge only needs dispatch and
// getState, so narrow through unknown.
installAiStatusBridge(this.store() as unknown as AiStatusStore);
initializeCommandService(this.store(), Setting.value('env') === 'dev');
const keymapService = KeymapService.instance();
@@ -652,7 +683,7 @@ class Application extends BaseApplication {
setTimeout(() => {
// Schedule sync with a delay of 0 and wrap with the desired timeout, as shim.setTimeout may not fire on first run or after an upgrade
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
void reg.scheduleSync(0).then(() => {
void reg.scheduleSync(0).finally(() => {
// Wait for the first sync before updating the notifications, since synchronisation
// might change the notifications.
void AlarmService.updateAllNotifications();
@@ -766,6 +797,15 @@ class Application extends BaseApplication {
});
});
addTask('app/listen for note lock session events', () => {
eventManager.on(EventName.NoteLockSessionChange, (event) => {
this.dispatch({
type: 'SET_NOTE_LOCK_SESSION_UNLOCKED',
value: event.unlocked,
});
});
});
addTask('app/setupOcrService', () => this.setupOcrService());
return tasks;
+8
View File
@@ -16,6 +16,8 @@ import { KB } from '@joplin/utils/bytes';
import { defaultWindowId } from '@joplin/lib/reducer';
import { execCommand } from '@joplin/utils';
const allowedExternalProtocols = ['http:', 'https:', 'mailto:', 'joplin:'];
interface LastSelectedPath {
file: string;
directory: string;
@@ -495,6 +497,12 @@ export class Bridge {
if (protocol === 'file:') {
await this.openItem(url);
} else {
// Some callers pass untrusted note content, and this reaches the OS
// URI dispatcher, where a dangerous scheme handler can mean RCE.
if (!allowedExternalProtocols.includes(protocol)) {
console.warn(`Bridge: openExternal: rejected URL with disallowed scheme "${protocol}"`);
return;
}
return shell.openExternal(url);
}
}
@@ -0,0 +1,2 @@
<!-- Cloud Offline Outline from Ionicons. See readme/licenses.md under "ionicon icons" for attribution and MIT license terms. -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M93.72 183.25C49.49 198.05 16 233.1 16 288c0 66 54 112 120 112h184.37M467.82 377.74C485.24 363.3 496 341.61 496 312c0-59.82-53-85.76-96-88-8.89-89.54-71-144-144-144-26.16 0-48.79 6.93-67.6 18.14" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/><path d="M448 448 64 64" fill="none" stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="32"/></svg>

After

Width:  |  Height:  |  Size: 628 B

+6
View File
@@ -36,6 +36,12 @@ async function fetchLatestReleases() {
return (await response.json()) as GitHubRelease[];
}
export async function isReleaseVersion(version: string): Promise<boolean | null> {
const releases = await fetchLatestReleases();
const release = releases.find(release => release.tag_name === `v${version}`);
return release ? !release.prerelease : null;
}
function truncateText(text: string, length: number) {
let truncated = text.substring(0, length);
const lastNewLine = truncated.lastIndexOf('\n');
+2
View File
@@ -12,6 +12,7 @@ import * as openNoteInNewWindow from './openNoteInNewWindow';
import * as openPrimaryAppInstance from './openPrimaryAppInstance';
import * as openProfileDirectory from './openProfileDirectory';
import * as openSecondaryAppInstance from './openSecondaryAppInstance';
import * as openSyncWizard from './openSyncWizard';
import * as replaceMisspelling from './replaceMisspelling';
import * as restoreNoteRevision from './restoreNoteRevision';
import * as showProfileEditor from './showProfileEditor';
@@ -39,6 +40,7 @@ const index: any[] = [
openPrimaryAppInstance,
openProfileDirectory,
openSecondaryAppInstance,
openSyncWizard,
replaceMisspelling,
restoreNoteRevision,
showProfileEditor,
@@ -2,8 +2,11 @@ import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/
import { _ } from '@joplin/lib/locale';
import { stateUtils } from '@joplin/lib/reducer';
import Note from '@joplin/lib/models/Note';
import Folder from '@joplin/lib/models/Folder';
import { createAppDefaultWindowState } from '../app.reducer';
import Setting from '@joplin/lib/models/Setting';
import { getDisplayParentId } from '@joplin/lib/services/trash';
import { ALL_NOTES_FILTER_ID } from '@joplin/lib/reserved-ids';
export const declaration: CommandDeclaration = {
name: 'openNoteInNewWindow',
@@ -18,14 +21,18 @@ export const runtime = (): CommandRuntime => {
execute: async (context: CommandContext, noteId: string = null) => {
noteId = noteId || stateUtils.selectedNoteId(context.state);
const note = await Note.load(noteId, { fields: ['parent_id'] });
const note = await Note.load(noteId, { fields: Note.previewFields() });
const parentFolder = await Folder.load(note.parent_id);
let folderId = note.is_conflict && !note.deleted_time ? Folder.conflictFolderId() : getDisplayParentId(note, parentFolder);
if (!note.is_conflict && !note.deleted_time && !parentFolder) folderId = ALL_NOTES_FILTER_ID;
context.dispatch({
type: 'WINDOW_OPEN',
noteId,
folderId: note.parent_id,
folderId,
windowId: `window-${noteId}-${idCounter++}`,
defaultAppWindowState: {
...createAppDefaultWindowState(),
notes: [note],
noteVisiblePanes: Setting.value('noteVisiblePanes'),
editorCodeView: Setting.value('editor.codeView'),
},
@@ -0,0 +1,20 @@
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
import { _ } from '@joplin/lib/locale';
export const declaration: CommandDeclaration = {
name: 'openSyncWizard',
label: () => _('Open sync wizard'),
};
export const runtime = (): CommandRuntime => {
return {
execute: async (context: CommandContext) => {
context.dispatch({
type: 'DIALOG_OPEN',
name: 'syncWizard',
});
},
// Prevents the user from attempting to open multiple copies of the sync wizard, which would throw
enabledCondition: '!modalDialogVisible',
};
};
@@ -3,6 +3,7 @@ import { _ } from '@joplin/lib/locale';
import { stateUtils } from '@joplin/lib/reducer';
import ExternalEditWatcher from '@joplin/lib/services/ExternalEditWatcher';
import Note from '@joplin/lib/models/Note';
import isNoteLockEnabled from '@joplin/lib/services/noteLock/isNoteLockEnabled';
import bridge from '../services/bridge';
export const declaration: CommandDeclaration = {
@@ -18,11 +19,14 @@ export const runtime = (): CommandRuntime => {
try {
const note = await Note.load(noteId);
// The enabled condition can be bypassed when the command is executed directly (e.g. via
// toggleExternalEditing): the external editor would get ciphertext, or write plaintext to disk.
if (isNoteLockEnabled() && note.is_locked) throw new Error(_('Locked notes cannot be opened in an external editor'));
void ExternalEditWatcher.instance().openAndWatch(note);
} catch (error) {
bridge().showErrorMessageBox(_('Error opening note in editor: %s', error.message));
}
},
enabledCondition: 'oneNoteSelected && !noteIsReadOnly',
enabledCondition: 'oneNoteSelected && !noteIsReadOnly && !noteIsLocked',
};
};
@@ -23,7 +23,7 @@ export const runtime = (): CommandRuntime => {
void CommandService.instance().execute('startExternalEditing', noteId);
}
},
enabledCondition: 'oneNoteSelected && !noteIsReadOnly && (!modalDialogVisible || gotoAnythingVisible)',
enabledCondition: 'oneNoteSelected && !noteIsReadOnly && !noteIsLocked && (!modalDialogVisible || gotoAnythingVisible)',
mapStateToTitle: (state: AppState) => {
const noteId = stateUtils.selectedNoteId(state);
return state.watchedNoteFiles.includes(noteId) ? _('Stop') : '';
@@ -0,0 +1,25 @@
import * as React from 'react';
import { _ } from '@joplin/lib/locale';
import bridge from '../services/bridge';
const joplinCloudCreditsUrl = 'https://joplincloud.com/users/me';
interface Props {
// Container class — each caller owns its own SCSS scope
// (chat-panel > .degraded-status vs. .ai-status.-degraded).
className: string;
}
const AiDegradedNotice: React.FC<Props> = ({ className }) => (
<div className={className} role='status'>
{_('AI is running in reduced-quality mode — monthly allowance exceeded. Credits replenish on a rolling 30-day basis, or you can')}
{' '}
<a
href='#'
onClick={(e) => { e.preventDefault(); void bridge().openExternal(joplinCloudCreditsUrl); }}
>{_('buy more credits')}</a>
{'.'}
</div>
);
export default AiDegradedNotice;
@@ -0,0 +1,141 @@
@mixin chat-history-input {
box-sizing: border-box;
font: inherit;
color: var(--joplin-color);
background-color: var(--joplin-background-color);
border: 1px solid var(--joplin-divider-color);
border-radius: 4px;
}
.chat-history {
position: absolute;
top: 100%;
left: 0;
right: 30px;
z-index: 10;
display: flex;
flex-direction: column;
max-width: 400px;
margin-left: auto;
padding: 6px 10px 10px;
font-weight: normal;
background-color: var(--joplin-background-color);
border-bottom: 1px solid var(--joplin-divider-color);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
> .search {
flex-shrink: 0;
}
}
.conversation-search {
position: relative;
display: flex;
align-items: center;
> .icon {
position: absolute;
left: 8px;
color: var(--joplin-color-faded);
pointer-events: none;
}
> .field {
@include chat-history-input;
width: 100%;
padding: 6px 6px 6px 26px;
}
}
.conversation-list {
list-style: none;
margin: 6px 0 0;
padding: 0;
max-height: 200px;
overflow-y: auto;
}
.conversation-row {
display: flex;
align-items: center;
line-height: 16px;
cursor: pointer;
&.-current {
font-weight: bold;
background-color: var(--joplin-selected-color);
}
&:hover:not(.-current) {
background-color: var(--joplin-background-color-hover3);
}
> .open {
flex: 1;
min-width: 0;
}
> .title-input {
@include chat-history-input;
flex: 1;
min-width: 0;
margin: 6px 20px;
padding: 3px 4px;
}
> .actions {
flex-shrink: 0;
margin-right: 20px;
}
&:hover,
&:focus-within {
--chat-history-timestamp-display: none;
}
&:hover > .actions,
&:focus-within > .actions {
display: flex;
}
}
.conversation-link {
display: flex;
padding: 10px 20px;
gap: 18px;
text-align: left;
font: inherit;
color: inherit;
background: none;
border: none;
cursor: pointer;
> .title {
flex: 1;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
> .timestamp {
display: var(--chat-history-timestamp-display, initial);
flex-shrink: 0;
text-align: right;
color: var(--joplin-color-faded);
}
}
.conversation-actions {
display: none;
gap: 10px;
align-items: center;
> button {
display: flex;
color: var(--joplin-color3);
cursor: pointer;
font-size: 16px;
line-height: 1;
}
}
@@ -0,0 +1,112 @@
import * as React from 'react';
import { useState } from 'react';
import { _ } from '@joplin/lib/locale';
import { formatMsToRelativeTime } from '@joplin/utils/time';
export interface Conversation {
id: string;
title: string;
updated_time: number;
}
interface Props {
conversations: Conversation[];
currentConversationId?: string|null;
id: string;
popupRef: React.Ref<HTMLDivElement>;
onSearchChange: (search: string)=> void;
onOpen: (conversationId: string)=> void;
onRename: (conversation: Conversation, title: string)=> void;
onDelete: (conversationId: string)=> void;
onClose: ()=> void;
}
const ChatHistory: React.FC<Props> = props => {
const [search, setSearch] = useState('');
const [editingId, setEditingId] = useState<string|null>(null);
const [draftTitle, setDraftTitle] = useState('');
const startEditing = (conversation: Conversation) => {
setEditingId(conversation.id);
setDraftTitle(conversation.title);
};
const stopEditing = () => setEditingId(null);
const onTitleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>, conversation: Conversation) => {
if (event.nativeEvent.isComposing) return;
if (event.key === 'Escape') {
event.stopPropagation();
stopEditing();
return;
}
if (event.key !== 'Enter') return;
event.preventDefault();
props.onRename(conversation, draftTitle);
stopEditing();
};
const onPopupKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.nativeEvent.isComposing || event.key !== 'Escape') return;
event.stopPropagation();
props.onClose();
};
return <div className='chat-history' id={props.id} ref={props.popupRef} onKeyDown={onPopupKeyDown}>
<div className='conversation-search search'>
<i className='icon icon-search' aria-hidden='true'/>
<input
className='field'
type='search'
autoFocus
aria-label={_('Search conversations')}
placeholder={_('Search conversations')}
value={search}
onChange={event => {
setSearch(event.target.value);
props.onSearchChange(event.target.value);
}}
/>
</div>
<ul className='conversation-list' aria-label={_('Conversations')}>
{props.conversations.map(conversation => {
const current = conversation.id === props.currentConversationId;
const title = conversation.title || _('(untitled)');
const editing = conversation.id === editingId;
return <li key={conversation.id} className={`conversation-row${current ? ' -current' : ''}`} aria-current={current ? 'true' : undefined}
onClick={event => {
if (event.target === event.currentTarget) props.onOpen(conversation.id);
}}
>
{editing ? (
<input
className='title-input'
type='text'
autoFocus
aria-label={_('Conversation title')}
value={draftTitle}
onChange={event => setDraftTitle(event.target.value)}
onKeyDown={event => onTitleKeyDown(event, conversation)}
onBlur={stopEditing}
/>
) : (
<button className='conversation-link open' type='button' onClick={() => props.onOpen(conversation.id)}>
<span className='title'>{title}</span>
<span className='timestamp'>{formatMsToRelativeTime(conversation.updated_time)}</span>
</button>
)}
<div className='conversation-actions actions'>
<button className='flat-button' type='button' title={_('Rename')} aria-label={_('Rename')} onClick={() => startEditing(conversation)}>
<i className='fa fa-pen' aria-hidden='true'/>
</button>
<button className='flat-button' type='button' title={_('Delete')} aria-label={_('Delete')} onClick={() => props.onDelete(conversation.id)}>
<i className='fas fa-trash' aria-hidden='true'/>
</button>
</div>
</li>;
})}
</ul>
</div>;
};
export default ChatHistory;
@@ -0,0 +1,84 @@
import * as React from 'react';
import { _ } from '@joplin/lib/locale';
import { AiChatMessage } from '../../app.reducer';
import InlineMarkdownDisplay from '../InlineMarkdownDisplay';
import { ChatMessage, ChatRole, ChatToolMessage } from '@joplin/lib/services/ai/types';
const toolResultSummary = (actions: ChatMessage[]) => {
const toolResults = actions.filter(action => action.role === ChatRole.Tool);
const editSummary = () => {
let applied = 0;
let missed = 0;
let lastEdit: ChatToolMessage|null = null;
for (const result of toolResults) {
if (!result.isEdit) continue;
if (result.isError) {
missed ++;
} else {
applied ++;
lastEdit = result;
}
}
if (applied + missed === 0) return '';
if (missed === 0 && applied > 1) return _('%d edit(s) applied.', applied);
if (missed === 0 && lastEdit) return lastEdit.userDescription;
return _('%d edit(s) applied, %d could not be placed automatically.', applied, missed);
};
return [
...toolResults
.filter(result => !result.isEdit)
.map(result => result.userDescription),
editSummary(),
]
.filter(item => !!item)
.join('\n');
};
const hasToolError = (actions: ChatMessage[]) => {
return actions.some(action => action.role === ChatRole.Tool && action.isError);
};
interface Props {
message: AiChatMessage;
}
const ChatMessageItem: React.FC<Props> = ({ message }) => {
if (message.role === 'separator') {
return <div className='separator'>{message.text}</div>;
}
if (message.role === 'error') {
return <div className='error'>{message.text}</div>;
}
const summary = message.role === 'assistant' ? toolResultSummary(message.raw) : '';
// Always show something in the message box:
const textContent = !message.text && !summary ? _('(no message)') : message.text;
const renderMarkdown = message.role === 'assistant';
const content = renderMarkdown
? <InlineMarkdownDisplay
className='content'
markdown={textContent}
allowLinks={false} />
: <div className='content'>{textContent}</div>;
return (
<div className={`chat-message turn -${message.role}`}>
{content}
{summary && (
<div className='meta'>
{hasToolError(message.raw)
? <span className='warning'>{summary}</span>
: <span>{summary}</span>}
</div>
)}
</div>
);
};
export default ChatMessageItem;
@@ -0,0 +1,200 @@
import * as React from 'react';
import { act, fireEvent, render, waitFor, within } from '@testing-library/react';
import BaseModel from '@joplin/lib/BaseModel';
import ChatConversation from '@joplin/lib/models/ChatConversation';
import Note from '@joplin/lib/models/Note';
import CommandService from '@joplin/lib/services/CommandService';
import { runNoteChat } from '@joplin/lib/services/ai/noteChat';
import { ChatRole } from '@joplin/lib/services/ai/types';
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
import appReducer, { AiChatMessage, createAppDefaultState, createAppDefaultWindowState } from '../../app.reducer';
import ChatPanel from './ChatPanel';
import { WindowIdContext } from '../NewWindowOrIFrame';
import uuid from '@joplin/lib/uuid';
import '../../utils/window/eventHandlerOverrides';
jest.mock('@joplin/lib/services/ai/noteChat', () => ({ runNoteChat: jest.fn() }));
jest.mock('./ChatMessageItem', () => ({ message }: { message: { text: string } }) => <div>{message.text}</div>);
const Panel = ChatPanel.WrappedComponent;
const message: AiChatMessage = { id: 'question', createdTime: 1, role: 'user', text: 'Question', raw: [], noteId: 'note-a', noteTitle: 'A' };
const renderPanel = (props: Partial<React.ComponentProps<typeof Panel>> = {}) => render(
<WindowIdContext.Provider value='second'><Panel
themeId={1} available={true} unavailableHint='' providerType='joplin-cloud'
noteId={null} noteTitle='' noteIsEncrypted={false} messages={[]} aiDegraded={false} dispatch={jest.fn()}
showToolbarButton={false}
{...props}
/></WindowIdContext.Provider>,
);
const openHistory = async (view: ReturnType<typeof render>) => {
await act(async () => { fireEvent.click(view.getByTitle('Chat history')); });
};
const saveConversation = async (id: string, title: string, updatedTime = 1) => {
await ChatConversation.save({ id, title, created_time: 1, updated_time: updatedTime }, { isNew: true, autoTimestamp: false });
};
const saveMessage = async (conversationId: string, text: string) => {
await ChatConversation.addMessage(conversationId, { id: uuid.create(), createdTime: 1, role: 'user', text, raw: [], noteId: '', noteTitle: '' });
};
describe('ChatPanel', () => {
let modelDispatch: jest.Mock;
beforeEach(async () => {
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
Element.prototype.scrollIntoView = jest.fn();
jest.mocked(runNoteChat).mockReset();
modelDispatch = jest.fn();
BaseModel.dispatch = modelDispatch;
});
afterEach(() => {
jest.restoreAllMocks();
BaseModel.dispatch = () => {};
});
it('should search conversation titles and messages', async () => {
await saveConversation('shopping', 'Shopping list');
await saveConversation('travel', 'Travel plans');
await saveMessage('travel', 'Visit Paris');
const view = renderPanel();
await openHistory(view);
await view.findByText('Shopping list');
const search = view.getByRole('searchbox', { name: 'Search conversations' });
fireEvent.change(search, { target: { value: 'Shopping' } });
await waitFor(() => expect(view.getAllByRole('listitem')).toHaveLength(1));
expect(view.getByText('Shopping list')).toBeTruthy();
fireEvent.change(search, { target: { value: 'Paris' } });
expect(await view.findByText('Travel plans')).toBeTruthy();
expect(view.getAllByRole('listitem')).toHaveLength(1);
fireEvent.change(search, { target: { value: '' } });
expect(await view.findByText('Shopping list')).toBeTruthy();
expect(view.getAllByRole('listitem')).toHaveLength(2);
});
it('should rename a conversation inline when Enter is pressed', async () => {
await saveConversation('chat-1', 'Old title');
const view = renderPanel();
await openHistory(view);
const row = (await view.findByText('Old title')).closest('li');
fireEvent.click(within(row).getByRole('button', { name: 'Rename' }));
const input = within(row).getByRole('textbox', { name: 'Conversation title' });
fireEvent.change(input, { target: { value: 'New title' } });
await act(async () => { fireEvent.keyDown(input, { key: 'Enter' }); });
expect(await view.findByText('New title')).toBeTruthy();
expect(view.queryByRole('textbox', { name: 'Conversation title' })).toBeNull();
expect(await ChatConversation.load('chat-1')).toMatchObject({ title: 'New title' });
});
it('should close the history popup when clicking outside of it', async () => {
await saveConversation('chat-1', 'Saved chat');
const view = renderPanel();
await openHistory(view);
await view.findByText('Saved chat');
fireEvent.mouseDown(view.getByRole('searchbox', { name: 'Search conversations' }));
expect(view.getByText('Saved chat')).toBeTruthy();
fireEvent.mouseDown(view.getByRole('textbox', { name: 'Chat message' }));
await waitFor(() => expect(view.queryByText('Saved chat')).toBeNull());
});
it('should delete the selected conversation and its messages', async () => {
await saveConversation('delete-chat', 'Delete this chat');
await saveConversation('keep-chat', 'Keep this chat');
await saveMessage('delete-chat', 'Delete this message');
await saveMessage('keep-chat', 'Keep this message');
const dispatch = jest.fn();
const view = renderPanel({ conversationId: 'delete-chat', dispatch });
await openHistory(view);
const row = (await view.findByText('Delete this chat')).closest('li');
await act(async () => { fireEvent.click(within(row).getByRole('button', { name: 'Delete' })); });
await waitFor(() => expect(view.queryByText('Delete this chat')).toBeNull());
expect(view.getByText('Keep this chat')).toBeTruthy();
expect((await ChatConversation.history()).map(conversation => conversation.id)).toEqual(['keep-chat']);
expect(await ChatConversation.messages('keep-chat')).toMatchObject([{ text: 'Keep this message' }]);
await saveConversation('delete-chat', 'Recreated chat');
expect(await ChatConversation.messages('delete-chat')).toEqual([]);
expect(dispatch).toHaveBeenCalledWith({ type: 'AI_CHAT_DELETE', conversationId: 'delete-chat' });
});
it('should keep history when New chat opens an empty conversation in the current window', async () => {
await saveConversation('chat-1', 'Current chat');
const dispatch = jest.fn();
const view = renderPanel({ conversationId: 'chat-1', messages: [message], dispatch });
await act(async () => { fireEvent.click(view.getByRole('button', { name: 'New chat' })); });
await waitFor(() => expect(dispatch).toHaveBeenCalled());
expect(await ChatConversation.load('chat-1')).toMatchObject({ title: 'Current chat' });
const action = dispatch.mock.calls[0][0];
expect(action).toEqual({ type: 'AI_CHAT_OPEN', windowId: 'second', conversationId: expect.any(String), messages: [] });
expect(action.conversationId).not.toBe('chat-1');
const state = createAppDefaultState({});
state.aiChatConversationId = 'main-chat';
state.aiChatMessages = [message];
state.backgroundWindows = { second: { ...createAppDefaultWindowState(), windowId: 'second' } };
const opened = appReducer(state, action);
expect(opened.aiChatConversationId).toBe('main-chat');
expect(opened.aiChatMessages).toEqual([message]);
expect(opened.backgroundWindows.second.aiChatConversationId).toBe(action.conversationId);
expect(opened.backgroundWindows.second.aiChatMessages).toEqual([]);
});
it('should not add an empty conversation to history', async () => {
await saveConversation('chat-1', 'Current chat');
const before = await ChatConversation.history();
const dispatch = jest.fn();
const view = renderPanel({ conversationId: 'chat-1', messages: [message], dispatch });
await act(async () => { fireEvent.click(view.getByRole('button', { name: 'New chat' })); });
await waitFor(() => expect(dispatch).toHaveBeenCalled());
const after = await ChatConversation.history();
expect(after).toHaveLength(before.length);
});
it('should open the selected history row in the current window', async () => {
await saveConversation('chat-1', 'Current chat');
await saveConversation('chat-2', 'Saved chat');
const execute = jest.spyOn(CommandService.instance(), 'executeInWindow').mockResolvedValue(undefined);
const view = renderPanel({ conversationId: 'chat-1', messages: [message] });
await openHistory(view);
fireEvent.click(await view.findByRole('button', { name: /Saved chat/ }));
await waitFor(() => expect(execute).toHaveBeenCalledWith('openAiChatConversation', { windowId: 'second', args: ['chat-2'] }));
});
it('should save the question and reply with the active note ID and title as they are created', async () => {
jest.spyOn(Note, 'load').mockResolvedValue({ id: 'note-a', title: 'A', body: '' });
jest.mocked(runNoteChat).mockImplementation(async (_context, _history, _text, _tools, onHistoryChanged) => {
onHistoryChanged([{ role: ChatRole.System, content: '' }, { role: ChatRole.Assistant, content: 'Reply' }]);
return [];
});
const view = renderPanel({ conversationId: 'chat-1', noteId: 'note-a', noteTitle: 'A' });
fireEvent.change(view.getByRole('textbox'), { target: { value: 'Question' } });
await act(async () => { fireEvent.click(view.getByRole('button', { name: 'Send' })); });
const expected = [
{ role: 'user', text: 'Question', noteId: 'note-a', noteTitle: 'A' },
{ role: 'assistant', text: 'Reply', noteId: 'note-a', noteTitle: 'A' },
];
await waitFor(async () => expect(await ChatConversation.messages('chat-1')).toMatchObject(expected));
expect(await ChatConversation.load('chat-1')).toMatchObject({ title: 'Question' });
const appended = modelDispatch.mock.calls.map(([action]) => action).filter(action => action.type === 'AI_CHAT_APPEND');
expect(appended).toMatchObject(expected.map(message => ({ conversationId: 'chat-1', message })));
});
it('should remove the saved question when the request fails before any reply', async () => {
jest.spyOn(Note, 'load').mockResolvedValue({ id: 'note-a', title: 'A', body: '' });
jest.mocked(runNoteChat).mockRejectedValue(new Error('Network down'));
const view = renderPanel({ conversationId: 'chat-1', noteId: 'note-a', noteTitle: 'A', dispatch: jest.fn() });
fireEvent.change(view.getByRole('textbox'), { target: { value: 'Question' } });
await act(async () => { fireEvent.click(view.getByRole('button', { name: 'Send' })); });
await waitFor(async () => expect(await ChatConversation.messages('chat-1')).toMatchObject([{ role: 'error', text: 'Network down' }]));
});
it('should assign an ID before messages are added to a new conversation', async () => {
const dispatch = jest.fn();
renderPanel({ dispatch });
await waitFor(() => expect(dispatch).toHaveBeenCalledWith({
type: 'AI_CHAT_OPEN', windowId: 'second', conversationId: expect.any(String), messages: [],
}));
});
});
@@ -0,0 +1,617 @@
import * as React from 'react';
import { useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from 'react';
import { connect } from 'react-redux';
import { Dispatch } from 'redux';
import { _ } from '@joplin/lib/locale';
import Setting from '@joplin/lib/models/Setting';
import Note from '@joplin/lib/models/Note';
import CommandService from '@joplin/lib/services/CommandService';
import Logger from '@joplin/utils/Logger';
import { stateUtils } from '@joplin/lib/reducer';
import { AiChatMessage, AppState } from '../../app.reducer';
import { runNoteChat } from '@joplin/lib/services/ai/noteChat';
import { AvailabilityReason, chatAvailability } from '@joplin/lib/services/ai/availability';
import { WindowIdContext } from '../NewWindowOrIFrame';
import AiDegradedNotice from '../AiDegradedNotice';
import { ChatMessage, ChatRole, ChatToolMessage } from '@joplin/lib/services/ai/types';
import JoplinError from '@joplin/lib/JoplinError';
import eventManager, { EventName, ItemChangeEvent } from '@joplin/lib/eventManager';
import { Second } from '@joplin/utils/time';
import ChatMessageItem from './ChatMessageItem';
import NavService from '@joplin/lib/services/NavService';
import ChatConversation from '@joplin/lib/models/ChatConversation';
import uuid from '@joplin/lib/uuid';
import { focus } from '@joplin/lib/utils/focusHandler';
import dialogs from '../dialogs';
import ChatHistory, { Conversation } from './ChatHistory';
import Button, { ButtonLevel } from '../Button/Button';
const logger = Logger.create('ChatPanel');
interface Props {
themeId: number;
available: boolean;
availabilityReason?: AvailabilityReason;
unavailableHint: string;
providerType: string;
noteId: string | null;
noteTitle: string;
noteIsEncrypted: boolean;
messages: AiChatMessage[];
conversationId?: string|null;
aiDegraded: boolean;
showToolbarButton: boolean;
dispatch: Dispatch;
}
const disclosureSetting = 'ai.chat.disclosureAcknowledged';
const makeId = () => uuid.create();
const waitForNextNoteChangeOrTimeout = (noteId: string, timeout: number) => {
return new Promise<void>((resolve) => {
const listener = (event: ItemChangeEvent) => {
if (event.itemId === noteId) {
onComplete();
}
};
const onComplete = () => {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
eventManager.off(EventName.ItemChange, listener);
resolve();
};
eventManager.on(EventName.ItemChange, listener);
let timeoutId = setTimeout(() => {
onComplete();
}, timeout);
});
};
const useCancelCallback = () => {
const abortControllerRef = useRef(new AbortController());
const cancelRequest = useCallback(() => {
abortControllerRef.current.abort();
abortControllerRef.current = new AbortController();
}, []);
// Cancel on Reset / unmount so an in-flight reply can detect it should
// abort instead of landing in a cleared or destroyed conversation.
const cancelRequestRef = useRef(cancelRequest);
cancelRequestRef.current = cancelRequest;
useEffect(() => () => {
cancelRequestRef.current();
}, []);
return { abortControllerRef, cancelRequest };
};
const useHasFocus = () => {
const [hasFocus, setHasFocus] = useState(false);
const onFocus = useCallback(() => setHasFocus(true), []);
const onBlur: React.FocusEventHandler<HTMLDivElement> = useCallback((event) => {
const chatPanelContainer = event.currentTarget;
const newElementWithFocus = event.relatedTarget;
const movingFocusOut = !chatPanelContainer.contains(newElementWithFocus);
// Avoid quickly toggling hasFocus: onBlur/onFocus are emitted when moving focus between
// elements within the container.
if (movingFocusOut) {
setHasFocus(false);
}
}, []);
return { hasFocus, onFocus, onBlur };
};
// Single-window for v1: mapStateToProps hard-codes defaultWindowId and the
// toggle writes to the app-wide layout. A second window would mirror the main.
const ChatPanel: React.FC<Props> = (props) => {
const { dispatch, messages } = props;
const [input, setInput] = useState('');
const [historyOpen, setHistoryOpen] = useState(false);
const historyId = useId();
const [conversations, setConversations] = useState<Conversation[]>([]);
const loadConversationHistory = useCallback(async (search = '') => {
try {
setConversations(await ChatConversation.history(search));
} catch (error) {
logger.error('Could not load chat conversations', error);
}
}, []);
const handleHistoryToggle = useCallback(async () => {
setHistoryOpen(!historyOpen);
if (historyOpen) return;
await loadConversationHistory();
}, [historyOpen, loadConversationHistory]);
const historyPopupRef = useRef<HTMLDivElement>(null);
const historyButtonRef = useRef<HTMLButtonElement>(null);
const closeHistory = useCallback(() => {
setHistoryOpen(false);
focus('ChatPanel::closeHistory', historyButtonRef.current);
}, []);
useEffect(() => {
if (!historyOpen) return () => {};
const onMouseDown = (event: MouseEvent) => {
const target = event.target as Node;
if (historyPopupRef.current?.contains(target) || historyButtonRef.current?.contains(target)) return;
setHistoryOpen(false);
};
const doc = historyButtonRef.current.ownerDocument;
doc.addEventListener('mousedown', onMouseDown);
return () => doc.removeEventListener('mousedown', onMouseDown);
}, [historyOpen]);
const [sending, setSending] = useState(false);
const archivingRef = useRef(false);
const [disclosureShown, setDisclosureShown] = useState<boolean>(() => {
try {
return !!Setting.value(disclosureSetting);
} catch {
return false;
}
});
const lastNoteIdRef = useRef<string | null>(props.noteId);
const messagesLengthRef = useRef(messages.length);
messagesLengthRef.current = messages.length;
// Lets async work detect note switches without re-running its closure.
const noteIdRef = useRef(props.noteId);
noteIdRef.current = props.noteId;
const messagesEndRef = useRef<HTMLDivElement>(null);
const { hasFocus, onFocus, onBlur } = useHasFocus();
const { abortControllerRef, cancelRequest } = useCancelCallback();
const windowId = useContext(WindowIdContext);
const appendMessage = useCallback(async (message: Omit<AiChatMessage, 'noteId' | 'noteTitle' | 'createdTime'>) => {
const fullMessage: AiChatMessage = { ...message, createdTime: Date.now(), noteId: props.noteId ?? '', noteTitle: props.noteId ? props.noteTitle : '' };
try {
await ChatConversation.addMessage(props.conversationId, fullMessage);
} catch (error) {
logger.error('Could not save chat message:', error);
}
}, [props.noteId, props.noteTitle, props.conversationId]);
const removeMessage = useCallback(async (id: string) => {
try {
await ChatConversation.removeMessage(props.conversationId, id);
} catch (error) {
logger.error('Could not remove chat message:', error);
}
}, [props.conversationId]);
const addToolResult = useCallback(async (result: ChatToolMessage) => {
try {
await ChatConversation.addToolResult(props.conversationId, result);
} catch (error) {
logger.error('Could not save chat tool result:', error);
}
}, [props.conversationId]);
useEffect(() => {
if (props.conversationId) return;
dispatch({ type: 'AI_CHAT_OPEN', windowId, conversationId: uuid.create(), messages });
}, [props.conversationId, dispatch, windowId, messages]);
// Drop a separator when the active note changes mid-conversation. Skip
// the first ever opened note (no prior context to separate from).
useEffect(() => {
const prev = lastNoteIdRef.current;
lastNoteIdRef.current = props.noteId;
if (prev === null || prev === props.noteId || !props.noteId) return;
if (messagesLengthRef.current === 0) return;
const text = _('— now viewing: %s —', props.noteTitle || _('(untitled)'));
void appendMessage({
id: makeId(),
role: 'separator',
text,
raw: [{ role: ChatRole.User, content: _('Switched notes: %s', text) }],
});
}, [props.noteId, props.noteTitle, appendMessage]);
useEffect(() => {
if (messages.length === 0) return;
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
const conversationTurns = useMemo<ChatMessage[]>(() => {
return messages
.map(m => m.raw)
.flat();
}, [messages]);
// Joplin Cloud is remote but the user already consented via sync setup.
const requiresDisclosure = props.providerType !== 'joplin-cloud';
const showDisclosure = requiresDisclosure && !disclosureShown && messages.length === 0;
const handleCancel = useCallback(() => {
cancelRequest();
void appendMessage({ id: makeId(), role: 'assistant', text: _('(Cancelled)'), raw: [] });
}, [cancelRequest, appendMessage]);
const handleSend = useCallback(async () => {
const text = input.trim();
if (!text || sending) return;
if (!props.noteId) {
void appendMessage({
id: makeId(), role: 'error', text: _('Open a note to start chatting.'), raw: [],
});
return;
}
const abortController = abortControllerRef.current;
const noteIdAtStart = props.noteId;
setSending(true);
setInput('');
// Captured so we can roll it back on failure — otherwise a retry would
// send the prior user turn as history alongside the new prompt.
const userTurnId = makeId();
let hadVisibleResponse = false;
void appendMessage({
id: userTurnId,
role: 'user',
text,
raw: [{ role: ChatRole.User, content: text }],
});
try {
const note = await Note.load(props.noteId);
if (abortController.signal.aborted) return;
if (!note) throw new Error(`Note not found: ${props.noteId}`);
const getContext = async () => {
let selection = '';
try {
selection = await CommandService.instance().executeInWindow(
'selectedText', { windowId, args: [] },
) as string || '';
} catch {
// Editor may not be ready; treat as no selection.
}
const note = await Note.load(props.noteId);
if (!note) throw new Error(`Note not found: ${props.noteId}`);
return {
body: note.body,
title: note.title,
selection,
noteId: note.id,
folderId: note.parent_id,
};
};
let lastHistory = [
{ role: ChatRole.System, content: 'placeholder' },
...conversationTurns,
];
const onHistoryChanged = (history: ChatMessage[]) => {
if (abortController.signal.aborted) return;
for (let i = lastHistory.length; i < history.length; i++) {
const entry = history[i];
// User messages are added elsewhere, when the user submits the chat
if (entry.role === ChatRole.User) continue;
// System messages are not shown in the UI
if (entry.role === ChatRole.System) continue;
if (entry.role === ChatRole.Tool) {
void addToolResult(entry);
} else {
hadVisibleResponse ||= !entry.hide;
void appendMessage({
id: makeId(),
role: entry.role,
text: entry.content,
hide: entry.hide,
raw: [entry],
});
}
}
lastHistory = history;
};
const assertSameNote = () => {
if (noteIdAtStart !== noteIdRef.current) {
cancelRequest();
throw new JoplinError(
_('You switched notes while the request was running; edits were not applied. Try again.'), 'aiNoteChanged',
);
}
};
await runNoteChat(
getContext, conversationTurns, text, {
replaceSelection: async (text, oldText) => {
if (text === oldText) return;
assertSameNote();
const changeListener = waitForNextNoteChangeOrTimeout(props.noteId, Second);
await CommandService.instance().executeInWindow('replaceSelection', {
windowId,
args: [text],
});
await changeListener;
},
updateNoteBody: async (newBody, oldBody) => {
if (newBody === oldBody) return;
assertSameNote();
const changeListener = waitForNextNoteChangeOrTimeout(props.noteId, Second);
await CommandService.instance().executeInWindow('editor.setText', {
windowId,
args: [newBody],
});
await changeListener;
},
displayError: (message) => {
void appendMessage({ id: makeId(), role: 'error', text: message, raw: [] });
},
}, onHistoryChanged, abortController.signal,
);
} catch (error) {
logger.warn('Chat failed:', error);
if (abortController.signal.aborted) return;
if (!hadVisibleResponse) {
void removeMessage(userTurnId);
setInput(text);
}
void appendMessage({ id: makeId(), role: 'error', text: error.message || _('Something went wrong.'), raw: [] });
} finally {
setSending(false);
}
}, [input, sending, props.noteId, conversationTurns, windowId, addToolResult, appendMessage, removeMessage, cancelRequest, abortControllerRef]);
const handleAcknowledgeDisclosure = useCallback(() => {
Setting.setValue(disclosureSetting, true);
setDisclosureShown(true);
}, []);
const handleNewChat = useCallback(() => {
if (archivingRef.current) return;
cancelRequest();
// Only saved once a message is sent so empty chats don't show up in history
dispatch({ type: 'AI_CHAT_OPEN', windowId, conversationId: uuid.create(), messages: [] });
}, [dispatch, windowId, cancelRequest]);
const handleOpenConversation = useCallback(async (conversationId: string) => {
if (conversationId === props.conversationId || archivingRef.current) return;
archivingRef.current = true;
cancelRequest();
try {
await CommandService.instance().executeInWindow('openAiChatConversation', { windowId, args: [conversationId] });
} catch (error) {
logger.error('Could not open chat conversation:', error);
} finally {
archivingRef.current = false;
}
}, [windowId, cancelRequest, props.conversationId]);
const handleClose = useCallback(() => {
void CommandService.instance().executeInWindow('toggleAiChat', { windowId: windowId, args: [] });
}, [windowId]);
const handleDeleteConversation = useCallback(async (conversationId: string) => {
if (archivingRef.current) return;
archivingRef.current = true;
if (conversationId === props.conversationId) cancelRequest();
try {
await ChatConversation.deleteConversation(conversationId);
setConversations(current => current.filter(item => item.id !== conversationId));
dispatch({ type: 'AI_CHAT_DELETE', conversationId });
} catch (error) {
logger.error('Could not delete chat conversation:', error);
await dialogs.alert(_('Could not delete conversation.'));
} finally {
archivingRef.current = false;
}
}, [dispatch, cancelRequest, props.conversationId]);
const handleRenameConversation = useCallback(async (conversation: Conversation, newTitle: string) => {
const title = newTitle.trim();
if (!title || title === conversation.title) return;
try {
await ChatConversation.renameConversation(conversation.id, title);
setConversations(current => current.map(item => item.id === conversation.id ? { ...item, title } : item));
} catch (error) {
logger.error('Could not rename chat conversation:', error);
await dialogs.alert(_('Could not rename conversation.'));
}
}, []);
const handleHideToolbarButton = useCallback(() => {
Setting.setValue('ai.chat.showToolbarButton', false);
handleClose();
}, [handleClose]);
const handleKeyDown = useCallback((e: React.KeyboardEvent<HTMLTextAreaElement>) => {
// Don't send while an IME composition is in flight — Enter commits
// the composition for CJK / accented input.
if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
e.preventDefault();
if (!sending) void handleSend();
}
}, [handleSend, sending]);
const renderContent = () => {
if (!props.available) {
const content = (
<div className='disabled-message'>
<div className='hint'>{props.unavailableHint}</div>
{props.showToolbarButton && <>
<div className='hint'>{_('If you do not want to use the AI features, you may hide the toolbar button:')}</div>
<Button
level={ButtonLevel.Secondary}
title={_('Hide the AI Chat button')}
onClick={handleHideToolbarButton}
/>
<div className='hint'>{_('(You can show it again from Settings → AI)')}</div>
</>}
</div>
);
return { content, showingMessages: false };
}
if (props.noteIsEncrypted) {
const content = (
<div className='disabled-message'>
{_('This note is encrypted and cannot be used with AI Chat.')}
</div>
);
return { content, showingMessages: false };
}
const visibleMessages = messages.filter(m => !m.hide);
const content = <>
{props.aiDegraded && <AiDegradedNotice className='degraded-status' />}
<div className='messages' aria-live={hasFocus ? 'polite' : undefined}>
{visibleMessages.length === 0 && (
<div className='empty'>
{_('Ask about this note, or request changes. Select text in the editor first to scope the request to that selection.')}
<br/>
<br/>
<button
type='button'
className='link-button'
onClick={() => NavService.go('Config', { props: { defaultSection: 'ai.tools' } })}
>{_('Manage capabilities')}</button>
</div>
)}
{visibleMessages.map(m => <ChatMessageItem key={m.id} message={m}/>)}
<div ref={messagesEndRef} />
</div>
<div className='composer'>
{showDisclosure && (
<div className='disclosure'>
{_('Your note will be sent to the configured AI provider (%s).', props.providerType)}
{' '}
<a href='#' onClick={(e) => { e.preventDefault(); handleAcknowledgeDisclosure(); }}>{_('Don\'t show again')}</a>
</div>
)}
<div className='input-wrapper'>
<textarea
className='input'
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={_('Ask about this note, or request a change…')}
aria-label={_('Chat message')}
/>
<button
type='button'
className='send'
onClick={sending ? handleCancel : handleSend}
disabled={!sending && !input.trim()}
aria-label={sendButtonLabel}
title={sendButtonLabel}
>
<i className={sending ? 'fas fa-stop' : 'fas fa-paper-plane'} aria-hidden='true' />
</button>
</div>
</div>
</>;
return { content, showingMessages: visibleMessages.length > 0 };
};
const sendButtonLabel = sending ? _('Stop generating') : _('Send');
const closeLabel = _('Close');
const newChatLabel = _('New chat');
const headerId = useId();
const { content, showingMessages } = renderContent();
return (
<div
className='chat-panel'
role='region'
aria-labelledby={headerId}
onFocus={onFocus}
onBlur={onBlur}
>
<div className='header chat-panel-header'>
<h1 className='title' id={headerId}>{_('AI Chat')}</h1>
{props.availabilityReason !== AvailabilityReason.Disabled && (
<button
type='button'
className='history toolbar-button'
ref={historyButtonRef}
onClick={handleHistoryToggle}
title={_('Chat history')}
aria-label={_('Chat history')}
aria-expanded={historyOpen}
aria-controls={historyId}
>
<i className='toolbar-icon far fa-clock' aria-hidden='true'/>
</button>
)}
{historyOpen && (
// Placed right after its button so that Tab moves from the button into the popup
<ChatHistory
conversations={conversations}
currentConversationId={props.conversationId}
id={historyId}
popupRef={historyPopupRef}
onSearchChange={loadConversationHistory}
onOpen={handleOpenConversation}
onRename={handleRenameConversation}
onDelete={handleDeleteConversation}
onClose={closeHistory}
/>
)}
<button type='button' className='reset toolbar-button' onClick={handleNewChat} disabled={!showingMessages} title={newChatLabel} aria-label={newChatLabel}>
<i className='toolbar-icon fas fa-comment-medical' aria-hidden='true'/>
</button>
<button
type='button'
className='close toolbar-button'
onClick={handleClose}
title={closeLabel}
aria-label={closeLabel}
><i className='toolbar-icon fas fa-times' role='img' aria-hidden={true}/></button>
</div>
{content}
</div>
);
};
interface OwnProps {
windowId: string;
}
const mapStateToProps = (state: AppState, ownProps: OwnProps) => {
const windowState = stateUtils.windowStateById(state, ownProps.windowId);
const noteId = stateUtils.selectedNoteId(windowState);
const note = noteId ? windowState.notes.find(n => n.id === noteId) : null;
const availability = chatAvailability();
return {
themeId: state.settings.theme,
available: availability.available,
availabilityReason: availability.reason,
unavailableHint: availability.hint ?? '',
providerType: state.settings['ai.chat.providerType'] || 'openai-compatible',
noteId,
noteTitle: note?.title || '',
noteIsEncrypted: !!note?.encryption_applied,
messages: windowState.aiChatMessages || [],
conversationId: windowState.aiChatConversationId,
aiDegraded: !!state.aiStatus?.degraded,
showToolbarButton: !!state.settings['ai.chat.showToolbarButton'],
};
};
const ConversationPanel: React.FC<Props> = props => <ChatPanel key={props.conversationId} {...props} />;
export default connect(mapStateToProps)(ConversationPanel);
@@ -0,0 +1,227 @@
@use './ChatHistory.scss';
.chat-panel {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
overflow-y: auto;
box-sizing: border-box;
background-color: var(--joplin-background-color);
color: var(--joplin-color);
border-left: 1px solid var(--joplin-divider-color);
font-size: var(--joplin-font-size);
}
.chat-panel > .header {
position: relative;
display: flex;
flex-direction: row;
align-items: center;
padding: 6px 10px;
border-bottom: 1px solid var(--joplin-divider-color);
font-weight: 600;
}
.chat-panel-header {
> .title {
flex: 1;
font-size: inherit;
margin: 0;
}
> .reset, > .close, > .history {
background: none;
border: none;
color: var(--joplin-color-faded);
cursor: pointer;
padding: 2px 6px;
&:hover:not(:disabled) {
color: var(--joplin-color);
}
}
}
.chat-panel > .degraded-status {
padding: 6px 10px;
font-size: 0.8em;
color: var(--joplin-color-warn);
background-color: var(--joplin-background-color3);
border-bottom: 1px solid var(--joplin-divider-color);
line-height: 1.3;
> a {
color: var(--joplin-url-color);
text-decoration: underline;
}
}
.chat-panel > .messages {
flex: 1;
overflow-y: auto;
padding: 8px;
display: flex;
flex-direction: column;
gap: 8px;
}
.chat-panel > .messages > .empty {
color: var(--joplin-color-faded);
font-style: italic;
text-align: center;
margin-top: 20px;
padding: 0 20px;
line-height: 1.5;
}
.chat-panel > .messages > .turn {
padding: 8px 10px;
border-radius: 6px;
overflow-wrap: anywhere;
line-height: 1.4;
}
// User turn uses the sidebar colour scheme (dark blue bg, white text).
.chat-panel > .messages > .turn.-user {
background-color: var(--joplin-background-color2);
color: var(--joplin-color2);
align-self: flex-end;
max-width: 90%;
> .content {
white-space: pre-wrap;
}
}
// Assistant turn uses the secondary panel scheme (light gray in light theme,
// near-black in dark theme), with regular text colour so contrast is correct
// in both themes. align-self stretch so a short reply still fills the column
// width instead of leaving empty space on the right.
.chat-panel > .messages > .turn.-assistant {
background-color: var(--joplin-background-color3);
color: var(--joplin-color);
align-self: stretch;
}
.chat-panel > .messages > .turn > .meta {
font-size: 0.8em;
color: var(--joplin-color-faded);
margin-top: 4px;
white-space: pre-wrap;
}
.chat-panel > .messages > .turn > .meta > .warning {
color: var(--joplin-color-warn);
}
.chat-panel > .messages > .separator {
text-align: center;
color: var(--joplin-color-faded);
font-size: 0.8em;
margin: 6px 0;
}
.chat-panel > .messages > .error {
color: var(--joplin-color-warn);
background-color: var(--joplin-background-color3);
border-left: 3px solid var(--joplin-color-warn);
padding: 6px 10px;
border-radius: 0 4px 4px 0;
}
.chat-panel > .composer {
border-top: 1px solid var(--joplin-divider-color);
padding: 8px;
display: flex;
flex-direction: column;
gap: 6px;
}
.chat-panel > .composer > .disclosure {
font-size: 0.8em;
color: var(--joplin-color-faded);
padding: 4px 0;
}
// Compose box: an outer wrapper that visually behaves as the input, with the
// real <textarea> filling it and the send button floating in the
// bottom-right corner.
.chat-panel > .composer > .input-wrapper {
position: relative;
display: flex;
border: 1px solid var(--joplin-divider-color);
border-radius: 4px;
background-color: var(--joplin-background-color);
&:focus-within {
border-color: var(--joplin-background-color5);
}
}
.chat-panel > .composer > .input-wrapper > .input {
flex: 1;
resize: none;
min-height: 70px;
max-height: 200px;
padding: 8px 44px 8px 10px;
border: none;
border-radius: 4px;
background-color: transparent;
color: var(--joplin-color);
font-family: inherit;
font-size: inherit;
outline: none;
}
.chat-panel > .composer > .input-wrapper > .send {
position: absolute;
right: 6px;
bottom: 6px;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--joplin-background-color5);
color: var(--joplin-color5);
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9em;
padding: 0;
}
.chat-panel > .composer > .input-wrapper > .send:hover:not(:disabled) {
background-color: var(--joplin-background-color-hover5);
}
.chat-panel > .composer > .input-wrapper > .send:active:not(:disabled) {
background-color: var(--joplin-background-color-active5);
}
.chat-panel > .composer > .input-wrapper > .send:disabled {
opacity: 0.4;
cursor: not-allowed;
}
// Spinner animation while the request is in flight.
.chat-panel > .composer > .input-wrapper > .send > .fa-spinner {
animation: chat-panel-spin 0.9s linear infinite;
}
@keyframes chat-panel-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.chat-panel > .disabled-message {
flex: 1;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
padding: 12px;
color: var(--joplin-color-faded);
line-height: 1.5;
}
@@ -4,9 +4,11 @@ import ButtonBar from './ButtonBar';
import Button, { ButtonLevel } from '../Button/Button';
import { _ } from '@joplin/lib/locale';
import bridge from '../../services/bridge';
import Setting, { AppType, SettingMetadataSection, SettingValueType, SyncStartupOperation } from '@joplin/lib/models/Setting';
import Setting, { AppType, SettingMetadataSection, SettingsRecord, SettingValueType } from '@joplin/lib/models/Setting';
import { AppState } from '../../app.reducer';
import EncryptionConfigScreen from '../EncryptionConfigScreen/EncryptionConfigScreen';
import NoteLockSettings from './controls/NoteLockSettings';
import isNoteLockEnabled from '@joplin/lib/services/noteLock/isNoteLockEnabled';
import { reg } from '@joplin/lib/registry';
import { connect } from 'react-redux';
import { themeStyle } from '@joplin/lib/theme';
@@ -21,11 +23,14 @@ import { normalizeQuery } from '@joplin/lib/components/shared/config/config-sear
import { searchResultGroups, matchedSearchSections } from './configSearch';
import MacOSMissingPasswordHelpLink from './controls/MissingPasswordHelpLink';
import AiIndexStatus from './controls/AiIndexStatus';
import AiStatus from './controls/AiStatus';
const { KeymapConfigScreen } = require('../KeymapConfig/KeymapConfigScreen');
import SettingComponent, { UpdateSettingValueEvent } from './controls/SettingComponent';
import SettingComponent from './controls/SettingComponent';
import shim, { MessageBoxType } from '@joplin/lib/shim';
import { OnChangeEvent } from '../lib/SearchInput/SearchInput';
import highlightSearchText from './searchHighlight';
import { UpdateSettingValueEvent } from './types';
import { Dispatch } from 'redux';
interface Font {
@@ -39,8 +44,21 @@ declare global {
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old class component without props/state interfaces; tightening requires structural refactor
class ConfigScreenComponent extends React.Component<any, any> {
interface Props {
dispatch: Dispatch;
defaultSection?: string;
settings: SettingsRecord;
themeId: number;
style: React.CSSProperties;
}
interface State extends shared.ConfigScreenState {
selectedSectionName: string;
screenName: string;
needRestart: boolean;
fonts: string[];
}
class ConfigScreenComponent extends React.Component<Props, State> {
private rowStyle_: React.CSSProperties = null;
@@ -99,7 +117,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
});
}
}
await shared.checkSyncConfig(this, this.state.settings);
return await shared.checkSyncConfig(this, this.state.settings);
}
public UNSAFE_componentWillMount() {
@@ -119,35 +137,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
}
private async handleSettingButton(key: string) {
if (key === 'sync.clearLocalSyncStateButton') {
if (!await shim.showConfirmationDialog('This cannot be undone. Do you want to continue?')) return;
Setting.setValue('sync.startupOperation', SyncStartupOperation.ClearLocalSyncState);
await Setting.saveAll();
await restart();
} else if (key === 'sync.clearLocalDataButton') {
if (!await shim.showConfirmationDialog('This cannot be undone. Do you want to continue?')) return;
Setting.setValue('sync.startupOperation', SyncStartupOperation.ClearLocalData);
await Setting.saveAll();
await restart();
} else if (key === 'ocr.clearLanguageDataCacheButton') {
if (!await shim.showConfirmationDialog(this.restartMessage())) return;
Setting.setValue('ocr.clearLanguageDataCache', true);
await restart();
} else if (key === 'ai.usage.resetButton') {
if (!await shim.showConfirmationDialog(_('Reset AI token usage counters?'))) return;
Setting.setValue('ai.usage.inputTokens', 0);
Setting.setValue('ai.usage.outputTokens', 0);
await Setting.saveAll();
} else if (key === 'ai.chat.testButton') {
await shared.checkAiConfig(this);
} else if (key === 'sync.openSyncWizard') {
this.props.dispatch({
type: 'DIALOG_OPEN',
name: 'syncWizard',
});
} else {
throw new Error(`Unhandled key: ${key}`);
}
await shared.onSettingButtonPress(this, Setting.settingMetadata(key));
}
public sectionByName(name: string) {
@@ -219,7 +209,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
);
}
public sectionToComponent(key: string, section: SettingMetadataSection, settings: Record<string, unknown>, selected: boolean) {
public sectionToComponent(key: string, section: SettingMetadataSection, settings: shared.SettingsMap, selected: boolean) {
const theme = themeStyle(this.props.themeId);
const searchMode = !!normalizeQuery(this.state.searchQuery);
@@ -268,6 +258,11 @@ class ConfigScreenComponent extends React.Component<any, any> {
);
}
settingComps.push(<AiIndexStatus key='ai_index_status' />);
settingComps.push(<AiStatus key='ai_status' />);
}
if (section.name === 'noteLock' && isNoteLockEnabled()) {
settingComps.push(<NoteLockSettings key='note_lock_settings'/>);
}
if (section.name === 'sync') {
@@ -300,24 +295,6 @@ class ConfigScreenComponent extends React.Component<any, any> {
</div>
);
if (settings['sync.target'] === SyncTargetRegistry.nameToId('joplinCloud')) {
const goToJoplinCloudLogin = () => {
this.props.dispatch({
type: 'NAV_GO',
routeName: 'JoplinCloudLogin',
});
};
settingComps.push(
<div key="connect_to_joplin_cloud_button" style={this.rowStyle_}>
<Button
title={_('Connect to Joplin Cloud')}
level={ButtonLevel.Primary}
onClick={goToJoplinCloudLogin}
/>
</div>,
);
}
if (settings['sync.target'] === SyncTargetRegistry.nameToId('joplinServerSaml')) {
const server = settings['sync.11.path'] as string;
@@ -389,7 +366,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
);
}
private onUpdateSettingValue = ({ key, value }: UpdateSettingValueEvent) => {
private onUpdateSettingValue = ({ key, value }: UpdateSettingValueEvent<string>) => {
const md = Setting.settingMetadata(key);
if (md.needRestart) {
this.setState({ needRestart: true });
@@ -397,6 +374,10 @@ class ConfigScreenComponent extends React.Component<any, any> {
shared.updateSettingValue(this, key, value);
};
public setSettingValue<Key extends string>(key: Key, value: SettingValueType<Key>) {
this.onUpdateSettingValue({ key: key as string, value });
}
private renderSearchHighlightedText = (text: string): React.ReactNode => {
return highlightSearchText(text, this.state.searchQuery);
};
@@ -417,7 +398,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
}
private restartMessage() {
return _('The application must be restarted for these changes to take effect.');
return shared.restartMessage();
}
private async restartApp() {
@@ -463,7 +444,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
const searchMode = !!searchQuery;
const sectionFilter = this.state.searchSectionFilter;
const style = {
const style: React.CSSProperties = {
...this.props.style,
overflow: 'hidden',
display: 'flex',
@@ -23,6 +23,7 @@ const indexerStateLabel = (s: IndexStatus['indexerState']) => {
case 'running': return _('Indexing…');
case 'ai-disabled': return _('AI is disabled');
case 'index-disabled': return _('Indexing is disabled');
case 'vector-search-unavailable': return _('Vector search is not available on this platform');
}
};
@@ -0,0 +1,17 @@
.ai-status {
margin-top: 16px;
padding: 10px 14px;
border-radius: 4px;
line-height: 1.4;
&.-degraded {
background: var(--joplin-background-color3);
color: var(--joplin-color-warn);
border: 1px solid var(--joplin-color-warn);
}
> a {
color: var(--joplin-url-color);
text-decoration: underline;
}
}
@@ -0,0 +1,19 @@
import * as React from 'react';
import { connect } from 'react-redux';
import AiDegradedNotice from '../../AiDegradedNotice';
import { AppState } from '../../../app.reducer';
interface Props {
degraded: boolean;
}
const AiStatus: React.FC<Props> = ({ degraded }) => {
if (!degraded) return null;
return <AiDegradedNotice className='ai-status -degraded' />;
};
const mapStateToProps = (state: AppState) => ({
degraded: !!state.aiStatus?.degraded,
});
export default connect(mapStateToProps)(AiStatus);
@@ -0,0 +1,54 @@
.note-lock-settings > .section {
padding-bottom: 16px;
margin-bottom: 16px;
border-bottom: 1px solid var(--joplin-divider-color);
}
.note-lock-settings > .section:last-child {
padding-bottom: 0;
margin-bottom: 0;
border-bottom: 0;
}
.note-lock-settings > .section > h2 {
margin-top: 0;
}
.note-lock-settings > .section > .form {
max-width: 420px;
}
.note-lock-settings > .section > .form > .buttons {
display: flex;
gap: 8px;
margin-top: 12px;
}
.note-lock-settings > .section > .form > .buttons + .buttons {
margin-top: 20px;
}
.note-lock-settings > .section > .form > .description {
margin-top: 8px;
margin-bottom: 0;
color: var(--joplin-color-faded);
font-style: italic;
line-height: var(--joplin-line-height);
}
.note-lock-settings > .section > .form > .reminder {
margin-top: 16px;
margin-bottom: 0;
}
.note-lock-settings > .section > .form > .warning {
padding: 12px;
border-left: 3px solid var(--joplin-color-warn);
background-color: var(--joplin-background-color);
color: var(--joplin-color);
line-height: 1.4;
}
.note-lock-settings > .section > .form > .error {
color: var(--joplin-color-error);
}
@@ -0,0 +1,255 @@
import * as React from 'react';
import { useCallback, useState } from 'react';
import { connect } from 'react-redux';
import { _ } from '@joplin/lib/locale';
import NoteLockKey from '@joplin/lib/services/noteLock/NoteLockKey';
import NoteLockSession from '@joplin/lib/services/noteLock/NoteLockSession';
import shim, { MessageBoxType } from '@joplin/lib/shim';
import { SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils';
import LabelledPasswordInput from '../../PasswordInput/LabelledPasswordInput';
import Button, { ButtonLevel } from '../../Button/Button';
import { AppState } from '../../../app.reducer';
import useNoteLockMode, { ActionMode } from './useNoteLockMode';
interface Props {
hasNoteLockKey: boolean;
needsNoteLockKeyUpgrade: boolean;
}
// WebCrypto reports a wrong password as an OperationError with an unhelpful generic message
const errorMessage = (error: unknown) => {
if (!(error instanceof Error)) return String(error);
if (error.name === 'OperationError') return _('Invalid password');
return error.message;
};
const NoteLockSettings: React.FC<Props> = props => {
const hasKey = props.hasNoteLockKey;
const {
mode,
onModeChange,
currentPassword,
setCurrentPassword,
password,
setPassword,
passwordRepeat,
setPasswordRepeat,
error,
setError,
} = useNoteLockMode(hasKey);
const [upgradePassword, setUpgradePassword] = useState('');
const [upgradeError, setUpgradeError] = useState('');
const [saving, setSaving] = useState(false);
const passwordMismatch = !!passwordRepeat && password !== passwordRepeat;
const canSave = !!password && !!passwordRepeat && !passwordMismatch && !saving && (mode !== ActionMode.Change || !!currentPassword);
const canUpgrade = !!upgradePassword && !saving;
const forgotPasswordDescription = _('Only do this if you\'ve forgotten your current password. A new password will be created, and any notes locked with the old one will become permanently unreadable.');
const forgotPasswordWarning = `${_('Warning:')} ${forgotPasswordDescription}`;
const resetConfirmationMessage = `${forgotPasswordWarning}\n\n${_('Continue?')}`;
const onCurrentPasswordChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
setCurrentPassword(event.target.value);
}, [setCurrentPassword]);
const onPasswordChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
setPassword(event.target.value);
}, [setPassword]);
const onPasswordRepeatChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
setPasswordRepeat(event.target.value);
}, [setPasswordRepeat]);
const onUpgradePasswordChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
setUpgradePassword(event.target.value);
}, []);
const submit = useCallback(async () => {
if (!canSave) return;
if (mode === ActionMode.Reset) {
const response = await shim.showMessageBox(resetConfirmationMessage, {
buttons: [_('Yes'), _('No')],
type: MessageBoxType.Confirm,
});
if (response !== 0) return;
}
setSaving(true);
setError('');
try {
const noteLockKey = NoteLockKey.instance();
if (mode === ActionMode.Create) {
await noteLockKey.create(password);
} else if (mode === ActionMode.Change) {
await noteLockKey.changePassword(currentPassword, password);
} else if (mode === ActionMode.Reset) {
await NoteLockSession.instance().reset(password);
}
onModeChange(ActionMode.Collapsed);
} catch (error) {
setError(errorMessage(error));
} finally {
setSaving(false);
}
}, [canSave, onModeChange, currentPassword, mode, password, setError, resetConfirmationMessage]);
const submitUpgrade = useCallback(async () => {
if (!canUpgrade) return;
setSaving(true);
setUpgradeError('');
try {
await NoteLockKey.instance().upgrade(upgradePassword);
setUpgradePassword('');
} catch (error) {
setUpgradeError(errorMessage(error));
} finally {
setSaving(false);
}
}, [canUpgrade, upgradePassword]);
const onChangePasswordClick = useCallback(() => {
onModeChange(ActionMode.Change);
}, [onModeChange]);
const onForgotPasswordClick = useCallback(() => {
onModeChange(ActionMode.Reset);
}, [onModeChange]);
const onCancel = useCallback(() => {
onModeChange(ActionMode.Collapsed);
}, [onModeChange]);
const resetPasswordTitle = `⚠️ ${_('Reset password')} ⚠️`;
const actionButtonTitle = mode === ActionMode.Reset ? resetPasswordTitle : _('Save');
// The sync advice only applies to a first setup, where there may be nothing to lose yet.
const passwordReminder = mode === ActionMode.Create ?
_('Please make sure you remember your password. It cannot be recovered if lost, and any locked notes will become inaccessible. If you have already set up the note lock password on another device, there is no need to set it again, as the information required to access your locked notes is updated when you set up sync.') :
_('Please make sure you remember your password. It cannot be recovered if lost, and any locked notes will become inaccessible.');
const getSectionTitle = () => {
if (mode === ActionMode.Reset) return resetPasswordTitle;
if (mode === ActionMode.Change) return _('Change note lock password');
if (mode === ActionMode.Collapsed) return _('Manage password');
return _('Password setup');
};
const renderManageButtons = () => {
return (
<div className='form'>
<div className='buttons'>
<Button
title={_('Change note lock password')}
level={ButtonLevel.Secondary}
onClick={onChangePasswordClick}
/>
</div>
<div className='buttons'>
<Button
title={_('Forgot password?')}
level={ButtonLevel.Secondary}
onClick={onForgotPasswordClick}
/>
</div>
<p className='description'>{forgotPasswordDescription}</p>
</div>
);
};
const renderPasswordForm = () => {
return (
<div className='form'>
{mode === ActionMode.Reset && <p className='warning' role='alert'><strong>{_('Warning:')}</strong> {forgotPasswordDescription}</p>}
{mode === ActionMode.Change && (
<LabelledPasswordInput
labelText={_('Current password')}
value={currentPassword}
onChange={onCurrentPasswordChange}
reserveIconGutter={true}
/>
)}
<LabelledPasswordInput
labelText={mode === ActionMode.Create ? _('Password') : _('New password')}
value={password}
onChange={onPasswordChange}
reserveIconGutter={true}
/>
<LabelledPasswordInput
labelText={_('Repeat password')}
value={passwordRepeat}
onChange={onPasswordRepeatChange}
valid={passwordRepeat ? !passwordMismatch : undefined}
reserveIconGutter={true}
/>
{passwordMismatch && <p className='error'>{_('Passwords do not match')}</p>}
{error ? <p className='error' role='alert'>{error}</p> : null}
<div className='buttons'>
<Button
title={actionButtonTitle}
level={mode === ActionMode.Reset ? ButtonLevel.Secondary : ButtonLevel.Primary}
disabled={!canSave}
onClick={submit}
/>
{mode !== ActionMode.Create && (
<Button
title={_('Cancel')}
level={ButtonLevel.Secondary}
disabled={saving}
onClick={onCancel}
/>
)}
</div>
<p className='reminder'><strong>{passwordReminder}</strong></p>
</div>
);
};
const renderUpgradeSection = () => {
if (!props.needsNoteLockKeyUpgrade) return null;
return (
<div className='section'>
<h2>{_('Key upgrade')}</h2>
<p>{_('For better security, your note lock protection can be upgraded to a newer method. Enter your password to upgrade it. Your notes will keep locking and unlocking as usual.')}</p>
<div className='form'>
<LabelledPasswordInput
labelText={_('Password')}
value={upgradePassword}
onChange={onUpgradePasswordChange}
reserveIconGutter={true}
/>
{upgradeError ? <p className='error' role='alert'>{upgradeError}</p> : null}
<div className='buttons'>
<Button
title={_('Upgrade key')}
level={ButtonLevel.Primary}
disabled={!canUpgrade}
onClick={submitUpgrade}
/>
</div>
</div>
</div>
);
};
return (
<div className='note-lock-settings'>
<div className='section'>
<h2>{getSectionTitle()}</h2>
{mode === ActionMode.Collapsed ? renderManageButtons() : renderPasswordForm()}
</div>
{renderUpgradeSection()}
</div>
);
};
const mapStateToProps = (state: AppState) => {
return {
hasNoteLockKey: !!new SyncInfo(state.settings['syncInfoCache']).noteLockKey,
needsNoteLockKeyUpgrade: NoteLockKey.instance().needsUpgrade(),
};
};
export default connect(mapStateToProps)(NoteLockSettings);
@@ -0,0 +1,48 @@
import * as React from 'react';
import Setting, { SettingButtonStyle } from '@joplin/lib/models/Setting';
import Button, { ButtonLevel } from '../../Button/Button';
import { useCallback, useState } from 'react';
import Logger from '@joplin/utils/Logger';
const logger = Logger.create('SettingButton');
interface Props {
settingKey: string;
onSettingButtonClick: (key: string)=> Promise<void>;
}
const SettingButton: React.FC<Props> = ({
settingKey, onSettingButtonClick,
}) => {
const key = settingKey;
const md = Setting.settingMetadata(key);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string|null>(null);
const onClick = useCallback(async () => {
setError(null);
setLoading(true);
try {
await onSettingButtonClick(key);
} catch (error) {
logger.warn('Failed to run command for button', key, error);
setError(String(error));
} finally {
setLoading(false);
}
}, [key, onSettingButtonClick]);
return <>
<Button
level={md.buttonStyle === SettingButtonStyle.Highlighted ? ButtonLevel.Primary : ButtonLevel.Secondary}
title={md.label()}
onClick={onClick}
disabled={loading}
/>
{error && <span className='error'>{error}</span>}
</>;
};
export default SettingButton;
Loaded 100 of 1051 files, more files were not shown because too many files have changed in this diff. Show more