1
0
mirror of https://github.com/laurent22/joplin.git synced 2026-04-08 11:04:42 +02:00

Compare commits

..

322 Commits

Author SHA1 Message Date
Laurent Cozic
737a494db8 update 2026-04-08 09:44:23 +01:00
Laurent Cozic
b8bfe85f21 update 2026-04-05 19:05:44 +01:00
Laurent Cozic
379a53eca5 Revert "Desktop: Fixes #14613: Fix JPEG image paste from clipboard on Linux (#14750)"
This reverts commit 05fc3e9104.

Ref: https://github.com/laurent22/joplin/issues/15022#issuecomment-4189250496
2026-04-05 18:48:56 +01:00
renovate[bot]
548d1a49ba Update dependency @types/uuid to v11 (#15018)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-05 18:46:19 +01:00
Laurent Cozic
7e08d0c77d iOS 13.6.4 2026-04-05 18:44:40 +01:00
Laurent Cozic
cb3878afa6 Android 3.6.15 2026-04-05 16:20:59 +01:00
Laurent Cozic
af0cab64cf Desktop release v3.6.7 2026-04-05 13:49:51 +01:00
Aissa Benfodda
671a7c9acf Server: Fixes #583: Redirect to correct share when following links between independently published notes (#14963) 2026-04-05 13:44:50 +01:00
Sriram Varun Kumar
a8a6fe2520 Mobile: Fixes #14984: Fix encrypted notes not decrypting after updating master password (#14996) 2026-04-05 13:44:18 +01:00
Zain Ul Abedin
c5bcb170e9 Mobile: Fixes #14771: Prevent Note Tags dialog from closing before discard confirmation on web (#14998) 2026-04-05 13:43:49 +01:00
Sriram Varun Kumar
b6ce57aad5 Chore: Mobile: Fixes #14974: Settings > Editor > Editor font has no effect on the Rich Text Editor (#15014) 2026-04-05 13:37:14 +01:00
renovate[bot]
d26c2cc96f Update dependency esbuild to v0.27.2 (#15011)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-05 13:34:47 +01:00
Harsh Gupta
c9cebd6016 Desktop: Fixes #12910: Fixed Custom Dictionary.txt being saved to wrong directory (#14749) 2026-04-05 13:16:30 +01:00
renovate[bot]
b90458f387 Update dependency esbuild to v0.27.1 (#15009)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-04 18:15:55 +01:00
renovate[bot]
1a2d045ed2 Update dependency @crowdin/cli to v4.12.0 (#14985)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-04 16:44:25 +01:00
renovate[bot]
719d5ce4bb Update dependency nan to v2.24.0 (#14986)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-04 16:44:20 +01:00
Henry Heino
76f0f1494e Chore: Development: Fix VSCode 1.114.0 shows a large number of tsc errors (#14989) 2026-04-04 16:44:12 +01:00
Harsh Gupta
e4f916bea5 Desktop: Fixes #14990: Fix inline formatting with trailing/leading whitespace (#14991) 2026-04-04 16:43:57 +01:00
Sriram Varun Kumar
cf9098e6a3 Mobile: Fixes #14974: Fix editor font setting being ignored in the Rich Text Editor (#14995) 2026-04-04 16:38:21 +01:00
Vishal Patel
18ffdb2f50 Mobile: Add toolbar button reordering with up/down arrows (#14485)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Happy <yesreply@happy.engineering>
2026-04-04 16:23:30 +01:00
Himanshu Mishra
acd2ef4edf Desktop: Replace smalltalk with React Dialog to add password visibility in encryption setup (#14739) 2026-04-02 16:43:52 +01:00
Aissa Benfodda
9d91d4f85c Desktop: Fixes #12994: Share owner sees "Leave notebook" instead of "Share notebook" when server is offline (#14923) 2026-04-02 16:34:48 +01:00
Devrajsinh Gohil
635af9748a macOS: Resolves #9637: Added fullscreen shortcut (Ctrl + Cmd + F) (#14926) 2026-04-02 16:34:20 +01:00
Harsh Gupta
612e5a08f3 Desktop: Fixes #14950: Inline computed styles when copying from the Markdown preview pane (#14973) 2026-04-02 16:29:13 +01:00
Henry Heino
d3477f8626 Desktop: Importing from OneNote: Fix import of ink with negative bounding box coordinates (#14981) 2026-04-02 16:22:49 +01:00
Zain Ul Abedin
9e836a8984 Mobile: Fixes #14771: Show confirmation dialog before closing tags dialog with unsaved changes (#14777) 2026-04-02 16:08:26 +01:00
Laurent Cozic
3f14ffdf73 lock file 2026-04-02 14:42:35 +01:00
Joplin Bot
fd9f6c11ab Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-04-01 02:47:55 +00:00
FischLu
0cc79724c3 Desktop: Enable Copy and Select All in viewer and read-only modes (#14956) 2026-03-31 19:30:44 +01:00
mrjo118
e6fddd054a Desktop: Fixes #14628: Fix incorrectly re-instated code (#14962) 2026-03-31 15:58:12 +01:00
Henry Heino
860b22b0e7 Desktop,Cli: Fixes #14954: Fix changes made in an external editor are sometimes ignored (#14957) 2026-03-31 15:57:52 +01:00
Fardin96
281b0ed124 Mobile: Fixes #11122: Tag's note list fails to update after removing the tag from a note (#14944) 2026-03-31 15:47:27 +01:00
krevad
5dc5cb62db All: Translation: Update sv.po (#14943) 2026-03-31 15:45:46 +01:00
renovate[bot]
28bb43b3b5 Update dependency @playwright/test to v1.57.0 (#14902)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-03-31 15:33:32 +01:00
Dipanshu Rawat
c8bfcb16be Desktop: Fixes #14890: Disable "Expand all notebooks" button when no sub-notebooks exist (#14891) 2026-03-31 15:24:36 +01:00
Alex
634956bcc6 Desktop: Fixes #9436: Fix Markdown export losing folders that differ only by special characters (#14869) 2026-03-31 15:13:22 +01:00
Henry Heino
346ab98133 Desktop: Upgrade Electron to v40.8.3 (#14882) 2026-03-31 15:11:15 +01:00
Henry Heino
55008c9de9 Chore: Mobile: Add /pluginAssets to .gitignore (#14961) 2026-03-31 11:30:42 +01:00
mrjo118
f4ba70c49c Desktop: Fixes #14628: Fix renderer crashes still occuring due to incorrect merge (#14953) 2026-03-30 13:06:52 +01:00
renovate[bot]
e61379ed59 Update dependency git to v2.51.2 (#14951)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-29 20:28:09 +01:00
Sandesh Prakash Dawkhar
75cd9b4cb7 Desktop: Fixes #14919: Prevent Plugin API callback registry memory leak (#14920) 2026-03-29 20:21:30 +01:00
Kaushalendra Singh
43120d2b3e Desktop: Fixes #14628: prevent renderer crash when closing secondary window (#14849) 2026-03-29 11:40:30 +01:00
jellyfrostt
5656731dca Chore: Mobile: Fixes #14834: Fix JSDOM scrollIntoView error in tests (#14870) 2026-03-29 11:39:22 +01:00
Laurent Cozic
4cfe54161d Chore: Add appClose logger statements to desktop app (#14927) 2026-03-29 11:38:05 +01:00
renovate[bot]
7f2a95f66e Update dependency ldapts to v8.0.36 (#14938)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-29 05:53:44 +00:00
renovate[bot]
75819f3be3 Update dependency react-native-localize to v3.6.1 (#14932)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-28 14:30:05 +00:00
Manvendra Kumar Singh
e709921310 Chore: Fixes #14878: Prevent focusing undefined titleInputRef in dialog (#14879) 2026-03-27 12:25:28 +00:00
renovate[bot]
b19d47ca4a Update dependency python to v3.14.0 (#14884)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-27 12:22:53 +00:00
Henry Heino
516981b80c Desktop: Fixes #14628: Fix crash when closing secondary windows (#14892) 2026-03-27 12:21:13 +00:00
Laurent Cozic
a90d162989 Server: Move deletion of objects outside of transaction block (#14898) 2026-03-27 12:18:40 +00:00
Henry Heino
6cf9f1cc11 Windows: Fixes #14903: Fix most Windows-specific test failures (#14904) 2026-03-27 12:17:12 +00:00
Henry Heino
ee7362564c Chore: CI: Fix test warnings (#14907) 2026-03-27 12:13:25 +00:00
Muhammad Zohaib Irshad
cdf5367934 Clipper: Make the styling of dialogues consistent (#14908) 2026-03-27 12:13:18 +00:00
Henry Heino
7a76c31c26 Chore: Server: Fix incorrect error message (#14915) 2026-03-27 12:10:56 +00:00
Sandesh Prakash Dawkhar
004ab78a7a Desktop: Fixes #14914: RTE checklists should create unchecked items on Enter (#14918) 2026-03-27 12:10:27 +00:00
Ashutosh Singh
a7067c30c4 Desktop: Fixes #9673: Frontmatter export: Include notebook icon in frontmatter export (#14582) 2026-03-27 11:47:41 +00:00
renovate[bot]
be081316b3 Update dependency @types/serviceworker to v0.0.172 (#14901)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-25 15:05:36 +00:00
renovate[bot]
c9fb33cb20 Update dependency ldapts to v8.0.35 (#14894)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-25 13:47:05 +00:00
renovate[bot]
dfdc0f3c35 Update dependency ldapts to v8.0.33 (#14887)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-24 12:42:21 +00:00
Laurent Cozic
0fa3a509d6 Mobile, Desktop: Revert: Start sync when app opens or resumes (#14889) 2026-03-24 12:39:45 +00:00
renovate[bot]
18cf0a95ad Update dependency @types/serviceworker to v0.0.171 (#14885)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-24 09:05:36 +00:00
Laurent Cozic
7d454123f9 CI: Revert cancel-in-progress change as it does not do anything 2026-03-23 20:39:37 +00:00
renovate[bot]
e4fb72cd08 Update dependency ldapts to v8.0.32 (#14876)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-23 20:28:25 +00:00
Laurent Cozic
741e1b19e5 Revert "Desktop Fix: #14788 prevent sync panel from jumping during sync" (#14875) 2026-03-23 15:51:59 +00:00
dipok
6637c05cc8 CLI: Add clear command to clear console output (#14844) 2026-03-23 12:37:18 +00:00
Yousef Genedy
5877670e33 Mobile: Resolves #14789: Implement note attachments management screen (#14818) 2026-03-23 12:33:19 +00:00
Dipanshu Rawat
2320beec39 Desktop Fixes #14788: Prevent sync panel from jumping during sync (#14792) 2026-03-23 12:26:14 +00:00
Ehtesham Zahid
a0effc9ff8 Desktop: Resolves #14778: Improve checkbox completion icon in detailed note list (#14780) 2026-03-23 12:12:34 +00:00
Henry Heino
92cd5630f7 Chore: OneNote importer: Fix linter errors and standardize formatting (#14858) 2026-03-23 09:19:18 +00:00
Laurent Cozic
9fbca68062 Chore: Removed some "any" variables (#14825) 2026-03-22 23:14:16 +00:00
Jose Riha
953fb20006 All: Translation: Update sk_SK.po (#14867) 2026-03-22 14:25:27 -04:00
renovate[bot]
fb18be14a1 Update dependency ldapts to v8.0.31 (#14866)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-22 14:02:33 +00:00
Eric Duarte
75c4dbc9df All: Translation: Update es_ES.po (#14862)
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-03-21 18:15:22 -04:00
renovate[bot]
1f5b4269ab Update dependency pg-boss to v10.4.0 (#14837)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-03-21 12:21:29 +00:00
Henry Heino
9c23574977 Chore: Server: Remove auto-generated explicit any in types.ts (#14855) 2026-03-21 12:17:43 +00:00
Henry Heino
fe5ff98429 Chore: Editor: Strengthen compatibility layer event types (#14856) 2026-03-21 12:17:30 +00:00
Henry Heino
b721b3ac77 Chore: OneNote importer: Remove unused callback (#14859) 2026-03-21 12:17:06 +00:00
Rohit
638485376c All: Fixes #14540: Prevent duplicate tags caused by Unicode normalization (#14599) 2026-03-21 12:14:37 +00:00
Victor Gherardi
575f4235c3 iOS: Fixes #12968: Fix mobile app unable to attach file with special characters in the name (#14736) 2026-03-21 12:05:55 +00:00
renovate[bot]
8184d3ef37 Update Slashgear/action-check-pr-title action to v5 (#14857)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-20 22:58:59 +00:00
renovate[bot]
1262a5a1ff Update dependency ldapts to v8.0.30 (#14853)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-20 21:19:49 +00:00
moaaz
05fc3e9104 Desktop: Fixes #14613: Fix JPEG image paste from clipboard on Linux (#14750) 2026-03-20 20:02:06 +00:00
Laurent Cozic
064e72c43a Revert "Desktop: Resolves #7914: Add support for Ctrl/Cmd+Wheel to zoom in and out (#14684)"
This reverts commit 21d12a2b46.

Due to: https://github.com/laurent22/joplin/pull/14817
2026-03-20 10:20:10 +00:00
mrjo118
088d8eb159 Mobile: Disable auto correct, auto complete and auto capitalize for setting search field (#14810) 2026-03-20 10:18:42 +00:00
renovate[bot]
333bc5d123 Update dependency ldapts to v8.0.29 (#14842)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-20 10:17:47 +00:00
Henry Heino
93f4c97433 Chore: Desktop: Fix warning during build: Migrate away from the deprecated Sass API (#14803) 2026-03-20 10:17:23 +00:00
Henry Heino
eeeb7d6ba1 Desktop: Accessibility: Fix accessibility issues flagged by automated tools in the note properties dialog (#14798) 2026-03-20 10:16:04 +00:00
renovate[bot]
bda1dc2aa8 Update dependency @types/serviceworker to v0.0.170 (#14838)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-20 08:45:24 +00:00
Victor Gherardi
4073596373 Chore: Fixes #14832: CI: Update handleAnchorClick tests to use dispatchEvent for click simulation (#14833) 2026-03-20 00:46:43 +00:00
Henry Heino
c16eb16af4 Chore: Desktop: Re-enable settings screen accessibility tests (#14793) 2026-03-19 07:20:02 -07:00
renovate[bot]
0c0d7713df Update dependency ldapts to v8.0.28 (#14829)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-19 08:20:01 +00:00
renovate[bot]
5161d18d19 Update dependency fs-extra to v11.3.3 (#14819)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-18 21:05:09 +00:00
renovate[bot]
2dcb5374fa Update dependency ldapts to v8.0.27 (#14814)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-18 11:37:22 +00:00
Vinayreddy765
4c103173ba Desktop: Resolves #14717: Improve clarity of master password warning message (#14724)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-03-18 10:03:42 +00:00
Moaz Hashem
6d919376dc Desktop: Resolves #14797: Completed date/time is shown as a number (#14808) 2026-03-18 09:55:44 +00:00
Harsh Gupta
08dac7f60b Desktop: Fixes #14245: Incomplete (out of screen) ABC Sheet Music rendering (#14767) 2026-03-18 09:55:33 +00:00
Kaushalendra Singh
087e271f61 Desktop: Fixes #14223: Fix OneNote zip import path when .one files are at root level (#14605) 2026-03-18 09:54:50 +00:00
renovate[bot]
7713bbf65d Update dependency @types/serviceworker to v0.0.169 (#14813)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-18 08:43:54 +00:00
Laurent Cozic
ea4efa6a16 Revert "Resolves #12208 Joplin Desktop UI overlap in top left corner on small screens (#14667)"
This reverts commit 745a68f26b.

Due to: https://github.com/laurent22/joplin/issues/14801
2026-03-17 20:50:00 +00:00
renovate[bot]
c6dc7aa05c Update dependency glob to v11.1.0 (#14799)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-17 19:41:51 +00:00
Henry Heino
0e6cafbe9f Server: Fix share processing task failure (#14795)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-03-17 17:53:05 +00:00
Joplin Bot
559559c2d1 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-03-17 13:25:32 +00:00
Laurent Cozic
3223d9c6f9 Desktop release v3.6.6 2026-03-17 09:06:13 +00:00
renovate[bot]
991cbc4dc0 Update dependency nodejs to v24.11.1 (#14783)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-17 03:38:46 +00:00
Joplin Bot
af0318293c Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-03-17 02:17:20 +00:00
Laurent Cozic
6033f9c6e6 iOS 13.6.3 2026-03-16 22:30:03 +00:00
Laurent Cozic
f400f3839d Android 3.6.14 2026-03-16 22:17:00 +00:00
Laurent Cozic
c7736f9e80 Desktop release v3.6.5 2026-03-16 22:05:59 +00:00
mrjo118
5abd5803fb Mobile: Fixes #14722: Fix incorrect sizing of the tag association screen when opening and dismissing the onscreen keyboard (#14772) 2026-03-16 21:34:40 +00:00
Kareem Abu Al Enein
615677cf18 Mobile: Fixes #13957: Fixed log screen auto-scroll loop during search (#14757) 2026-03-16 21:33:42 +00:00
Ashutosh Singh
21d12a2b46 Desktop: Resolves #7914: Add support for Ctrl/Cmd+Wheel to zoom in and out (#14684) 2026-03-16 21:25:28 +00:00
Anuradha Verma
11c17d43eb Desktop: Fix note list losing focus after moving note to trash (#14650) 2026-03-16 21:23:40 +00:00
Laurent Cozic
120cdaabac Chore: Upgrade gettext-extractor to fix broken CI
The package was printing thousands of debugging statement due to the developer forgetting console.error statements in his lib
2026-03-16 21:08:15 +00:00
Keshav
3a9fbc7e67 Mobile: Fixes #14727: Fix OneDrive auth code double-encoding on mobile (#14776) 2026-03-16 20:51:16 +00:00
madhan112007
c9aaaa952e Mobile: Fixes #14286: Fix openFolder x-callback-url link on Android (#14765) 2026-03-16 20:50:39 +00:00
Keshav
d97121a63a Mobile: Fixes #14685: Restrict sidebar gestures to notebook list (#14733) 2026-03-16 20:50:11 +00:00
Moaz Hashem
4bd98f4819 Desktop: Resolves #14621: Application crashes when deleting a notebook (#14651)
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-03-16 20:49:41 +00:00
Henry Heino
426500437d All: Sync: Fix notebook sharing can fail due to incorrect read-only share state (#14770) 2026-03-16 20:48:07 +00:00
Avanish Gupta
c9dace8b4d Desktop: Make encryption-related strings translatable (#14752) 2026-03-16 12:10:44 +00:00
mrjo118
f9654a3438 Mobile: Disable auto correct, auto complete and auto capitalize for search fields (#14759) 2026-03-16 11:22:27 +00:00
renovate[bot]
7d888a50af Update dependency ldapts to v8.0.25 (#14762)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-16 08:36:22 +00:00
renovate[bot]
65eb5e3afe Update dependency nodejs to v24.11.0 (#14744)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-15 09:49:05 +00:00
renovate[bot]
7a9dc4a607 Update dependency ldapts to v8.0.24 (#14743)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-15 08:50:00 +00:00
Surendra Manjhi
d20cc87756 Chore: Regression: Skip base64 conversion for svg images (#14721)
Ref: https://github.com/laurent22/joplin/pull/14632
2026-03-14 12:50:02 +00:00
Justin Charles
6d310f6b27 Desktop: Fixes #14658: Always require password confirmation when changing master password after encryption (#14692)
Signed-off-by: justin212407 <charlesjustin2124@gmail.com>
2026-03-14 12:45:51 +00:00
Ehtesham Zahid
d2b273bfb0 Desktop: Fixes #14687: Prevent TypeError in handleAppFailure when window is destroyed (#14689) 2026-03-14 12:44:10 +00:00
Ash092016
1ff71a64e1 All: Fixes #14419: Added validation for Joplin Server URL protocol (#14612) 2026-03-14 12:38:00 +00:00
renovate[bot]
e442544070 Update dependency ldapts to v8.0.23 (#14731)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-14 11:09:58 +00:00
renovate[bot]
ba93bcc06d Update dependency ldapts to v8.0.22 (#14729)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-14 09:55:31 +00:00
ChimzyFire
2d545158d0 Desktop: Fixes #10795: Add WCAG 2.2 accessibility labels to Mermaid chart button (#14617) 2026-03-14 01:14:23 +00:00
renovate[bot]
364ea03e5d Update dependency prosemirror-history to v1.5.0 (#14725)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-13 10:36:23 +00:00
renovate[bot]
082aa70a48 Update dependency ldapts to v8.0.21 (#14723)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-13 08:45:59 +00:00
renovate[bot]
e9fe4036b1 Update dependency react-native-svg to v15.15.1 (#14718)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-12 22:33:54 +00:00
renovate[bot]
5ab02cfe52 Update dependency sass to v1.94.3 (#14720)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-12 22:33:45 +00:00
Ashutosh Singh
ce8d9a1cdf Desktop: Resolves #14659: Reuse master password dialog when enabling E2EE (#14664) 2026-03-12 18:58:16 +00:00
Jalina Hirushan
745a68f26b Resolves #12208 Joplin Desktop UI overlap in top left corner on small screens (#14667) 2026-03-12 18:51:53 +00:00
bwat47
667ff1797d Desktop: Fixes #13107: html to markdown conversion sometimes converting links to plaintext with <ins> tags (#14683) 2026-03-12 18:47:49 +00:00
renovate[bot]
e5274c5cff Update dependency sass to v1.94.0 (#14690)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-12 18:41:09 +00:00
Dev Gupta
03c3d6ae4a Desktop: Fixes #14660: Text inputs are disabled after re-encrypt notes dialog journey (#14691) 2026-03-12 18:40:55 +00:00
Aayushi Rajesh
9a3673a38f Doc: Fixes #14645: Fix for navigation order mentioned (#14715) 2026-03-12 18:36:02 +00:00
renovate[bot]
99124e4feb Update dependency react-native-svg to v15.15.0 (#14716)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-12 18:35:33 +00:00
Laurent Cozic
cf51782f4f Desktop: Fixes #14637: cut/copy context menu options don't appear when right clicking joplin resource links in markdown editor (#14711) 2026-03-12 18:35:17 +00:00
Ashutosh Singh
8bde0bf0ec Desktop: Fixes #14079: Improve updater error message on rate limit (#14549) 2026-03-12 18:25:18 +00:00
Kanishka..
e8372c76aa Desktop: Resolves #8611: Add toggle button to hide/show sync panel (#14593) 2026-03-12 18:16:52 +00:00
Ash092016
e1dc36c0a5 Mobile: Resolves #14603: Password and confirm password fields when enabling encryption auto capitalise first character (#14611) 2026-03-12 18:13:06 +00:00
Henry Heino
8d168dc330 Server: Fix delta API can return changes in wrong order (#14713) 2026-03-12 12:22:26 +00:00
renovate[bot]
321afbe110 Update dependency ldapts to v8.0.20 (#14693)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-12 10:24:18 +00:00
Henry Heino
2e3daad78e Desktop: Fixes #14663: Fix crash when rapidly closing secondary windows (#14702) 2026-03-12 01:18:56 +00:00
Laurent Cozic
2132c2cdf4 Revert "Desktop: Fix context menu missing cut/copy when selecting resource links in markdown editor" (#14710) 2026-03-11 21:19:55 +00:00
Sergio
67aff20e39 Desktop: Fixes #14676: Toggling checkboxes in the note history viewer opens an open with prompt on Windows (#14679) 2026-03-11 16:19:46 +00:00
Gnana Pragadeesh K
3719e1eee0 Mobile: Fixes #14152: Fix font-size inconsistency of code block and inline code (#14463) 2026-03-11 16:19:08 +00:00
Laurent Cozic
4abe83fdb6 Doc: Fix broken language selector on website 2026-03-10 18:49:04 +00:00
Laurent Cozic
6ba912e5aa Chore: Fixed website localisation issue 2026-03-10 15:32:29 +00:00
Surendra Manjhi
8533083730 Desktop: Fixes #14627: use resourceUrl() for base64 images in pasteAsMarkdown (#14632) 2026-03-10 12:38:00 +00:00
Yugal Kaushik
754ff28b36 Desktop: Fixes #101111: ENEX import no longer breaks bullet items with a line break into separate paragraphs (#14642) 2026-03-10 12:36:48 +00:00
Ahmed Idani
b663c64def All: Fixes #14412: Skip share consistency check when not using Joplin Server/Cloud (#14649) 2026-03-10 12:35:55 +00:00
Laurent Cozic
998b26d9a4 Doc: Update CLAUDE.md to specify whitespace rules
Clarified guidelines on whitespace changes in code.
2026-03-10 12:31:00 +00:00
Veivel P
b097cf9a6a Desktop: Resolves #14143: Fixed scrolling behaviour in long lines for TinyMCE and CodeMirror (#14669) 2026-03-10 12:26:03 +00:00
Vinayreddy765
e22c367566 Desktop: Fixes #14637: Fix context menu missing cut/copy when selecting resource links in markdown editor (#14638)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-03-10 12:20:40 +00:00
Justin Charles
71a2e98155 Desktop: Fixes #14661: hide new note/todo buttons when no notebook exists (#14674)
Signed-off-by: justin212407 <charlesjustin2124@gmail.com>
2026-03-10 12:15:34 +00:00
Davideb18
714bbd6d23 Desktop: Fixes #11823: Fixed cancel behavior labels when switching config screens (#14677) 2026-03-10 12:14:04 +00:00
Akshaj Rawat
eda03333a6 Desktop: Fixes #12394: Fix search bar remaining empty when navigating back (#14488) 2026-03-10 12:01:16 +00:00
divyanshkhurana06
93f17a87fa Desktop: Fixes #14142: Fix search highlights breaking mermaid diagram rendering (#14516) 2026-03-10 11:57:15 +00:00
Dipanshu Rawat
c765306e6f Chore: ArrayUtils optimize unique and removeElement functions, improve type handling (#14552)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-03-10 11:38:28 +00:00
Justin Charles
f05fe5754d Desktop: Fixes #14542: Fix Prevent unclosed frontmatter from breaking Markdown rendering (#14563)
Signed-off-by: justin212407 <charlesjustin2124@gmail.com>
2026-03-10 11:31:38 +00:00
Keshav
d046bfa14b Desktop: Resolves #10562: Preserve table customization made on RTE (#14572) 2026-03-10 11:29:59 +00:00
Yousef Genedy
2a681008dd Mobile, Desktop: Resolves #9481: Start sync when app opens or resumes (#14574) 2026-03-10 11:27:46 +00:00
Ashutosh Singh
7214823c74 Chore: Resolves #12037: Remove JSDOM from Turndown package (#14653) 2026-03-09 14:09:08 +00:00
renovate[bot]
ed5b92a91e Update dependency ldapts to v8.0.18 (#14655)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-09 10:27:43 +00:00
Aayushi Rajesh
2c8a9eee61 Doc: Add Plugins link to website navigation (#14645) 2026-03-09 09:10:33 +00:00
renovate[bot]
6451305c89 Update dependency esbuild to v0.26.0 (#14654)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-09 09:05:27 +00:00
Henry Heino
5fd0dc23da Desktop: Fixes #14584: Fix changes to editor settings not applied until editor reloads (#14586) 2026-03-08 21:11:54 +00:00
renovate[bot]
fd3b133b16 Update dependency dompurify to v3.3.1 (#14648)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-08 17:12:46 +00:00
renovate[bot]
118bc3edf1 Update dependency git to v2.51.0 (#14646)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-08 14:39:59 +00:00
renovate[bot]
d90836bc50 Update dependency ldapts to v8.0.17 (#14641)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-08 13:45:27 +00:00
Dipanshu Rawat
9a477dbeb9 Chore: Fix typo for enum for Right value (#14575)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-03-08 12:01:31 +00:00
Justin Charles
5271081b3a Docs: Add plugin website link in README (#14626)
Signed-off-by: justin212407 <charlesjustin2124@gmail.com>
2026-03-08 11:58:25 +00:00
bwat47
b26370fc5a Desktop, Mobile: Fixes #14630: underline disappearing from ++insert++ syntax when cursor is on that line (#14631) 2026-03-08 11:57:51 +00:00
Laurent Cozic
737c7dcdb4 CI: Do not cancel CI execution on dev branch 2026-03-08 11:17:08 +00:00
Joplin Bot
04babe0261 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-03-07 18:44:23 +00:00
Laurent Cozic
85e5bbd246 Desktop release v3.6.4 2026-03-07 16:17:05 +00:00
bwat47
f819e1c88b Desktop, Mobile: Fixes #14564: Implement cursor-aware markup rendering and hide bulletpoints on task lists (#14573) 2026-03-07 16:15:48 +00:00
Ashutosh Singh
79c153c498 Desktop, Mobile: Fixes #12793: Prevent a failing plugin from blocking other plugins (#14577) 2026-03-07 16:14:05 +00:00
Harsh Gupta
1db9903926 Desktop: Fixes #12355: Auto-scroll to selected note from 'Go to Anything' search results (#14591) 2026-03-07 16:12:31 +00:00
Surendra Manjhi
e736e05d1c Desktop: Fixes #13140: Normalize img alt line breaks and convert data: URLs when pasting from Word (#14518) 2026-03-07 15:44:35 +00:00
Surendra Manjhi
5ef10676d8 Desktop: Fixes #14525: Show only relevant options in context menu when right-clicking a note link (#14528) 2026-03-07 15:42:55 +00:00
yentropysack
b38613ca22 Mobile: Fixes #9938: Fix in-page links don't work if clicked in succession (#14538)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-03-07 15:42:08 +00:00
Justin Charles
ea486fbe13 All: Fixes #14543: Fix ++insert++ syntax rendering fix in markdown (#14547)
Signed-off-by: justin212407 <charlesjustin2124@gmail.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-03-07 15:39:53 +00:00
Laurent Cozic
d2784aff54 Plugins: Add support for joplin.fs.archiveExtract plugin method (#14625) 2026-03-07 15:33:27 +00:00
renovate[bot]
7308d9541e Update dependency ldapts to v8.0.14 (#14614)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-07 00:57:38 +00:00
renovate[bot]
d6ac709e5f Update dependency ldapts to v8.0.13 (#14592)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-05 22:46:54 +00:00
Sriram Varun Kumar
b290046e66 Mobile: Fixes #14555: Fix tapping rendered image scrolling to cursor position (#14580) 2026-03-05 13:20:23 +00:00
Henry Heino
c2321a04ae Chore: Importing from OneNote: Add test to verify that errors are reported to JavaScript (#14550) 2026-03-05 09:13:18 +00:00
mrjo118
3df77a4395 Desktop, Mobile: Fix issue where the revision service does not start on the first launch of the app (#14554) 2026-03-05 09:06:04 +00:00
mrjo118
38fd790719 Mobile: Add ability to set per notebook sorting on mobile (#14562) 2026-03-05 09:04:26 +00:00
Vinayreddy765
40bfa9dd3d Desktop: Show feedback message when master passwords do not match (#14566) 2026-03-05 09:01:03 +00:00
Henry Heino
8d08e5df60 Desktop: Importing from OneNote: Fix importing cross-page links (#14567) 2026-03-05 09:00:16 +00:00
Ash092016
4121c47e18 CI: Add concurrency block to cancel outdated workflow runs (#14570) 2026-03-05 09:00:02 +00:00
Yugal Kaushik
d30e6ad0da Desktop: Fixes #13178: Invisible cursor in legacy editor when using dark theme in separate window (#14557)
Co-authored-by: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com>
2026-03-05 08:32:09 +00:00
Yugal Kaushik
be712df89d Mobile: Fixes #14534: Call unmount() in Note.test.tsx tests to suppress act() warnings (#14535) 2026-03-05 08:31:44 +00:00
Sriram Varun Kumar
f7762c403e Mobile: Rich Text Editor: Fix extra blank line above nested lists (#14504) 2026-03-05 08:31:22 +00:00
renovate[bot]
b89d37de84 Update dependency @types/serviceworker to v0.0.168 (#14578)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-05 08:26:56 +00:00
Ashutosh Singh
a7b9af61c0 Desktop: Fixes #14500: Fixes zh_TW locale detection on first start (#14527) 2026-03-04 20:00:12 +00:00
Laurent Cozic
a3186cdfe1 Doc: Add CLAUDE.md rule regarding duplicate tests 2026-03-04 18:48:05 +00:00
Laurent Cozic
0a580493a2 Doc: Added YouTube link to main website page and removed Lemmy link 2026-03-04 16:07:39 +00:00
Laurent Cozic
7a7bf72aa8 Chore: Minor fix to Paste as Markdown feature 2026-03-04 16:06:27 +00:00
Laurent Cozic
a20a584273 Desktop: Add "Paste as Markdown" command for Markdown editor (#14556) 2026-03-04 14:31:54 +00:00
Sriram Varun Kumar
ae30e8cf00 CLI: Fix trailing spaces in ls -l output (#14559) 2026-03-04 10:16:22 +00:00
Joplin Bot
1a7bb9131a Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-03-04 02:14:13 +00:00
Harsh Gupta
81ed35b117 Desktop: Resolves #12210: Translate Find and Replace dialog in Rich Text editor (#14529) 2026-03-03 16:48:40 +00:00
Sriram Varun Kumar
2704495ac6 Desktop: Fixes #14196: Fix file:// links with backslashes for Windows UNC paths (#14541) 2026-03-03 16:38:08 +00:00
Parth Thirwani
a96f7c6ee7 Desktop: Fixes #13883: Secondary windows no longer follow primary selection after moving notes (#14498)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-03-03 15:32:04 +00:00
Henry Heino
af706ac1b3 Web: Add welcome notes specific to the web app (#14499) 2026-03-03 15:30:03 +00:00
Henry Heino
766ef933b9 Web: Link to the official web app when attempting to sync with Joplin Cloud (#14523) 2026-03-03 15:09:14 +00:00
Surendra Manjhi
35de2aca18 Desktop: Fixes #12313: Prevent All Notes sort order from overwriting shared notebook sort on relaunch (#14524) 2026-03-03 15:08:35 +00:00
Ahmed Idani
c1827e1b9e Desktop: Fixes #14522: App fails to restart on Linux AppImage (#14530) 2026-03-03 14:59:53 +00:00
Henry Heino
89e3544a0c Chore: Desktop: Fix automated tests fail when the system locale is not English (#14531) 2026-03-03 14:58:47 +00:00
mrjo118
7f40e9e661 Mobile: Prevent focus issues and keyboard opening when opening a note in view mode (#14533) 2026-03-03 14:58:10 +00:00
Akshaj Rawat
20405ea95f Desktop: Resolves #12326: Add keyboard shortcuts to toolbar buttons (#14408) 2026-03-03 13:39:57 +00:00
Akshaj Rawat
2574e18c2f Desktop: Fixes #14271: Error message is incorrect when plugin manifest is invalid (#14374) 2026-03-03 13:36:08 +00:00
Joplin Bot
36b25a9517 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-03-03 02:18:36 +00:00
Laurent Cozic
b3e0575361 iOS 13.6.2 2026-03-02 22:16:27 +00:00
Joplin Bot
f9f40b3c9b Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-03-02 18:59:44 +00:00
Laurent Cozic
b59721f4b3 Android 3.6.13 2026-03-02 17:47:33 +00:00
Laurent Cozic
891ab3e317 Desktop release v3.6.3 2026-03-02 17:26:21 +00:00
Laurent Cozic
0e156796bc Desktop: Fix editor plugins receiving stale note body during navigation (#14513) 2026-03-01 18:54:42 +00:00
renovate[bot]
f2b558cb75 Update dependency react-native-localize to v3.6.0 (#14511)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-01 15:47:39 +00:00
renovate[bot]
322657ef72 Update dependency gettext-extractor to v4.0.3 (#14508)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-01 13:17:45 +00:00
renovate[bot]
c1e99afd2e Update dependency samlify to v2.10.2 (#14509)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-01 13:12:44 +00:00
Sriram Varun Kumar
b3822e2700 CLI: Fixes #13158: Fix null crash in e2ee decrypt command (#14461)
Co-authored-by: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com>
2026-03-01 12:21:21 +00:00
Nagmani Upadhyay
a43f46fc01 Mobile: Fixes #11793: uses consistent padding in plugin info dialog (#14466) 2026-03-01 12:20:22 +00:00
Kanishka..
50a26b63c8 Desktop: Fixes #13679: Fix sidebar scroll jump when expanding/collapsing folders (#14467) 2026-03-01 12:20:02 +00:00
Harsh Gupta
02c1c75587 Desktop: Fixes #12401: copying from markdown preview including theme background colour (#14474) 2026-03-01 12:11:16 +00:00
Sriram Varun Kumar
345632324d Desktop: Fix UI freeze when closing plugin dialog with Escape key (#14477)
Co-authored-by: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com>
2026-03-01 12:08:58 +00:00
mrjo118
8073e03daf Mobile: Prevent race condition when refreshing note contents on mobile (#14486) 2026-03-01 11:58:15 +00:00
Laurent Cozic
7feb953c70 Chore: Trying to use CodeRabbit to validate pull request descriptions 2026-03-01 11:57:51 +00:00
yentropysack
8e895fb2c0 Desktop: Fixes #12385: Copy and paste from markdown preview includes search highlight effect (#14493) 2026-03-01 11:24:41 +00:00
Harsh Gupta
ee97c41309 Desktop, Mobile: Resolves #12220: Add new option to disable the Joplin icon for internal note links (#14503) 2026-03-01 11:16:14 +00:00
Joplin Bot
c9a55563b5 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-03-01 02:33:23 +00:00
GeorgiPopovIT
39f5dc8c95 All: Translation: Update bg_BG.po (#14505) 2026-02-28 19:48:02 -05:00
Joplin Bot
9256ab197a Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-02-28 18:44:15 +00:00
Laurent Cozic
ee2869da86 Doc: Fix sponsor link 2026-02-28 17:51:48 +00:00
Henry Heino
e11441cfbc Web: Move web app out of beta (#14497) 2026-02-28 12:01:50 +00:00
Yugal Kaushik
2b5be639ce All: Resolves #14336: Store note history settings in sync info (#14449)
Signed-off-by: yugalkaushik <yugalkaushik14@gmail.com>
2026-02-28 12:01:23 +00:00
GeorgiPopovIT
cf3d7f5b88 All: Translation: Update bg_BG.po (#14482) 2026-02-27 12:32:19 -05:00
Mihai Vasiliu
595452f30e All: Translation: Update ro_RO.po and ro_MD.po (#14470) 2026-02-27 12:30:11 -05:00
Henry Heino
33c4029547 Server: Performance: Improve performance of share maintenance task (#14484) 2026-02-27 14:31:48 +00:00
Laurent Cozic
eb238efc7b Transcribe v3.6.6 2026-02-26 21:08:39 +00:00
Laurent Cozic
513341f103 Transcribe: Upgraded image to node:24-bookworm to fix build issue 2026-02-26 21:08:18 +00:00
Henry Heino
65b7c4be26 Doc: Link to GSoC pull request guidelines in the pull request template (#14478) 2026-02-26 17:45:58 +00:00
Henry Heino
344a3c2605 Server: Fixes #14107: Update item ownership information when the original owner no longer has access (#14469) 2026-02-26 15:25:22 +00:00
Laurent Cozic
3fc724c076 Transcribe v3.6.5 2026-02-26 15:06:58 +00:00
Laurent Cozic
044fab96c2 Chore: Update dictionary 2026-02-26 15:06:36 +00:00
Laurent Cozic
df10bbdf2d Transcribe: Fixed location of llamacpp library 2026-02-26 15:05:38 +00:00
Joplin Bot
65d7d12533 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-02-26 02:14:48 +00:00
Laurent Cozic
814a09035a Transcribe v3.6.3 2026-02-25 21:20:24 +00:00
Laurent Cozic
aadc05bd6c Merge branch 'release-3.5' into dev 2026-02-25 17:50:37 +00:00
Laurent Cozic
0c1511f39e Desktop release v3.5.13 2026-02-25 17:46:43 +00:00
Laurent Cozic
d75d0df88a Chore: Refactor and simplify Transcribe server (#14462) 2026-02-25 17:46:08 +00:00
Henry Heino
2249b3aa7f Desktop: Upgrade tar to v7.5.8 (#14464) 2026-02-25 17:38:10 +00:00
Henry Heino
5d9a6151ea Merge remote-tracking branch 'origin/release-3.5' into dev 2026-02-25 07:41:50 -08:00
Henry Heino
d3ea5bc4a2 Mobile,Desktop: Resolves #13215: Markdown editor: Enable in-editor rendering by default (#13878)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-02-25 08:56:54 +00:00
horvatkm
0ea374cc87 All: Fix status 400 error on Tomcat WebDAV servers (#14332)
Co-authored-by: horvatkm <horvatkm@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
Co-authored-by: mrjo118 <jo.118@hotmail.com>
2026-02-25 08:56:24 +00:00
Sriram Varun Kumar
a53f196cae All: Fixes #14335: Support include_deleted parameter for GET /folders endpoint (#14421) 2026-02-25 08:46:59 +00:00
Ashutosh Singh
7b73b4ba87 Desktop: Resolves #9336: Add editor and sync target to about dialog (#14443) 2026-02-25 08:46:35 +00:00
Henry Heino
99e6d3961f All: Fix unexpected conflicts created during sync (#14453) 2026-02-25 08:45:42 +00:00
renovate[bot]
d0f82fb03b Update dependency nodejs to v24.10.0 (#14454)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-25 08:45:18 +00:00
mrjo118
b1b96e9529 Mobile: Fixes #14452: Make the view / edit note button hidden when an editor plugin is visible (#14458) 2026-02-25 08:42:48 +00:00
Henry Heino
18e178e6cf Chore: Update plugin types (#14457) 2026-02-25 08:41:55 +00:00
Harsh Gupta
075b16a4d2 Desktop: Fixes #12569: Prevent 4th backtick when closing fenced code block (#14423)
Co-authored-by: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com>
2026-02-24 22:19:09 +00:00
mrjo118
6d50a947dd Mobile: Fixes #14387: Reset undo/redo button state when toggling an editor plugin (#14444) 2026-02-24 22:18:14 +00:00
Kanishka..
cb12e4efb0 All: Fixes #12545: Handle missing script assets in HTML export (#14442) 2026-02-24 22:17:46 +00:00
renovate[bot]
c63eac19c9 Update dependency axios to v1.13.0 (#14450)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-24 22:15:30 +00:00
Henry Heino
2544a55373 Chore: Fix incorrect test for versionInfo (#14451) 2026-02-24 22:13:40 +00:00
Henry Heino
932dbbed1a Server: Fix user can incorrectly retain access to shared items in some cases (#14438) 2026-02-24 22:08:15 +00:00
renovate[bot]
af040cbb79 Update dependency @pmmmwh/react-refresh-webpack-plugin to v0.6.2 (#14448)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-24 13:11:46 +00:00
Henry Heino
9833250bea Chore: Sync fuzzer: Include the step at which an action happened in the action log (#14407) 2026-02-24 09:54:05 +00:00
Henry Heino
3bcdc1b362 Server: Fix user can incorrectly retain access to shared items in some cases (#14445)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-02-24 09:50:02 +00:00
renovate[bot]
036e503d39 Update dependency @react-native-community/datetimepicker to v8.5.1 (#14441)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-23 21:15:58 +00:00
Joplin Bot
8667b28db3 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-02-23 19:12:48 +00:00
renovate[bot]
3c317ccdc1 Update dependency @react-native-community/datetimepicker to v8.5.0 (#14439)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-23 19:02:07 +00:00
Laurent Cozic
6a0fc3e36e Doc: Added news item for warrant canary 2026-02-23 18:34:55 +00:00
Laurent Cozic
a95b5744ad Doc: Update sponsors 2026-02-23 16:32:31 +00:00
Sriram Varun Kumar
33eb2f02f8 Desktop: Fixes #14328: "Copy dev mode command to clipboard" does not work when path contains spaces (#14432) 2026-02-23 13:48:55 +00:00
Yugal Kaushik
93732f8df6 Desktop, Cli: Fixes #14139: Remove empty hidden divs from ENEX imports (#14411) 2026-02-22 18:15:17 +00:00
Kanishka..
f589197915 All: Resolves #13216: Move editor settings to dedicated editor section (#14403) 2026-02-22 09:00:16 +00:00
Harsh Gupta
55199244ba Doc: Added video tutorials to documentation pages (#14410) 2026-02-22 08:54:31 +00:00
Ashutosh Singh
6ea3180aee Desktop, Mobile: Resolves #13755: Add waving hand emoji to welcome notebook (#14398) 2026-02-21 11:15:48 +00:00
Laurent Cozic
471bb1bf2b Chore: Fix Code Rabbit auto-labelling 2026-02-20 21:47:30 +00:00
Laurent Cozic
e3948dab24 Desktop: Add context menu to non-image resources in Markdown editor (#14402) 2026-02-20 21:30:32 +00:00
Henry Heino
950cc54bf0 Docs: Add information about the sync fuzzer to the "Debugging the server project" page (#14404) 2026-02-20 21:29:41 +00:00
Anmol Garg
56d43fc3a5 Server: Fixes #14355: Admin emails sorting fails due to invalid user_id column (#14399) 2026-02-20 19:46:39 +00:00
Yugal Kaushik
45ad3ee078 Server: Fixes #14384: Remove warning logged on first startup (#14401)
Co-authored-by: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com>
2026-02-20 19:35:37 +00:00
Laurent Cozic
8b2b0dfd8b Update CLAUDE.md 2026-02-20 19:15:39 +00:00
mrjo118
90de267c62 Desktop, Mobile: Fixes #13660: Defer starting revision service maintenance until the initial sync has completed (#14394) 2026-02-20 11:27:39 +00:00
Henry Heino
085fe0a1cf Server: Remove support for DELTA_INCLUDES_ITEMS (#14393) 2026-02-20 11:26:46 +00:00
Henry Heino
009f3ed692 Desktop,Mobile,Cli: Fixes #14383: Fix unexpected conflicts sometimes created after a full sync (#14388) 2026-02-20 11:26:36 +00:00
Henry Heino
2763a219e4 Chore: Sync fuzzer: Add utilities to allow reproducing a conflict-related share issue (#14382) 2026-02-20 11:26:24 +00:00
Henry Heino
01a51589fd Server: Fixes #14343: Fix certain note content is corrupted when uploaded to the server (#14379) 2026-02-20 11:26:16 +00:00
mrjo118
ba414a4e01 Mobile: Refresh note when updated via the API when in edit mode (#14378) 2026-02-20 11:26:05 +00:00
mrjo118
2da78b37b8 Mobile: Resolves #11521: Remember the viewing / editing mode (updated) (#14363) 2026-02-20 11:22:34 +00:00
Henry Heino
3ef21b0fff Server: Improve name generation for uploaded files (#14392) 2026-02-20 11:04:55 +00:00
Laurent Cozic
77353b015e Desktop: Resolves #12400: Add a text layer over OCR-ed PDF files to make them accessible (#14390) 2026-02-20 11:04:36 +00:00
Laurent Cozic
b6dc7730fc Chore: Update Code Rabbit auto-tagging feature 2026-02-20 10:42:21 +00:00
Laurent Cozic
e96b8d1079 Add comment guideline for duplicated code
Add guideline for commenting on duplicated code.
2026-02-19 22:58:21 +00:00
renovate[bot]
16abb027c2 Update dependency raw-body to v3.0.2 (#14389)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-19 22:24:11 +00:00
Laurent Cozic
8af0c451c6 Chore: Add CLAUDE.md to configure Claude 2026-02-19 21:57:22 +00:00
Joplin Bot
9badf985cb Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-02-19 19:02:02 +00:00
Laurent Cozic
59c6be2234 Doc: Update sponsors 2026-02-19 17:19:42 +00:00
Laurent Cozic
f418b0cc6f Transcribe v3.6.2 2026-02-19 17:03:00 +00:00
Laurent Cozic
9649beea4e Transcribe v3.6.1 2026-02-19 13:58:14 +00:00
Laurent Cozic
440b93f40f Chore: Fixed git-changelog for Transcribe 2026-02-19 13:57:02 +00:00
Laurent Cozic
5beccb9a86 Chore: Refactor Transcribe server (#14381) 2026-02-19 12:51:42 +00:00
Laurent Cozic
8aca7445c7 Doc: Update canary warrant 2026-02-19 10:31:44 +00:00
Laurent Cozic
89f9c6a5e1 Refine instructions for PR labels in .coderabbit.yaml 2026-02-18 23:01:08 +00:00
Laurent Cozic
b240c7fafc Doc: Resolves #13124: Create a warrant canary page (#14375)
Co-authored-by: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com>
2026-02-18 20:45:57 +00:00
renovate[bot]
d893680a84 Update dependency @axe-core/playwright to v4.11.0 (#14377)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-18 20:44:24 +00:00
Laurent Cozic
d1b316516a Chore: Fixed issue with image path in Transcribe 2026-02-18 19:15:37 +00:00
Henry Heino
df7a04f552 Desktop: Importing from OneNote: Enable stricter path sanitization on Windows (#14321) 2026-02-10 10:09:34 +00:00
Henry Heino
8ad1dfa2bf Desktop, Cli: Upgrade tar to v7.5.7 (#14313) 2026-02-10 08:41:12 +00:00
Henry Heino
eeaed07a53 Mobile: Fix heading links (#14201) 2026-02-04 10:09:19 +00:00
Henry Heino
f497d898bc Windows: Fixes #14084: .onepkg file import: Fix import failure when notebook titles contain certain Unicode characters (#14090) 2026-01-26 16:56:39 +00:00
611 changed files with 13880 additions and 6818 deletions

View File

@@ -12,84 +12,99 @@ reviews:
auto_apply_labels: true
labeling_instructions:
- label: "accessibility"
instructions: "Apply when the PR contains changes related to accessibility, screen readers, keyboard navigation, or ARIA attributes."
instructions: "Apply when the PR contains changes related to accessibility, screen readers, keyboard navigation, or ARIA attributes"
- label: "android"
instructions: "Apply when the PR contains changes specific to the Android platform or Android app."
instructions: "Apply when the PR modifies files under packages/app-mobile/android/. Or when the PR modifies files under packages/app-mobile and the change is specific to Android only"
- label: "api"
instructions: "Apply when the PR modifies the Joplin API, REST endpoints, or API-related code."
instructions: "Apply when the PR modifies files under packages/lib/services/rest/"
- label: "bug"
instructions: "Apply when the PR fixes a bug or unexpected behaviour."
instructions: "Apply when the PR fixes a bug or unexpected behaviour"
- label: "ci"
instructions: "Apply when the PR modifies CI/CD configuration, GitHub Actions workflows, or build pipelines."
instructions: "Apply when the PR modifies files under .github/workflows/ or .circleci/"
- label: "cli"
instructions: "Apply when the PR contains changes specific to the Joplin CLI (command-line) application."
instructions: "Apply when the PR modifies files under packages/app-cli/, except if all the modified files are under packages/app-cli/tests/"
- label: "clipper"
instructions: "Apply when the PR contains changes to the Joplin Web Clipper browser extension."
instructions: "Apply when the PR modifies files under packages/app-clipper/"
- label: "database"
instructions: "Apply when the PR modifies database schema, migrations, or database-related logic."
instructions: "Apply when the PR is mainly about modifying database schema, migrations, or database-related logic"
- label: "desktop"
instructions: "Apply when the PR contains changes specific to the Joplin desktop (Electron) application."
instructions: "Apply when the PR modifies files under packages/app-desktop/"
- label: "documentation"
instructions: "Apply when the PR adds or updates documentation, README files, or code comments."
instructions: "Apply when the PR modifies files under readme/"
- label: "draw"
instructions: "Apply when the PR contains changes related to the drawing or sketching feature."
instructions: "Apply when the PR modifies files under packages/default-plugins and relates to the JS-Draw drawing plugin"
- label: "editor"
instructions: "Apply when the PR contains changes to the note editor (CodeMirror, TinyMCE, or the editor infrastructure)."
instructions: "Apply when the PR modifies files under packages/editor/ or packages/app-mobile/components/NoteEditor/"
- label: "enhancement"
instructions: "Apply when the PR adds a new feature or improves existing functionality (not a bug fix)."
instructions: "Apply when the PR adds a new feature or improves existing functionality (not a bug fix)"
- label: "export"
instructions: "Apply when the PR contains changes to export functionality (PDF, HTML, JEX, etc.)."
instructions: "Apply when the PR is mainly about changes to the export functionality (PDF, HTML, JEX, etc.)"
- label: "import"
instructions: "Apply when the PR contains changes to import functionality (Evernote, Markdown, etc.)."
instructions: "Apply when the PR is mainly about changes to the import functionality (Evernote, Markdown, etc.)"
- label: "iOS"
instructions: "Apply when the PR contains changes specific to the iOS platform or iOS app."
instructions: "Apply when the PR modifies files under packages/app-mobile/ios/. Or when the PR modifies files under packages/app-mobile and the change is specific to iOS only"
- label: "linux"
instructions: "Apply when the PR contains changes specific to Linux."
instructions: "Apply when the PR is mainly about changes specific to Linux"
- label: "linux/wayland"
instructions: "Apply when the PR contains changes specific to Linux Wayland."
instructions: "Apply when the PR is mainly about changes specific to Linux Wayland"
- label: "macOS"
instructions: "Apply when the PR contains changes specific to macOS."
instructions: "Apply when the PR is mainly about changes specific to macOS"
- label: "markdown-editor"
instructions: "Apply when the PR contains changes to the Markdown editor or Markdown rendering."
instructions: "Apply when the PR modifies files under packages/editor/CodeMirror"
- label: "mobile"
instructions: "Apply when the PR contains changes to the mobile app (iOS or Android)."
- label: "multi-window"
instructions: "Apply when the PR contains changes related to multi-window support."
instructions: "Apply when the PR modifies files under packages/app-mobile/"
- label: "OCR"
instructions: "Apply when the PR contains changes related to OCR (optical character recognition) functionality."
instructions: "Apply when the PR contains changes related to OCR (optical character recognition) functionality"
- label: "performance"
instructions: "Apply when the PR improves performance, reduces memory usage, or optimises speed."
instructions: "Apply when the PR improves performance, reduces memory usage, or optimises speed"
- label: "plugins"
instructions: "Apply when the PR contains changes to the plugin system, plugin API, or specific plugins."
instructions: "Apply when the PR modifies files under packages/lib/services/plugins/ or packages/plugin-repo-cli/"
- label: "Regression"
instructions: "Apply when the linked issue, if any, has the Regression label."
instructions: "Apply when the linked issue, if any, has the Regression label"
- label: "renderer"
instructions: "Apply when the PR contains changes to the note renderer or how notes are displayed."
instructions: "Apply when the PR modifies files under packages/renderer/ or packages/turndown/"
- label: "search"
instructions: "Apply when the PR contains changes to search functionality."
instructions: "Apply when the PR is mainly about changes to the search functionality"
- label: "security"
instructions: "Apply when the PR addresses a security vulnerability or improves security."
instructions: "Apply when the PR is mainly about addressing a security vulnerability or improving security"
- label: "server"
instructions: "Apply when the PR contains changes to Joplin Server."
instructions: "Apply when the PR modifies files under packages/server/"
- label: "Sharing"
instructions: "Apply when the PR contains changes to note or notebook sharing features."
instructions: "Apply when the PR is mainly about changes to the note or notebook/folder sharing features"
- label: "sync"
instructions: "Apply when the PR contains changes to synchronisation logic or sync targets."
instructions: "Apply when the PR modifies files under packages/lib/services/synchronizer/, packages/lib/Sync*.ts or packages/lib/services/e2ee/"
- label: "tags"
instructions: "Apply when the PR contains changes to tag management or tagging functionality."
instructions: "Apply when the PR is mainly about changes to the tag management or tagging functionality"
- label: "transcribe"
instructions: "Apply when the PR contains changes to audio transcription functionality."
instructions: "Apply when the PR modifies files under packages/transcribe"
- label: "translation"
instructions: "Apply when the PR adds or updates translations or localisation strings."
instructions: "Apply when the PR modifies files under packages/tools/locales/ or **/locales/"
- label: "Voice typing"
instructions: "Apply when the PR contains changes to voice typing functionality."
instructions: "Apply when the PR is mainly about changes to the voice typing functionality"
- label: "web"
instructions: "Apply when the PR contains changes to the Joplin web application or web-related features."
instructions: "Apply when the PR modifies files under packages/app-web/. Or when the PR modifies files under packages/app-mobile and the change is specific to the web app only"
- label: "windows"
instructions: "Apply when the PR contains changes specific to Windows."
instructions: "Apply when the PR is mainly about changes specific to Windows"
pre_merge_checks:
description:
mode: "warning"
custom_checks:
- name: "PR Description Must Follow Guidelines"
mode: "error"
instructions: |
Fail if the pull request description does not include clear sections for:
- Problem or user-impact description
- A high-level Solution explanation
- Any Test Plan or verification steps
The description should align with our PR guidelines
at https://github.com/joplin/gsoc/blob/master/pull_request_guidelines.md
and should not just restate the diff or implementation details.
knowledge_base:
code_guidelines:
enabled: true
filePatterns:
- "readme/dev/coding_style.md"
- "readme/dev/index.md"
- "CLAUDE.md"

View File

@@ -17,3 +17,4 @@ packages/server/db-*.sqlite
packages/server/dist/
packages/server/logs/
packages/server/temp/
packages/transcribe/.env

View File

@@ -31,6 +31,7 @@
# QUEUE_DATABASE_PASSWORD=transcribe
# QUEUE_DATABASE_PORT=5431
# HTR_CLI_IMAGES_FOLDER=/home/user/images_storage
# HTR_CLI_MODELS_FOLDER=/home/user/transcribe_models
# =============================================================================
# DEV CONFIG EXAMPLE

View File

@@ -1,35 +1,33 @@
# Joplin Transcribe Configuration
#
# Copy this file to .env-transcribe and update the values.
# =============================================================================
# Required
# -----------------------------------------------------------------------------
# =============================================================================
SERVER_PORT=4567
# Set a secure API key for authentication
API_KEY=changeme
API_KEY=random-string
QUEUE_TTL=900000
QUEUE_RETRY_COUNT=2
QUEUE_MAINTENANCE_INTERVAL=30000
IMAGE_MAX_DIMENSION=400
# =============================================================================
# Optional (defaults are set in the Docker image)
# =============================================================================
HTR_CLI_DOCKER_IMAGE=joplin/htr-cli:latest
# Fullpath to images folder e.g.:
#HTR_CLI_IMAGES_FOLDER=/home/user/joplin/packages/transcribe/images
HTR_CLI_IMAGES_FOLDER=
# Server port (default: 4567)
# SERVER_PORT=4567
QUEUE_DRIVER=pg
# Maximum image dimension for processing (default: 400)
# IMAGE_MAX_DIMENSION=400
# Queue driver: sqlite (default) or pg
# QUEUE_DRIVER=sqlite
FILE_STORAGE_MAINTENANCE_INTERVAL=3600000
FILE_STORAGE_TTL=604800000 # one week
# =============================================================================
# PostgreSQL settings (only if QUEUE_DRIVER=pg)
# =============================================================================
# =============================================================================
# Queue driver
# -----------------------------------------------------------------------------
# =============================================================================
#
# QUEUE_DATABASE_NAME=./queue.sqlite3
QUEUE_DATABASE_NAME=transcribe
QUEUE_DATABASE_USER=transcribe
QUEUE_DATABASE_PASSWORD=transcribe
QUEUE_DATABASE_PORT=5432
QUEUE_DATABASE_HOST=localhost
# QUEUE_DATABASE_NAME=transcribe
# QUEUE_DATABASE_USER=transcribe
# QUEUE_DATABASE_PASSWORD=transcribe
# QUEUE_DATABASE_PORT=5432
# QUEUE_DATABASE_HOST=localhost

View File

@@ -103,6 +103,7 @@ 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.js
packages/app-cli/app/command-clear.js
packages/app-cli/app/command-config.js
packages/app-cli/app/command-cp.js
packages/app-cli/app/command-done.test.js
@@ -169,6 +170,7 @@ packages/app-desktop/bridge.js
packages/app-desktop/checkForUpdates.js
packages/app-desktop/commands/copyDevCommand.js
packages/app-desktop/commands/copyToClipboard.js
packages/app-desktop/commands/createAccessibleDocument.js
packages/app-desktop/commands/editProfileConfig.js
packages/app-desktop/commands/emptyTrash.js
packages/app-desktop/commands/exportDeletionLog.test.js
@@ -217,6 +219,8 @@ packages/app-desktop/gui/EditFolderDialog/Dialog.js
packages/app-desktop/gui/EditFolderDialog/IconSelector.js
packages/app-desktop/gui/EmojiBox.js
packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.js
packages/app-desktop/gui/EncryptionConfigScreen/enableFlow.test.js
packages/app-desktop/gui/EncryptionConfigScreen/enableFlow.js
packages/app-desktop/gui/ErrorBoundary.js
packages/app-desktop/gui/ExtensionBadge.js
packages/app-desktop/gui/FolderIconBox.js
@@ -270,6 +274,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/Editor.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/useEditorCommands.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/localisation.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useContentScriptRegistration.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useEditorSettings.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useKeymap.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useRefocusOnVisiblePaneChange.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useSyncEditorValue.js
@@ -302,6 +307,7 @@ packages/app-desktop/gui/NoteEditor/commands/focusElementNoteTitle.js
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteViewer.js
packages/app-desktop/gui/NoteEditor/commands/focusElementToolbar.js
packages/app-desktop/gui/NoteEditor/commands/index.js
packages/app-desktop/gui/NoteEditor/commands/pasteAsMarkdown.js
packages/app-desktop/gui/NoteEditor/commands/pasteAsText.js
packages/app-desktop/gui/NoteEditor/commands/showLocalSearch.js
packages/app-desktop/gui/NoteEditor/commands/showRevisions.js
@@ -339,9 +345,11 @@ packages/app-desktop/gui/NoteEditor/utils/useWindowCommandHandler.js
packages/app-desktop/gui/NoteList/NoteList2.js
packages/app-desktop/gui/NoteList/commands/focusElementNoteList.js
packages/app-desktop/gui/NoteList/commands/index.js
packages/app-desktop/gui/NoteList/utils/UseAutoScroll.test.js
packages/app-desktop/gui/NoteList/utils/canManuallySortNotes.js
packages/app-desktop/gui/NoteList/utils/types.js
packages/app-desktop/gui/NoteList/utils/useActiveDescendantId.js
packages/app-desktop/gui/NoteList/utils/useAutoScroll.js
packages/app-desktop/gui/NoteList/utils/useDragAndDrop.js
packages/app-desktop/gui/NoteList/utils/useFocusNote.js
packages/app-desktop/gui/NoteList/utils/useFocusVisible.js
@@ -350,6 +358,8 @@ packages/app-desktop/gui/NoteList/utils/useMoveNote.js
packages/app-desktop/gui/NoteList/utils/useOnKeyDown.js
packages/app-desktop/gui/NoteList/utils/useOnNoteClick.js
packages/app-desktop/gui/NoteList/utils/useOnNoteDoubleClick.js
packages/app-desktop/gui/NoteList/utils/useRefocusOnDeletion.test.js
packages/app-desktop/gui/NoteList/utils/useRefocusOnDeletion.js
packages/app-desktop/gui/NoteList/utils/useScroll.js
packages/app-desktop/gui/NoteList/utils/useVisibleRange.test.js
packages/app-desktop/gui/NoteList/utils/useVisibleRange.js
@@ -371,8 +381,8 @@ packages/app-desktop/gui/NoteListItem/utils/getNoteTitleHtml.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
packages/app-desktop/gui/NoteListItem/utils/useItemElement.test.js
packages/app-desktop/gui/NoteListItem/utils/useItemElement.js
packages/app-desktop/gui/NoteListItem/utils/useItemEventHandlers.js
packages/app-desktop/gui/NoteListItem/utils/useOnContextMenu.js
packages/app-desktop/gui/NoteListItem/utils/useRenderedNote.js
packages/app-desktop/gui/NoteListItem/utils/useRootElement.test.js
@@ -558,6 +568,7 @@ packages/app-desktop/integration-tests/models/NoteEditorScreen.js
packages/app-desktop/integration-tests/models/NoteList.js
packages/app-desktop/integration-tests/models/SettingsScreen.js
packages/app-desktop/integration-tests/models/Sidebar.js
packages/app-desktop/integration-tests/multiWindow.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
@@ -579,6 +590,7 @@ packages/app-desktop/integration-tests/util/setMessageBoxResponse.js
packages/app-desktop/integration-tests/util/setSettingValue.js
packages/app-desktop/integration-tests/util/test.js
packages/app-desktop/integration-tests/util/waitForNextOpenPath.js
packages/app-desktop/integration-tests/util/waitForNextWindowMatching.js
packages/app-desktop/integration-tests/wcag.spec.js
packages/app-desktop/main-html.js
packages/app-desktop/main.js
@@ -607,10 +619,6 @@ packages/app-desktop/services/plugins/hooks/useViewIsReady.js
packages/app-desktop/services/plugins/hooks/useWebviewToPluginMessages.js
packages/app-desktop/services/plugins/types.js
packages/app-desktop/services/restart.js
packages/app-desktop/services/sortOrder/PerFolderSortOrderService.test.js
packages/app-desktop/services/sortOrder/PerFolderSortOrderService.js
packages/app-desktop/services/sortOrder/notesSortOrderUtils.test.js
packages/app-desktop/services/sortOrder/notesSortOrderUtils.js
packages/app-desktop/services/spellChecker/SpellCheckerServiceDriverNative.js
packages/app-desktop/tools/bundleJs.js
packages/app-desktop/tools/copy7Zip.js
@@ -646,6 +654,7 @@ packages/app-mobile/commands/newNote.js
packages/app-mobile/commands/openItem.js
packages/app-mobile/commands/openNote.js
packages/app-mobile/commands/scrollToHash.js
packages/app-mobile/commands/util/goToFolder.js
packages/app-mobile/commands/util/goToNote.js
packages/app-mobile/commands/util/showResource.js
packages/app-mobile/components/BetaChip.js
@@ -689,13 +698,16 @@ packages/app-mobile/components/EditorToolbar/utils/isSelected.js
packages/app-mobile/components/EditorToolbar/utils/selectedCommandNamesFromState.js
packages/app-mobile/components/EditorToolbar/utils/toolbarButtonsFromState.js
packages/app-mobile/components/EditorToolbar/utils/useButtonSize.js
packages/app-mobile/components/EditorToolbar/utils/useSaveToolbarButtons.test.js
packages/app-mobile/components/EditorToolbar/utils/useSaveToolbarButtons.js
packages/app-mobile/components/EditorToolbar/utils/useToolbarEditorState.test.js
packages/app-mobile/components/EditorToolbar/utils/useToolbarEditorState.js
packages/app-mobile/components/ExtendedWebView/index.jest.js
packages/app-mobile/components/ExtendedWebView/index.js
packages/app-mobile/components/ExtendedWebView/index.web.js
packages/app-mobile/components/ExtendedWebView/types.js
packages/app-mobile/components/ExtendedWebView/utils/polyfillScrollFunctions.js
packages/app-mobile/components/ExtendedWebView/utils/useCss.js
packages/app-mobile/components/FeedbackBanner.test.js
packages/app-mobile/components/FeedbackBanner.js
packages/app-mobile/components/FolderPicker.js
packages/app-mobile/components/Icon.js
packages/app-mobile/components/IconButton.js
@@ -740,7 +752,6 @@ packages/app-mobile/components/ScreenHeader/Menu.js
packages/app-mobile/components/ScreenHeader/WarningBanner.test.js
packages/app-mobile/components/ScreenHeader/WarningBanner.js
packages/app-mobile/components/ScreenHeader/WarningBox.js
packages/app-mobile/components/ScreenHeader/WebBetaButton.js
packages/app-mobile/components/ScreenHeader/index.js
packages/app-mobile/components/SearchInput.js
packages/app-mobile/components/SelectDateTimeDialog.js
@@ -866,6 +877,7 @@ packages/app-mobile/components/screens/Notes/NewNoteButton.test.js
packages/app-mobile/components/screens/Notes/NewNoteButton.js
packages/app-mobile/components/screens/Notes/Notes.js
packages/app-mobile/components/screens/Notes/TextWrapCalculator.js
packages/app-mobile/components/screens/ResourceScreen.js
packages/app-mobile/components/screens/SearchScreen/SearchBar.js
packages/app-mobile/components/screens/SearchScreen/SearchResults.test.js
packages/app-mobile/components/screens/SearchScreen/SearchResults.js
@@ -882,6 +894,8 @@ packages/app-mobile/components/screens/dropbox-login.js
packages/app-mobile/components/screens/encryption-config.test.js
packages/app-mobile/components/screens/encryption-config.js
packages/app-mobile/components/screens/folder.js
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
@@ -904,6 +918,7 @@ packages/app-mobile/contentScripts/markdownEditorBundle/useWebViewSetup.js
packages/app-mobile/contentScripts/markdownEditorBundle/utils/useCodeMirrorPlugins.js
packages/app-mobile/contentScripts/rendererBundle/contentScript/Renderer.test.js
packages/app-mobile/contentScripts/rendererBundle/contentScript/Renderer.js
packages/app-mobile/contentScripts/rendererBundle/contentScript/index.handleAnchorClick.test.js
packages/app-mobile/contentScripts/rendererBundle/contentScript/index.js
packages/app-mobile/contentScripts/rendererBundle/contentScript/types.js
packages/app-mobile/contentScripts/rendererBundle/contentScript/utils/addPluginAssets.js
@@ -1265,6 +1280,7 @@ packages/lib/SyncTargetRegistry.js
packages/lib/Synchronizer.js
packages/lib/TaskQueue.js
packages/lib/WebDavApi.js
packages/lib/WelcomeUtils.test.js
packages/lib/WelcomeUtils.js
packages/lib/array.js
packages/lib/callbackUrlUtils.test.js
@@ -1293,6 +1309,7 @@ packages/lib/components/EncryptionConfigScreen/utils.test.js
packages/lib/components/EncryptionConfigScreen/utils.js
packages/lib/components/shared/NoteEditor/WarningBanner/onRichTextDismissLinkClick.js
packages/lib/components/shared/NoteEditor/WarningBanner/onRichTextReadMoreLinkClick.js
packages/lib/components/shared/NoteEditor/WarningBanner/useEditorTypeMigrationBanner.js
packages/lib/components/shared/NoteList/getEmptyFolderMessage.js
packages/lib/components/shared/NoteRevisionViewer/getHelpMessage.js
packages/lib/components/shared/NoteRevisionViewer/useDeleteHistoryClick.js
@@ -1431,7 +1448,9 @@ packages/lib/services/AlarmServiceDriverNode.js
packages/lib/services/BaseService.js
packages/lib/services/CommandService.test.js
packages/lib/services/CommandService.js
packages/lib/services/DecryptionWorker.test.js
packages/lib/services/DecryptionWorker.js
packages/lib/services/ExternalEditWatcher.test.js
packages/lib/services/ExternalEditWatcher.js
packages/lib/services/ExternalEditWatcher/utils.js
packages/lib/services/ItemChangeUtils.js
@@ -1519,6 +1538,7 @@ 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_OneNote.postprocessHtml.test.js
packages/lib/services/interop/InteropService_Importer_OneNote.test.js
packages/lib/services/interop/InteropService_Importer_OneNote.js
packages/lib/services/interop/InteropService_Importer_Raw.test.js
@@ -1552,6 +1572,7 @@ packages/lib/services/ocr/OcrService.js
packages/lib/services/ocr/drivers/OcrDriverTesseract.js
packages/lib/services/ocr/drivers/OcrDriverTranscribe.test.js
packages/lib/services/ocr/drivers/OcrDriverTranscribe.js
packages/lib/services/ocr/utils/createAccessiblePdf.js
packages/lib/services/ocr/utils/filterOcrText.test.js
packages/lib/services/ocr/utils/filterOcrText.js
packages/lib/services/ocr/utils/types.js
@@ -1573,6 +1594,7 @@ packages/lib/services/plugins/api/JoplinCommands.js
packages/lib/services/plugins/api/JoplinContentScripts.js
packages/lib/services/plugins/api/JoplinData.js
packages/lib/services/plugins/api/JoplinFilters.js
packages/lib/services/plugins/api/JoplinFs.js
packages/lib/services/plugins/api/JoplinImaging.js
packages/lib/services/plugins/api/JoplinInterop.js
packages/lib/services/plugins/api/JoplinPlugins.js
@@ -1671,6 +1693,10 @@ packages/lib/services/share/ShareService.test.js
packages/lib/services/share/ShareService.js
packages/lib/services/share/invitationRespond.js
packages/lib/services/share/reducer.js
packages/lib/services/sortOrder/PerFolderSortOrderService.test.js
packages/lib/services/sortOrder/PerFolderSortOrderService.js
packages/lib/services/sortOrder/notesSortOrderUtils.test.js
packages/lib/services/sortOrder/notesSortOrderUtils.js
packages/lib/services/spellChecker/SpellCheckerService.js
packages/lib/services/spellChecker/SpellCheckerServiceDriverBase.js
packages/lib/services/style/cssToTheme.test.js
@@ -1889,11 +1915,11 @@ packages/tools/fuzzer/utils/SeededRandom.js
packages/tools/fuzzer/utils/diffSortedStringArrays.test.js
packages/tools/fuzzer/utils/diffSortedStringArrays.js
packages/tools/fuzzer/utils/extractResourceIds.js
packages/tools/fuzzer/utils/getBinaryDiffDebugMessage.js
packages/tools/fuzzer/utils/getNumberProperty.js
packages/tools/fuzzer/utils/getProperty.js
packages/tools/fuzzer/utils/getStringProperty.js
packages/tools/fuzzer/utils/hangingIndent.js
packages/tools/fuzzer/utils/logDiffDebug.js
packages/tools/fuzzer/utils/openDebugSession.js
packages/tools/fuzzer/utils/randomId.test.js
packages/tools/fuzzer/utils/randomId.js
@@ -1940,6 +1966,7 @@ packages/tools/update-readme-contributors.js
packages/tools/update-readme-download.test.js
packages/tools/update-readme-download.js
packages/tools/update-readme-sponsors.js
packages/tools/updateCanary.js
packages/tools/updateMarkdownDoc.js
packages/tools/utils/discourse.test.js
packages/tools/utils/discourse.js

View File

@@ -214,7 +214,6 @@ module.exports = {
'packages/tools/**',
'packages/app-mobile/tools/**',
'packages/app-desktop/tools/**',
'packages/transcribe/src/tools/**',
],
'rules': {
'no-console': 'off',

View File

@@ -1,6 +1,12 @@
<!--
Please prefix the title with the platform you are targetting:
Before contributing, please read the contribution guidelines: https://github.com/laurent22/joplin/blob/dev/readme/dev/index.md
If this is a Google Summer of Code pull request, please read the [GSoC pull request guidelines](https://github.com/joplin/gsoc/blob/master/pull_request_guidelines.md).
---
**Pull request title**: Please prefix the title with the platform you are targetting.
Here are some examples of good titles:
@@ -20,6 +26,4 @@ If it's not related to any platform (such as a translation, change to the docume
Then please append the issue that you've addressed or fixed. Use "Resolves #123" for new features or improvements and "Fixes #123" for bug fixes.
AND PLEASE READ THE GUIDE: https://github.com/laurent22/joplin/blob/dev/readme/dev/index.md
-->

View File

@@ -1,5 +1,6 @@
name: Build macOS M1
on: [push, pull_request]
jobs:
Main:
# We always process desktop release tags, because they also publish the release

View File

@@ -4,6 +4,6 @@ jobs:
main:
runs-on: ubuntu-latest
steps:
- uses: Slashgear/action-check-pr-title@v4.3.0
- uses: Slashgear/action-check-pr-title@v5.0.1
with:
regexp: "(Desktop|Mobile|All|Cli|Tools|Chore|Clipper|Server|Android|iOS|Plugins|CI|Plugin Repo|Doc): (Fixes|Resolves) #[0-9]+: .+"

View File

@@ -1,5 +1,6 @@
name: Joplin Continuous Integration
on: [push, pull_request]
jobs:
Main:
# We always process server or desktop release tags, because they also publish the release

View File

@@ -1,5 +1,6 @@
name: Joplin UI tests
on: [push, pull_request]
permissions:
contents: read
jobs:

46
.gitignore vendored
View File

@@ -76,6 +76,7 @@ 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.js
packages/app-cli/app/command-clear.js
packages/app-cli/app/command-config.js
packages/app-cli/app/command-cp.js
packages/app-cli/app/command-done.test.js
@@ -142,6 +143,7 @@ packages/app-desktop/bridge.js
packages/app-desktop/checkForUpdates.js
packages/app-desktop/commands/copyDevCommand.js
packages/app-desktop/commands/copyToClipboard.js
packages/app-desktop/commands/createAccessibleDocument.js
packages/app-desktop/commands/editProfileConfig.js
packages/app-desktop/commands/emptyTrash.js
packages/app-desktop/commands/exportDeletionLog.test.js
@@ -190,6 +192,8 @@ packages/app-desktop/gui/EditFolderDialog/Dialog.js
packages/app-desktop/gui/EditFolderDialog/IconSelector.js
packages/app-desktop/gui/EmojiBox.js
packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.js
packages/app-desktop/gui/EncryptionConfigScreen/enableFlow.test.js
packages/app-desktop/gui/EncryptionConfigScreen/enableFlow.js
packages/app-desktop/gui/ErrorBoundary.js
packages/app-desktop/gui/ExtensionBadge.js
packages/app-desktop/gui/FolderIconBox.js
@@ -243,6 +247,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/Editor.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/useEditorCommands.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/localisation.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useContentScriptRegistration.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useEditorSettings.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useKeymap.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useRefocusOnVisiblePaneChange.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useSyncEditorValue.js
@@ -275,6 +280,7 @@ packages/app-desktop/gui/NoteEditor/commands/focusElementNoteTitle.js
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteViewer.js
packages/app-desktop/gui/NoteEditor/commands/focusElementToolbar.js
packages/app-desktop/gui/NoteEditor/commands/index.js
packages/app-desktop/gui/NoteEditor/commands/pasteAsMarkdown.js
packages/app-desktop/gui/NoteEditor/commands/pasteAsText.js
packages/app-desktop/gui/NoteEditor/commands/showLocalSearch.js
packages/app-desktop/gui/NoteEditor/commands/showRevisions.js
@@ -312,9 +318,11 @@ packages/app-desktop/gui/NoteEditor/utils/useWindowCommandHandler.js
packages/app-desktop/gui/NoteList/NoteList2.js
packages/app-desktop/gui/NoteList/commands/focusElementNoteList.js
packages/app-desktop/gui/NoteList/commands/index.js
packages/app-desktop/gui/NoteList/utils/UseAutoScroll.test.js
packages/app-desktop/gui/NoteList/utils/canManuallySortNotes.js
packages/app-desktop/gui/NoteList/utils/types.js
packages/app-desktop/gui/NoteList/utils/useActiveDescendantId.js
packages/app-desktop/gui/NoteList/utils/useAutoScroll.js
packages/app-desktop/gui/NoteList/utils/useDragAndDrop.js
packages/app-desktop/gui/NoteList/utils/useFocusNote.js
packages/app-desktop/gui/NoteList/utils/useFocusVisible.js
@@ -323,6 +331,8 @@ packages/app-desktop/gui/NoteList/utils/useMoveNote.js
packages/app-desktop/gui/NoteList/utils/useOnKeyDown.js
packages/app-desktop/gui/NoteList/utils/useOnNoteClick.js
packages/app-desktop/gui/NoteList/utils/useOnNoteDoubleClick.js
packages/app-desktop/gui/NoteList/utils/useRefocusOnDeletion.test.js
packages/app-desktop/gui/NoteList/utils/useRefocusOnDeletion.js
packages/app-desktop/gui/NoteList/utils/useScroll.js
packages/app-desktop/gui/NoteList/utils/useVisibleRange.test.js
packages/app-desktop/gui/NoteList/utils/useVisibleRange.js
@@ -344,8 +354,8 @@ packages/app-desktop/gui/NoteListItem/utils/getNoteTitleHtml.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
packages/app-desktop/gui/NoteListItem/utils/useItemElement.test.js
packages/app-desktop/gui/NoteListItem/utils/useItemElement.js
packages/app-desktop/gui/NoteListItem/utils/useItemEventHandlers.js
packages/app-desktop/gui/NoteListItem/utils/useOnContextMenu.js
packages/app-desktop/gui/NoteListItem/utils/useRenderedNote.js
packages/app-desktop/gui/NoteListItem/utils/useRootElement.test.js
@@ -531,6 +541,7 @@ packages/app-desktop/integration-tests/models/NoteEditorScreen.js
packages/app-desktop/integration-tests/models/NoteList.js
packages/app-desktop/integration-tests/models/SettingsScreen.js
packages/app-desktop/integration-tests/models/Sidebar.js
packages/app-desktop/integration-tests/multiWindow.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
@@ -552,6 +563,7 @@ packages/app-desktop/integration-tests/util/setMessageBoxResponse.js
packages/app-desktop/integration-tests/util/setSettingValue.js
packages/app-desktop/integration-tests/util/test.js
packages/app-desktop/integration-tests/util/waitForNextOpenPath.js
packages/app-desktop/integration-tests/util/waitForNextWindowMatching.js
packages/app-desktop/integration-tests/wcag.spec.js
packages/app-desktop/main-html.js
packages/app-desktop/main.js
@@ -580,10 +592,6 @@ packages/app-desktop/services/plugins/hooks/useViewIsReady.js
packages/app-desktop/services/plugins/hooks/useWebviewToPluginMessages.js
packages/app-desktop/services/plugins/types.js
packages/app-desktop/services/restart.js
packages/app-desktop/services/sortOrder/PerFolderSortOrderService.test.js
packages/app-desktop/services/sortOrder/PerFolderSortOrderService.js
packages/app-desktop/services/sortOrder/notesSortOrderUtils.test.js
packages/app-desktop/services/sortOrder/notesSortOrderUtils.js
packages/app-desktop/services/spellChecker/SpellCheckerServiceDriverNative.js
packages/app-desktop/tools/bundleJs.js
packages/app-desktop/tools/copy7Zip.js
@@ -619,6 +627,7 @@ packages/app-mobile/commands/newNote.js
packages/app-mobile/commands/openItem.js
packages/app-mobile/commands/openNote.js
packages/app-mobile/commands/scrollToHash.js
packages/app-mobile/commands/util/goToFolder.js
packages/app-mobile/commands/util/goToNote.js
packages/app-mobile/commands/util/showResource.js
packages/app-mobile/components/BetaChip.js
@@ -662,13 +671,16 @@ packages/app-mobile/components/EditorToolbar/utils/isSelected.js
packages/app-mobile/components/EditorToolbar/utils/selectedCommandNamesFromState.js
packages/app-mobile/components/EditorToolbar/utils/toolbarButtonsFromState.js
packages/app-mobile/components/EditorToolbar/utils/useButtonSize.js
packages/app-mobile/components/EditorToolbar/utils/useSaveToolbarButtons.test.js
packages/app-mobile/components/EditorToolbar/utils/useSaveToolbarButtons.js
packages/app-mobile/components/EditorToolbar/utils/useToolbarEditorState.test.js
packages/app-mobile/components/EditorToolbar/utils/useToolbarEditorState.js
packages/app-mobile/components/ExtendedWebView/index.jest.js
packages/app-mobile/components/ExtendedWebView/index.js
packages/app-mobile/components/ExtendedWebView/index.web.js
packages/app-mobile/components/ExtendedWebView/types.js
packages/app-mobile/components/ExtendedWebView/utils/polyfillScrollFunctions.js
packages/app-mobile/components/ExtendedWebView/utils/useCss.js
packages/app-mobile/components/FeedbackBanner.test.js
packages/app-mobile/components/FeedbackBanner.js
packages/app-mobile/components/FolderPicker.js
packages/app-mobile/components/Icon.js
packages/app-mobile/components/IconButton.js
@@ -713,7 +725,6 @@ packages/app-mobile/components/ScreenHeader/Menu.js
packages/app-mobile/components/ScreenHeader/WarningBanner.test.js
packages/app-mobile/components/ScreenHeader/WarningBanner.js
packages/app-mobile/components/ScreenHeader/WarningBox.js
packages/app-mobile/components/ScreenHeader/WebBetaButton.js
packages/app-mobile/components/ScreenHeader/index.js
packages/app-mobile/components/SearchInput.js
packages/app-mobile/components/SelectDateTimeDialog.js
@@ -839,6 +850,7 @@ packages/app-mobile/components/screens/Notes/NewNoteButton.test.js
packages/app-mobile/components/screens/Notes/NewNoteButton.js
packages/app-mobile/components/screens/Notes/Notes.js
packages/app-mobile/components/screens/Notes/TextWrapCalculator.js
packages/app-mobile/components/screens/ResourceScreen.js
packages/app-mobile/components/screens/SearchScreen/SearchBar.js
packages/app-mobile/components/screens/SearchScreen/SearchResults.test.js
packages/app-mobile/components/screens/SearchScreen/SearchResults.js
@@ -855,6 +867,8 @@ packages/app-mobile/components/screens/dropbox-login.js
packages/app-mobile/components/screens/encryption-config.test.js
packages/app-mobile/components/screens/encryption-config.js
packages/app-mobile/components/screens/folder.js
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
@@ -877,6 +891,7 @@ packages/app-mobile/contentScripts/markdownEditorBundle/useWebViewSetup.js
packages/app-mobile/contentScripts/markdownEditorBundle/utils/useCodeMirrorPlugins.js
packages/app-mobile/contentScripts/rendererBundle/contentScript/Renderer.test.js
packages/app-mobile/contentScripts/rendererBundle/contentScript/Renderer.js
packages/app-mobile/contentScripts/rendererBundle/contentScript/index.handleAnchorClick.test.js
packages/app-mobile/contentScripts/rendererBundle/contentScript/index.js
packages/app-mobile/contentScripts/rendererBundle/contentScript/types.js
packages/app-mobile/contentScripts/rendererBundle/contentScript/utils/addPluginAssets.js
@@ -1238,6 +1253,7 @@ packages/lib/SyncTargetRegistry.js
packages/lib/Synchronizer.js
packages/lib/TaskQueue.js
packages/lib/WebDavApi.js
packages/lib/WelcomeUtils.test.js
packages/lib/WelcomeUtils.js
packages/lib/array.js
packages/lib/callbackUrlUtils.test.js
@@ -1266,6 +1282,7 @@ packages/lib/components/EncryptionConfigScreen/utils.test.js
packages/lib/components/EncryptionConfigScreen/utils.js
packages/lib/components/shared/NoteEditor/WarningBanner/onRichTextDismissLinkClick.js
packages/lib/components/shared/NoteEditor/WarningBanner/onRichTextReadMoreLinkClick.js
packages/lib/components/shared/NoteEditor/WarningBanner/useEditorTypeMigrationBanner.js
packages/lib/components/shared/NoteList/getEmptyFolderMessage.js
packages/lib/components/shared/NoteRevisionViewer/getHelpMessage.js
packages/lib/components/shared/NoteRevisionViewer/useDeleteHistoryClick.js
@@ -1404,7 +1421,9 @@ packages/lib/services/AlarmServiceDriverNode.js
packages/lib/services/BaseService.js
packages/lib/services/CommandService.test.js
packages/lib/services/CommandService.js
packages/lib/services/DecryptionWorker.test.js
packages/lib/services/DecryptionWorker.js
packages/lib/services/ExternalEditWatcher.test.js
packages/lib/services/ExternalEditWatcher.js
packages/lib/services/ExternalEditWatcher/utils.js
packages/lib/services/ItemChangeUtils.js
@@ -1492,6 +1511,7 @@ 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_OneNote.postprocessHtml.test.js
packages/lib/services/interop/InteropService_Importer_OneNote.test.js
packages/lib/services/interop/InteropService_Importer_OneNote.js
packages/lib/services/interop/InteropService_Importer_Raw.test.js
@@ -1525,6 +1545,7 @@ packages/lib/services/ocr/OcrService.js
packages/lib/services/ocr/drivers/OcrDriverTesseract.js
packages/lib/services/ocr/drivers/OcrDriverTranscribe.test.js
packages/lib/services/ocr/drivers/OcrDriverTranscribe.js
packages/lib/services/ocr/utils/createAccessiblePdf.js
packages/lib/services/ocr/utils/filterOcrText.test.js
packages/lib/services/ocr/utils/filterOcrText.js
packages/lib/services/ocr/utils/types.js
@@ -1546,6 +1567,7 @@ packages/lib/services/plugins/api/JoplinCommands.js
packages/lib/services/plugins/api/JoplinContentScripts.js
packages/lib/services/plugins/api/JoplinData.js
packages/lib/services/plugins/api/JoplinFilters.js
packages/lib/services/plugins/api/JoplinFs.js
packages/lib/services/plugins/api/JoplinImaging.js
packages/lib/services/plugins/api/JoplinInterop.js
packages/lib/services/plugins/api/JoplinPlugins.js
@@ -1644,6 +1666,10 @@ packages/lib/services/share/ShareService.test.js
packages/lib/services/share/ShareService.js
packages/lib/services/share/invitationRespond.js
packages/lib/services/share/reducer.js
packages/lib/services/sortOrder/PerFolderSortOrderService.test.js
packages/lib/services/sortOrder/PerFolderSortOrderService.js
packages/lib/services/sortOrder/notesSortOrderUtils.test.js
packages/lib/services/sortOrder/notesSortOrderUtils.js
packages/lib/services/spellChecker/SpellCheckerService.js
packages/lib/services/spellChecker/SpellCheckerServiceDriverBase.js
packages/lib/services/style/cssToTheme.test.js
@@ -1862,11 +1888,11 @@ packages/tools/fuzzer/utils/SeededRandom.js
packages/tools/fuzzer/utils/diffSortedStringArrays.test.js
packages/tools/fuzzer/utils/diffSortedStringArrays.js
packages/tools/fuzzer/utils/extractResourceIds.js
packages/tools/fuzzer/utils/getBinaryDiffDebugMessage.js
packages/tools/fuzzer/utils/getNumberProperty.js
packages/tools/fuzzer/utils/getProperty.js
packages/tools/fuzzer/utils/getStringProperty.js
packages/tools/fuzzer/utils/hangingIndent.js
packages/tools/fuzzer/utils/logDiffDebug.js
packages/tools/fuzzer/utils/openDebugSession.js
packages/tools/fuzzer/utils/randomId.test.js
packages/tools/fuzzer/utils/randomId.js
@@ -1913,6 +1939,7 @@ packages/tools/update-readme-contributors.js
packages/tools/update-readme-download.test.js
packages/tools/update-readme-download.js
packages/tools/update-readme-sponsors.js
packages/tools/updateCanary.js
packages/tools/updateMarkdownDoc.js
packages/tools/utils/discourse.test.js
packages/tools/utils/discourse.js
@@ -1945,4 +1972,3 @@ packages/tools/website/utils/types.js
packages/whisper-voice-typing/src/index.js
packages/whisper-voice-typing/src/specs/Whisper.nitro.js
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD

View File

@@ -0,0 +1,24 @@
# Resolves an issue in which notes and attachments larger than 16 KB
# could become corrupted during the upload process.
# See https://github.com/laurent22/joplin/issues/14343
diff --git a/src/parsers/JSON.js b/src/parsers/JSON.js
index 9a096c25778c7c68be1ddd9dd78faa85bd1d8ec3..6d6bfd2d3789313a7adc8966ab8e58c3d3167356 100644
--- a/src/parsers/JSON.js
+++ b/src/parsers/JSON.js
@@ -12,13 +12,14 @@ class JSONParser extends Transform {
}
_transform(chunk, encoding, callback) {
- this.chunks.push(String(chunk)); // todo consider using a string decoder
+ this.chunks.push(chunk); // type: Uint8Array
callback();
}
_flush(callback) {
try {
- const fields = JSON.parse(this.chunks.join(''));
+ const data = Buffer.concat(this.chunks);
+ const fields = JSON.parse(data.toString('utf-8'));
Object.keys(fields).forEach((key) => {
const value = fields[key];
this.push({ key, value });

View File

@@ -1351,11 +1351,7 @@ footer .bottom-links-row p {
ENGLISH VERSION
*****************************************************************/
:lang(en-gb) #made-in-france-section {
display: none;
}
:lang(en-gb) .top-section-img-cn {
:not(:lang(zh-cn)) .top-section-img-cn {
display: none;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

@@ -145,7 +145,7 @@ function setupLocaleRedirect() {
if (!isRootPage) return;
// Check if user has explicitly chosen to stay on current locale
const localePreference = localStorage.getItem('joplin-locale-preference');
const localePreference = (localStorage.getItem('joplin-locale-preference') || '').toLowerCase();
if (localePreference === 'en') return;
// Get user's preferred language from browser
@@ -160,9 +160,10 @@ function setupLocaleRedirect() {
window.location.href = getLocalePath(langCode) + '/';
}
// Allow users to switch back to English and remember their preference
function setLocalePreference(locale) {
// Allow users to switch language and remember their preference
function setLocalePreference(locale, url) {
localStorage.setItem('joplin-locale-preference', locale);
window.location.href = url;
}
// Expose globally for language switcher links

View File

@@ -1,4 +1,24 @@
<?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, 10 Feb 2026 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate><item><title><![CDATA[Joplin will come preloaded on the HMD Terra M]]></title><description><![CDATA[<div style="overflow: auto;">
<?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>Mon, 23 Feb 2026 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Mon, 23 Feb 2026 00:00:00 GMT</pubDate><item><title><![CDATA[Introducing our Warrant Canary]]></title><description><![CDATA[<p>We have introduced a publicly signed warrant canary for Joplin.</p>
<p>A warrant canary is a regularly updated statement confirming that, as of the stated date, the project has not received secret legal orders, gag orders, or demands requiring the introduction of backdoors into the software or its infrastructure.</p>
<p>The canary is:</p>
<ul>
<li>
<p>Cryptographically signed using a dedicated OpenPGP key</p>
</li>
<li>
<p>Updated every 60 days</p>
</li>
<li>
<p>Published in plain text for independent verification</p>
</li>
</ul>
<p>If the canary is not updated within its stated validity window, it should be considered expired.</p>
<p>You can view and verify the current canary here:</p>
<p><a href="https://raw.githubusercontent.com/laurent22/joplin/refs/heads/dev/readme/canary.txt">https://raw.githubusercontent.com/laurent22/joplin/refs/heads/dev/readme/canary.txt</a></p>
<p>With additional information on how it is generated and managed there:</p>
<p><a href="https://github.com/laurent22/joplin/blob/dev/readme/canary.md">https://github.com/laurent22/joplin/blob/dev/readme/canary.md</a></p>
<p>This measure is intended to improve transparency and provide an additional signal to the community. It does not prevent legal orders, but it helps ensure that any material change in our legal status cannot occur silently.</p>
]]></description><link>https://joplinapp.org/news/20260223-warrant-canary</link><guid isPermaLink="false">20260223-warrant-canary</guid><pubDate>Mon, 23 Feb 2026 00:00:00 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[Joplin will come preloaded on the HMD Terra M]]></title><description><![CDATA[<div style="overflow: auto;">
<img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20260210-hmd-joplin-logo.png" width="200px" style="float: left; margin-right: 16px; margin-bottom: 16px;"/>
<p>We’re happy to announce a collaboration with <a href="https://www.hmdsecure.com/">HMD Secure</a>, who will preload Joplin on their upcoming device, the HMD Terra M.</p>
<p>This partnership brings Joplin to a new class of rugged, professional devices built for instant reliable communication, and reflects a shared focus on reliability, security, and long-term use.</p>
@@ -508,15 +528,4 @@ sys 0m38.013s</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><item><title><![CDATA[Joplin will participate in JdLL 2023!]]></title><description><![CDATA[<p>On 1 and 2 April 2023, we will have a stand for Joplin at the <a href="https://www.jdll.org/">Journées du Logiciel Libre</a> in Lyon, France. The JdLL has been taking place in Lyon for 24 years and is a popular open source conference in France. We had a stand in 2020 and 2021 but that was cancelled due to Covid, so this year is a first for Joplin!</p>
<p>Admission is free, so don't hesitate to come and meet us, exchange ideas and learn more about Joplin!</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230202-jdll.jpg" alt="Joplin at JdLL 2023"></p>
]]></description><link>https://joplinapp.org/news/20230302-jdll-2023</link><guid isPermaLink="false">20230302-jdll-2023</guid><pubDate>Thu, 02 Mar 2023 00:00:00 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[Introducing the "GitHub Actions Raw Log Viewer" extension for Chrome]]></title><description><![CDATA[<p>If you've ever used GitHub Actions, you will find that they provide by default a nice coloured output for the log. It looks good and it's even interactive! (You can click to collapse/expand blocks of text) But unfortunately it doesn't scale to large workflows, like we have for Joplin - the log can freeze and it will take forever to search for something. Indeed searching is done in &quot;real time&quot;... which mostly means it will freeze for a minute or two for each letter you type in the search box. Not great.</p>
<p>Thankfully GitHub provides an alternative access: the raw logs. This is much better because they will open as plain text, without any styling or JS magic, which means you can use the browser native search and it will be fast.</p>
<p>But now the problem is that raw logs look like this:</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230116-ga-raw-log.png" alt="Raw log without extension"></p>
<p>While it's not impossible to read, all colours that would display nicely in a terminal are gone and replaced by <a href="https://en.wikipedia.org/wiki/ANSI_escape_code">ANSI codes</a>. You can find what you need in there but it's not particularly easy.</p>
<p>This is where the new <strong>GitHub Action Raw Log Viewer</strong> extension for Chrome can help. It will parse your raw log and convert the ANSI codes to proper colours. This results in a much more readable rendering:</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230116-ga-raw-log-colored.png" alt="Raw log with extension"></p>
<p>The extension is fast even for very large logs and it's of course easy to search for text since it simply works with your browser built-in search.</p>
<p>The extension is open source, with the code available here: <a href="https://github.com/laurent22/github-actions-logs-extension">https://github.com/laurent22/github-actions-logs-extension</a></p>
<p>And to install it, follow this link:</p>
<p><a href="https://chrome.google.com/webstore/detail/github-action-raw-log-vie/lgejlnoopmcdglhfjblaeldbcfnmjddf"><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230116-extension-get-it-now.png" alt="Download GitHub Action Raw Log Viewer extension"></a></p>
]]></description><link>https://joplinapp.org/news/20230116-github-actions-log-viewer</link><guid isPermaLink="false">20230116-github-actions-log-viewer</guid><pubDate>Mon, 16 Jan 2023 00:00:00 GMT</pubDate><twitter-text>Introducing the &quot;GitHub Action Raw Log Viewer&quot; extension for Chrome</twitter-text></item></channel></rss>
]]></description><link>https://joplinapp.org/news/20230302-jdll-2023</link><guid isPermaLink="false">20230302-jdll-2023</guid><pubDate>Thu, 02 Mar 2023 00:00:00 GMT</pubDate><twitter-text></twitter-text></item></channel></rss>

View File

@@ -13,6 +13,7 @@
</div>
<div class="col-9 text-right d-none d-md-block">
{{> twitterLink}}
<a href="{{baseUrl}}/plugins/" class="fw500">Plugins</a>
<a href="{{baseUrl}}/news/" class="fw500">News</a>
<a href="{{baseUrl}}/help/" class="fw500">Help</a>
<a href="{{forumUrl}}" class="fw500">Forum</a>
@@ -23,7 +24,7 @@
</button>
<ul class="dropdown-menu dropdown-menu-end">
{{#availableLocales}}
<li><a class="dropdown-item {{#isActive}}active{{/isActive}}" href="{{baseUrl}}/{{pathPrefix}}" onclick="setLocalePreference('{{code}}')">{{name}}</a></li>
<li><a class="dropdown-item {{#isActive}}active{{/isActive}}" href="{{baseUrl}}/{{pathPrefix}}" onclick="setLocalePreference('{{code}}', this.href); return false;">{{name}}</a></li>
{{/availableLocales}}
</ul>
</div>
@@ -58,6 +59,7 @@
</div>
<div class="text-center menu-mobile-top">
<a href="{{baseUrl}}/plugins/" class="fw500 mobile-menu-link">Plugins</a>
<a href="{{baseUrl}}/news/" class="fw500 mobile-menu-link">News</a>
<a href="{{baseUrl}}/help/" class="fw500 mobile-menu-link">Help</a>
<a href="{{forumUrl}}" class="fw500 mobile-menu-link">Forum</a>
@@ -73,7 +75,7 @@
<div class="text-center menu-mobile-language">
<p class="fw500 mobile-menu-language-label"><i class="fas fa-globe"></i> Language</p>
{{#availableLocales}}
<a href="{{baseUrl}}/{{pathPrefix}}" class="fw500 mobile-menu-link mobile-language-link {{#isActive}}active{{/isActive}}" onclick="setLocalePreference('{{code}}')">{{name}}</a>
<a href="{{baseUrl}}/{{pathPrefix}}" class="fw500 mobile-menu-link mobile-language-link {{#isActive}}active{{/isActive}}" onclick="setLocalePreference('{{code}}', this.href); return false;">{{name}}</a>
{{/availableLocales}}
</div>
</div>

View File

@@ -3,9 +3,9 @@
<a class="social-link-bluesky" href="https://bsky.app/profile/joplinapp.bsky.social" title="Joplin Bluesky feed"><i class="fa-brands fa-bluesky"></i></a>
<a class="social-link-mastodon" href="https://mastodon.social/@joplinapp" title="Joplin Mastodon feed"><i class="fab fa-mastodon"></i></a>
<a class="social-link-patreon" href="https://www.patreon.com/joplin" title="Joplin Patreon"><i class="fab fa-patreon"></i></a>
<a class="social-link-youtube" href="https://youtube.com/@joplinapp" title="Joplin YouTube channel"><i class="fab fa-youtube"></i></a>
<a class="social-link-discord" href="https://discord.gg/VSj7AFHvpq" title="Joplin Discord chat"><i class="fab fa-discord"></i></a>
<a class="social-link-linkedin" href="https://www.linkedin.com/company/joplin" title="Joplin LinkedIn Feed"><i class="fab fa-linkedin"></i></a>
<a class="social-link-lemmy" href="https://sopuli.xyz/c/joplinapp" title="Joplin Lemmy Community"><i class="fas fa-otter"></i></a>
<a class="social-link-github" href="https://github.com/laurent22/joplin/" title="Joplin GitHub repository"><i class="fab fa-github"></i></a>
</div>
</div>

View File

@@ -0,0 +1,14 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mDMEaZWFlBYJKwYBBAHaRw8BAQdAIh3xQbjaS0EC+8WuKXNPjVF/ayq0/2GZlheR
qj1G3Qe0RUpvcGxpbiBDYW5hcnkgU2lnbmluZyBLZXkgKFdhcnJhbnQgQ2FuYXJ5
IEtleSkgPGNhbmFyeUBqb3BsaW5hcHAub3JnPoiZBBMWCgBBFiEE+CD4MG3QBaEC
0YzVlGrp+lkV71MFAmmVhZQCGwMFCQPCZwAFCwkIBwICIgIGFQoJCAsCBBYCAwEC
HgcCF4AACgkQlGrp+lkV71MZtwD/Ufd4OAcgkl5T6MSB+WDFg8BXvpaBZfNnZkoo
LrOoqNAA/iqGiiBRoarlus2ATOiWhyXaEpRUQcEeaRhhqHW0BGcCuDgEaZWFlBIK
KwYBBAGXVQEFAQEHQFORKWRLp4hDYzR8Q5IRyF9AIjoziR+sj4icUdvZx4Z6AwEI
B4h+BBgWCgAmFiEE+CD4MG3QBaEC0YzVlGrp+lkV71MFAmmVhZQCGwwFCQPCZwAA
CgkQlGrp+lkV71Nu+AD9Gw4qEmL8WNCNs7idc8CRpGpS2DhasNTV398lbKYzco0B
ANlMrGlMc0w1KhuFxdU4fF3s/ktUUnjJwosxK94l5/MJ
=C9VN
-----END PGP PUBLIC KEY BLOCK-----

18
CLAUDE.md Normal file
View File

@@ -0,0 +1,18 @@
# Joplin Guidelines
## Quick Reference
- Tabs for indentation
- Single quotes for strings
- Proper TypeScript types (avoid `any`)
- Comments should be only with `//` and should not contain jsdoc syntax
- If you duplicate a substantial block of code, add a comment above it noting the duplication and referencing the original location.
- When creating Jest tests, there should be only one `describe()` statement in the file.
- Focus on testing essential behaviour and edge cases — avoid adding tests for every minor detail.
- Avoid duplicating code in tests; when testing the same logic with different inputs, use `test.each` or shared helpers instead of repeating similar test blocks.
- Do not make white space changes - do not add unnecessary new lines, or spaces to existing code, or wrap existing code.
## Full Documentation
- Coding style: [readme/dev/coding_style.md](readme/dev/coding_style.md)
- Contributing: [CONTRIBUTING.md](CONTRIBUTING.md)

View File

@@ -1,26 +1,25 @@
FROM node:24-bullseye
FROM node:24-bookworm
RUN apt-get update \
&& apt-get install -y \
ca-certificates curl \
python3 tini
## install docker
RUN install -m 0755 -d /etc/apt/keyrings
RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
RUN chmod a+r /etc/apt/keyrings/docker.asc
RUN echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \
$(. /etc/os-release && echo bullseye) stable" | \
tee /etc/apt/sources.list.d/docker.list > /dev/null
RUN apt-get update \
&& apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin \
ca-certificates curl wget unzip \
python3 tini \
&& rm -rf /var/lib/apt/lists/*
ENV NODE_ENV=production
RUN corepack enable
# Download llama.cpp binary
WORKDIR /opt/llama
RUN wget -q https://github.com/ggml-org/llama.cpp/releases/download/b5449/llama-b5449-bin-ubuntu-x64.zip \
&& unzip llama-b5449-bin-ubuntu-x64.zip \
&& rm llama-b5449-bin-ubuntu-x64.zip \
&& chmod +x /opt/llama/build/bin/llama-mtmd-cli
# Create non-root user for security
RUN groupadd -r transcribe && useradd -r -g transcribe -m transcribe
WORKDIR /app
COPY .yarn/releases ./.yarn/releases
@@ -44,7 +43,21 @@ RUN BUILD_SEQUENCIAL=1 yarn install --inline-builds \
&& yarn cache clean \
&& rm -rf .yarn/berry
# Create data directory and set permissions
RUN mkdir -p /data/images \
&& chown -R transcribe:transcribe /data
WORKDIR /app/packages/transcribe
# Switch to non-root user
USER transcribe
# Set environment variables
ENV HTR_CLI_BINARY_PATH=/opt/llama/build/bin/llama-mtmd-cli
ENV LD_LIBRARY_PATH=/opt/llama/build/bin
ENV DATA_DIR=/data
ENV QUEUE_DRIVER=sqlite
# Start the Node.js application
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["yarn", "start"]

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://www.slotozilla.com/nz/no-deposit-bonus"><img title="casino without making any upfront cost" width="256" src="https://joplinapp.org/images/sponsors/Slotozilla.png" alt="casino without making any upfront cost"/></a> <a href="https://writepaper.com/"><img title="best service to write my paper for me" width="256" src="https://joplinapp.org/images/sponsors/WritePaper.png" alt="best service to write my paper for me"/></a> <a href="https://paperwriter.com/"><img title="high-quality paper writing service PaperWriter" width="256" src="https://joplinapp.org/images/sponsors/PaperWriter.png" alt="high-quality paper writing service PaperWriter"/></a> <a href="https://www.bestetf.net/"><img title="BestETF" width="256" src="https://joplinapp.org/images/sponsors/BestEtf.png" alt="BestETF"/></a> <a href="https://freespinny.io/free-spins-no-deposit/"><img title="Freespinny.io Free Spins Bonus site" width="256" src="https://joplinapp.org/images/sponsors/Freespinny.png" alt="Freespinny.io Free Spins Bonus site"/></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://domyessay.com"><img title="Essay writers DoMyEssay are dedicated to providing top-notch, custom-written papers that meet your academic requirements" width="256" src="https://joplinapp.org/images/sponsors/DoMyEssay.png" alt="DoMyEssay"/></a> <a href="https://essaypro.com/"><img title="best essay writing service" width="256" src="https://joplinapp.org/images/sponsors/EssayPro.png" alt="best essay writing service"/></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://uk.notgamstop.com/bonuses/free-spins-no-deposit-no-gamstop/"><img title="free spins no deposit at NotGamstop" width="256" src="https://joplinapp.org/images/sponsors/NotGamStop.jpg" alt="free spins no deposit at NotGamstop"/></a> <a href="https://www.writemyessay.com/"><img title="writing service for students WriteMyEssay" width="256" src="https://joplinapp.org/images/sponsors/WriteMyEssay.png" alt="writing service for students WriteMyEssay"/></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://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>
<!-- SPONSORS-ORG -->
* * *
@@ -39,9 +39,9 @@ Please see the [donation page](https://github.com/laurent22/joplin/blob/dev/read
<!-- SPONSORS-GITHUB -->
| | | | |
| :---: | :---: | :---: | :---: |
| <img width="50" src="https://avatars2.githubusercontent.com/u/97193607?s=96&v=4"/></br>[Akhil-CM](https://github.com/Akhil-CM) | <img width="50" src="https://avatars2.githubusercontent.com/u/552452?s=96&v=4"/></br>[andypiper](https://github.com/andypiper) | <img width="50" src="https://avatars2.githubusercontent.com/u/215668?s=96&v=4"/></br>[avanderberg](https://github.com/avanderberg) | <img width="50" src="https://avatars2.githubusercontent.com/u/67130?s=96&v=4"/></br>[chr15m](https://github.com/chr15m) |
| <img width="50" src="https://avatars2.githubusercontent.com/u/1177810?s=96&v=4"/></br>[felixstorm](https://github.com/felixstorm) | <img width="50" src="https://avatars2.githubusercontent.com/u/11947658?s=96&v=4"/></br>[KentBrockman](https://github.com/KentBrockman) | <img width="50" src="https://avatars2.githubusercontent.com/u/42319182?s=96&v=4"/></br>[marcdw1289](https://github.com/marcdw1289) | <img width="50" src="https://avatars2.githubusercontent.com/u/668977?s=96&v=4"/></br>[ugoertz](https://github.com/ugoertz) |
| | | | |
| <img width="50" src="https://avatars2.githubusercontent.com/u/552452?s=96&v=4"/></br>[andypiper](https://github.com/andypiper) | <img width="50" src="https://avatars2.githubusercontent.com/u/215668?s=96&v=4"/></br>[avanderberg](https://github.com/avanderberg) | <img width="50" src="https://avatars2.githubusercontent.com/u/67130?s=96&v=4"/></br>[chr15m](https://github.com/chr15m) | <img width="50" src="https://avatars2.githubusercontent.com/u/1177810?s=96&v=4"/></br>[felixstorm](https://github.com/felixstorm) |
| <img width="50" src="https://avatars2.githubusercontent.com/u/8030470?s=96&v=4"/></br>[Galliver7](https://github.com/Galliver7) | <img width="50" src="https://avatars2.githubusercontent.com/u/4721118?s=96&v=4"/></br>[GPrimola](https://github.com/GPrimola) | <img width="50" src="https://avatars2.githubusercontent.com/u/64712218?s=96&v=4"/></br>[Hegghammer](https://github.com/Hegghammer) | <img width="50" src="https://avatars2.githubusercontent.com/u/42319182?s=96&v=4"/></br>[marcdw1289](https://github.com/marcdw1289) |
| <img width="50" src="https://avatars2.githubusercontent.com/u/668977?s=96&v=4"/></br>[ugoertz](https://github.com/ugoertz) | | | |
<!-- SPONSORS-GITHUB -->
# Community
@@ -61,6 +61,14 @@ Name | Description
Please see the guide for information on how to contribute to the development of Joplin: https://github.com/laurent22/joplin/blob/dev/readme/dev/index.md
## Warrant Canary Signing Key
Fingerprint:
F820 F830 6DD0 05A1 02D1 8CD5 946A E9FA 5915 EF53
Public key: https://github.com/laurent22/joplin/raw/dev/Assets/keys/joplin-canary-signing-key.asc
# Contributors
Thank you to everyone who've contributed to Joplin's source code!

View File

@@ -63,6 +63,7 @@
"/readme/_i18n",
"/readme/about/changelog/desktop.md",
"/readme/licenses.md",
"/readme/canary.txt",
"/readme/i18n",
"cspell.json",
"node_modules"

View File

@@ -9,15 +9,15 @@
"vips.dev": {
"platforms": ["aarch64-darwin"],
},
"nodejs": "24.9.0",
"nodejs": "24.11.1",
"pkg-config": "latest",
"python": "3.13.3",
"python": "3.14.0",
"bat": "latest",
"electron": {
"version": "latest",
"excluded_platforms": ["aarch64-darwin", "x86_64-darwin"],
},
"git": "2.50.1",
"git": "2.51.2",
},
"shell": {
"init_hook": [

View File

@@ -84,8 +84,8 @@ services:
profiles:
- full
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ${HTR_CLI_IMAGES_FOLDER}:/app/packages/transcribe/images
- ${HTR_CLI_MODELS_FOLDER}:/opt/models:ro
depends_on:
- transcribe-db
ports:
@@ -94,6 +94,16 @@ services:
- transcribe-network
- shared-network
restart: unless-stopped
# Security: limit resources to prevent runaway processes
deploy:
resources:
limits:
memory: 16G
cpus: '4'
# Security: read-only root filesystem with only images folder writable
read_only: true
tmpfs:
- /tmp
environment:
- APP_PORT=4567
- DB_CLIENT=pg
@@ -103,5 +113,6 @@ services:
- QUEUE_DATABASE_PORT=${QUEUE_DATABASE_PORT}
- QUEUE_DATABASE_HOST=transcribe-db
- API_KEY=${TRANSCRIBE_API_KEY}
- HTR_CLI_IMAGES_FOLDER=${HTR_CLI_IMAGES_FOLDER}
- HTR_CLI_IMAGES_FOLDER=/app/packages/transcribe/images
- HTR_CLI_MODELS_FOLDER=/opt/models

View File

@@ -0,0 +1,44 @@
# Standalone docker-compose for Joplin Transcribe
#
# Uses SQLite for the queue (no external database needed).
# Data is stored in a named volume for proper permissions.
#
# Usage:
#
# 1. Download models:
# mkdir -p ./data/models
# wget -O ./data/models/Model-7.6B-Q4_K_M.gguf https://huggingface.co/openbmb/MiniCPM-o-2_6-gguf/resolve/main/Model-7.6B-Q4_K_M.gguf
# wget -O ./data/models/mmproj-model-f16.gguf https://huggingface.co/openbmb/MiniCPM-o-2_6-gguf/resolve/main/mmproj-model-f16.gguf
#
# 2. Configure:
# cp .env-transcribe-sample .env
# # Edit .env and set API_KEY
#
# 3. Run:
# docker compose -f docker-compose.transcribe.yml up
volumes:
transcribe-data:
services:
transcribe:
image: joplin/transcribe:amd64-latest
ports:
- "4567:4567"
volumes:
- transcribe-data:/data
- ./data/models:/data/models:ro
restart: unless-stopped
# Security: limit resources to prevent runaway processes
deploy:
resources:
limits:
memory: 16G
cpus: '4'
# Security: read-only root filesystem
read_only: true
tmpfs:
- /tmp
- /home/transcribe/.cache
env_file:
- .env

View File

@@ -60,6 +60,7 @@
"test": "yarn workspaces foreach --worktree --parallel --verbose --interlaced --jobs 2 run test",
"tsc": "yarn workspaces foreach --worktree --parallel --verbose --interlaced run tsc",
"updateIgnored": "node packages/tools/gulp/tasks/updateIgnoredTypeScriptBuildRun.js",
"updateCanary": "node ./packages/tools/updateCanary",
"updateMarkdownDoc": "node ./packages/tools/updateMarkdownDoc",
"updateNews": "node ./packages/tools/website/updateNews",
"updatePluginTypes": "./packages/generator-joplin/updateTypes.sh",
@@ -71,6 +72,7 @@
"@crowdin/cli": "4",
"@joplin/utils": "~2.12",
"@seiyab/eslint-plugin-react-hooks": "4.5.1-beta.0",
"@types/jest": "29.5.14",
"@typescript-eslint/eslint-plugin": "6.21.0",
"@typescript-eslint/parser": "6.21.0",
"cspell": "5.21.2",
@@ -81,8 +83,8 @@
"eslint-plugin-promise": "6.6.0",
"eslint-plugin-react": "7.37.5",
"execa": "5.1.1",
"fs-extra": "11.3.2",
"glob": "11.0.3",
"fs-extra": "11.3.3",
"glob": "11.1.0",
"gulp": "4.0.2",
"husky": "9.1.7",
"lerna": "3.22.1",
@@ -123,6 +125,7 @@
"depd@npm:~1.1.2": "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",
"depd@npm:^1.1.0": "patch:depd@npm%3A2.0.0#~/.yarn/patches/depd-npm-2.0.0-b6c51a4b43.patch"
"depd@npm:^1.1.0": "patch:depd@npm%3A2.0.0#~/.yarn/patches/depd-npm-2.0.0-b6c51a4b43.patch",
"formidable@npm:^2.0.1": "patch:formidable@npm%3A2.1.2#~/.yarn/patches/formidable-npm-2.1.2-40ba18d67f.patch"
}
}

View File

@@ -1,7 +1,7 @@
import BaseApplication from '@joplin/lib/BaseApplication';
import { refreshFolders } from '@joplin/lib/folders-screen-utils.js';
import ResourceService from '@joplin/lib/services/ResourceService';
import BaseModel, { ModelType } from '@joplin/lib/BaseModel';
import { ModelType } from '@joplin/lib/BaseModel';
import Folder from '@joplin/lib/models/Folder';
import BaseItem from '@joplin/lib/models/BaseItem';
import Note from '@joplin/lib/models/Note';
@@ -15,20 +15,22 @@ import RevisionService from '@joplin/lib/services/RevisionService';
import shim from '@joplin/lib/shim';
import setupCommand from './setupCommand';
import { FolderEntity, NoteEntity } from '@joplin/lib/services/database/types';
type FolderOrNoteType = ModelType.Note | ModelType.Folder | 'folderOrNote';
import initializeCommandService from './utils/initializeCommandService';
const { cliUtils } = require('./cli-utils.js');
const Cache = require('@joplin/lib/Cache');
class Application extends BaseApplication {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Dynamic command loading system
private commands_: Record<string, any> = {};
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Dynamic command metadata
private commandMetadata_: any = null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Dynamic command type
private activeCommand_: any = null;
private allCommandsLoaded_ = false;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Dynamic GUI type with many optional methods
private gui_: any = null;
private cache_ = new Cache();
@@ -40,18 +42,16 @@ class Application extends BaseApplication {
return this.gui().stdoutMaxWidth();
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
public async guessTypeAndLoadItem(pattern: string, options: any = null) {
let type = BaseModel.TYPE_NOTE;
public async guessTypeAndLoadItem(pattern: string, options: { parent?: FolderEntity } | null = null) {
let type: FolderOrNoteType = ModelType.Note;
if (pattern.indexOf('/') === 0) {
type = BaseModel.TYPE_FOLDER;
type = ModelType.Folder;
pattern = pattern.substr(1);
}
return this.loadItem(type, pattern, options);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
public async loadItem(type: ModelType | 'folderOrNote', pattern: string, options: any = null) {
public async loadItem(type: FolderOrNoteType, pattern: string, options: { parent?: FolderEntity } | null = null) {
const output = await this.loadItems(type, pattern, options);
if (output.length > 1) {
@@ -75,37 +75,36 @@ class Application extends BaseApplication {
}
}
public async loadItemOrFail(type: ModelType | 'folderOrNote', pattern: string) {
public async loadItemOrFail(type: FolderOrNoteType, pattern: string) {
const output = await this.loadItem(type, pattern);
if (!output) throw new Error(_('Cannot find "%s".', pattern));
return output;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
public async loadItems(type: ModelType | 'folderOrNote', pattern: string, options: any = null): Promise<(FolderEntity | NoteEntity)[]> {
public async loadItems(type: FolderOrNoteType, pattern: string, options: { parent?: FolderEntity } | null = null): Promise<(FolderEntity | NoteEntity)[]> {
if (type === 'folderOrNote') {
const folders: FolderEntity[] = await this.loadItems(BaseModel.TYPE_FOLDER, pattern, options);
const folders: FolderEntity[] = await this.loadItems(ModelType.Folder, pattern, options);
if (folders.length) return folders;
return await this.loadItems(BaseModel.TYPE_NOTE, pattern, options);
return await this.loadItems(ModelType.Note, pattern, options);
}
pattern = pattern ? pattern.toString() : '';
if (type === BaseModel.TYPE_FOLDER && (pattern === Folder.conflictFolderTitle() || pattern === Folder.conflictFolderId())) return [Folder.conflictFolder()];
if (type === ModelType.Folder && (pattern === Folder.conflictFolderTitle() || pattern === Folder.conflictFolderId())) return [Folder.conflictFolder()];
if (!options) options = {};
const parent = options.parent ? options.parent : app().currentFolder();
const ItemClass = BaseItem.itemClass(type);
if (type === BaseModel.TYPE_NOTE && pattern.indexOf('*') >= 0) {
if (type === ModelType.Note && pattern.indexOf('*') >= 0) {
// Handle it as pattern
if (!parent) throw new Error(_('No notebook selected.'));
return await Note.previews(parent.id, { titlePattern: pattern });
} else {
// Single item
let item = null;
if (type === BaseModel.TYPE_NOTE) {
if (type === ModelType.Note) {
if (!parent) throw new Error(_('No notebook has been specified.'));
item = await (ItemClass as typeof Note).loadFolderNoteByField(parent.id, 'title', pattern);
} else {
@@ -172,7 +171,7 @@ class Application extends BaseApplication {
}
if (uiType !== null) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Dynamic command type
const temp: Record<string, any> = {};
for (const n in this.commands_) {
if (!this.commands_.hasOwnProperty(n)) continue;
@@ -233,8 +232,7 @@ class Application extends BaseApplication {
CommandClass = require(`${__dirname}/command-${name}.js`);
} catch (error) {
if (error.message && error.message.indexOf('Cannot find module') >= 0) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const e: any = new Error(_('No such command: %s', name));
const e: Error & { type?: string } = new Error(_('No such command: %s', name));
e.type = 'notFound';
throw e;
} else {
@@ -253,8 +251,7 @@ class Application extends BaseApplication {
isDummy: () => {
return true;
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
prompt: (initialText = '', promptString = '', options: any = null) => {
prompt: (initialText = '', promptString = '', options: Record<string, unknown> | null = null) => {
return cliUtils.prompt(initialText, promptString, options);
},
showConsole: () => {},
@@ -276,8 +273,7 @@ class Application extends BaseApplication {
};
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
public async execCommand(argv: string[]): Promise<any> {
public async execCommand(argv: string[]): Promise<void> {
if (!argv.length) return this.execCommand(['help']);
// reg.logger().debug('execCommand()', argv);
const commandName = argv[0];
@@ -396,8 +392,7 @@ class Application extends BaseApplication {
const keychainEnabled = this.checkIfKeychainEnabled(argv);
argv = await super.start(argv, { keychainEnabled });
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
cliUtils.setStdout((object: any) => {
cliUtils.setStdout((object: string) => {
return this.stdout(object);
});
@@ -448,7 +443,7 @@ class Application extends BaseApplication {
this.gui_.setLogger(this.logger());
await this.gui_.start();
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Redux dispatch type requires AnyAction
await refreshFolders((action: any) => this.store().dispatch(action), '');
const tags = await Tag.allWithNotes();

View File

@@ -31,9 +31,14 @@ cliUtils.printArray = function(logFunction, rows) {
const line = [];
for (let col = 0; col < colWidths.length; col++) {
const item = rows[row][col];
const width = colWidths[col];
const dir = colAligns[col] === ALIGN_LEFT ? stringPadding.RIGHT : stringPadding.LEFT;
line.push(stringPadding(item, width, ' ', dir));
const isLastCol = col === colWidths.length - 1;
if (isLastCol) {
line.push(item ? item.toString() : '');
} else {
const width = colWidths[col];
const dir = colAligns[col] === ALIGN_LEFT ? stringPadding.RIGHT : stringPadding.LEFT;
line.push(stringPadding(item, width, ' ', dir));
}
}
logFunction(line.join(' '));
}

View File

@@ -1,7 +1,7 @@
import BaseCommand from './base-command';
import app from './app';
import { _ } from '@joplin/lib/locale';
import BaseModel from '@joplin/lib/BaseModel';
import { ModelType } from '@joplin/lib/BaseModel';
import shim from '@joplin/lib/shim';
class Command extends BaseCommand {
@@ -17,7 +17,7 @@ class Command extends BaseCommand {
public override async action(args: any) {
const title = args['note'];
const note = await app().loadItem(BaseModel.TYPE_NOTE, title, { parent: app().currentFolder() });
const note = await app().loadItem(ModelType.Note, title, { parent: app().currentFolder() });
this.encryptionCheck(note);
if (!note) throw new Error(_('Cannot find "%s".', title));

View File

@@ -1,7 +1,7 @@
import BaseCommand from './base-command';
import app from './app';
import { _ } from '@joplin/lib/locale';
import BaseModel from '@joplin/lib/BaseModel';
import { ModelType } from '@joplin/lib/BaseModel';
import BaseItem from '@joplin/lib/models/BaseItem';
import Note from '@joplin/lib/models/Note';
@@ -22,7 +22,7 @@ class Command extends BaseCommand {
public override async action(args: any) {
const title = args['note'];
const item = await app().loadItem(BaseModel.TYPE_NOTE, title, { parent: app().currentFolder() });
const item = await app().loadItem(ModelType.Note, title, { parent: app().currentFolder() });
if (!item) throw new Error(_('Cannot find "%s".', title));
let content = '';

View File

@@ -0,0 +1,19 @@
import BaseCommand from './base-command';
import app from './app';
import { _ } from '@joplin/lib/locale';
class Command extends BaseCommand {
public override usage() {
return 'clear';
}
public override description() {
return _('Clears the console output.');
}
public override async action() {
app().gui().widget('console').clear();
}
}
module.exports = Command;

View File

@@ -1,7 +1,7 @@
import BaseCommand from './base-command';
import app from './app';
import { _ } from '@joplin/lib/locale';
import BaseModel from '@joplin/lib/BaseModel';
import { ModelType } from '@joplin/lib/BaseModel';
import Note from '@joplin/lib/models/Note';
class Command extends BaseCommand {
@@ -17,14 +17,14 @@ class Command extends BaseCommand {
public override async action(args: any) {
let folder = null;
if (args['notebook']) {
folder = await app().loadItem(BaseModel.TYPE_FOLDER, args['notebook']);
folder = await app().loadItem(ModelType.Folder, args['notebook']);
} else {
folder = app().currentFolder();
}
if (!folder) throw new Error(_('Cannot find "%s".', args['notebook']));
const notes = await app().loadItems(BaseModel.TYPE_NOTE, args['note']);
const notes = await app().loadItems(ModelType.Note, args['note']);
if (!notes.length) throw new Error(_('Cannot find "%s".', args['note']));
for (let i = 0; i < notes.length; i++) {

View File

@@ -1,7 +1,7 @@
import BaseCommand from './base-command';
import app from './app';
import { _ } from '@joplin/lib/locale';
import BaseModel from '@joplin/lib/BaseModel';
import { ModelType } from '@joplin/lib/BaseModel';
import Note from '@joplin/lib/models/Note';
import time from '@joplin/lib/time';
import { NoteEntity } from '@joplin/lib/services/database/types';
@@ -17,7 +17,7 @@ class Command extends BaseCommand {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
public static async handleAction(commandInstance: BaseCommand, args: any, isCompleted: boolean) {
const note: NoteEntity = await app().loadItem(BaseModel.TYPE_NOTE, args.note);
const note: NoteEntity = await app().loadItem(ModelType.Note, args.note);
commandInstance.encryptionCheck(note);
if (!note) throw new Error(_('Cannot find "%s".', args.note));
if (!note.is_todo) throw new Error(_('Note is not a to-do: "%s"', args.note));

View File

@@ -6,7 +6,7 @@ import app from './app';
import { _ } from '@joplin/lib/locale';
import Note from '@joplin/lib/models/Note';
import Setting from '@joplin/lib/models/Setting';
import BaseModel from '@joplin/lib/BaseModel';
import { ModelType } from '@joplin/lib/BaseModel';
class Command extends BaseCommand {
public override usage() {
@@ -39,7 +39,7 @@ class Command extends BaseCommand {
const title = args['note'];
if (!app().currentFolder()) throw new Error(_('No active notebook.'));
let note = await app().loadItem(BaseModel.TYPE_NOTE, title);
let note = await app().loadItem(ModelType.Note, title);
this.encryptionCheck(note);

View File

@@ -1,6 +1,6 @@
import BaseCommand from './base-command';
import InteropService from '@joplin/lib/services/interop/InteropService';
import BaseModel from '@joplin/lib/BaseModel';
import { ModelType } from '@joplin/lib/BaseModel';
import app from './app';
import { _ } from '@joplin/lib/locale';
import { ExportOptions } from '@joplin/lib/services/interop/types';
@@ -34,12 +34,12 @@ class Command extends BaseCommand {
if (exportOptions.format === 'html') throw new Error('HTML export is not supported. Please use the desktop application.');
if (args.options.note) {
const notes = await app().loadItems(BaseModel.TYPE_NOTE, args.options.note, { parent: app().currentFolder() });
const notes = await app().loadItems(ModelType.Note, args.options.note, { parent: app().currentFolder() });
if (!notes.length) throw new Error(_('Cannot find "%s".', args.options.note));
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
exportOptions.sourceNoteIds = notes.map((n: any) => n.id);
} else if (args.options.notebook) {
const folders = await app().loadItems(BaseModel.TYPE_FOLDER, args.options.notebook);
const folders = await app().loadItems(ModelType.Folder, args.options.notebook);
if (!folders.length) throw new Error(_('Cannot find "%s".', args.options.notebook));
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
exportOptions.sourceFolderIds = folders.map((n: any) => n.id);

View File

@@ -1,7 +1,7 @@
import BaseCommand from './base-command';
import app from './app';
import { _ } from '@joplin/lib/locale';
import BaseModel from '@joplin/lib/BaseModel';
import { ModelType } from '@joplin/lib/BaseModel';
import Note from '@joplin/lib/models/Note';
class Command extends BaseCommand {
@@ -17,7 +17,7 @@ class Command extends BaseCommand {
public override async action(args: any) {
const title = args['note'];
const item = await app().loadItem(BaseModel.TYPE_NOTE, title, { parent: app().currentFolder() });
const item = await app().loadItem(ModelType.Note, title, { parent: app().currentFolder() });
if (!item) throw new Error(_('Cannot find "%s".', title));
const url = Note.geolocationUrl(item);
this.stdout(url);

View File

@@ -1,6 +1,6 @@
import BaseCommand from './base-command';
import InteropService from '@joplin/lib/services/interop/InteropService';
import BaseModel from '@joplin/lib/BaseModel';
import { ModelType } from '@joplin/lib/BaseModel';
const { cliUtils } = require('./cli-utils.js');
import app from './app';
import { _ } from '@joplin/lib/locale';
@@ -33,7 +33,7 @@ class Command extends BaseCommand {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
public override async action(args: any) {
let destinationFolder = await app().loadItem(BaseModel.TYPE_FOLDER, args.notebook);
let destinationFolder = await app().loadItem(ModelType.Folder, args.notebook);
if (args.notebook && !destinationFolder) throw new Error(_('Cannot find "%s".', args.notebook));

View File

@@ -1,7 +1,7 @@
const BaseCommand = require('./base-command').default;
import app from './app';
import { _ } from '@joplin/lib/locale';
import BaseModel from '@joplin/lib/BaseModel';
import { ModelType } from '@joplin/lib/BaseModel';
import Folder from '@joplin/lib/models/Folder';
import { FolderEntity } from '@joplin/lib/services/database/types';
@@ -23,7 +23,7 @@ class Command extends BaseCommand {
// validDestinationFolder check for presents and ambiguous folders
public async validDestinationFolder(targetFolder: string) {
const destinationFolder = await app().loadItem(BaseModel.TYPE_FOLDER, targetFolder);
const destinationFolder = await app().loadItem(ModelType.Folder, targetFolder);
if (!destinationFolder) {
throw new Error(_('Cannot find: "%s"', targetFolder));
}

View File

@@ -1,7 +1,7 @@
import BaseCommand from './base-command';
import app from './app';
import { _ } from '@joplin/lib/locale';
import BaseModel from '@joplin/lib/BaseModel';
import { ModelType } from '@joplin/lib/BaseModel';
import Folder from '@joplin/lib/models/Folder';
import Note from '@joplin/lib/models/Note';
@@ -21,7 +21,7 @@ class Command extends BaseCommand {
let folder = null;
if (destination !== 'root') {
folder = await app().loadItem(BaseModel.TYPE_FOLDER, destination);
folder = await app().loadItem(ModelType.Folder, destination);
if (!folder) throw new Error(_('Cannot find "%s".', destination));
}
@@ -30,7 +30,7 @@ class Command extends BaseCommand {
throw new Error(_('Ambiguous notebook "%s". Please use short notebook id instead - press "ti" to see the short notebook id', destination));
}
const itemFolder = await app().loadItem(BaseModel.TYPE_FOLDER, pattern);
const itemFolder = await app().loadItem(ModelType.Folder, pattern);
if (itemFolder) {
const sourceDuplicates = await Folder.search({ titlePattern: pattern, limit: 2 });
if (sourceDuplicates.length > 1) {
@@ -42,7 +42,7 @@ class Command extends BaseCommand {
await Folder.moveToFolder(itemFolder.id, folder.id);
}
} else {
const notes = await app().loadItems(BaseModel.TYPE_NOTE, pattern);
const notes = await app().loadItems(ModelType.Note, pattern);
if (notes.length === 0) throw new Error(_('Cannot find "%s".', pattern));
for (let i = 0; i < notes.length; i++) {
await Note.moveToFolder(notes[i].id, folder.id);

View File

@@ -2,7 +2,7 @@ import BaseCommand from './base-command';
import app from './app';
import { _ } from '@joplin/lib/locale';
import Folder from '@joplin/lib/models/Folder';
import BaseModel from '@joplin/lib/BaseModel';
import { ModelType } from '@joplin/lib/BaseModel';
import { substrWithEllipsis } from '@joplin/lib/string-utils';
class Command extends BaseCommand {
@@ -26,7 +26,7 @@ class Command extends BaseCommand {
const pattern = args['notebook'];
const force = args.options && args.options.force === true;
const folder = await app().loadItemOrFail(BaseModel.TYPE_FOLDER, pattern);
const folder = await app().loadItemOrFail(ModelType.Folder, pattern);
const permanent = args.options?.permanent === true || !!folder.deleted_time;
const ellipsizedFolderTitle = substrWithEllipsis(folder.title, 0, 32);

View File

@@ -2,7 +2,7 @@ import BaseCommand from './base-command';
import app from './app';
import { _, _n } from '@joplin/lib/locale';
import Note from '@joplin/lib/models/Note';
import BaseModel, { DeleteOptions } from '@joplin/lib/BaseModel';
import { DeleteOptions, ModelType } from '@joplin/lib/BaseModel';
import { NoteEntity } from '@joplin/lib/services/database/types';
class Command extends BaseCommand {
@@ -26,7 +26,7 @@ class Command extends BaseCommand {
const pattern = args['note-pattern'];
const force = args.options && args.options.force === true;
const notes: NoteEntity[] = await app().loadItems(BaseModel.TYPE_NOTE, pattern);
const notes: NoteEntity[] = await app().loadItems(ModelType.Note, pattern);
if (!notes.length) throw new Error(_('Cannot find "%s".', pattern));
let ok = true;

View File

@@ -1,7 +1,7 @@
import BaseCommand from './base-command';
import app from './app';
import { _ } from '@joplin/lib/locale';
import BaseModel from '@joplin/lib/BaseModel';
import { ModelType } from '@joplin/lib/BaseModel';
import Database from '@joplin/lib/database';
import Note from '@joplin/lib/models/Note';
@@ -29,7 +29,7 @@ class Command extends BaseCommand {
let propValue = args['value'];
if (!propValue) propValue = '';
const notes = await app().loadItems(BaseModel.TYPE_NOTE, title);
const notes = await app().loadItems(ModelType.Note, title);
if (!notes.length) throw new Error(_('Cannot find "%s".', title));
for (let i = 0; i < notes.length; i++) {

View File

@@ -1,7 +1,7 @@
import BaseCommand from './base-command';
import app from './app';
import { _ } from '@joplin/lib/locale';
import BaseModel from '@joplin/lib/BaseModel';
import { ModelType } from '@joplin/lib/BaseModel';
import Folder from '@joplin/lib/models/Folder';
class Command extends BaseCommand {
@@ -19,7 +19,7 @@ class Command extends BaseCommand {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
public override async action(args: any) {
const folder = await app().loadItem(BaseModel.TYPE_FOLDER, args['notebook']);
const folder = await app().loadItem(ModelType.Folder, args['notebook']);
if (!folder) throw new Error(_('Cannot find "%s".', args['notebook']));
// Auto-expand parent folders in GUI if present

View File

@@ -34,6 +34,12 @@ class ConsoleWidget extends TextWidget {
super.onBlur();
}
clear() {
this.lines_ = [];
this.updateText_ = true;
this.invalidate();
}
render() {
if (this.updateText_) {
if (this.lines_.length > this.maxLines_) {

View File

@@ -48,7 +48,7 @@
"chalk": "4.1.2",
"compare-version": "0.1.2",
"file-type": "16.5.4",
"fs-extra": "11.3.2",
"fs-extra": "11.3.3",
"html-entities": "1.4.0",
"keytar": "7.9.0",
"md5": "2.3.0",

View File

@@ -45,6 +45,10 @@ describe('HtmlToMd', () => {
htmlToMdOptions.preserveColorStyles = true;
}
if (htmlFilename.indexOf('table_with') === 0 || htmlFilename.indexOf('table_default') === 0) {
htmlToMdOptions.preserveTableStyles = true;
}
const html = await readFile(htmlPath, 'utf8');
let expectedMd = await readFile(mdPath, 'utf8');
@@ -96,4 +100,34 @@ describe('HtmlToMd', () => {
expect(htmlToMd.parse('> 1 _2_ 3.pdf', { disableEscapeContent: false })).toBe('\\> 1 \\_2_ 3.pdf');
});
it('should support tightLists option', async () => {
const htmlToMd = new HtmlToMd();
const html = '<ul><li><p><strong>Item 1</strong></p></li><li><p><strong>Item 2</strong></p></li><li><p><strong>Item 3</strong></p></li></ul>';
// Without tightLists, paragraphs inside list items produce extra blank lines
const looseResult = htmlToMd.parse(html, { tightLists: false });
expect(looseResult).toContain('\n \n');
// With tightLists, list items are compact without blank lines
const tightResult = htmlToMd.parse(html, { tightLists: true });
expect(tightResult).toBe('- **Item 1**\n- **Item 2**\n- **Item 3**');
});
it('should support collapseMultipleBlankLines option', async () => {
const htmlToMd = new HtmlToMd();
const html = '<p>First</p><br><br><br><p>Second</p>';
// Without collapseMultipleBlankLines, multiple blank lines are preserved
const looseResult = htmlToMd.parse(html, { collapseMultipleBlankLines: false });
expect(looseResult).toContain('\n\n \n');
// With collapseMultipleBlankLines, multiple blank lines are collapsed into one
const collapsedResult = htmlToMd.parse(html, { collapseMultipleBlankLines: true });
expect(collapsedResult).not.toContain('\n\n\n');
expect(collapsedResult).not.toContain('\n\n \n');
// Verify that a single blank line is preserved (not fully removed)
expect(collapsedResult).toContain('\n\n');
});
});

View File

@@ -0,0 +1,7 @@
<ul>
<li>First line<br/>Second line</li>
<li>Normal item</li>
<li>With sub-list<ul>
<li>Sub-list<br/>Paragraph<br/>Also another line</li>
</ul></li>
</ul>

View File

@@ -0,0 +1,8 @@
- First line
Second line
- Normal item
- With sub-list
- Sub-list
Paragraph
Also another line

View File

@@ -0,0 +1 @@
<a href="#section" style="text-decoration: underline">Section Link</a>

View File

@@ -0,0 +1 @@
[Section Link](#section)

View File

@@ -0,0 +1,18 @@
<table style="border-collapse: collapse; width: 100%;">
<thead>
<tr>
<th style="width: 50%;">Name</th>
<th style="width: 50%;">Value</th>
</tr>
</thead>
<tbody>
<tr>
<td style="width: 50%;">Cell A</td>
<td style="width: 50%;">Cell B</td>
</tr>
<tr>
<td style="width: 50%;">Cell C</td>
<td style="width: 50%;">Cell D</td>
</tr>
</tbody>
</table>

View File

@@ -0,0 +1,4 @@
| Name | Value |
| --- | --- |
| Cell A | Cell B |
| Cell C | Cell D |

View File

@@ -0,0 +1,18 @@
<table bgcolor="#f0f0f0" cellpadding="8">
<thead>
<tr>
<th>Name</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Cell A</td>
<td>Cell B</td>
</tr>
<tr>
<td>Cell C</td>
<td>Cell D</td>
</tr>
</tbody>
</table>

View File

@@ -0,0 +1 @@
<div class="joplin-table-wrapper"><table bgcolor="#f0f0f0" cellpadding="8"><thead><tr><th>Name</th><th>Value</th></tr></thead><tbody><tr><td>Cell A</td><td>Cell B</td></tr><tr><td>Cell C</td><td>Cell D</td></tr></tbody></table></div>

View File

@@ -0,0 +1,18 @@
<table style="border-collapse: collapse">
<thead>
<tr>
<th>Name</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td style="background-color: #e03e2d">Red cell</td>
<td style="padding: 10px 15px">Padded cell</td>
</tr>
<tr>
<td style="border-color: #2dc26b; border-style: solid">Green border</td>
<td>Normal cell</td>
</tr>
</tbody>
</table>

View File

@@ -0,0 +1 @@
<div class="joplin-table-wrapper"><table style="border-collapse: collapse"><thead><tr><th>Name</th><th>Value</th></tr></thead><tbody><tr><td style="background-color: #e03e2d">Red cell</td><td style="padding: 10px 15px">Padded cell</td></tr><tr><td style="border-color: #2dc26b; border-style: solid">Green border</td><td>Normal cell</td></tr></tbody></table></div>

View File

@@ -10,6 +10,7 @@ import Folder from '@joplin/lib/models/Folder';
import { expectNotThrow, setupDatabaseAndSynchronizer, switchClient, expectThrow, createTempDir, supportDir, mockMobilePlatform } from '@joplin/lib/testing/test-utils';
import { newPluginScript } from '../../testUtils';
import { join } from 'path';
import { PluginManifest } from '@joplin/lib/services/plugins/utils/types';
const testPluginDir = `${supportDir}/plugins`;
@@ -472,4 +473,18 @@ describe('services_PluginService', () => {
await fs.remove(testDir);
}
});
it('should report a missing app_min_version field specifically', () => {
const service = newPluginService();
const manifest = {
manifest_version: 1,
id: 'test.plugin',
name: 'Test Plugin',
version: '1.0.0',
// Missing app_min_version
};
const error = service.describeIncompatibility(manifest as unknown as PluginManifest);
expect(error).toContain('Invalid plugin manifest: Missing required field: app_min_version');
});
});

Binary file not shown.

View File

@@ -297,7 +297,11 @@ class AppComponent extends Component {
if (!this.state.contentScriptLoaded) {
let msg = 'Loading...';
if (this.state.contentScriptError) msg = `The Joplin extension is not available on this tab due to: ${this.state.contentScriptError}`;
return <div style={{ padding: 10, fontSize: 12, maxWidth: 200 }}>{msg}</div>;
return (
<div className="App Startup">
{msg}
</div>
);
}
const warningComponent = !this.props.warning ? null : <div className="Warning">{ this.props.warning }</div>;

View File

@@ -6,7 +6,7 @@ const shim: typeof ShimType = require('@joplin/lib/shim').default;
import { isCallbackUrl } from '@joplin/lib/callbackUrlUtils';
import { FileLocker } from '@joplin/utils/fs';
import { IpcMessageHandler, IpcServer, Message, newHttpError, sendMessage, SendMessageOptions, startServer, stopServer } from '@joplin/utils/ipc';
import { BrowserWindow, Tray, WebContents, screen, App, nativeTheme } from 'electron';
import { BrowserWindow, Tray, WebContents, screen, App, nativeTheme, Menu, session as electronSession, Session } from 'electron';
import bridge from './bridge';
import * as url from 'url';
const path = require('path');
@@ -30,8 +30,7 @@ interface RendererProcessQuitReply {
}
interface PluginWindows {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
[key: string]: any;
[key: string]: BrowserWindow;
}
type SecondaryWindowId = string;
@@ -48,7 +47,6 @@ export interface Options {
}
export default class ElectronAppWrapper {
private logger_: Logger = null;
private electronApp_: App;
private env_: string;
private isDebugMode_: boolean;
@@ -61,8 +59,7 @@ export default class ElectronAppWrapper {
private secondaryWindows_: Map<SecondaryWindowId, SecondaryWindowData> = new Map();
private willQuitApp_ = false;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
private tray_: any = null;
private tray_: Tray = null;
private buildDir_: string = null;
private rendererProcessQuitReply_: RendererProcessQuitReply = null;
@@ -70,13 +67,15 @@ export default class ElectronAppWrapper {
private updaterService_: AutoUpdaterService = null;
private customProtocolHandlers_: CustomProtocolHandlers|null = null;
private updatePollInterval_: ReturnType<typeof setTimeout>|null = null;
private joplinSession_: Session|null = null;
private profileLocker_: FileLocker|null = null;
private ipcServer_: IpcServer|null = null;
private ipcStartPort_ = 2658;
private ipcLogger_: Logger;
private ipcLoggerFilePath_: string;
private mainProcessLoggerFilePath_: string;
private ipcLogger_: LoggerWrapper;
private appLogger_: LoggerWrapper;
public constructor(electronApp: App, { env, profilePath, isDebugMode, initialCallbackUrl, isEndToEndTesting }: Options) {
this.electronApp_ = electronApp;
@@ -88,28 +87,20 @@ export default class ElectronAppWrapper {
this.profileLocker_ = new FileLocker(`${this.profilePath_}/lock`);
// Note: in certain contexts `this.logger_` doesn't seem to be available, especially for IPC
// calls, either because it hasn't been set or other issue. So we set one here specifically
// for this.
this.ipcLogger_ = new Logger();
this.ipcLoggerFilePath_ = `${profilePath}/log-cross-app-ipc.txt`;
this.ipcLogger_.addTarget(TargetType.File, {
path: this.ipcLoggerFilePath_,
const mainProcessLogger = new Logger();
this.mainProcessLoggerFilePath_ = `${profilePath}/log-main-process.txt`;
mainProcessLogger.addTarget(TargetType.File, {
path: this.mainProcessLoggerFilePath_,
});
this.ipcLogger_ = Logger.create('IPC', mainProcessLogger);
this.appLogger_ = Logger.create('App', mainProcessLogger);
}
public electronApp() {
return this.electronApp_;
}
public setLogger(v: Logger) {
this.logger_ = v;
}
public logger() {
return this.logger_;
}
public mainWindow() {
return this.win_;
}
@@ -122,8 +113,8 @@ export default class ElectronAppWrapper {
return !!this.ipcServer_;
}
public ipcLoggerFilePath() {
return this.ipcLoggerFilePath_;
public mainProcessLogFilePath() {
return this.mainProcessLoggerFilePath_;
}
public windowById(joplinId: string) {
@@ -176,6 +167,10 @@ export default class ElectronAppWrapper {
public async handleAppFailure(errorMessage: string, canIgnore: boolean, isTesting?: boolean) {
await bridge().captureException(new Error(errorMessage));
if (this.win_ && this.win_.isDestroyed()) {
return;
}
const buttons = [];
buttons.push(_('Quit'));
const exitIndex = 0;
@@ -199,7 +194,7 @@ export default class ElectronAppWrapper {
//
// Also only run this if not testing (crashing the renderer breaks automated
// tests).
if (this.win_ && !this.win_.webContents.isCrashed() && !isTesting) {
if (this.win_ && !this.win_.isDestroyed() && !this.win_.webContents.isCrashed() && !isTesting) {
this.win_.webContents.forcefullyCrashRenderer();
}
} else if (response === exitIndex) {
@@ -207,13 +202,46 @@ export default class ElectronAppWrapper {
}
}
private createJoplinSession_() {
const sessionPath = path.join(this.profilePath_, 'internal');
const joplinSession = electronSession.fromPath(sessionPath, { cache: false });
// One-time migration: copy existing dictionary words from the old Electron userData location into the new session.
const migrationFlagPath = path.join(this.profilePath_, 'spell-checker-migration-done');
if (!fs.existsSync(migrationFlagPath)) {
try {
const wordsToMigrate = new Set<string>();
const oldElectronDictPath = path.join(this.electronApp_.getPath('userData'), 'Custom Dictionary.txt');
if (fs.existsSync(oldElectronDictPath)) {
const content = fs.readFileSync(oldElectronDictPath, 'utf8');
const words = content.split('\n')
.map((w: string) => w.trim())
.filter((w: string) => w.length > 0 && !/^checksum_v1\s*=/.test(w));
for (const word of words) {
wordsToMigrate.add(word);
}
}
for (const word of wordsToMigrate) {
joplinSession.addWordToSpellCheckerDictionary(word);
}
fs.writeFileSync(migrationFlagPath, '', 'utf8');
} catch (error) {
console.warn('Failed to migrate spell-check dictionary:', error);
}
}
return joplinSession;
}
public createWindow() {
// Set to true to view errors if the application does not start
const debugEarlyBugs = this.env_ === 'dev' || this.isDebugMode_;
const windowStateKeeper = require('electron-window-state');
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const stateOptions: any = {
defaultWidth: Math.round(0.8 * screen.getPrimaryDisplay().workArea.width),
@@ -239,6 +267,7 @@ export default class ElectronAppWrapper {
// this needs to be a non-transparent color:
backgroundColor: nativeTheme.shouldUseDarkColors ? '#333' : '#fff',
webPreferences: {
session: this.joplinSession_,
nodeIntegration: true,
contextIsolation: false,
spellcheck: true,
@@ -348,7 +377,7 @@ export default class ElectronAppWrapper {
} catch (error) {
// This will throw an exception "Object has been destroyed" if the app is closed
// in less that the timeout interval. It can be ignored.
console.warn('Error opening dev tools', error);
this.appLogger_.warn('Error opening dev tools', error);
}
}, 1000);
}
@@ -410,12 +439,15 @@ export default class ElectronAppWrapper {
// On Windows and Linux, the app is closed when the window is closed *except* if the tray icon is used. In which
// case the app must be explicitly closed with Ctrl+Q or by right-clicking on the tray icon and selecting "Exit".
this.appLogger_.info('[appClose] Window close event - willQuitApp_:', this.willQuitApp_, 'rendererProcessQuitReply_:', this.rendererProcessQuitReply_, 'secondaryWindows:', this.secondaryWindows_.size, 'trayShown:', this.trayShown());
let isGoingToExit = false;
if (process.platform === 'darwin') {
if (this.willQuitApp_) {
isGoingToExit = true;
} else {
this.appLogger_.info('[appClose] macOS: willQuitApp_ is false, hiding window instead of closing');
event.preventDefault();
const w = this.win_;
@@ -439,21 +471,27 @@ export default class ElectronAppWrapper {
}
}
this.appLogger_.info('[appClose] isGoingToExit:', isGoingToExit);
if (isGoingToExit) {
if (!this.rendererProcessQuitReply_) {
// If we haven't notified the renderer process yet, do it now
// so that it can tell us if we can really close the app or not.
// Search for "appClose" event for closing logic on renderer side.
this.appLogger_.info('[appClose] Sending appClose to renderer, waiting for reply...');
event.preventDefault();
if (this.win_) this.win_.webContents.send('appClose');
} else {
// If the renderer process has responded, check if we can close or not
this.appLogger_.info('[appClose] Got renderer reply - canClose:', this.rendererProcessQuitReply_.canClose);
if (this.rendererProcessQuitReply_.canClose) {
// Really quit the app
this.appLogger_.info('[appClose] Closing app now');
this.rendererProcessQuitReply_ = null;
this.win_ = null;
} else {
// Wait for renderer to finish task
this.appLogger_.info('[appClose] Renderer says cannot close yet, waiting...');
event.preventDefault();
this.rendererProcessQuitReply_ = null;
}
@@ -469,8 +507,31 @@ export default class ElectronAppWrapper {
// Match the main window's zoom:
window.webContents.setZoomFactor(this.mainWindow().webContents.getZoomFactor());
window.once('close', () => {
this.secondaryWindows_.delete(windowId);
window.once('close', (event) => {
// Check both: BrowserWindow and webContents can be destroyed independently
if (this.win_ && !this.win_.isDestroyed() && !this.win_.webContents.isDestroyed()) {
this.win_.webContents.send('secondary-window-closing', windowId);
}
if (this.secondaryWindows_.has(windowId)) {
this.secondaryWindows_.delete(windowId);
// Avoid closing a destroyed window. Closing a destroyed window results in the following error:
// Error: Render frame was disposed before WebFrameMain could be accessed
const stillOpen = !window.isDestroyed();
if (stillOpen) {
event.preventDefault();
// As of March 2026, Electron crashes with "Assertion failed: (Environment::GetCurrent(isolate)) == (env)" if the native 'close'
// event is allowed to close a secondary window. As a workaround, briefly hide the window and .close() it later.
// See https://github.com/laurent22/joplin/issues/14628.
window.hide();
setTimeout(() => {
if (!window.isDestroyed()) {
window.close();
}
}, 100);
}
}
const allSecondaryWindowsClosed = this.secondaryWindows_.size === 0;
const mainWindowVisuallyClosed = this.mainWindowHidden_;
@@ -518,8 +579,8 @@ export default class ElectronAppWrapper {
// sends a message. In which case, the above code would try to
// access a destroyed webview.
// https://github.com/laurent22/joplin/issues/4570
console.error('Could not process plugin message:', message);
console.error(error);
this.appLogger_.error('Could not process plugin message:', message);
this.appLogger_.error(error);
}
});
@@ -544,8 +605,7 @@ export default class ElectronAppWrapper {
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
public registerPluginWindow(pluginId: string, window: any) {
public registerPluginWindow(pluginId: string, window: BrowserWindow) {
this.pluginWindows_[pluginId] = window;
}
@@ -574,6 +634,7 @@ export default class ElectronAppWrapper {
}
public quit() {
this.appLogger_.info('[appClose] quit() called');
this.onExit();
this.electronApp_.quit();
}
@@ -582,6 +643,7 @@ export default class ElectronAppWrapper {
dispatch: (action: { type: string; [key: string]: unknown })=> void,
syncPending: boolean,
) {
this.appLogger_.info('[appClose] quitWithSyncCheck() called - syncPending:', syncPending);
if (syncPending) {
dispatch({ type: 'QUIT_SYNC_DIALOG_OPEN' });
} else {
@@ -631,8 +693,7 @@ export default class ElectronAppWrapper {
}
// Note: this must be called only after the "ready" event of the app has been dispatched
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
public createTray(contextMenu: any) {
public createTray(contextMenu: Menu) {
try {
this.tray_ = new Tray(`${this.buildDir()}/icons/${this.trayIconFilename_()}`);
this.tray_.setToolTip(this.electronApp_.name);
@@ -640,7 +701,7 @@ export default class ElectronAppWrapper {
this.tray_.on('click', () => {
if (!this.mainWindow()) {
console.warn('The window object was not available during the click event from tray icon');
this.appLogger_.warn('The window object was not available during the click event from tray icon');
return;
}
if (!this.mainWindow().isVisible()) {
@@ -650,7 +711,7 @@ export default class ElectronAppWrapper {
}
});
} catch (error) {
console.error('Cannot create tray', error);
this.appLogger_.error('Cannot create tray', error);
}
}
@@ -797,7 +858,7 @@ export default class ElectronAppWrapper {
}
this.quit();
if (this.env() === 'dev') console.warn(`Closing the application because another instance is already running, or the previous instance was force-quit within the last ${Math.round(this.profileLocker_.options.interval / Second)} seconds.`);
if (this.env() === 'dev') this.appLogger_.warn(`Closing the application because another instance is already running, or the previous instance was force-quit within the last ${Math.round(this.profileLocker_.options.interval / Second)} seconds.`);
return true;
}
@@ -845,8 +906,7 @@ export default class ElectronAppWrapper {
return matchingProcesses.trim().length > 0;
} catch (error) {
if (error.stderr || error.exitCode !== 1) {
// eslint-disable-next-line no-console -- The main logger is not available at this point.
console.error('Failed to check for and enable accessibility support:', error.stderr);
this.appLogger_.error('Failed to check for and enable accessibility support:', error.stderr);
}
return false;
@@ -856,8 +916,7 @@ export default class ElectronAppWrapper {
// Work around https://issues.chromium.org/issues/431257156 by force-enabling accessibility
// when Orca (a screen reader) is running:
if (await isOrcaRunning()) {
// eslint-disable-next-line no-console -- The main logger is not available at this point.
console.log('Linux accessibility: Enabling full accessibility support.');
this.appLogger_.info('Linux accessibility: Enabling full accessibility support.');
this.electronApp().setAccessibilitySupportEnabled(true);
}
}
@@ -872,14 +931,18 @@ export default class ElectronAppWrapper {
await this.fixLinuxAccessibility_();
this.customProtocolHandlers_ = handleCustomProtocols();
// Session must be created before handleCustomProtocols() so both use the same object.
this.joplinSession_ = this.createJoplinSession_();
this.customProtocolHandlers_ = handleCustomProtocols(this.joplinSession_);
this.createWindow();
this.electronApp_.on('before-quit', () => {
this.appLogger_.info('[appClose] before-quit event fired, setting willQuitApp_ = true');
this.willQuitApp_ = true;
});
this.electronApp_.on('window-all-closed', () => {
this.appLogger_.info('[appClose] window-all-closed event fired');
this.quit();
});

View File

@@ -11,8 +11,7 @@ const logger = Logger.create('app.reducer');
export interface AppStateRoute {
type: string;
routeName: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
props: any;
props: Record<string, unknown>;
}
export enum AppStateDialogName {
@@ -22,8 +21,7 @@ export enum AppStateDialogName {
export interface AppStateDialog {
name: AppStateDialogName;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
props: Record<string, any>;
props: Record<string, unknown>;
}
export interface NoteIdToScrollPercent {

View File

@@ -43,7 +43,7 @@ const electronContextMenu = require('./services/electron-context-menu');
// Commands that are not tied to any particular component.
// The runtime for these commands can be loaded when the app starts.
import PerFolderSortOrderService from './services/sortOrder/PerFolderSortOrderService';
import PerFolderSortOrderService from '@joplin/lib/services/sortOrder/PerFolderSortOrderService';
import ShareService from '@joplin/lib/services/share/ShareService';
import checkForUpdates from './checkForUpdates';
import { AppState } from './app.reducer';
@@ -78,8 +78,7 @@ type StartupTask = { label: string; task: ()=> void|Promise<void> };
class Application extends BaseApplication {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
private checkAllPluginStartedIID_: any = null;
private checkAllPluginStartedIID_: ReturnType<typeof setInterval> = null;
private initPluginServiceDone_ = false;
private ocrService_: OcrService;
private protocolHandler_: CustomContentProtocolHandler;
@@ -638,18 +637,23 @@ class Application extends BaseApplication {
if (Setting.value('env') === 'dev') {
void AlarmService.updateAllNotifications();
RevisionService.instance().runInBackground();
} else {
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
void reg.scheduleSync(1000).then(() => {
// Wait for the first sync before updating the notifications, since synchronisation
// might change the notifications.
void AlarmService.updateAllNotifications();
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(() => {
// Wait for the first sync before updating the notifications, since synchronisation
// might change the notifications.
void AlarmService.updateAllNotifications();
void DecryptionWorker.instance().scheduleStart();
});
void DecryptionWorker.instance().scheduleStart();
RevisionService.instance().runInBackground();
});
}, 1000);
}
RevisionService.instance().runInBackground();
this.startRotatingLogMaintenance(Setting.value('profileDir'));
});
@@ -728,6 +732,10 @@ class Application extends BaseApplication {
});
}
});
ipcRenderer.on('secondary-window-closing', (_event, windowId: string) => {
this.dispatch({ type: 'WINDOW_CLOSE', windowId });
});
});
addTask('app/initPluginService', () => this.initPluginService());

View File

@@ -1,7 +1,7 @@
import ElectronAppWrapper from './ElectronAppWrapper';
import shim, { MessageBoxType } from '@joplin/lib/shim';
import { _, setLocale } from '@joplin/lib/locale';
import { BrowserWindow, nativeTheme, nativeImage, shell, dialog, MessageBoxSyncOptions, safeStorage, Menu, MenuItemConstructorOptions, MenuItem } from 'electron';
import { BrowserWindow, nativeTheme, nativeImage, shell, dialog, MessageBoxSyncOptions, safeStorage, Menu, MenuItemConstructorOptions, MenuItem, BrowserWindowConstructorOptions, FileFilter, SaveDialogOptions } from 'electron';
import { dirname, toSystemSlashes } from '@joplin/lib/path-utils';
import { fileUriToPath } from '@joplin/utils/url';
import { urlDecode } from '@joplin/lib/string-utils';
@@ -25,8 +25,7 @@ interface OpenDialogOptions {
properties?: string[];
defaultPath?: string;
createDirectory?: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
filters?: any[];
filters?: FileFilter[];
}
type OnAllowedExtensionsChange = (newExtensions: string[])=> void;
@@ -208,8 +207,7 @@ export class Bridge {
this.onAllowedExtensionsChangeListener_ = listener;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
public async captureException(error: any) {
public async captureException(error: unknown) {
Sentry.captureException(error);
// We wait to give the "beforeSend" event handler time to process the crash dump and write
// it to file.
@@ -335,8 +333,7 @@ export class Bridge {
return require('electron').shell.showItemInFolder(toSystemSlashes(fullPath));
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
public newBrowserWindow(options: any) {
public newBrowserWindow(options: BrowserWindowConstructorOptions) {
return new BrowserWindow(options);
}
@@ -353,8 +350,7 @@ export class Bridge {
return this.activeWindow().webContents.closeDevTools();
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
public async showSaveDialog(options: any) {
public async showSaveDialog(options: SaveDialogOptions) {
if (!options) options = {};
if (!('defaultPath' in options) && this.lastSelectedPaths_.file) options.defaultPath = this.lastSelectedPaths_.file;
const { filePath } = await dialog.showSaveDialog(this.activeWindow(), options);
@@ -381,8 +377,7 @@ export class Bridge {
}
// Don't use this directly - call one of the showXxxxxxxMessageBox() instead
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
private showMessageBox_(window: any, options: MessageDialogOptions): number {
private showMessageBox_(window: BrowserWindow, options: MessageDialogOptions): number {
if (!window) window = this.activeWindow();
return dialog.showMessageBoxSync(window, { message: '', ...options });
}
@@ -428,8 +423,7 @@ export class Bridge {
return result;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
public showInfoMessageBox(message: string, options: any = {}) {
public showInfoMessageBox(message: string, options: MessageDialogOptions = {}) {
const result = this.showMessageBox_(this.activeWindow(), { type: 'info',
message: message,
buttons: [_('OK')], ...options });
@@ -559,7 +553,7 @@ export class Bridge {
});
if (buttonIndex === 1) {
void this.openItem(this.electronApp().ipcLoggerFilePath());
void this.openItem(this.electronApp().mainProcessLogFilePath());
}
}
}
@@ -583,6 +577,11 @@ export class Bridge {
execPath: process.env.PORTABLE_EXECUTABLE_FILE,
};
app.relaunch(options);
} else if (process.env.APPIMAGE && !this.altInstanceId_) {
app.relaunch({
execPath: process.env.APPIMAGE,
args: ['--appimage-extract-and-run'],
});
} else if (this.altInstanceId_) {
// Couldn't get it to work using relaunch() - it would just "close" the app, but it
// would still be open in the tray except unusable. Or maybe it reopens it quickly but

View File

@@ -4,7 +4,7 @@ import { _ } from '@joplin/lib/locale';
import bridge from './services/bridge';
import KvStore from '@joplin/lib/services/KvStore';
import * as ArrayUtils from '@joplin/lib/ArrayUtils';
import { CheckForUpdateOptions, extractVersionInfo, GitHubRelease } from './utils/checkForUpdatesUtils';
import { CheckForUpdateOptions, extractVersionInfo, GitHubRelease, handleReleaseResponseError } from './utils/checkForUpdatesUtils';
import { PackageInfo } from '@joplin/lib/versionInfo';
import { compareVersions } from 'compare-versions';
const packageInfo: PackageInfo = require('./packageInfo.js');
@@ -29,7 +29,8 @@ async function fetchLatestReleases() {
if (!response.ok) {
const responseText = await response.text();
throw new Error(`Cannot get latest release info: ${responseText.substr(0, 500)}`);
logger.error(`Cannot get latest release info (${response.status}): ${responseText.substr(0, 500)}`);
handleReleaseResponseError(response.status, responseText);
}
return (await response.json()) as GitHubRelease[];
@@ -48,8 +49,8 @@ function truncateText(text: string, length: number) {
}
async function getSkippedVersions(): Promise<string[]> {
const r = await KvStore.instance().value<string>('updateCheck::skippedVersions');
return r ? JSON.parse(r) : [];
const r = await KvStore.instance().value('updateCheck::skippedVersions');
return r && typeof r === 'string' ? JSON.parse(r) : [];
}
async function isSkippedVersion(v: string): Promise<boolean> {

View File

@@ -13,7 +13,8 @@ export const runtime = (): CommandRuntime => {
return {
execute: async () => {
const appPath = app.getPath('exe');
const cmd = `${appPath} --env dev`;
// Quote the path so it works when it contains spaces (e.g. "C:\Program Files\Joplin\Joplin.exe" on Windows)
const cmd = `"${appPath}" --env dev`;
clipboard.writeText(cmd);
await shim.showMessageBox(`The dev mode command has been copied to clipboard:\n\n${cmd}`, { type: MessageBoxType.Info });
},

View File

@@ -0,0 +1,79 @@
import { CommandRuntime, CommandDeclaration } from '@joplin/lib/services/CommandService';
import Resource from '@joplin/lib/models/Resource';
import Setting from '@joplin/lib/models/Setting';
import shim from '@joplin/lib/shim';
import { _ } from '@joplin/lib/locale';
import { ResourceOcrStatus } from '@joplin/lib/services/database/types';
import bridge from '../services/bridge';
import Logger from '@joplin/utils/Logger';
const logger = Logger.create('createAccessibleDocument');
export const declaration: CommandDeclaration = {
name: 'createAccessibleDocument',
};
export const runtime = (): CommandRuntime => {
return {
execute: async (_context: unknown, resourceId: string) => {
const resource = await Resource.load(resourceId);
if (!resource) {
bridge().showErrorMessageBox(_('Resource not found'));
return;
}
const resourcePath = Resource.fullPath(resource);
if (resource.mime !== 'application/pdf') {
bridge().showInfoMessageBox(_('This feature is only available for PDF files.'));
return;
}
if (resource.ocr_status !== ResourceOcrStatus.Done) {
bridge().showInfoMessageBox(_('OCR is not complete. Please wait for OCR to finish before creating an accessible document.'));
return;
}
const ocrDetails = resource.ocr_details;
// If ocr_details is missing (legacy PDF processed before this feature),
// automatically re-run OCR to get the coordinate data
if (!ocrDetails) {
const result = await bridge().showMessageBox(_('OCR needs to run to generate an accessible document. This may take a moment. Would you like to continue?'), {
buttons: [_('Run OCR'), _('Cancel')],
});
if (result === 1) return; // User cancelled
// Trigger OCR re-run with TodoAccessible status to request full OCR details
await Resource.save({
id: resource.id,
ocr_status: ResourceOcrStatus.TodoAccessible,
ocr_details: '',
ocr_error: '',
ocr_text: '',
});
bridge().showInfoMessageBox(_('OCR has been queued. Please wait for it to complete and then try again.'));
return;
}
// Show save dialog
const defaultFilename = `${(resource.filename || resource.title || resource.id).replace(/\.pdf$/i, '')}_accessible.pdf`;
const outputPath = await bridge().showSaveDialog({
defaultPath: defaultFilename,
filters: [{ name: 'PDF', extensions: ['pdf'] }],
});
if (!outputPath) return;
try {
await shim.createAccessiblePdf(resourcePath, ocrDetails, outputPath, Setting.value('tempDir'));
await bridge().openItem(outputPath);
} catch (error) {
logger.error(error);
bridge().showErrorMessageBox(_('Failed to create accessible document: %s', error.message));
}
},
};
};

View File

@@ -1,6 +1,7 @@
// AUTO-GENERATED using `gulp buildScriptIndexes`
import * as copyDevCommand from './copyDevCommand';
import * as copyToClipboard from './copyToClipboard';
import * as createAccessibleDocument from './createAccessibleDocument';
import * as editProfileConfig from './editProfileConfig';
import * as emptyTrash from './emptyTrash';
import * as exportDeletionLog from './exportDeletionLog';
@@ -27,6 +28,7 @@ import * as toggleTabMovesFocus from './toggleTabMovesFocus';
const index: any[] = [
copyDevCommand,
copyToClipboard,
createAccessibleDocument,
editProfileConfig,
emptyTrash,
exportDeletionLog,

View File

@@ -19,7 +19,7 @@ import shouldShowMissingPasswordWarning from '@joplin/lib/components/shared/conf
import MacOSMissingPasswordHelpLink from './controls/MissingPasswordHelpLink';
const { KeymapConfigScreen } = require('../KeymapConfig/KeymapConfigScreen');
import SettingComponent, { UpdateSettingValueEvent } from './controls/SettingComponent';
import shim from '@joplin/lib/shim';
import shim, { MessageBoxType } from '@joplin/lib/shim';
interface Font {
@@ -145,8 +145,16 @@ class ConfigScreenComponent extends React.Component<any, any> {
screenName = section.name;
if (this.hasChanges()) {
const ok = await shim.showConfirmationDialog(_('This will open a new screen. Save your current changes?'));
if (ok) {
const answer = await shim.showMessageBox(
_('This will open a new screen. Save your current changes?'),
{
type: MessageBoxType.Confirm,
buttons: [_('Save changes'), _('Discard changes')],
defaultId: 0,
cancelId: 1,
},
);
if (answer === 0) {
await shared.saveSettings(this);
}
}

View File

@@ -1,7 +1,6 @@
import styled from 'styled-components';
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const Root = styled.h1<any>`
const Root = styled.h1<{ justifyContent?: string }>`
display: flex;
justify-content: ${props => props.justifyContent ? props.justifyContent : 'center'};
font-family: ${props => props.theme.fontFamily};

View File

@@ -8,7 +8,7 @@ const { themeStyle } = require('@joplin/lib/theme');
const Shared = require('@joplin/lib/components/shared/dropbox-login-shared');
interface Props {
themeId: string;
themeId: number;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
@@ -63,8 +63,7 @@ class DropboxLoginScreenComponent extends React.Component<any, any> {
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const mapStateToProps = (state: any) => {
const mapStateToProps = (state: { settings: { theme: number } }) => {
return {
themeId: state.settings.theme,
};

View File

@@ -47,10 +47,13 @@ export default function(props: Props) {
}, [props.dispatch]);
useEffect(() => {
if (!titleInputRef.current) return;
focus('Dialog::titleInputRef', titleInputRef.current);
setTimeout(() => {
titleInputRef.current.select();
if (titleInputRef.current) {
titleInputRef.current.select();
}
}, 100);
}, []);

View File

@@ -10,18 +10,25 @@ import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types';
import { getEncryptionEnabled, masterKeyEnabled, SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils';
import { getDefaultMasterKey, getMasterPasswordStatusMessage, masterPasswordIsValid, toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils';
import Button, { ButtonLevel } from '../Button/Button';
import { useCallback, useId, useMemo, useState } from 'react';
import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
import { connect } from 'react-redux';
import { AppState } from '../../app.reducer';
import { AppState, AppStateDialogName } from '../../app.reducer';
import Setting from '@joplin/lib/models/Setting';
import CommandService from '@joplin/lib/services/CommandService';
import { PublicPrivateKeyPair } from '@joplin/lib/services/e2ee/ppk/ppk';
import ToggleAdvancedSettingsButton from '../ConfigScreen/controls/ToggleAdvancedSettingsButton';
import MacOSMissingPasswordHelpLink from '../ConfigScreen/controls/MissingPasswordHelpLink';
import { Dispatch } from 'redux';
import { shouldCancelPendingEnableAfterMasterPasswordDialog, shouldOpenMasterPasswordDialogForEnable, shouldResumeEnableAfterMasterPasswordDialog } from './enableFlow';
import Dialog from '@joplin/lib/components/Dialog';
import DialogButtonRow from '../DialogButtonRow';
import DialogTitle from '../DialogTitle';
import PasswordInput from '../PasswordInput/PasswordInput';
interface Props {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
themeId: any;
dispatch: Dispatch;
masterKeys: MasterKeyEntity[];
passwords: Record<string, string>;
notLoadedMasterKeys: string[];
@@ -30,10 +37,17 @@ interface Props {
activeMasterKeyId: string;
masterPassword: string;
ppk: PublicPrivateKeyPair;
masterPasswordDialogOpen: boolean;
}
const EncryptionConfigScreen = (props: Props) => {
export const EncryptionConfigScreen = (props: Props) => {
const { inputPasswords, onInputPasswordChange } = useInputPasswords(props.passwords);
const [pendingEnableEncryption, setPendingEnableEncryption] = useState(false);
const [enableEncryptionPromptVisible, setEnableEncryptionPromptVisible] = useState(false);
const [enableEncryptionPassword, setEnableEncryptionPassword] = useState('');
const promptPromiseRef = useRef<(password: string | null)=> void>(null);
const wasMasterPasswordDialogOpen = useRef(props.masterPasswordDialogOpen);
const theme = useMemo(() => {
return themeStyle(props.themeId);
@@ -44,6 +58,41 @@ const EncryptionConfigScreen = (props: Props) => {
const { showDisabledMasterKeys, toggleShowDisabledMasterKeys } = useToggleShowDisabledMasterKeys();
const needMasterPassword = useNeedMasterPassword(passwordChecks, props.masterKeys);
useEffect(() => {
const wasOpen = wasMasterPasswordDialogOpen.current;
wasMasterPasswordDialogOpen.current = props.masterPasswordDialogOpen;
if (shouldCancelPendingEnableAfterMasterPasswordDialog({
pendingEnableEncryption,
wasMasterPasswordDialogOpen: wasOpen,
masterPasswordDialogOpen: props.masterPasswordDialogOpen,
masterPassword: props.masterPassword,
})) {
setPendingEnableEncryption(false);
return;
}
if (!shouldResumeEnableAfterMasterPasswordDialog({
pendingEnableEncryption,
wasMasterPasswordDialogOpen: wasOpen,
masterPasswordDialogOpen: props.masterPasswordDialogOpen,
masterPassword: props.masterPassword,
})) return;
const masterKey = getDefaultMasterKey();
void (async () => {
try {
await toggleAndSetupEncryption(EncryptionService.instance(), true, masterKey, props.masterPassword);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
await dialogs.alert(message);
} finally {
setPendingEnableEncryption(false);
}
})();
}, [pendingEnableEncryption, props.masterPasswordDialogOpen, props.masterPassword]);
const onUpgradeMasterKey = useCallback(async (mk: MasterKeyEntity) => {
const password = determineKeyPassword(mk.id, masterPasswordKeys, props.masterPassword, props.passwords);
const result = await upgradeMasterKey(mk, password);
@@ -194,14 +243,32 @@ const EncryptionConfigScreen = (props: Props) => {
const newEnabled = !isEnabled;
const masterKey = getDefaultMasterKey();
const hasMasterPassword = !!props.masterPassword;
let newPassword = '';
let newPassword: string | null = '';
if (isEnabled) {
const answer = await dialogs.confirm(_('Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?'));
if (!answer) return;
} else {
const msg = enableEncryptionConfirmationMessages(masterKey, hasMasterPassword);
newPassword = await dialogs.prompt(msg.join('\n\n'), '', '', { type: 'password' });
if (shouldOpenMasterPasswordDialogForEnable({
hasMasterPassword,
masterPasswordDialogOpen: props.masterPasswordDialogOpen,
})) {
setPendingEnableEncryption(true);
props.dispatch({
type: 'DIALOG_OPEN',
name: AppStateDialogName.MasterPassword,
});
return;
}
// Wait for the custom React Dialog to resolve
setEnableEncryptionPassword('');
setEnableEncryptionPromptVisible(true);
newPassword = await new Promise<string | null>((resolve) => {
promptPromiseRef.current = resolve;
});
if (newPassword === null) return; // User cancelled
}
if (hasMasterPassword && newEnabled) {
@@ -216,7 +283,64 @@ const EncryptionConfigScreen = (props: Props) => {
} catch (error) {
await dialogs.alert(error.message);
}
}, [props.masterPassword]);
}, [props.dispatch, props.masterPassword, props.masterPasswordDialogOpen]);
const renderEnableEncryptionDialog = () => {
if (!enableEncryptionPromptVisible) return null;
const masterKey = getDefaultMasterKey();
const hasMasterPassword = !!props.masterPassword;
const msg = enableEncryptionConfirmationMessages(masterKey, hasMasterPassword);
const messageComps = msg.map((m, index) => <p key={index} style={theme.textStyle}>{m}</p>);
const onClose = () => {
setEnableEncryptionPromptVisible(false);
if (promptPromiseRef.current) promptPromiseRef.current(null);
};
const onDialogButtonRowClick = (event: { buttonName: string }) => {
if (event.buttonName === 'cancel') {
onClose();
return;
}
if (event.buttonName === 'ok') {
setEnableEncryptionPromptVisible(false);
if (promptPromiseRef.current) promptPromiseRef.current(enableEncryptionPassword);
}
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Required because PasswordInput's ChangeEventHandler type is incorrect
const onPasswordInputChange = (event: any) => {
setEnableEncryptionPassword(event.target.value);
};
return (
<Dialog onCancel={onClose} className="enable-encryption-dialog">
<div className="dialog-root">
<DialogTitle title={_('Enable encryption')}/>
<div className="dialog-content">
<div style={{ marginBottom: 16 }}>
{messageComps}
</div>
<div style={{ marginBottom: 16 }}>
<label style={{ ...theme.textStyle, marginBottom: 5, display: 'block' }} htmlFor="enable-encryption-password">{_('Password:')}</label>
<PasswordInput
inputId="enable-encryption-password"
value={enableEncryptionPassword}
onChange={onPasswordInputChange}
/>
</div>
</div>
<DialogButtonRow
themeId={props.themeId}
onClick={onDialogButtonRowClick}
okButtonDisabled={!enableEncryptionPassword}
/>
</div>
</Dialog>
);
};
const renderEncryptionSection = () => {
const decryptedItemsInfo = <p>{decryptedStatText(stats)}</p>;
@@ -398,6 +522,7 @@ const EncryptionConfigScreen = (props: Props) => {
{renderMasterKeySection(props.masterKeys.filter(mk => !masterKeyEnabled(mk)), false)}
{renderNonExistingMasterKeysSection()}
{renderAdvancedSection()}
{renderEnableEncryptionDialog()}
</div>
);
};
@@ -415,6 +540,7 @@ const mapStateToProps = (state: AppState) => {
notLoadedMasterKeys: state.notLoadedMasterKeys,
masterPassword: state.settings['encryption.masterPassword'],
ppk: syncInfo.ppk,
masterPasswordDialogOpen: !!state.dialogs.find(dialog => dialog.name === AppStateDialogName.MasterPassword),
};
};

View File

@@ -0,0 +1,51 @@
import { shouldCancelPendingEnableAfterMasterPasswordDialog, shouldOpenMasterPasswordDialogForEnable, shouldResumeEnableAfterMasterPasswordDialog } from './enableFlow';
describe('enableFlow', () => {
test('opens the master password dialog when enabling encryption without a stored master password', () => {
expect(shouldOpenMasterPasswordDialogForEnable({
hasMasterPassword: false,
masterPasswordDialogOpen: false,
})).toBe(true);
});
test('does not reopen the master password dialog if it is already open', () => {
expect(shouldOpenMasterPasswordDialogForEnable({
hasMasterPassword: false,
masterPasswordDialogOpen: true,
})).toBe(false);
});
test('does not open the master password dialog when a master password already exists', () => {
expect(shouldOpenMasterPasswordDialogForEnable({
hasMasterPassword: true,
masterPasswordDialogOpen: false,
})).toBe(false);
});
test('resumes enabling encryption after the dialog closes with a saved password', () => {
expect(shouldResumeEnableAfterMasterPasswordDialog({
pendingEnableEncryption: true,
wasMasterPasswordDialogOpen: true,
masterPasswordDialogOpen: false,
masterPassword: 'new-password',
})).toBe(true);
});
test('cancels the pending enable flow if the dialog closes without a password', () => {
expect(shouldCancelPendingEnableAfterMasterPasswordDialog({
pendingEnableEncryption: true,
wasMasterPasswordDialogOpen: true,
masterPasswordDialogOpen: false,
masterPassword: '',
})).toBe(true);
});
test('does not resume while the dialog is still open', () => {
expect(shouldResumeEnableAfterMasterPasswordDialog({
pendingEnableEncryption: true,
wasMasterPasswordDialogOpen: true,
masterPasswordDialogOpen: true,
masterPassword: 'new-password',
})).toBe(false);
});
});

View File

@@ -0,0 +1,23 @@
interface OpenDialogInput {
hasMasterPassword: boolean;
masterPasswordDialogOpen: boolean;
}
interface ResumeEnableInput {
pendingEnableEncryption: boolean;
wasMasterPasswordDialogOpen: boolean;
masterPasswordDialogOpen: boolean;
masterPassword: string;
}
export const shouldOpenMasterPasswordDialogForEnable = ({ hasMasterPassword, masterPasswordDialogOpen }: OpenDialogInput) => {
return !hasMasterPassword && !masterPasswordDialogOpen;
};
export const shouldResumeEnableAfterMasterPasswordDialog = ({ pendingEnableEncryption, wasMasterPasswordDialogOpen, masterPasswordDialogOpen, masterPassword }: ResumeEnableInput) => {
return pendingEnableEncryption && wasMasterPasswordDialogOpen && !masterPasswordDialogOpen && !!masterPassword;
};
export const shouldCancelPendingEnableAfterMasterPasswordDialog = ({ pendingEnableEncryption, wasMasterPasswordDialogOpen, masterPasswordDialogOpen, masterPassword }: ResumeEnableInput) => {
return pendingEnableEncryption && wasMasterPasswordDialogOpen && !masterPasswordDialogOpen && !masterPassword;
};

View File

@@ -31,8 +31,7 @@ interface State {
interface Props {
message?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
children: any;
children: React.ReactNode;
}
interface BannerProps {

View File

@@ -6,14 +6,12 @@ import { _ } from '@joplin/lib/locale';
interface Props {
tip: string;
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
onClick: Function;
onClick: ()=> void;
themeId: number;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
style: any;
style?: React.CSSProperties;
'aria-controls'?: string;
'aria-expanded'?: string;
'aria-expanded'?: boolean;
}
class HelpButtonComponent extends React.Component<Props> {
@@ -31,8 +29,7 @@ class HelpButtonComponent extends React.Component<Props> {
const theme = themeStyle(this.props.themeId);
const style = { ...this.props.style, color: theme.color, textDecoration: 'none' };
const helpIconStyle = { flex: 0, width: 16, height: 16, marginLeft: 10 };
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const extraProps: any = {};
const extraProps: Record<string, string> = {};
if (this.props.tip) {
extraProps['data-tip'] = this.props.tip;
extraProps['aria-description'] = this.props.tip;

View File

@@ -3,11 +3,9 @@ import { themeStyle } from '@joplin/lib/theme';
interface Props {
themeId: number;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
style: any;
style?: React.CSSProperties;
iconName: string;
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
onClick: Function;
onClick: ()=> void;
}
class IconButton extends React.Component<Props> {
@@ -20,7 +18,7 @@ class IconButton extends React.Component<Props> {
};
const icon = <i style={iconStyle} className={`fas ${this.props.iconName}`}></i>;
const rootStyle = {
const rootStyle: React.CSSProperties = {
display: 'flex',
textDecoration: 'none',
padding: 10,

View File

@@ -45,6 +45,9 @@ import PluginNotification from './PluginNotification/PluginNotification';
import { Toast } from '@joplin/lib/services/plugins/api/types';
import PluginService from '@joplin/lib/services/plugins/PluginService';
import QuitSyncDialog from './QuitSyncDialog';
import Logger from '@joplin/utils/Logger';
const logger = Logger.create('MainScreen');
const ipcRenderer = require('electron').ipcRenderer;
@@ -277,10 +280,12 @@ class MainScreenComponent extends React.Component<Props, State> {
// If a note is being saved, we wait till it is saved and then call
// "appCloseReply" again.
ipcRenderer.on('appClose', async () => {
logger.info('[appClose] Received appClose event - hasNotesBeingSaved:', this.props.hasNotesBeingSaved);
if (this.waitForNotesSavedIID_) shim.clearInterval(this.waitForNotesSavedIID_);
this.waitForNotesSavedIID_ = null;
const sendCanClose = async (canClose: boolean) => {
logger.info('[appClose] Sending appCloseReply - canClose:', canClose);
if (canClose) {
Setting.setValue('wasClosedSuccessfully', true);
await Setting.saveAll();
@@ -291,8 +296,10 @@ class MainScreenComponent extends React.Component<Props, State> {
await sendCanClose(!this.props.hasNotesBeingSaved);
if (this.props.hasNotesBeingSaved) {
logger.info('[appClose] Notes are being saved, waiting...');
this.waitForNotesSavedIID_ = shim.setInterval(() => {
if (!this.props.hasNotesBeingSaved) {
logger.info('[appClose] Notes saved, now sending canClose: true');
shim.clearInterval(this.waitForNotesSavedIID_);
this.waitForNotesSavedIID_ = null;
void sendCanClose(true);

View File

@@ -41,7 +41,7 @@ export default function(props: Props) {
if (mode === Mode.Reset) return false;
return true;
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [status]);
}, [status, mode]);
const onClose = useCallback(() => {
props.dispatch({
@@ -90,10 +90,12 @@ export default function(props: Props) {
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [currentPassword, password1, onClose, mode]);
// Show the "Re-enter password" confirmation field
const needToRepeatPassword = useMemo(() => {
if (mode === Mode.Reset) return true;
if (showCurrentPassword) return true;
return !hasMasterPasswordEncryptedData;
}, [hasMasterPasswordEncryptedData, mode]);
}, [mode, showCurrentPassword, hasMasterPasswordEncryptedData]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const onCurrentPasswordChange = useCallback((event: any) => {
@@ -138,6 +140,7 @@ export default function(props: Props) {
}, [currentPassword]);
function renderPasswordForm() {
const passwordsMatch = password1 === password2;
const renderCurrentPassword = () => {
if (!showCurrentPassword) return null;
@@ -161,11 +164,11 @@ export default function(props: Props) {
const renderResetMasterPasswordLink = () => {
if (mode === Mode.Reset) return null;
if (status === MasterPasswordStatus.Valid) return null;
return <p><a href="#" onClick={onToggleMode}>Reset master password</a></p>;
return <p><a href="#" onClick={onToggleMode}>{_('Reset master password')}</a></p>;
};
if (showPasswordForm) {
const enterPasswordLabel = [MasterPasswordStatus.Loaded, MasterPasswordStatus.Valid].includes(status) ? 'Enter new password' : 'Enter password';
const enterPasswordLabel = [MasterPasswordStatus.Loaded, MasterPasswordStatus.Valid].includes(status) ? _('Enter new password') : _('Enter password');
return (
<div>
@@ -176,22 +179,32 @@ export default function(props: Props) {
value={password1}
onChange={onPasswordChange1}
/>
{needToRepeatPassword && (
<LabelledPasswordInput
labelText={_('Re-enter password')}
value={password2}
onChange={onPasswordChange2}
/>
<>
<LabelledPasswordInput
labelText={_('Re-enter password')}
value={password2}
onChange={onPasswordChange2}
valid={password2 ? passwordsMatch : undefined}
/>
{password2 && !passwordsMatch && (
<p className="error-message">
{_('Passwords do not match')}
</p>
)}
</>
)}
</div>
<p className="bold">Please make sure you remember your password. For security reasons, it is not possible to recover it if it is lost.</p>
<p className="bold">{_('Please make sure you remember your password. It cannot be recovered if lost, and any data encrypted with it will become inaccessible.')}</p>
{renderResetMasterPasswordLink()}
</div>
);
} else {
return (
<p>
<a onClick={onShowPasswordForm} href="#">Change master password</a>
<a onClick={onShowPasswordForm} href="#">{_('Change master password')}</a>
</p>
);
}
@@ -201,16 +214,16 @@ export default function(props: Props) {
if (mode === Mode.Reset) {
return (
<div className="dialog-content">
<p>Attention: After resetting your password it will no longer be possible to decrypt any data encrypted with your current password. All encrypted shared notebooks will also be unshared, so please ask the notebook owner to share it again with you.</p>
<p>{_('Attention: After resetting your password it will no longer be possible to decrypt any data encrypted with your current password. All encrypted shared notebooks will also be unshared, so please ask the notebook owner to share it again with you.')}</p>
{renderPasswordForm()}
</div>
);
} else {
return (
<div className="dialog-content">
<p>Your master password is used to protect sensitive information. In particular, it is used to encrypt your notes when end-to-end encryption (E2EE) is enabled, or to share and encrypt notes with someone who has E2EE enabled.</p>
<p>{_('Your master password is used to protect sensitive information. In particular, it is used to encrypt your notes when end-to-end encryption (E2EE) is enabled, or to share and encrypt notes with someone who has E2EE enabled.')}</p>
<p>
<span>{'Master password status:'}</span> <span className="bold">{getMasterPasswordStatusMessage(status)}</span>
<span>{_('Master password status:')}</span> <span className="bold">{getMasterPasswordStatusMessage(status)}</span>
</p>
{renderPasswordForm()}
</div>

View File

@@ -709,6 +709,7 @@ function useMenu(props: Props) {
menuItemDic.textCut,
menuItemDic.textPaste,
menuItemDic.pasteAsText,
menuItemDic.pasteAsMarkdown,
menuItemDic.textSelectAll,
separator(),
menuItemDic.globalUndo,
@@ -822,6 +823,12 @@ function useMenu(props: Props) {
Setting.incValue('windowContentZoomFactor', -10);
},
accelerator: 'CommandOrControl+-',
}, {
type: 'separator',
visible: shim.isMac(),
}, {
role: 'togglefullscreen',
visible: shim.isMac(),
}],
},
go: {

View File

@@ -1,5 +1,4 @@
import { defaultWindowId } from '@joplin/lib/reducer';
import shim from '@joplin/lib/shim';
import * as React from 'react';
import { useState, useEffect, useRef, createContext } from 'react';
import { createPortal } from 'react-dom';
@@ -40,7 +39,7 @@ const useDocument = (
useEffect(() => {
let openedWindow: Window|null = null;
const unmounted = false;
let unmounted = false;
if (iframeElement) {
setDoc(iframeElement?.contentWindow?.document);
} else if (mode === WindowMode.NewWindow) {
@@ -52,11 +51,16 @@ const useDocument = (
void (async () => {
while (!unmounted) {
await new Promise<void>(resolve => {
shim.setTimeout(() => resolve(), 2000);
setTimeout(() => resolve(), 2000);
});
// Re-check after sleep to avoid duplicate WINDOW_CLOSE if IPC already fired.
if (unmounted) break;
if (openedWindow?.closed) {
onCloseRef.current?.();
// Null out doc first so React stops rendering into the destroyed window
// before WINDOW_CLOSE triggers unmounting (prevents renderer crash on Windows).
setDoc(null);
openedWindow = null;
break;
}
@@ -65,6 +69,8 @@ const useDocument = (
}
return () => {
unmounted = true;
// Delay: Closing immediately causes Electron to crash
setTimeout(() => {
if (!openedWindow?.closed) {

View File

@@ -22,18 +22,21 @@ interface KeyToLabelMap {
[key: string]: string;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
let markupToHtml_: any = null;
let markupToHtml_: ReturnType<typeof markupLanguageUtils.newMarkupToHtml> = null;
function markupToHtml() {
if (markupToHtml_) return markupToHtml_;
markupToHtml_ = markupLanguageUtils.newMarkupToHtml();
return markupToHtml_;
}
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
function countElements(text: string, wordSetter: Function, characterSetter: Function, characterNoSpaceSetter: Function, cjkCharacterSetter: React.Dispatch<React.SetStateAction<number>>, lineSetter: Function) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
Countable.count(text, (counter: any) => {
interface CounterResult {
words: number;
all: number;
characters: number;
}
function countElements(text: string, wordSetter: React.Dispatch<React.SetStateAction<number>>, characterSetter: React.Dispatch<React.SetStateAction<number>>, characterNoSpaceSetter: React.Dispatch<React.SetStateAction<number>>, cjkCharacterSetter: React.Dispatch<React.SetStateAction<number>>, lineSetter: React.Dispatch<React.SetStateAction<number>>) {
Countable.count(text, (counter: CounterResult) => {
wordSetter(counter.words);
characterSetter(counter.all);
characterNoSpaceSetter(counter.characters);
@@ -53,8 +56,7 @@ function formatReadTime(readTimeMinutes: number) {
export default function NoteContentPropertiesDialog(props: NoteContentPropertiesDialogProps) {
const theme = themeStyle(props.themeId);
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const tableBodyComps: any[] = [];
const tableBodyComps: React.JSX.Element[] = [];
// For the source Markdown
const [lines, setLines] = useState<number>(0);
const [words, setWords] = useState<number>(0);

View File

@@ -4,62 +4,49 @@ describe('useContextMenu', () => {
const resourceId = 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4';
const resourceId2 = 'b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5';
it('should return resource ID when cursor is inside markdown image', () => {
it('should return type=image when cursor is inside markdown image', () => {
const line = `![alt text](:/${resourceId})`;
expect(getResourceIdFromMarkup(line, 0)).toBe(resourceId);
expect(getResourceIdFromMarkup(line, 15)).toBe(resourceId);
expect(getResourceIdFromMarkup(line, line.length - 1)).toBe(resourceId);
const result = getResourceIdFromMarkup(line, 15);
expect(result.resourceId).toBe(resourceId);
expect(result.type).toBe('image');
expect(line.substring(result.markupStart, result.markupEnd)).toBe(line);
});
it('should return null when cursor is outside markdown image', () => {
it('should return type=file when cursor is inside markdown link', () => {
const line = `[document.pdf](:/${resourceId})`;
const result = getResourceIdFromMarkup(line, 15);
expect(result.resourceId).toBe(resourceId);
expect(result.type).toBe('file');
expect(line.substring(result.markupStart, result.markupEnd)).toBe(line);
});
it('should return null when cursor is outside markup', () => {
const line = `Some text ![alt](:/${resourceId}) more text`;
expect(getResourceIdFromMarkup(line, 5)).toBeNull();
expect(getResourceIdFromMarkup(line, line.length - 5)).toBeNull();
});
it('should handle markdown image without alt text', () => {
const line = `![](:/${resourceId})`;
expect(getResourceIdFromMarkup(line, 5)).toBe(resourceId);
});
it('should correctly distinguish between image and file on same line', () => {
const line = `![image](:/${resourceId}) [file](:/${resourceId2})`;
const imageResult = getResourceIdFromMarkup(line, 10);
expect(imageResult.resourceId).toBe(resourceId);
expect(imageResult.type).toBe('image');
it('should return resource ID when cursor is inside HTML img tag', () => {
const line = `<img src=":/${resourceId}" />`;
expect(getResourceIdFromMarkup(line, 10)).toBe(resourceId);
});
it('should handle HTML img tag with additional attributes', () => {
const line = `<img alt="test" src=":/${resourceId}" width="100" />`;
expect(getResourceIdFromMarkup(line, 25)).toBe(resourceId);
});
it('should return null when cursor is outside HTML img tag', () => {
const line = `text <img src=":/${resourceId}" /> more`;
expect(getResourceIdFromMarkup(line, 2)).toBeNull();
expect(getResourceIdFromMarkup(line, line.length - 2)).toBeNull();
});
it('should return correct resource ID when multiple images on same line', () => {
const line = `![first](:/${resourceId}) ![second](:/${resourceId2})`;
expect(getResourceIdFromMarkup(line, 10)).toBe(resourceId);
expect(getResourceIdFromMarkup(line, 50)).toBe(resourceId2);
const fileResult = getResourceIdFromMarkup(line, 48);
expect(fileResult.resourceId).toBe(resourceId2);
expect(fileResult.type).toBe('file');
});
it('should return null for empty line', () => {
expect(getResourceIdFromMarkup('', 0)).toBeNull();
});
it('should return null for line without images', () => {
it('should return null for line without resources', () => {
expect(getResourceIdFromMarkup('Just some regular text', 10)).toBeNull();
});
it('should return null for non-resource links', () => {
const line = '![alt](https://example.com/image.png)';
expect(getResourceIdFromMarkup(line, 10)).toBeNull();
});
it('should handle cursor at exact boundaries of image markup', () => {
const line = `![a](:/${resourceId})`;
expect(getResourceIdFromMarkup(line, 0)).toBe(resourceId);
expect(getResourceIdFromMarkup(line, line.length)).toBe(resourceId);
it('should return null for non-resource URLs', () => {
expect(getResourceIdFromMarkup('![alt](https://example.com/image.png)', 10)).toBeNull();
expect(getResourceIdFromMarkup('[link](https://example.com)', 10)).toBeNull();
});
});

View File

@@ -11,15 +11,24 @@ import type CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl'
import bridge from '../../../../../services/bridge';
import Setting from '@joplin/lib/models/Setting';
import Resource from '@joplin/lib/models/Resource';
import { ContextMenuItemType, ContextMenuOptions, buildMenuItems, handleEditorContextMenuFilter } from '../../../utils/contextMenuUtils';
import { ContextMenuItemType, ContextMenuOptions, buildMenuItems, handleEditorContextMenuFilter, resolveContextMenuItemType } from '../../../utils/contextMenuUtils';
import { menuItems } from '../../../utils/contextMenu';
import isItemId from '@joplin/lib/models/utils/isItemId';
import { extractResourceUrls } from '@joplin/lib/urlUtils';
import { WindowIdContext } from '../../../../NewWindowOrIFrame';
// Extract resource ID from image markup at a given cursor position within a line.
// Returns the resource ID if the cursor is within an image markup, null otherwise.
export const getResourceIdFromMarkup = (lineContent: string, cursorPosInLine: number): string | null => {
export type ResourceMarkupType = 'image' | 'file';
export interface ResourceMarkupInfo {
resourceId: string;
type: ResourceMarkupType;
markupStart: number;
markupEnd: number;
}
// Extract resource ID from resource markup (images or file attachments) at a given cursor position within a line.
// Returns the resource ID and its type if the cursor is within a resource markup, null otherwise.
export const getResourceIdFromMarkup = (lineContent: string, cursorPosInLine: number): ResourceMarkupInfo | null => {
const resourceUrls = extractResourceUrls(lineContent);
if (!resourceUrls.length) return null;
@@ -27,16 +36,38 @@ export const getResourceIdFromMarkup = (lineContent: string, cursorPosInLine: nu
const resourcePattern = new RegExp(`[:](/?${resourceInfo.itemId})`, 'g');
let match;
while ((match = resourcePattern.exec(lineContent)) !== null) {
// Look backwards for ![ or <img
let markupStart = lineContent.lastIndexOf('![', match.index);
// Look backwards for ![, [, <img, or <a
const imageMarkupStart = lineContent.lastIndexOf('![', match.index);
const linkMarkupStart = lineContent.lastIndexOf('[', match.index);
const imgTagStart = lineContent.lastIndexOf('<img', match.index);
if (imgTagStart > markupStart) markupStart = imgTagStart;
const aTagStart = lineContent.lastIndexOf('<a', match.index);
// Find the closest markup start and determine type
let markupStart = -1;
let markupType: ResourceMarkupType = 'file';
if (imageMarkupStart !== -1 && imageMarkupStart > markupStart) {
markupStart = imageMarkupStart;
markupType = 'image';
}
if (linkMarkupStart !== -1 && linkMarkupStart > markupStart && lineContent[linkMarkupStart - 1] !== '!') {
markupStart = linkMarkupStart;
markupType = 'file';
}
if (imgTagStart !== -1 && imgTagStart > markupStart) {
markupStart = imgTagStart;
markupType = 'image';
}
if (aTagStart !== -1 && aTagStart > markupStart) {
markupStart = aTagStart;
markupType = 'file';
}
if (markupStart === -1) continue;
// Find the end of the markup
let markupEnd: number;
if (lineContent[markupStart] === '!') {
if (lineContent[markupStart] === '!' || lineContent[markupStart] === '[') {
markupEnd = lineContent.indexOf(')', match.index);
if (markupEnd !== -1) markupEnd += 1;
} else {
@@ -45,7 +76,7 @@ export const getResourceIdFromMarkup = (lineContent: string, cursorPosInLine: nu
}
if (markupEnd !== -1 && cursorPosInLine >= markupStart && cursorPosInLine <= markupEnd) {
return resourceInfo.itemId;
return { resourceId: resourceInfo.itemId, type: markupType, markupStart, markupEnd };
}
}
}
@@ -84,6 +115,9 @@ const useContextMenu = (props: ContextMenuProps) => {
// It might be buggy, refer to the below issue
// https://github.com/laurent22/joplin/pull/3974#issuecomment-718936703
useEffect(() => {
const targetWindow = bridge().windowById(windowId);
if (!targetWindow) return ()=> {};
const isAncestorOfCodeMirrorEditor = (elem: Element) => {
for (; elem.parentElement; elem = elem.parentElement) {
if (elem.classList.contains(props.editorClassName)) {
@@ -132,30 +166,24 @@ const useContextMenu = (props: ContextMenuProps) => {
return clickedElement?.closest(`.${imageClassName}`) as HTMLElement | null;
};
// Get resource ID from image markup at click position (not cursor position)
const getResourceIdAtClickPos = (params: ContextMenuParams): string | null => {
if (!editorRef.current) return null;
const editor = editorRef.current.editor;
if (!editor) return null;
const zoom = Setting.value('windowContentZoomFactor');
const x = convertFromScreenCoordinates(zoom, params.x);
const y = convertFromScreenCoordinates(zoom, params.y);
const clickPos = editor.posAtCoords({ x, y });
if (clickPos === null) return null;
const line = editor.state.doc.lineAt(clickPos);
return getResourceIdFromMarkup(line.text, clickPos - line.from);
const appendEditMenuItems = (menu: typeof Menu.prototype) => {
const hasSelectedText = editorRef.current && !!editorRef.current.getSelection();
const isReadOnly = editorRef.current?.editor?.state.readOnly ?? false;
menu.append(new MenuItem({ label: _('Cut'), enabled: hasSelectedText && !isReadOnly, click: () => props.editorCutText() }));
menu.append(new MenuItem({ label: _('Copy'), enabled: hasSelectedText, click: () => props.editorCopyText() }));
menu.append(new MenuItem({ label: _('Paste'), enabled: !isReadOnly, click: () => props.editorPaste() }));
menu.append(new MenuItem({ label: _('Paste as Markdown'), enabled: !isReadOnly, click: () => CommandService.instance().execute('pasteAsMarkdown') }));
};
const targetWindow = bridge().windowById(windowId);
const showImageContextMenu = async (resourceId: string) => {
const showResourceContextMenu = async (resourceId: string, type: ResourceMarkupType) => {
const menu = new Menu();
// Add resource-specific options first
const baseType = type === 'image' ? ContextMenuItemType.Image : ContextMenuItemType.Resource;
const itemType = await resolveContextMenuItemType(baseType, resourceId);
const isReadOnly = editorRef.current?.editor?.state.readOnly ?? false;
const contextMenuOptions: ContextMenuOptions = {
itemType: ContextMenuItemType.Image,
itemType,
resourceId,
filename: null,
mime: null,
@@ -163,18 +191,34 @@ const useContextMenu = (props: ContextMenuProps) => {
linkToOpen: null,
textToCopy: null,
htmlToCopy: null,
insertContent: () => {},
isReadOnly: true,
insertContent: () => { editorRef.current?.insertText(''); },
isReadOnly,
fireEditorEvent: () => {},
htmlToMd: null,
mdToHtml: null,
};
const imageMenuItems = await buildMenuItems(menuItems(props.dispatch), contextMenuOptions);
for (const item of imageMenuItems) {
const resourceMenuItems = await buildMenuItems(menuItems(props.dispatch), contextMenuOptions, { excludeEditItems: true, excludePluginItems: true });
for (const item of resourceMenuItems) {
menu.append(item);
}
// Add edit items
menu.append(new MenuItem({ type: 'separator' }));
appendEditMenuItems(menu);
// Add plugin items last
const extraItems = await handleEditorContextMenuFilter({
resourceId,
itemType,
});
if (extraItems.length) {
menu.append(new MenuItem({ type: 'separator' }));
for (const item of extraItems) {
menu.append(item);
}
}
menu.popup({ window: targetWindow });
};
@@ -196,7 +240,25 @@ const useContextMenu = (props: ContextMenuProps) => {
});
};
interface ResourceContextInfo {
resourceId: string;
type: ResourceMarkupType;
}
const getResourceInfoAtPos = (docPos: number): ResourceContextInfo | null => {
const editor = editorRef.current?.editor;
if (!editor) return null;
const line = editor.state.doc.lineAt(docPos);
const info = getResourceIdFromMarkup(line.text, docPos - line.from);
if (!info) return null;
return { resourceId: info.resourceId, type: info.type };
};
const onContextMenu = async (event: Event, params: ContextMenuParams) => {
let resourceInfo: ResourceContextInfo | null = null;
// Check if right-clicking on a rendered image first (images may not be "editable")
const imageContainer = getClickedImageContainer(params);
if (imageContainer && pointerInsideEditor(params, true)) {
@@ -204,19 +266,40 @@ const useContextMenu = (props: ContextMenuProps) => {
if (imgElement) {
const resourceId = pathToId(imgElement.src);
if (resourceId) {
event.preventDefault();
moveCursorToImageLine(imageContainer);
await showImageContextMenu(resourceId);
return;
const sourceFrom = imageContainer.dataset.sourceFrom;
if (sourceFrom !== undefined) {
const editor = editorRef.current?.editor;
if (editor) {
const pos = Math.min(Number(sourceFrom), editor.state.doc.length);
resourceInfo = getResourceInfoAtPos(pos);
}
}
// Fallback if we couldn't get markup info
if (!resourceInfo) {
resourceInfo = { resourceId, type: 'image' };
}
}
}
}
// Check if right-clicking on image markup text
const markupResourceId = getResourceIdAtClickPos(params);
if (markupResourceId && pointerInsideEditor(params)) {
// Check if right-clicking on resource markup text (images or file attachments)
if (!resourceInfo && pointerInsideEditor(params)) {
const editor = editorRef.current?.editor;
if (editor) {
const zoom = Setting.value('windowContentZoomFactor');
const x = convertFromScreenCoordinates(zoom, params.x);
const y = convertFromScreenCoordinates(zoom, params.y);
const clickPos = editor.posAtCoords({ x, y });
if (clickPos !== null) {
resourceInfo = getResourceInfoAtPos(clickPos);
}
}
}
if (resourceInfo) {
event.preventDefault();
await showImageContextMenu(markupResourceId);
await showResourceContextMenu(resourceInfo.resourceId, resourceInfo.type);
return;
}
@@ -227,38 +310,7 @@ const useContextMenu = (props: ContextMenuProps) => {
event.preventDefault();
const menu = new Menu();
const hasSelectedText = editorRef.current && !!editorRef.current.getSelection() ;
menu.append(
new MenuItem({
label: _('Cut'),
enabled: hasSelectedText,
click: async () => {
props.editorCutText();
},
}),
);
menu.append(
new MenuItem({
label: _('Copy'),
enabled: hasSelectedText,
click: async () => {
props.editorCopyText();
},
}),
);
menu.append(
new MenuItem({
label: _('Paste'),
enabled: true,
click: async () => {
props.editorPaste();
},
}),
);
appendEditMenuItems(menu);
const spellCheckerMenuItems = SpellCheckerService.instance().contextMenuItems(params.misspelledWord, params.dictionarySuggestions);

View File

@@ -221,7 +221,14 @@ const translateLE_ = (codeMirror: any, percent: number, l2e: boolean) => {
linInterp = percent * lineCount - lineU;
result = ePercentU + (ePercentL - ePercentU) * linInterp;
} else {
linInterp = Math.max(0, Math.min(1, (percent - ePercentU) / (ePercentL - ePercentU))) || 0;
const rawLinInterp = (percent - ePercentU) / (ePercentL - ePercentU);
if (ePercentL === ePercentU) {
// Prevents the Viewer from jumping to the bottom of
// the document when there is division by zero.
linInterp = percent;
} else {
linInterp = Math.max(0, Math.min(1, rawLinInterp)) || 0;
}
result = (lineU + linInterp) / lineCount;
}
return Math.max(0, Math.min(1, result));

View File

@@ -338,7 +338,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
}, [editorPasteText, onEditorPaste]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const loadScript = async (script: any) => {
const loadScript = async (script: any, document: Document) => {
return new Promise((resolve) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
let element: any = document.createElement('script');
@@ -367,6 +367,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
};
useEffect(() => {
if (!editorRoot) return () => { };
let cancelled = false;
async function loadScripts() {
@@ -393,13 +394,14 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
});
}
const ownerDoc = editorRoot.ownerDocument;
for (const s of scriptsToLoad) {
if (document.getElementById(s.id)) {
if (ownerDoc.getElementById(s.id)) {
s.loaded = true;
continue;
}
await loadScript(s);
await loadScript(s, ownerDoc);
if (cancelled) return;
s.loaded = true;
@@ -411,7 +413,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
return () => {
cancelled = true;
};
}, [styles.editor.codeMirrorTheme]);
}, [styles.editor.codeMirrorTheme, editorRoot]);
useEffect(() => {
if (!editorRoot) return () => {};
@@ -646,6 +648,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
useCustomPdfViewer: props.useCustomPdfViewer,
noteId: props.noteId,
vendorDir: bridge().vendorDir(),
showNoteLinkIcon: props.showNoteLinkIcon,
}));
if (cancelled) return;
@@ -666,7 +669,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
shim.clearTimeout(timeoutId);
};
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [props.content, props.contentKey, renderedBodyContentKey, props.contentMarkupLanguage, props.visiblePanes, props.resourceInfos, props.markupToHtml]);
}, [props.content, props.contentKey, renderedBodyContentKey, props.contentMarkupLanguage, props.visiblePanes, props.resourceInfos, props.markupToHtml, props.showNoteLinkIcon]);
useEffect(() => {
if (!webviewReady) return;

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { useState, useEffect, useRef, forwardRef, useCallback, useImperativeHandle, useMemo, ForwardedRef, useContext } from 'react';
import { useState, useEffect, useRef, forwardRef, useCallback, useImperativeHandle, ForwardedRef, useContext } from 'react';
import { EditorCommand, MarkupToHtmlOptions, NoteBodyEditorProps, NoteBodyEditorRef, OnChangeEvent } from '../../../utils/types';
import { getResourcesFromPasteEvent } from '../../../utils/resourceHandling';
@@ -12,11 +12,10 @@ import Note from '@joplin/lib/models/Note';
import { _ } from '@joplin/lib/locale';
import bridge from '../../../../../services/bridge';
import shim from '@joplin/lib/shim';
import { MarkupToHtml } from '@joplin/renderer';
import { clipboard } from 'electron';
import { reg } from '@joplin/lib/registry';
import ErrorBoundary from '../../../../ErrorBoundary';
import { EditorKeymap, EditorLanguageType, EditorSettings, SearchState, UserEventSource } from '@joplin/editor/types';
import { SearchState, UserEventSource } from '@joplin/editor/types';
import useStyles from '../utils/useStyles';
import { EditorEvent, EditorEventType } from '@joplin/editor/events';
import useScrollHandler from '../utils/useScrollHandler';
@@ -33,6 +32,7 @@ import { WindowIdContext } from '../../../../NewWindowOrIFrame';
import eventManager, { EventName, ResourceChangeEvent } from '@joplin/lib/eventManager';
import useSyncEditorValue from './utils/useSyncEditorValue';
import { getGlobalSettings } from '@joplin/renderer/types';
import useEditorSettings from './utils/useEditorSettings';
const logger = Logger.create('CodeMirror6');
const logDebug = (message: string) => logger.debug(message);
@@ -222,6 +222,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
noteId: props.noteId,
vendorDir: bridge().vendorDir(),
globalSettings: getGlobalSettings(Setting),
showNoteLinkIcon: props.showNoteLinkIcon,
}));
if (cancelled) return;
@@ -244,7 +245,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
}, [
props.content, props.contentKey, renderedBodyContentKey, props.contentMarkupLanguage,
props.visiblePanes, props.resourceInfos, props.markupToHtml, props.contentMaxWidth,
props.noteId, props.useCustomPdfViewer,
props.noteId, props.useCustomPdfViewer, props.showNoteLinkIcon,
]);
useEffect(() => {
@@ -337,46 +338,6 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
void CommandService.instance().execute('focusElement', 'noteTitle');
}, []);
const editorSettings = useMemo((): EditorSettings => {
const isHTMLNote = props.contentMarkupLanguage === MarkupToHtml.MARKUP_LANGUAGE_HTML;
let keyboardMode = EditorKeymap.Default;
if (props.keyboardMode === 'vim') {
keyboardMode = EditorKeymap.Vim;
} else if (props.keyboardMode === 'emacs') {
keyboardMode = EditorKeymap.Emacs;
}
return {
language: isHTMLNote ? EditorLanguageType.Html : EditorLanguageType.Markdown,
readOnly: props.disabled,
markdownMarkEnabled: Setting.value('markdown.plugin.mark'),
katexEnabled: Setting.value('markdown.plugin.katex'),
inlineRenderingEnabled: Setting.value('editor.inlineRendering'),
imageRenderingEnabled: Setting.value('editor.imageRendering'),
highlightActiveLine: Setting.value('editor.highlightActiveLine'),
themeData: {
...styles.globalTheme,
marginLeft: 0,
marginRight: 0,
monospaceFont: Setting.value('style.editor.monospaceFontFamily'),
},
automatchBraces: Setting.value('editor.autoMatchingBraces'),
autocompleteMarkup: Setting.value('editor.autocompleteMarkup'),
useExternalSearch: false,
ignoreModifiers: true,
spellcheckEnabled: Setting.value('editor.spellcheckBeta'),
keymap: keyboardMode,
preferMacShortcuts: shim.isMac(),
indentWithTabs: true,
tabMovesFocus: props.tabMovesFocus,
editorLabel: _('Markdown editor'),
};
}, [
props.contentMarkupLanguage, props.disabled, props.keyboardMode, styles.globalTheme,
props.tabMovesFocus,
]);
const initialCursorLocationRef = useRef(0);
initialCursorLocationRef.current = props.initialCursorLocation.markdown ?? 0;
@@ -389,6 +350,14 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
initialCursorLocationRef,
});
const settings = useEditorSettings({
baseTheme: styles.globalTheme,
contentMarkupLanguage: props.contentMarkupLanguage,
disabled: props.disabled,
keyboardMode: props.keyboardMode,
tabMovesFocus: props.tabMovesFocus,
});
const renderEditor = () => {
return (
<div className='editor'>
@@ -398,7 +367,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
initialSelectionRef={initialCursorLocationRef}
initialNoteId={props.noteId}
ref={editorRef}
settings={editorSettings}
settings={settings}
pluginStates={props.plugins}
onPasteFile={null}
onEvent={onEditorEvent}

View File

@@ -45,8 +45,8 @@ const Editor = (props: Props, ref: ForwardedRef<CodeMirrorControl>) => {
return () => {};
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const pasteEventHandler = (_editor: any, event: Event) => {
const pasteEventHandler = (_editor: unknown, ...args: unknown[]) => {
const event = args[0] as Event;
props.onEditorPaste(event);
};

View File

@@ -0,0 +1,76 @@
import { EditorKeymap, EditorLanguageType, EditorSettings, EditorTheme } from '@joplin/editor/types';
import shim from '@joplin/lib/shim';
import { MarkupLanguage, MarkupToHtml } from '@joplin/renderer';
import { useMemo } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from '../../../../../../app.reducer';
import { _ } from '@joplin/lib/locale';
import { isDeepStrictEqual } from 'node:util';
interface EditorSettingsProps {
contentMarkupLanguage: MarkupLanguage;
keyboardMode: string;
disabled: boolean;
tabMovesFocus: boolean;
baseTheme: EditorTheme;
}
const useEditorSettings = (props: EditorSettingsProps) => {
const stateToSettings = (state: AppState) => ({
markdownMark: state.settings['markdown.plugin.mark'],
markdownInsert: state.settings['markdown.plugin.insert'],
katex: state.settings['markdown.plugin.katex'],
inlineRendering: state.settings['editor.inlineRendering'],
imageRendering: state.settings['editor.imageRendering'],
highlightActiveLine: state.settings['editor.highlightActiveLine'],
monospaceFont: state.settings['style.editor.monospaceFontFamily'],
automatchBraces: state.settings['editor.autoMatchingBraces'],
autocompleteMarkup: state.settings['editor.autocompleteMarkup'],
spellcheckEnabled: state.settings['editor.spellcheckBeta'],
});
type SelectedSettings = ReturnType<typeof stateToSettings>;
const settings = useSelector<AppState, SelectedSettings>(stateToSettings, isDeepStrictEqual);
return useMemo((): EditorSettings => {
const isHTMLNote = props.contentMarkupLanguage === MarkupToHtml.MARKUP_LANGUAGE_HTML;
let keyboardMode = EditorKeymap.Default;
if (props.keyboardMode === 'vim') {
keyboardMode = EditorKeymap.Vim;
} else if (props.keyboardMode === 'emacs') {
keyboardMode = EditorKeymap.Emacs;
}
return {
language: isHTMLNote ? EditorLanguageType.Html : EditorLanguageType.Markdown,
readOnly: props.disabled,
markdownMarkEnabled: settings.markdownMark,
markdownInsertEnabled: settings.markdownInsert,
katexEnabled: settings.katex,
inlineRenderingEnabled: settings.inlineRendering,
imageRenderingEnabled: settings.imageRendering,
highlightActiveLine: settings.highlightActiveLine,
themeData: {
...props.baseTheme,
marginLeft: 0,
marginRight: 0,
monospaceFont: settings.monospaceFont,
},
automatchBraces: settings.automatchBraces,
autocompleteMarkup: settings.autocompleteMarkup,
useExternalSearch: false,
ignoreModifiers: true,
spellcheckEnabled: settings.spellcheckEnabled,
keymap: keyboardMode,
preferMacShortcuts: shim.isMac(),
indentWithTabs: true,
tabMovesFocus: props.tabMovesFocus,
editorLabel: _('Markdown editor'),
};
}, [
props.contentMarkupLanguage, props.disabled, props.keyboardMode, props.baseTheme,
props.tabMovesFocus, settings,
]);
};
export default useEditorSettings;

View File

@@ -705,6 +705,15 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const containerWindow = editorContainerDom.defaultView as any;
const isDefaultEnglishLocale = ['en_US', 'en_GB'].includes(language);
if (!isDefaultEnglishLocale) {
await loadScript({
id: `tinyMceLang_${language}`,
src: `${bridge().vendorDir()}/lib/tinymce/langs/${language}.js`,
}, editorContainerDom);
}
const editors = await containerWindow.tinymce.init({
selector: `#${editorContainer.id}`,
@@ -735,7 +744,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
// Handle the first table row as table header.
// https://www.tiny.cloud/docs/plugins/table/#table_header_type
table_header_type: 'sectionCells',
language_url: ['en_US', 'en_GB'].includes(language) ? undefined : `${bridge().vendorDir()}/lib/tinymce/langs/${language}`,
language: isDefaultEnglishLocale ? undefined : language,
toolbar: toolbar.join(' '),
localization_function: _,
// See https://www.tiny.cloud/docs/tinymce/latest/tinymce-and-csp/#content_security_policy
@@ -887,6 +896,30 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
editor.addShortcut('Meta+Shift+8', '', () => editor.execCommand('InsertUnorderedList'));
editor.addShortcut('Meta+Shift+9', '', () => editor.execCommand('InsertJoplinChecklist'));
// Override ScrollIntoView to scroll to the cursor's character position
// instead of the start of the paragraph.
// See: https://github.com/laurent22/joplin/issues/14143
editor.on('ScrollIntoView', (event) => {
const sel = editor.getDoc().getSelection();
if (!sel || sel.rangeCount === 0) return;
const rect = sel.getRangeAt(0).getBoundingClientRect();
const win = editor.getWin();
const viewHeight = win.innerHeight;
if (rect.top < 0) {
win.scrollBy(0, rect.top);
} else if (rect.bottom > viewHeight) {
win.scrollBy(0, rect.bottom - viewHeight);
} else if (rect.top === 0 && rect.height === 0) {
// Handles edge case where rect is not rendered
// See: https://stackoverflow.com/a/14384220/5757550
return;
}
event.preventDefault();
return;
});
// TODO: remove event on unmount?
editor.on('drop', (event) => {
// Prevent the message "Dropped file type is not supported" from showing up.
@@ -1326,13 +1359,35 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const onSetAttrib = (event: EditorEvent<any>) => {
// Dispatch onChange when a link is edited
// Dispatch onChange when a link or table-related formatting is edited
const target = Array.isArray(event.attrElm) ? event.attrElm[0] : event.attrElm;
if (!target) return;
if (target.nodeName === 'A') {
if (event.attrName === 'title' || event.attrName === 'href' || event.attrName === 'rel') {
onChangeHandler();
}
}
if (['TABLE', 'TR', 'TD', 'TH'].includes(target.nodeName)) {
const attributeName = (event.attrName ?? '').toLowerCase();
if (
attributeName === 'style' ||
attributeName === 'class' ||
attributeName === 'bgcolor' ||
attributeName === 'bordercolor' ||
attributeName === 'background' ||
attributeName === 'cellpadding' ||
attributeName === 'cellspacing'
) {
onChangeHandler();
}
}
};
// Table plugin fires this on structure/style changes from dialogs.
const onTableModified = () => {
onChangeHandler();
};
// Keypress means that a printable key (letter, digit, etc.) has been
@@ -1438,6 +1493,23 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
}
}
const clearInheritedCheckedStateOnChecklistEnter = () => {
const currentNode = editor.selection.getStart();
const currentListItem = editor.dom.getParent(currentNode, 'li') as HTMLLIElement;
if (!currentListItem) return;
const parentChecklist = editor.dom.getParent(currentListItem, 'ul.joplin-checklist');
if (!parentChecklist) return;
if (!currentListItem.classList.contains('checked')) return;
const textContent = (currentListItem.textContent ?? '').replace(/\u200B/g, '').trim();
if (textContent !== '') return;
currentListItem.classList.remove('checked');
onChangeHandler();
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
async function onKeyDown(event: any) {
// It seems "paste as text" is handled automatically on Windows and Linux,
@@ -1453,6 +1525,13 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
event.preventDefault();
pasteAsPlainText(null);
}
if (event.key === 'Enter' && !event.metaKey && !event.ctrlKey && !event.altKey && !event.shiftKey && !event.isComposing) {
shim.setTimeout(() => {
if (!editor || !editor.getDoc()) return;
clearInheritedCheckedStateOnChecklistEnter();
}, 0);
}
}
function onPasteAsText() {
@@ -1481,6 +1560,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
editor.on(TinyMceEditorEvents.Redo, onChangeHandler);
editor.on(TinyMceEditorEvents.ExecCommand, onExecCommand);
editor.on(TinyMceEditorEvents.SetAttrib, onSetAttrib);
editor.on('TableModified', onTableModified);
return () => {
try {
@@ -1497,6 +1577,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
editor.off(TinyMceEditorEvents.Redo, onChangeHandler);
editor.off(TinyMceEditorEvents.ExecCommand, onExecCommand);
editor.off(TinyMceEditorEvents.SetAttrib, onSetAttrib);
editor.off('TableModified', onTableModified);
} catch (error) {
console.warn('Error removing events', error);
}

View File

@@ -35,8 +35,10 @@ export default function(editor: Editor, plugins: PluginStates, dispatch: Dispatc
useEffect(() => {
if (!editor) return () => {};
const contextMenuItems = menuItems(dispatch);
const targetWindow = bridge().windowById(windowId);
if (!targetWindow) return () => {};
const contextMenuItems = menuItems(dispatch);
const makeMainMenuItems = async (element: Element) => {
let itemType: ContextMenuItemType = ContextMenuItemType.None;

View File

@@ -14,7 +14,7 @@ import useFormNote, { OnLoadEvent, OnSetFormNote } from './utils/useFormNote';
import useEffectiveNoteId from './utils/useEffectiveNoteId';
import useFolder from './utils/useFolder';
import styles_ from './styles';
import { NoteEditorProps, FormNote, OnChangeEvent, AllAssetsOptions, NoteBodyEditorRef, NoteBodyEditorPropsAndRef } from './utils/types';
import { NoteEditorProps, FormNote, OnChangeEvent, AllAssetsOptions, NoteBodyEditorRef, NoteBodyEditorPropsAndRef, NoteBodyEditorType } from './utils/types';
import CommandService from '@joplin/lib/services/CommandService';
import Button, { ButtonLevel } from '../Button/Button';
import eventManager, { EventName } from '@joplin/lib/eventManager';
@@ -474,6 +474,7 @@ function NoteEditorContent(props: NoteEditorProps) {
noteId: props.noteId,
watchedNoteFiles: props.watchedNoteFiles,
enableHtmlToMarkdownBanner: props.enableHtmlToMarkdownBanner,
showNoteLinkIcon: props.showNoteLinkIcon,
};
let editor = null;
@@ -715,11 +716,11 @@ const mapStateToProps = (state: AppState, ownProps: ConnectProps) => {
const windowState = stateUtils.windowStateById(state, ownProps.windowId);
const noteId = stateUtils.selectedNoteId(windowState);
let bodyEditor = windowState.editorCodeView ? 'CodeMirror6' : 'TinyMCE';
let bodyEditor = windowState.editorCodeView ? NoteBodyEditorType.CodeMirror6 : NoteBodyEditorType.TinyMce;
if (state.settings.isSafeMode) {
bodyEditor = 'PlainText';
bodyEditor = NoteBodyEditorType.PlainText;
} else if (windowState.editorCodeView && state.settings['editor.legacyMarkdown']) {
bodyEditor = 'CodeMirror5';
bodyEditor = NoteBodyEditorType.CodeMirror5;
}
const mainWindowState = stateUtils.windowStateById(state, defaultWindowId);
@@ -766,6 +767,8 @@ const mapStateToProps = (state: AppState, ownProps: ConnectProps) => {
shareCacheSetting: state.settings['sync.shareCache'],
searchResults: state.searchResults,
enableHtmlToMarkdownBanner: state.settings['editor.enableHtmlToMarkdownBanner'],
enableInEditorRendering: state.settings['editor.inlineRendering'],
showNoteLinkIcon: state.settings['notes.showNoteLinkIcon'],
};
};

View File

@@ -6,6 +6,7 @@ interface Props {
acceptMessage: string;
onAccept: ()=> void;
onDismiss?: ()=> void;
dismissMessage?: string;
visible: boolean;
}
@@ -17,7 +18,7 @@ const BannerContent: React.FC<Props> = props => {
return <div className='warning-banner'>
{props.children}
&nbsp;&nbsp;<a onClick={props.onAccept} className='warning-banner-link' href="#">[ {props.acceptMessage} ]</a>
&nbsp;&nbsp;{ props.onDismiss ? <a onClick={props.onDismiss} className='warning-banner-link' href="#">[ {_('Dismiss')} ]</a> : null }
&nbsp;&nbsp;{ props.onDismiss ? <a onClick={props.onDismiss} className='warning-banner-link' href="#">[ {props.dismissMessage ?? _('Dismiss')} ]</a> : null }
</div>;
};

View File

@@ -6,13 +6,17 @@ import BannerContent from './BannerContent';
import { _ } from '@joplin/lib/locale';
import onRichTextReadMoreLinkClick from '@joplin/lib/components/shared/NoteEditor/WarningBanner/onRichTextReadMoreLinkClick';
import onRichTextDismissLinkClick from '@joplin/lib/components/shared/NoteEditor/WarningBanner/onRichTextDismissLinkClick';
import useEditorTypeMigrationBanner from '@joplin/lib/components/shared/NoteEditor/WarningBanner/useEditorTypeMigrationBanner';
import { useMemo } from 'react';
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
import PluginService from '@joplin/lib/services/plugins/PluginService';
import { NoteBodyEditorType } from '../utils/types';
interface Props {
bodyEditor: string;
bodyEditor: NoteBodyEditorType;
editorMigrationVersion: number;
richTextBannerDismissed: boolean;
inEditorRenderingEnabled: boolean;
pluginCompatibilityBannerDismissedFor: string[];
plugins: PluginStates;
}
@@ -35,6 +39,22 @@ const incompatiblePluginIds = [
];
const WarningBanner: React.FC<Props> = props => {
const editorMigrationMessage = useEditorTypeMigrationBanner({
markdownEditorEnabled: props.bodyEditor === 'CodeMirror6',
editorMigrationVersion: props.editorMigrationVersion,
inEditorRenderingEnabled: props.inEditorRenderingEnabled,
});
const editorMigrationBanner = (
<BannerContent
visible={editorMigrationMessage.enabled}
acceptMessage={editorMigrationMessage.keepEnabled.label}
onAccept={editorMigrationMessage.keepEnabled.onPress}
onDismiss={editorMigrationMessage.disable.onPress}
dismissMessage={editorMigrationMessage.disable.label}
>{editorMigrationMessage.label}</BannerContent>
);
const wysiwygBanner = (
<BannerContent
acceptMessage={_('Read more about it')}
@@ -83,6 +103,7 @@ const WarningBanner: React.FC<Props> = props => {
return <>
{wysiwygBanner}
{markdownPluginBanner}
{editorMigrationBanner}
</>;
};
@@ -91,5 +112,7 @@ export default connect((state: AppState) => {
richTextBannerDismissed: state.settings.richTextBannerDismissed,
pluginCompatibilityBannerDismissedFor: state.settings['editor.pluginCompatibilityBannerDismissedFor'],
plugins: state.pluginService.plugins,
editorMigrationVersion: state.settings['editor.migration'],
inEditorRenderingEnabled: state.settings['editor.inlineRendering'],
};
})(WarningBanner);

View File

@@ -1,6 +1,7 @@
import { CommandRuntime, CommandDeclaration } from '@joplin/lib/services/CommandService';
import { _ } from '@joplin/lib/locale';
import { focus } from '@joplin/lib/utils/focusHandler';
import { RefObject } from 'react';
export const declaration: CommandDeclaration = {
name: 'focusElementNoteTitle',
@@ -8,8 +9,7 @@ export const declaration: CommandDeclaration = {
parentLabel: () => _('Focus'),
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
export const runtime = (comp: any): CommandRuntime => {
export const runtime = (comp: { titleInputRef: RefObject<HTMLInputElement> }): CommandRuntime => {
return {
execute: async () => {
if (!comp.titleInputRef.current) return;

View File

@@ -3,6 +3,7 @@ import * as focusElementNoteBody from './focusElementNoteBody';
import * as focusElementNoteTitle from './focusElementNoteTitle';
import * as focusElementNoteViewer from './focusElementNoteViewer';
import * as focusElementToolbar from './focusElementToolbar';
import * as pasteAsMarkdown from './pasteAsMarkdown';
import * as pasteAsText from './pasteAsText';
import * as showLocalSearch from './showLocalSearch';
import * as showRevisions from './showRevisions';
@@ -12,6 +13,7 @@ const index: any[] = [
focusElementNoteTitle,
focusElementNoteViewer,
focusElementToolbar,
pasteAsMarkdown,
pasteAsText,
showLocalSearch,
showRevisions,

View File

@@ -0,0 +1,42 @@
import { CommandRuntime, CommandDeclaration } from '@joplin/lib/services/CommandService';
import { _ } from '@joplin/lib/locale';
import HtmlToMd from '@joplin/lib/HtmlToMd';
import { processImagesInPastedHtml } from '../utils/resourceHandling';
const { clipboard } = require('electron');
export const declaration: CommandDeclaration = {
name: 'pasteAsMarkdown',
label: () => _('Paste as Markdown'),
};
let htmlToMd_: HtmlToMd | null = null;
const htmlToMd = () => {
if (!htmlToMd_) {
htmlToMd_ = new HtmlToMd();
}
return htmlToMd_;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Props passed from NoteEditor component
export const runtime = (comp: any): CommandRuntime => {
return {
execute: async () => {
let html = clipboard.readHTML();
if (html) {
// Download images and convert them to Joplin resources
html = await processImagesInPastedHtml(html, { useInternalUrls: true });
const markdown = htmlToMd().parse(html, { tightLists: true, collapseMultipleBlankLines: true });
comp.editorRef.current.execCommand({ name: 'insertText', value: markdown });
} else {
// Fall back to plain text if no HTML is available
const text = clipboard.readText();
if (text) {
comp.editorRef.current.execCommand({ name: 'insertText', value: text });
}
}
},
enabledCondition: 'oneNoteSelected && markdownEditorVisible',
};
};

View File

@@ -6,10 +6,12 @@ const baseContext: Record<string, any> = {
modalDialogVisible: false,
gotoAnythingVisible: false,
markdownEditorPaneVisible: true,
markdownViewerPaneVisible: false,
oneNoteSelected: true,
noteIsMarkdown: true,
noteIsReadOnly: false,
richTextEditorVisible: false,
hasActivePluginEditor: false,
};
describe('editorCommandDeclarations', () => {
@@ -98,9 +100,38 @@ describe('editorCommandDeclarations', () => {
{
textBold: false,
textPaste: false,
// TODO: textCopy should be enabled in read-only notes:
// textCopy: false,
textCopy: true,
textSelectAll: true,
},
],
[
// Viewer-only mode (no editor pane visible, only the rendered viewer)
{
markdownEditorPaneVisible: false,
richTextEditorVisible: false,
markdownViewerPaneVisible: true,
},
{
textCopy: true,
textSelectAll: true,
textCut: false,
textPaste: false,
textBold: false,
},
],
[
// Viewer-only mode with a read-only note
{
markdownEditorPaneVisible: false,
richTextEditorVisible: false,
markdownViewerPaneVisible: true,
noteIsReadOnly: true,
},
{
textCopy: true,
textSelectAll: true,
textCut: false,
textPaste: false,
},
],
])('should correctly determine whether command is enabled (case %#)', (context, expectedStates) => {

Some files were not shown because too many files have changed in this diff Show More