1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-12-08 23:07:32 +02:00

Compare commits

...

269 Commits

Author SHA1 Message Date
Laurent Cozic
d1aec4a9f7 Plugin Generator release v3.5.1 2025-10-15 20:02:14 +01:00
Henry Heino
cab1525589 Chore: Plugin repository script: Fix certain plugins are not being published (#13443) 2025-10-15 20:00:29 +01:00
Henry Heino
a52f3fea9e Mobile: Resolves: #12823: Disable auto-search for 1-2 character searches (#13444)
Co-authored-by: pedr <pedr@users.noreply.github.com>
2025-10-15 20:00:02 +01:00
renovate[bot]
dfbd5eb8ed Update dependency expo to v53.0.20 (#13441)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-15 14:18:13 +01:00
Laurent Cozic
3131f36033 Chore: Trying to fix random CI failure 2025-10-15 12:55:40 +01:00
renovate[bot]
dc5b2cfa21 Update dependency form-data to v4.0.4 (#13439)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-15 09:42:15 +00:00
renovate[bot]
cad0f35fcc Update dependency expo-camera to v16.1.11 (#13438)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-15 02:43:03 +00:00
renovate[bot]
38ea92ff57 Update dependency axios to v1.10.0 (#13431)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-14 14:35:53 +01:00
renovate[bot]
830deada22 Update dependency @types/serviceworker to v0.0.142 (#13434)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-13 18:47:47 +01:00
renovate[bot]
38cd4033ea Update dependency @types/node to v18.19.119 (#13435)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-13 13:36:57 +00:00
Shania
02900752d9 Doc: Missing hashtag in rich_text_editor.md (#13418) 2025-10-11 12:56:26 +01:00
renovate[bot]
091e9813b5 Update dependency @react-native/babel-preset to v0.80.1 (#13426)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-11 01:23:33 +01:00
renovate[bot]
e61e5ac32a Update dependency @react-native/babel-preset to v0.80.0 (#13423)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-10 21:28:24 +01:00
Joplin Bot
414970c9a1 Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-10-10 18:39:00 +00:00
Laurent Cozic
d4ed49ff23 Doc: Clarify how to disable spellchecking on Markdown files 2025-10-10 17:48:37 +01:00
Laurent Cozic
8751d5d152 Doc: Add documentation for LDAP and SAML support in Joplin Server 2025-10-10 17:47:28 +01:00
Laurent Cozic
2e846fe15d Desktop release v3.5.4 2025-10-10 15:48:13 +01:00
Laurent Cozic
e54b7696d9 Chore: Prevent sign tool from being added to the Windows app 2025-10-10 15:48:01 +01:00
Laurent Cozic
553c61d628 Desktop release v3.5.3 2025-10-10 12:12:32 +01:00
Laurent Cozic
6a15db3a36 Chore: Implement SSL eSigner for Windows app signing (#13397) 2025-10-10 11:18:43 +01:00
Laurent Cozic
6f1d0a4b90 Chore: Disable time drift check on Joplin Server tests (#13420) 2025-10-10 11:18:18 +01:00
renovate[bot]
33b995672c Update dependency @playwright/test to v1.53.2 (#13421)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-10 11:18:04 +01:00
mrjo118
8ee46bb4e7 All: Avoid excessive data usage when automatically triggering another sync (#13261) 2025-10-10 09:36:42 +01:00
renovate[bot]
b35d9a64cf Update dependency @playwright/test to v1.53.0 (#13410)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-09 22:36:48 +01:00
renovate[bot]
64ef74dd01 Update dependency @types/node to v18.19.118 (#13412)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-09 22:36:39 +01:00
mrjo118
53035839a5 Desktop, Mobile: Fix historic issue whereby the first revision created for a note does not contain the original contents (#12674)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-10-09 22:35:08 +01:00
Henry Heino
af5287de99 Desktop: OCR: Fully disable the handwriting transcription backend when disabled in settings (#13072) 2025-10-09 22:21:49 +01:00
mrjo118
45a7554774 All: Fixes #11902: Ensure notebook conflicts do not delete child notes and notebooks when resolved (#13167)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-10-09 22:21:14 +01:00
Henry Heino
b06ffe3d25 Mobile,Desktop: Resolves #12343: Markdown editor search: Auto-scroll to the next match when the search changes (#13242)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-10-09 22:20:34 +01:00
mrjo118
53ea51b758 All: Fixes 12810: Ensure the sync shows an error when the server is down, when using a local WebDAV server (#13301) 2025-10-09 21:59:58 +01:00
Tom Chedmail
820acdc1f0 All: Fixes #13328: Implement the config check for Joplin Server with SAML enabled (#13360) 2025-10-09 21:49:18 +01:00
Henry Heino
ef0a79666e Desktop: OneNote importer: Simplify reporting import issues to the forum (#13409) 2025-10-09 21:47:31 +01:00
Henry Heino
d096a90c0e Chore: shim.mobilePlatform: Use a stronger return type (#13415) 2025-10-09 21:46:04 +01:00
renovate[bot]
191775310e Update dependency react-select to v5.10.2 (#13417)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-09 20:42:35 +00:00
renovate[bot]
4fc351b861 Update dependency @react-native-documents/picker to v10.1.5 (#13416)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-09 04:55:42 +00:00
renovate[bot]
396decd26c Update dependency sharp to v0.34.3 (#13404)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-08 13:45:43 +01:00
yingli-lab
01f8fa7bef Desktop: Fixes #13267: Fixed image load failure when path contains '#' (13267) (#13375) 2025-10-08 09:34:52 +01:00
Henry Heino
c40856ac7e Docs: Mobile: Add documentation for the mobile document scanner (#13387) 2025-10-08 09:33:45 +01:00
Henry Heino
d869cce413 Mobile: Document scanner: Add "Recognise text" checkbox (#13398) 2025-10-08 09:33:05 +01:00
Henry Heino
a83e8311d8 Server: Fixes #13400: Fix password fields are always disabled (#13401) 2025-10-08 09:32:24 +01:00
renovate[bot]
aa884fcb39 Update dependency @react-native-documents/picker to v10.1.4 (#13403)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-08 06:58:50 +00:00
Henry Heino
be2a4c3e24 Chore: Correct license information for packages/onenote-converter (#13392) 2025-10-07 10:01:52 +01:00
renovate[bot]
520eec555b Update dependency @types/node to v18.19.117 (#13395)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-07 05:08:10 +00:00
renovate[bot]
1281fdb9d2 Update dependency @types/node to v18.19.116 (#13394)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-07 00:55:38 +01:00
renovate[bot]
6029353fd1 Update dependency react-native-webview to v13.15.0 (#13388)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-06 17:49:23 +01:00
Laurent Cozic
8d1d1be79e Doc: Resolves #13370: Add documentation for user profiles (#13377) 2025-10-06 09:45:22 +01:00
bwat47
fd180ae0b4 Desktop: Add write() method to Plugin Clipboard API (#13348) 2025-10-06 09:31:27 +01:00
Laurent Cozic
6fdfd6eae6 Desktop: Resolves #13371: Open the Joplin Plugin web page when clicking on a plugin name (#13376) 2025-10-06 09:30:04 +01:00
Laurent Cozic
cd5bb575c8 Server: Resolves #13369: SAML users cannot modify their own profile at all (#13378) 2025-10-06 09:29:53 +01:00
Laurent Cozic
2df56530ae All: Remove Beta mention for Joplin Server (#13367) 2025-10-06 09:28:24 +01:00
Laurent Cozic
7987137470 Chore: Trying to migrate to macOS 15 on CI (#13366) 2025-10-06 09:28:13 +01:00
Manu Erwin
a1dcd2fd8f Doc: Update trash.md (#13383) 2025-10-06 08:40:10 +01:00
renovate[bot]
7826dc064a Update dependency @types/serviceworker to v0.0.141 (#13385)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-06 08:39:30 +01:00
renovate[bot]
eedf083bfd Update dependency esbuild to v0.25.6 (#13380)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-05 20:53:13 +00:00
renovate[bot]
d4aa1f8f8d Update dependency pg-boss to v10.3.2 (#13353)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-10-05 17:57:51 +01:00
Henry Heino
738e749d51 Desktop: Fixes #13346: Fix startup error when a non-English locale is selected (#13347) 2025-10-04 16:08:23 +01:00
renovate[bot]
8fe818c0b0 Update dependency samlify to v2.10.1 (#13362)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-04 09:25:20 +01:00
maggie897
e603452fad Desktop: Fixes #13088: Hide 'Start application minimised' unless tray icon is enabled (#13340) 2025-10-03 14:42:45 +01:00
mrjo118
3827637b54 Mobile: Make the conflicts folder text use the error colour, like is done on desktop (#13343) 2025-10-03 14:41:55 +01:00
Henry Heino
1da7c54e5f Chore: Add test for joplinServerConnected condition (#13352) 2025-10-03 14:32:57 +01:00
Henry Heino
e24ebffba6 Desktop: Resolves #12803: Upgrade tesseract.js to v6 (#13345)
Co-authored-by: pedr <pedr@users.noreply.github.com>
2025-10-03 14:32:15 +01:00
Laurent Cozic
e5bd77836a Desktop release v3.5.2 2025-10-02 09:14:44 +01:00
Henry Heino
8f5e628303 Chore: Fix CI (#13344) 2025-10-02 09:14:14 +01:00
Laurent Cozic
6850c8128b Desktop release v3.5.1 2025-10-02 09:13:48 +01:00
Laurent Cozic
8a797fdf23 Server: Enable publish and share notebook for SAML login 2025-10-02 09:13:29 +01:00
Laurent Cozic
1ae550c0aa Chore: Setup new release 3.5 2025-10-02 09:12:11 +01:00
maggie897
e7e0529f52 Desktop: Resolves #12292: Add hover + expanded arrow behavior for Notebook/Tags header (#13190)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-10-01 23:20:33 +01:00
Henry Heino
2381e44c7f Chore: OneNote importer: Upgrade to Rust 2024 (#13298)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-10-01 10:22:33 +01:00
renovate[bot]
a59e975f73 Update dependency sharp to v0.34.2 (#13336)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-01 09:35:07 +01:00
Henry Heino
2d703b6292 Mobile: Resolves #13123: Add sync wizard (#13234) 2025-10-01 09:34:18 +01:00
renovate[bot]
b8db70f707 Update eslint (#13337)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-01 04:17:48 +00:00
Filbert Wijaya
c91513b6b5 All: Translation: Update ja_JP.po (#13339) 2025-10-01 00:15:56 -04:00
Joplin Bot
a57ada97ef Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-10-01 01:51:34 +00:00
Laurent Cozic
d8677a70dd Transcribe: Resolves #12874: Downscale images before storing (#13333) 2025-09-30 23:17:01 +01:00
renovate[bot]
15839a19fd Update dependency rate-limiter-flexible to v6.2.1 (#13334)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-30 23:10:14 +01:00
renovate[bot]
8f1d55c1fc Update dependency rate-limiter-flexible to v6 (#13325)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-09-30 20:03:59 +01:00
JZou-Code
98c18711f7 Desktop: Fixes #12531: Fix the order of attached images (#12531) (#12868) 2025-09-30 17:44:38 +01:00
Henry Heino
24ff4612fb Mobile: Resolves #13104: Accessibility: Allow changing the ALT text of images from the mobile Rich Text Editor (#13169)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-09-30 17:34:03 +01:00
Henry Heino
f832eb38ff Mobile: Improve inline search performance in large documents (#13259) 2025-09-30 17:22:55 +01:00
Henry Heino
91dc23c23f Android: Plugins: Fix inspecting note editor WebViews (#13272) 2025-09-30 17:17:34 +01:00
Henry Heino
d1913493ab Desktop: Fix "insecure content security policy" warning (#13288)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-09-30 17:08:48 +01:00
trap000d
fd2b22ed68 Desktop: Resolves #12572: Click on systray icon will show/hide Joplin main window (#13299)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-09-30 17:05:44 +01:00
Henry Heino
14b56f19df Chore: OneNote converter: Refactor to allow debugging the import process, reduce use of "unsafe" (#13300) 2025-09-30 17:03:38 +01:00
renovate[bot]
0b082a985b Update dependency @react-native-clipboard/clipboard to v1.16.3 (#13304)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-09-30 16:50:29 +01:00
mrjo118
53dcac22d0 Mobile: For notes over 100,000 characters, make the share note function share them as a file (#13305) 2025-09-30 16:49:54 +01:00
Henry Heino
2c721a76b7 Chore: Fix transcription server build (#13310) 2025-09-30 16:45:16 +01:00
yingli-lab
b68cfd6d9e Desktop: Fixes #13196: Fixed red close button not working on macOS 26 (#13311) 2025-09-30 16:43:58 +01:00
JZou-Code
affebedc4b Desktop: Fixes #12763: skip copy event in TinyMCE if no content is selected. (#13313) 2025-09-30 16:37:44 +01:00
JZou-Code
a714ef4807 Desktop: Fixes #13314: skip cut action in TinyMCE editor if no content is selected. (#13315) 2025-09-30 16:35:38 +01:00
renovate[bot]
596f99aad3 Update dependency @types/node to v18.19.115 (#13332)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-30 16:32:47 +01:00
renovate[bot]
c530d35b36 Update dependency @react-native/metro-config to v0.79.5 (#13331)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-30 02:49:10 +00:00
renovate[bot]
5a5c734e2a Update dependency @react-native/babel-preset to v0.79.5 (#13329)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-29 21:56:41 +00:00
renovate[bot]
f7eb483d9a Update dependency react-native-webview to v13.14.2 (#13327)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-29 17:10:00 +01:00
Laurent Cozic
7f3c7e807c Desktop: Use plugin repository URL when homepage URL is not available in config screen (#13318) 2025-09-29 14:35:55 +01:00
renovate[bot]
a50fc02b32 Update dependency react-native-webview to v13.14.0 (#13324)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-29 14:35:39 +01:00
renovate[bot]
63702e9e34 Update dependency @types/serviceworker to v0.0.140 (#13321)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-29 02:48:55 +00:00
renovate[bot]
92c67aab4e Update dependency @types/node to v18.19.113 (#13320)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-29 02:46:26 +00:00
renovate[bot]
91535870a2 Update dependency react-native-localize to v3.4.2 (#13289)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-09-28 21:48:19 +01:00
renovate[bot]
d4bb277417 Update dependency pg to v8.16.3 (#13296)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-09-28 21:48:06 +01:00
krevad
90f87d1496 All: Translation: Update sv.po (#13316) 2025-09-28 14:44:42 -04:00
Joplin Bot
b07752b3ab Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-09-28 12:47:37 +00:00
Laurent Cozic
98effef4c5 Server: Improve SAML login error handling and add doc regarding email and displayName attributes 2025-09-28 12:48:56 +01:00
Laurent Cozic
32a919eb81 Chore: Remove obsolete "version" parameter from docker-compose files 2025-09-28 12:44:00 +01:00
Laurent Cozic
e124fd5c9f Chore: Set URLs to absolute ones on release notes and tweak title 2025-09-28 10:23:56 +01:00
Joplin Bot
c5f9290402 Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-09-27 10:41:21 +00:00
Henry Heino
c80cdadc99 Doc: Add release notes for v3.4 (#13273) 2025-09-27 10:20:31 +01:00
Laurent Cozic
d96dcef109 Chore: Trying to fix GitHub Actions size issue (#13290) 2025-09-25 23:33:02 +01:00
Henry Heino
33b889ca38 Chore: Fix APK alignment check in CI (#13294) 2025-09-25 20:48:56 +01:00
Laurent Cozic
fa78ea0173 Server v3.4.4 2025-09-25 14:19:26 +01:00
Laurent Cozic
6705712f80 Server: Use "lax" cookies when using external authentication like SAML or LDAP 2025-09-25 12:12:14 +01:00
renovate[bot]
2785b7f7d9 Update dependency @react-native-community/datetimepicker to v8.4.2 (#13285)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-24 14:10:36 +01:00
Laurent Cozic
f04831406e Server: Trying to make logging more robust 2025-09-24 12:37:49 +01:00
renovate[bot]
fdffc81834 Update dependency @react-native-community/datetimepicker to v8.4.1 (#13279)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-24 11:13:51 +01:00
Henry Heino
6f113df2d6 Chore: Sync fuzzer: Fix failures related to publishing/unpublishing notes (#13282) 2025-09-24 11:12:58 +01:00
Henry Heino
8b8b6fbe36 Desktop: Accessibility: Disable sync icon animation when reduce motion is enabled (#13283) 2025-09-24 11:12:38 +01:00
Laurent Cozic
1ef8fd529b Server: Provide more logging information for each request to help debugging issues 2025-09-24 10:46:24 +01:00
Marcus Kida
9547a459cb All: Translation: Update de_DE.po (#13275) 2025-09-23 11:11:34 -04:00
Joplin Bot
be1d092cab Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-09-23 01:32:34 +00:00
Laurent Cozic
517669ee27 Doc: Update sponsors 2025-09-22 22:29:54 +01:00
Laurent Cozic
72fc97116f Chore: Update renovate.json5 - ignore @react-native-community/cli-platform-ios package 2025-09-22 17:46:53 +01:00
Laurent Cozic
77ca6b3447 Chore: Update renovate.json5 - ignore @react-native-community/cli* packages 2025-09-22 17:46:21 +01:00
renovate[bot]
b227d337d0 Update dependency sass to v1.93.0 (#13255)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 17:44:44 +01:00
Henry Heino
a6e671d45b Android: Improve location permission request (#13248) 2025-09-20 10:00:15 +01:00
Henry Heino
47c82a7e75 Chore: Mobile: Fix expo-related warnings in tests (#13260) 2025-09-20 01:25:49 +01:00
summoner
bafa1576f2 All: Translations: Update hu_HU.po (#13256) 2025-09-19 16:11:45 -04:00
Henry Heino
48956df439 Chore: Fix web app build (#13257) 2025-09-19 17:37:28 +01:00
renovate[bot]
4716065295 Update dependency pg to v8.16.2 (#13236)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-09-19 09:24:41 +01:00
renovate[bot]
f801bbfb27 Update dependency sass to v1.89.2 (#13237)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-09-19 09:24:24 +01:00
Henry Heino
4a043f68ad Chore: Web: Fix build (#13250) 2025-09-19 09:22:22 +01:00
renovate[bot]
cac93e9f9c Update dependency react-native-paper to v5.14.5 (#13230)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-09-18 14:25:08 +01:00
mrjo118
e1e5c9aeb0 Mobile: Remove expandable title field on Web client and fix icon alignment (#13240)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-09-18 14:24:49 +01:00
Henry Heino
382cb257ab Web: Fix multi-page document creation action fails for non-JPEG images (#13244)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-09-18 14:23:57 +01:00
Laurent Cozic
6f375be8b9 Server: Pin pm2-logrotate version to prevent supply chain attacks (#13235) 2025-09-18 12:03:57 +01:00
Laurent Cozic
a118615e06 Chore: Remove bitnami repository (#13239) 2025-09-18 10:34:01 +01:00
Joplin Bot
912bf7463f Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-09-17 18:38:33 +00:00
Laurent Cozic
cfc29832a2 Doc: Update sponsors 2025-09-17 18:07:34 +01:00
renovate[bot]
737fd132e3 Update dependency sass to v1.89.0 (#13231)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-17 16:53:56 +01:00
renovate[bot]
9fc76f4e4c Update dependency pg to v8.16.0 (#13223)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 16:27:13 +01:00
renovate[bot]
981f15d85c Update dependency node-mocks-http to v1.17.2 (#13221)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 16:26:43 +01:00
renovate[bot]
a59594db3b Update dependency nodejs to v23.11.0 (#13222)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 16:26:35 +01:00
JZou-Code
8c8190e2e9 Desktop: Fixes #12239: Prevent the default cut action handler to avoid double deletion (#13208) 2025-09-16 13:22:26 +01:00
Henry Heino
d7e7ff77e8 Chore: Mobile: Add additional logging to help debug toolbar issue (#13224)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-09-16 13:18:12 +01:00
Henry Heino
e33c142c5a Chore: Web: Skip secondary single-instance check in dev mode (#13225)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-09-16 12:52:43 +01:00
Laurent Cozic
97d3a8243d Chore: Fix CI (#13227) 2025-09-16 08:50:28 +01:00
renovate[bot]
f1716a3edb Update dependency @pmmmwh/react-refresh-webpack-plugin to ^0.6.0 (#13219)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 18:39:20 +01:00
renovate[bot]
1436f5867d Update dependency androidx.documentfile:documentfile to v1.1.0 (#13220)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 18:37:53 +01:00
renovate[bot]
d754b8fe0c Update dependency @rollup/plugin-commonjs to v28.0.6 (#13218)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 17:05:58 +01:00
renovate[bot]
4f58055cc1 Update dependency @react-native/metro-config to v0.79.4 (#13217)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 16:38:15 +01:00
renovate[bot]
98697e1db4 Update dependency style-to-js to v1.1.17 (#13209)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-09-15 16:30:10 +01:00
renovate[bot]
8ac65a08c1 Update dependency @react-native/babel-preset to v0.79.4 (#13213)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 15:06:33 +01:00
renovate[bot]
2b86d83290 Update dependency @babel/plugin-transform-export-namespace-from to v7.27.1 (#13212)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 13:18:34 +01:00
renovate[bot]
09cafe99d1 Update bitnami/postgresql Docker tag to v17.4.0 (#13211)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 11:33:22 +01:00
renovate[bot]
6fce844cbf Update dependency webpack-dev-server to v5.2.2 (#13210)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 09:01:07 +01:00
renovate[bot]
52de8c071f Update dependency glob to v11.0.3 (#13205)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-09-15 09:00:01 +01:00
renovate[bot]
537543cc8a Update dependency react-native-zip-archive to v7.0.2 (#13207)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 00:29:44 +00:00
renovate[bot]
ff16453299 Update dependency js-draw to v1.30.1 (#13206)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 00:27:57 +00:00
renovate[bot]
210deec495 Update dependency form-data to v4.0.3 (#13203)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-14 19:03:16 +01:00
renovate[bot]
e96baea005 Update dependency @types/tar-stream to v3.1.4 (#13202)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-14 19:03:05 +01:00
renovate[bot]
ae24b91f25 Update dependency @types/node to v18.19.112 (#13204)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-14 19:02:56 +01:00
renovate[bot]
f2e5118bf5 Update dependency @types/serviceworker to v0.0.139 (#13201)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-14 12:36:40 +00:00
renovate[bot]
72698ec573 Update dependency @react-native/metro-config to v0.79.3 (#13198)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-09-14 09:35:05 +01:00
renovate[bot]
68abc27c6a Update dependency @types/react to v18.3.23 (#13200)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-09-14 09:34:56 +01:00
renovate[bot]
1acb3d0726 Update dependency @rollup/plugin-commonjs to v28.0.5 (#13199)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-09-14 09:34:48 +01:00
Joplin Bot
5bf97dc3b8 Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-09-14 01:38:57 +00:00
Laurent Cozic
e0e04fbc91 Chore: Fixed type error 2025-09-14 00:44:36 +01:00
Laurent Cozic
625cd1221c Doc: Update sponsors 2025-09-14 00:41:03 +01:00
Henry Heino
110d5bde2d Desktop: Fix error dialogs fail to appear in certain cases (#13179) 2025-09-13 14:21:38 +01:00
renovate[bot]
93a85b3207 Update dependency @types/node to v18.19.111 (#13165)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-09-13 14:18:04 +01:00
renovate[bot]
ff305f42fd Update dependency @js-draw/material-icons to v1.30.1 (#13164)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-09-13 14:17:50 +01:00
Henry Heino
99ba854ee1 Chore: Cli: Fix CLI app integration tests (#13089) 2025-09-13 14:17:40 +01:00
mrjo118
38b368e997 Mobile: Resolves #12936: Allow expanding and collapsing the title field across multiple lines (#13016) 2025-09-13 14:08:31 +01:00
Henry Heino
f9ffe6c4e6 Desktop,Mobile,Cli: Fix notes are moved to the conflict folder when a folder is unshared (#12993)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-09-13 14:08:19 +01:00
pedr
5adc0170fc All: Resolves #8718: Delete all note revisions when the note is permanently deleted (#12609) 2025-09-13 14:06:56 +01:00
mrjo118
f54c364b4d Desktop, Mobile: Automatically retrigger the sync if there are more unsynced outgoing changes when sync completes (#12989) 2025-09-13 14:05:31 +01:00
mrjo118
9f541b9b9d Desktop, Mobile: Add support for mixed case tags (#12931)
Co-authored-by: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com>
2025-09-13 14:01:33 +01:00
Henry Heino
bd0af08c57 Docs: REST API: Add descriptions for "is_shared" and "share_id" (#13186) 2025-09-13 13:52:46 +01:00
Henry Heino
ac06c6750d Android: Fixes #13113: Fix compatibility with 16-KB-page-size devices: Remove Vosk (#13189) 2025-09-13 13:52:12 +01:00
renovate[bot]
23b07094b7 Update dependency @react-native/babel-preset to v0.79.3 (#13195)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-13 13:48:57 +01:00
Joplin Bot
7eefc016de Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-09-13 12:45:33 +00:00
Laurent Cozic
c002be76cd Doc: Update sponsors 2025-09-13 12:00:32 +01:00
renovate[bot]
2cd29aaaea Update dependency react-native-image-picker to v8.2.1 (#13002)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-09-12 22:48:44 +01:00
Laurent Cozic
4cb6b01c71 Server: Clean-up SAML login section 2025-09-12 15:14:22 +01:00
Joplin Bot
91c79b9488 Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-09-10 12:42:24 +00:00
Henry Heino
fc516d05b3 Docs: Update the Rich Text Editor documentation (#13171) 2025-09-10 10:44:29 +01:00
Henry Heino
2769c9586c Mobile: Fixes #13138: Rich Text Editor: Fix image size lost on change (#13172) 2025-09-10 10:06:40 +01:00
Henry Heino
fd15d5a6d3 Mobile: Rich Text Editor: Accessibility: Fix font size setting not respected (#13174) 2025-09-10 10:05:51 +01:00
krevad
7237d7faa7 All: Translation: Update sv.po (#13170)
Co-authored-by: Helmut K. C. Tessarek <tessarek@evermeet.cx>
2025-09-09 19:37:19 -04:00
Henry Heino
3025d62568 Chore: Fix CI (#13168) 2025-09-09 22:31:55 +01:00
renovate[bot]
5b5dcf34a1 Update dependency @axe-core/playwright to v4.10.2 (#13162)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-09 09:54:36 +01:00
Laurent Cozic
9e8500c148 Server v3.4.3 2025-09-09 09:47:29 +01:00
Laurent Cozic
4f1999f921 Merge branch 'release-3.4' into dev 2025-09-09 09:46:39 +01:00
Laurent Cozic
6ee9571069 iOS 13.4.3 2025-09-09 09:25:40 +01:00
krevad
10663b1494 All: Translation: Update sv.po (#13163) 2025-09-09 04:15:15 -04:00
Laurent Cozic
f25db9bbd7 Android 3.4.7 2025-09-09 09:14:11 +01:00
Laurent Cozic
44ac261304 Desktop release v3.4.11 2025-09-09 09:05:11 +01:00
renovate[bot]
eac995a209 Update dependency esbuild to v0.25.5 (#13040)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
Co-authored-by: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com>
2025-09-09 00:32:19 +01:00
Henry Heino
15c973e885 Chore: Mobile: Add additional plugin panel integration tests (#13152) 2025-09-09 00:31:12 +01:00
Henry Heino
1762f9485f Web: Fixes #13153: Fix installing certain plugins (#13154) 2025-09-09 00:30:48 +01:00
Henry Heino
7777f8428f Desktop: Upgrade to Electron 37.4.0 (#13156) 2025-09-09 00:30:06 +01:00
Henry Heino
948aa9db4f Mobile: Upgrade react-native-quick-crypto to v0.7.17 (#13155) 2025-09-09 00:07:29 +01:00
Henry Heino
fdde04ee85 Desktop,Mobile,Cli: Support accepting shares with a new key format (#12829) 2025-09-08 23:56:40 +01:00
Laurent Cozic
f77a20f5d5 Merge branch 'release-3.4' into dev 2025-09-08 23:55:24 +01:00
Henry Heino
d43aa2a3e6 Web: Update the beta notice (#13150) 2025-09-08 23:47:19 +01:00
Henry Heino
04d5ce13c2 Chore: Android: Compile Whisper with support for 16 KB pages (#13118) 2025-09-08 16:50:48 +01:00
Laurent Cozic
3b764ba06a Server: Remove the need to install pm2-logrotate on startup so that image can work in a closed environment (#13149) 2025-09-08 16:37:35 +01:00
Henry Heino
5492ce55fa Server: Fixes #12984: Improve handling of concurrent deletion requests for the same item (#13092) 2025-09-08 12:03:20 +01:00
Henry Heino
f6b3f9860c Cli: Fix last change sometimes lost when not in TUI mode (#13090) 2025-09-08 12:03:13 +01:00
Henry Heino
88f687ba6a Chore: Sync fuzzer: Add actions for publishing and unpublishing notes (#13062) 2025-09-08 12:02:53 +01:00
Henry Heino
1f0a98999f Desktop, Mobile: Fixes #12987: Fix images rendered in the Markdown editor don't reload when downloaded (#13045) 2025-09-08 12:01:54 +01:00
mrjo118
69135c3bea Mobile: Fixes #12956: Resize the notes menu to the viewport when the keyboard is open (#13035) 2025-09-08 12:01:24 +01:00
pedr
c27d542a4b Desktop: Fixes #12049: Fix files without extension not being imported properly (#12974)
Co-authored-by: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com>
2025-09-08 11:46:36 +01:00
mrjo118
bd1c2534c5 Mobile: Fixes #13095: Fix long note title doesn’t wrap properly for To Do type note (#13099) 2025-09-08 11:05:19 +01:00
mrjo118
72513b520c Android: Fixes #13079: Fix dropdown menus are offset on Android 15+ (#13106) 2025-09-08 11:04:46 +01:00
Henry Heino
ec0f9ef9bc Server: Fix unique constraint error when multiple createSharedFolderUserItems are run concurrently (#13112) 2025-09-08 11:03:28 +01:00
Henry Heino
818bc3218a Mobile: Improve tag dialog performance with long tags and many tags (#13117) 2025-09-08 11:03:01 +01:00
Henry Heino
82760a5b6a Web: Show a "Give feedback" banner and link to a survey (#13125) 2025-09-08 10:59:40 +01:00
mrjo118
5ba9a16cfd Mobile: Fixes #13116: Fix tag association screen no longer searches case insensitively or searches tag endings (#13128) 2025-09-08 10:59:01 +01:00
Henry Heino
68fc91fdc7 Desktop: Resolves #13096: Prefer user-specified CSS page sizing when printing to PDF (#13130) 2025-09-08 10:58:16 +01:00
Henry Heino
bdc4687327 Chore: Refactor WebViewController (#13133) 2025-09-08 10:56:51 +01:00
Henry Heino
3a9f57e13f Cli: Fixes #13086: Fix "use" command when not in TUI mode (#13091) 2025-09-08 10:56:08 +01:00
Henry Heino
b72c48c693 Mobile, Desktop: Fixes #13103: Fix error when saving in-editor rendering-related settings (#13105) 2025-09-08 10:56:01 +01:00
Henry Heino
f1e42f3bac iOS: Fixes #13111: Fix "scan notebook" tool on iOS (#13114) 2025-09-08 10:55:48 +01:00
Henry Heino
93c908286d Mobile: Plugins: Fix renderer plugins that use the settingValue API (#13131) 2025-09-08 10:55:42 +01:00
Henry Heino
4eb8777ed0 Mobile: Fix light bar shown above header in dark mode (#13132) 2025-09-08 10:55:15 +01:00
summoner
5e1909cee0 All: Translation: Update hu_HU.po (#13142) 2025-09-08 00:34:54 -04:00
pplulee
2e7b312415 All: Translation: Update zh_CN.po (#13137) 2025-09-06 17:02:31 -04:00
Joplin Bot
7735a59fc1 Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-09-04 18:26:55 +00:00
Laurent Cozic
41d6e912a7 Doc: Updated sponsors 2025-09-04 17:43:49 +02:00
Joplin Bot
4c2fae8423 Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-09-03 01:00:02 +00:00
Laurent Cozic
b72c134890 Doc: Update sponsors 2025-09-02 23:01:06 +02:00
Joplin Bot
58a9c229bb Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-09-01 18:25:20 +00:00
Laurent Cozic
d8c203bb8a Merge branch 'release-3.4' into dev 2025-09-01 14:48:55 +02:00
Laurent Cozic
9020c07825 lock files 2025-09-01 14:48:51 +02:00
Laurent Cozic
e884da8312 Android 3.4.6 2025-09-01 14:48:38 +02:00
Joplin Bot
d134ea8bfe Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-09-01 12:33:37 +00:00
Henry Heino
faa44468f3 Mobile: Plugins: Improve handling of invalid toolbar button enabled conditions (#13076) 2025-09-01 13:50:59 +02:00
Laurent Cozic
85585d16d2 Desktop release v3.4.10 2025-09-01 13:50:43 +02:00
Joplin Bot
b9c5b8f187 Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-09-01 01:12:51 +00:00
Henry Heino
da8e638359 Chore: Mobile: Add test to verify that content scripts load in the note viewer (#13093) 2025-08-31 00:32:10 +02:00
Henry Heino
6482ab5a4e Mobile: Plugin API: Fix compatibility with certain plugins targetting the desktop app (#13077) 2025-08-29 23:28:45 +02:00
Henry Heino
ec74abe754 Mobile: Plugin API: Fix certain renderer plugins fail to load (#13078) 2025-08-29 23:28:39 +02:00
Henry Heino
859bc8d88e Mobile: Plugins: Fix plugin panel buttons are offscreen on recent versions of Android (#13080) 2025-08-29 23:28:22 +02:00
Henry Heino
56ed471a2f Chore: Rich Text Editor: Refactor editor dialog to simplify toggling the dialog from external commands (#13082) 2025-08-29 23:28:11 +02:00
Henry Heino
650594ecea Chore: Sync fuzzer: Add action for deleting notes (#13083) 2025-08-29 23:28:00 +02:00
Henry Heino
3e9bb914e5 Android: Fixes #13015: Fix "edit profile" button is partially offscreen (#13084) 2025-08-29 23:27:51 +02:00
Henry Heino
f75e911a4e Docs: Update the privacy policy (#13087) 2025-08-29 23:27:44 +02:00
Eric Duarte
78fb07d4c7 All: Translation: Update ca.po (#13065) 2025-08-28 17:50:34 -04:00
Henry Heino
6390ef43ed Desktop: Clarify handwritten text transcription setting (#13073)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-08-28 09:20:55 +03:00
Henry Heino
78c5c4d7c3 Android: Accessibility: Fix tag search input loses focus when submitted by pressing "enter" (#13070) 2025-08-28 09:20:10 +03:00
Henry Heino
0d1d50768b Android: Fix shadow shown above the screen header (#13074) 2025-08-28 09:04:10 +03:00
Henry Heino
57093b35ea Android: Fixes #12960: Rich Text Editor: Fix pressing enter does nothing in some cases (#13075) 2025-08-28 09:03:37 +03:00
Laurent Cozic
cba5cf660b Desktop release v3.4.9 2025-08-27 22:09:47 +03:00
Laurent Cozic
0024722c79 Desktop: Clarified that handwritten transcription may not always work 2025-08-27 22:09:26 +03:00
Henry Heino
bc2832e78f Chore: Desktop: Allow access to more Joplin APIs from the desktop development tools in dev mode (#13052) 2025-08-27 22:05:52 +03:00
Henry Heino
424cc96d36 Chore: Sync fuzzer: Fix incorrect expected state after removing the last user from a share (#13061) 2025-08-27 22:03:17 +03:00
Henry Heino
56fd5d828f Android: Fixes #12952: External keyboard: Fix adding tags by pressing enter on certain Android devices (#13069) 2025-08-27 22:02:48 +03:00
Henry Heino
03843b087a Desktop: Fixes #12816: Accessibility: Fix dismissing the alarm dialog by pressing escape (#13068) 2025-08-27 22:02:34 +03:00
Henry Heino
b179509dd3 Desktop: Fixes #12855: Legacy editor: Fix plugin support (#13066) 2025-08-27 22:02:09 +03:00
Joplin Bot
f6851314d2 Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-08-27 18:26:06 +00:00
Laurent Cozic
eaec45cb3f Doc: Update sponsors 2025-08-27 18:38:56 +03:00
Laurent Cozic
9be954496c Doc: Update sponsors 2025-08-27 17:45:40 +03:00
Laurent Cozic
ac289c5198 Desktop: Clarified that handwritten transcription may not always work 2025-08-27 17:22:06 +03:00
Joplin Bot
98ef5e619b Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-08-27 12:33:13 +00:00
Laurent Cozic
62faa48aac lock files 2025-08-27 10:15:15 +03:00
Laurent Cozic
5daa7a1f4c Chore: By default, create new releases as pre-releases when publishing desktop app 2025-08-27 09:54:06 +03:00
Laurent Cozic
32be071601 CLI v3.4.1 2025-08-27 09:50:10 +03:00
Laurent Cozic
0dc63dd306 Lock file 2025-08-27 09:47:17 +03:00
Laurent Cozic
78ed58187a Releasing sub-packages 2025-08-27 09:46:45 +03:00
Laurent Cozic
b8b8dd8011 iOS 13.4.2 2025-08-27 09:33:18 +03:00
391 changed files with 10597 additions and 5438 deletions

View File

@@ -9,6 +9,7 @@ API_KEY=random-string
QUEUE_TTL=900000
QUEUE_RETRY_COUNT=2
QUEUE_MAINTENANCE_INTERVAL=30000
IMAGE_MAX_DIMENSION=400
HTR_CLI_DOCKER_IMAGE=joplin/htr-cli:latest
# Fullpath to images folder e.g.:

View File

@@ -96,6 +96,7 @@ packages/onenote-converter/pkg/onenote_converter.js
packages/app-cli/app/LinkSelector.js
packages/app-cli/app/app.js
packages/app-cli/app/base-command.js
packages/app-cli/app/cli-integration-tests.js
packages/app-cli/app/command-apidoc.js
packages/app-cli/app/command-attach.js
packages/app-cli/app/command-batch.js
@@ -676,6 +677,8 @@ 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/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
@@ -722,6 +725,8 @@ packages/app-mobile/components/SearchInput.js
packages/app-mobile/components/SelectDateTimeDialog.js
packages/app-mobile/components/SideMenu.js
packages/app-mobile/components/SideMenuContentNote.js
packages/app-mobile/components/SyncWizard/JoplinCloudIcon.js
packages/app-mobile/components/SyncWizard/SyncWizard.js
packages/app-mobile/components/TagEditor.test.js
packages/app-mobile/components/TagEditor.js
packages/app-mobile/components/TextInput.js
@@ -738,6 +743,7 @@ packages/app-mobile/components/base-screen.js
packages/app-mobile/components/biometrics/BiometricPopup.js
packages/app-mobile/components/biometrics/biometricAuthenticate.js
packages/app-mobile/components/biometrics/sensorInfo.js
packages/app-mobile/components/buttons/CardButton.js
packages/app-mobile/components/buttons/FloatingActionButton.js
packages/app-mobile/components/buttons/LabelledIconButton.js
packages/app-mobile/components/buttons/MultiTouchableOpacity.js
@@ -813,7 +819,6 @@ packages/app-mobile/components/screens/ConfigScreen/plugins/buttons/InstallButto
packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/WrappedPluginStates.js
packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/mockRepositoryApiConstructor.js
packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/newRepoApi.js
packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/pluginServiceSetup.js
packages/app-mobile/components/screens/ConfigScreen/plugins/utils/openWebsiteForPlugin.js
packages/app-mobile/components/screens/ConfigScreen/plugins/utils/usePluginCallbacks.js
packages/app-mobile/components/screens/ConfigScreen/plugins/utils/usePluginItem.js
@@ -839,6 +844,7 @@ packages/app-mobile/components/screens/NoteTagsDialog.js
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/SearchScreen/SearchResults.test.js
packages/app-mobile/components/screens/SearchScreen/SearchResults.js
packages/app-mobile/components/screens/SearchScreen/index.js
packages/app-mobile/components/screens/ShareManager/AcceptedShareItem.js
@@ -901,14 +907,13 @@ packages/app-mobile/services/AlarmServiceDriver.web.js
packages/app-mobile/services/BackButtonService.js
packages/app-mobile/services/commands/stateToWhenClauseContext.js
packages/app-mobile/services/e2ee/RSA.react-native.js
packages/app-mobile/services/e2ee/RSA.react-native.web.js
packages/app-mobile/services/e2ee/crypto.js
packages/app-mobile/services/plugins/PlatformImplementation.js
packages/app-mobile/services/profiles/index.js
packages/app-mobile/services/voiceTyping/VoiceTyping.js
packages/app-mobile/services/voiceTyping/utils/unzip.android.js
packages/app-mobile/services/voiceTyping/utils/unzip.js
packages/app-mobile/services/voiceTyping/vosk.android.js
packages/app-mobile/services/voiceTyping/vosk.js
packages/app-mobile/services/voiceTyping/whisper.test.js
packages/app-mobile/services/voiceTyping/whisper.js
packages/app-mobile/setupQuickActions.js
@@ -922,6 +927,7 @@ packages/app-mobile/utils/ShareUtils.test.js
packages/app-mobile/utils/ShareUtils.js
packages/app-mobile/utils/TlsUtils.js
packages/app-mobile/utils/appDefaultState.js
packages/app-mobile/utils/appReducer.js
packages/app-mobile/utils/autodetectTheme.js
packages/app-mobile/utils/buildStartupTasks.js
packages/app-mobile/utils/checkPermissions.js
@@ -961,6 +967,7 @@ packages/app-mobile/utils/pickDocument.js
packages/app-mobile/utils/polyfills/bufferPolyfill.js
packages/app-mobile/utils/polyfills/crypto-polyfill/index.js
packages/app-mobile/utils/polyfills/index.js
packages/app-mobile/utils/polyfills/index.web.js
packages/app-mobile/utils/setupNotifications.js
packages/app-mobile/utils/shareFile.js
packages/app-mobile/utils/shareHandler.js
@@ -971,6 +978,7 @@ packages/app-mobile/utils/shim-init-react/shimInitShared.js
packages/app-mobile/utils/testing/createMockReduxStore.js
packages/app-mobile/utils/testing/getWebViewDomById.js
packages/app-mobile/utils/testing/getWebViewWindowById.js
packages/app-mobile/utils/testing/mockPluginServiceSetup.js
packages/app-mobile/utils/testing/setupGlobalStore.js
packages/app-mobile/utils/testing/testingLibrary.js
packages/app-mobile/utils/types.js
@@ -1042,6 +1050,7 @@ packages/editor/CodeMirror/extensions/rendering/types.js
packages/editor/CodeMirror/extensions/rendering/utils/makeBlockReplaceExtension.js
packages/editor/CodeMirror/extensions/rendering/utils/makeInlineReplaceExtension.js
packages/editor/CodeMirror/extensions/rendering/utils/nodeIntersectsSelection.js
packages/editor/CodeMirror/extensions/searchExtension.test.js
packages/editor/CodeMirror/extensions/searchExtension.js
packages/editor/CodeMirror/extensions/selectedNoteIdExtension.js
packages/editor/CodeMirror/getScrollFraction.js
@@ -1088,12 +1097,15 @@ packages/editor/CodeMirror/utils/markdown/renumberSelectedLists.js
packages/editor/CodeMirror/utils/markdown/stripBlockquote.js
packages/editor/CodeMirror/utils/markdown/toggleCheckboxAt.js
packages/editor/CodeMirror/utils/setupVim.js
packages/editor/CodeMirror/vendor/announceSearchMatch.js
packages/editor/ProseMirror/commands.test.js
packages/editor/ProseMirror/commands.js
packages/editor/ProseMirror/createEditor.js
packages/editor/ProseMirror/index.js
packages/editor/ProseMirror/plugins/detailsPlugin.test.js
packages/editor/ProseMirror/plugins/detailsPlugin.js
packages/editor/ProseMirror/plugins/imagePlugin.test.js
packages/editor/ProseMirror/plugins/imagePlugin.js
packages/editor/ProseMirror/plugins/inputRulesPlugin.js
packages/editor/ProseMirror/plugins/joplinEditablePlugin/createEditorDialog.js
packages/editor/ProseMirror/plugins/joplinEditablePlugin/joplinEditablePlugin.test.js
@@ -1105,12 +1117,15 @@ packages/editor/ProseMirror/plugins/linkTooltipPlugin.test.js
packages/editor/ProseMirror/plugins/linkTooltipPlugin.js
packages/editor/ProseMirror/plugins/listPlugin.js
packages/editor/ProseMirror/plugins/originalMarkupPlugin.js
packages/editor/ProseMirror/plugins/resourcePlaceholderPlugin.js
packages/editor/ProseMirror/plugins/searchPlugin.js
packages/editor/ProseMirror/plugins/utils/createExternalEditorPlugin.js
packages/editor/ProseMirror/plugins/utils/createFloatingButtonPlugin.js
packages/editor/ProseMirror/schema.js
packages/editor/ProseMirror/styles.js
packages/editor/ProseMirror/testing/createTestEditor.js
packages/editor/ProseMirror/testing/createTestEditorWithSerializer.js
packages/editor/ProseMirror/types.js
packages/editor/ProseMirror/utils/SelectableNodeView.js
packages/editor/ProseMirror/utils/UndoStackSynchronizer.js
packages/editor/ProseMirror/utils/canReplaceSelectionWith.js
packages/editor/ProseMirror/utils/computeSelectionFormatting.js
@@ -1118,6 +1133,7 @@ packages/editor/ProseMirror/utils/dom/createButton.js
packages/editor/ProseMirror/utils/dom/createTextArea.js
packages/editor/ProseMirror/utils/dom/createTextNode.js
packages/editor/ProseMirror/utils/dom/createUniqueId.js
packages/editor/ProseMirror/utils/dom/showModal.js
packages/editor/ProseMirror/utils/extractSelectedLinesTo.test.js
packages/editor/ProseMirror/utils/extractSelectedLinesTo.js
packages/editor/ProseMirror/utils/forEachHeading.js
@@ -1128,6 +1144,7 @@ packages/editor/ProseMirror/utils/postprocessEditorOutput.js
packages/editor/ProseMirror/utils/preprocessEditorInput.test.js
packages/editor/ProseMirror/utils/preprocessEditorInput.js
packages/editor/ProseMirror/utils/sanitizeHtml.js
packages/editor/ProseMirror/utils/selectFirstInstanceOfNode.js
packages/editor/ProseMirror/utils/trimEmptyParagraphs.js
packages/editor/ProseMirror/vendor/changedDescendants.js
packages/editor/ProseMirror/vendor/splitBlockAs.js
@@ -1404,14 +1421,19 @@ packages/lib/services/database/types.js
packages/lib/services/debug/populateDatabase.js
packages/lib/services/e2ee/EncryptionService.test.js
packages/lib/services/e2ee/EncryptionService.js
packages/lib/services/e2ee/RSA.node.js
packages/lib/services/e2ee/crypto.test.js
packages/lib/services/e2ee/crypto.js
packages/lib/services/e2ee/cryptoShared.js
packages/lib/services/e2ee/cryptoTestUtils.js
packages/lib/services/e2ee/ppk.test.js
packages/lib/services/e2ee/ppk.js
packages/lib/services/e2ee/ppkTestUtils.js
packages/lib/services/e2ee/ppk/RSA.node.js
packages/lib/services/e2ee/ppk/ppk.test.js
packages/lib/services/e2ee/ppk/ppk.js
packages/lib/services/e2ee/ppk/ppkTestUtils.js
packages/lib/services/e2ee/ppk/webCrypto/LongDataWrapper.js
packages/lib/services/e2ee/ppk/webCrypto/StringToBufferWrapper.js
packages/lib/services/e2ee/ppk/webCrypto/WebCryptoRsa.js
packages/lib/services/e2ee/ppk/webCrypto/buildRsaCryptoProvider.test.js
packages/lib/services/e2ee/ppk/webCrypto/buildRsaCryptoProvider.js
packages/lib/services/e2ee/types.js
packages/lib/services/e2ee/utils.test.js
packages/lib/services/e2ee/utils.js
@@ -1516,6 +1538,7 @@ packages/lib/services/plugins/utils/createViewHandle.js
packages/lib/services/plugins/utils/executeSandboxCall.js
packages/lib/services/plugins/utils/getActivePluginEditorView.js
packages/lib/services/plugins/utils/getActivePluginEditorViews.js
packages/lib/services/plugins/utils/getPluginHelpUrl.js
packages/lib/services/plugins/utils/getPluginIssueReportUrl.test.js
packages/lib/services/plugins/utils/getPluginIssueReportUrl.js
packages/lib/services/plugins/utils/getPluginNamespacedSettingKey.js
@@ -1720,6 +1743,7 @@ packages/plugin-repo-cli/lib/gitCompareUrl.test.js
packages/plugin-repo-cli/lib/gitCompareUrl.js
packages/plugin-repo-cli/lib/overrideUtils.test.js
packages/plugin-repo-cli/lib/overrideUtils.js
packages/plugin-repo-cli/lib/searchPlugins.js
packages/plugin-repo-cli/lib/types.js
packages/plugin-repo-cli/lib/updateReadme.test.js
packages/plugin-repo-cli/lib/updateReadme.js
@@ -1835,6 +1859,8 @@ packages/tools/updateMarkdownDoc.js
packages/tools/utils/discourse.test.js
packages/tools/utils/discourse.js
packages/tools/utils/loadSponsors.js
packages/tools/utils/parsePluralLocalizationForm.js
packages/tools/utils/parsePlurallLocalizationForm.test.js
packages/tools/utils/translation.js
packages/tools/validateFilenames.js
packages/tools/website/build.js

View File

@@ -40,4 +40,29 @@ jobs:
cd packages/app-mobile/android
sed -i -- 's/signingConfig signingConfigs.release/signingConfig signingConfigs.debug/' app/build.gradle
./gradlew assembleRelease
- name: Verify alignment
run: |
cd packages/app-mobile/android/app
APK_FILE="./build/outputs/apk/release/app-release.apk"
if test ! -f "$APK_FILE" ; then
echo "APK file not found."
exit 1
else
echo "APK file found at: $APK_FILE"
fi
BUILD_TOOLS_PATH="$ANDROID_HOME/build-tools/"
if test ! -d "$BUILD_TOOLS_PATH" ; then
echo "Build tools not found at $BUILD_TOOLS_PATH ($ANDROID_HOME, $BUILD_TOOLS_VERSION)"
exit 1
fi
# The build-tools/ directory contains different subdirectories
# for each build tools version. As a result, there may be multiple
# zipalign tools. Select the most recent (biggest two-digit version number):
ZIPALIGN_PATH="$(find $BUILD_TOOLS_PATH -name "zipalign" -print | sort | tail -n1)"
if test ! -x "$ZIPALIGN_PATH" ; then
echo "zipalign not found (searching in $BUILD_TOOLS_PATH, candidate: $ZIPALIGN_PATH)"
exit 1
fi
"$ZIPALIGN_PATH" -c -P 16 -v 4 "$APK_FILE"

View File

@@ -9,7 +9,7 @@ jobs:
matrix:
# Do not use unbuntu-latest because it causes `The operation was canceled` failures:
# https://github.com/actions/runner-images/issues/6709
os: [macos-13, ubuntu-22.04, windows-2025, ubuntu-22.04-arm]
os: [macos-15-intel, ubuntu-22.04, windows-2025, ubuntu-22.04-arm]
steps:
- uses: actions/checkout@v4
@@ -31,6 +31,16 @@ jobs:
sudo apt-get update || true
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin
- name: Free disk space
if: runner.os == 'Linux'
run: |
sudo rm -rf /usr/local/lib/android || true
sudo rm -rf /usr/share/dotnet || true
sudo rm -rf /opt/ghc || true
docker system prune -af || true
docker builder prune -af || true
sudo rm -rf /var/lib/docker/tmp/* || true
# Login to Docker only if we're on a server release tag. If we run this on
# a pull request it will fail because the PR doesn't have access to
# secrets
@@ -40,6 +50,22 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# - name: Test Windows app signing
# if: runner.os == 'Windows'
# env:
# GH_TOKEN: ${{ secrets.GH_TOKEN }}
# IS_CONTINUOUS_INTEGRATION: 1
# BUILD_SEQUENCIAL: 1
# SSL_ESIGNER_USER_NAME: ${{ secrets.SSL_ESIGNER_USER_NAME }}
# SSL_ESIGNER_USER_PASSWORD: ${{ secrets.SSL_ESIGNER_USER_PASSWORD }}
# SSL_ESIGNER_CREDENTIAL_ID: ${{ secrets.SSL_ESIGNER_CREDENTIAL_ID }}
# SSL_ESIGNER_USER_TOTP: ${{ secrets.SSL_ESIGNER_USER_TOTP }}
# SIGN_APPLICATION: 1
# # To ensure that the operations stop on failure, all commands
# # should be on one line with "&&" in between.
# run: |
# yarn install && cd packages/app-desktop && yarn dist
- name: Run tests, build and publish Linux and macOS apps
if: runner.os == 'Linux' || runner.os == 'macOs'
env:
@@ -61,11 +87,14 @@ jobs:
- name: Build and publish Windows app
if: runner.os == 'Windows' && startsWith(github.ref, 'refs/tags/v')
env:
CSC_KEY_PASSWORD: ${{ secrets.WINDOWS_CSC_KEY_PASSWORD }}
CSC_LINK: ${{ secrets.WINDOWS_CSC_LINK }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
IS_CONTINUOUS_INTEGRATION: 1
BUILD_SEQUENCIAL: 1
SSL_ESIGNER_USER_NAME: ${{ secrets.SSL_ESIGNER_USER_NAME }}
SSL_ESIGNER_USER_PASSWORD: ${{ secrets.SSL_ESIGNER_USER_PASSWORD }}
SSL_ESIGNER_CREDENTIAL_ID: ${{ secrets.SSL_ESIGNER_CREDENTIAL_ID }}
SSL_ESIGNER_USER_TOTP: ${{ secrets.SSL_ESIGNER_USER_TOTP }}
SIGN_APPLICATION: 1
# To ensure that the operations stop on failure, all commands
# should be on one line with "&&" in between.
run: |
@@ -122,6 +151,16 @@ jobs:
with:
node-version: '18'
- name: Free disk space
if: runner.os == 'Linux'
run: |
sudo rm -rf /usr/local/lib/android || true
sudo rm -rf /usr/share/dotnet || true
sudo rm -rf /opt/ghc || true
docker system prune -af || true
docker builder prune -af || true
sudo rm -rf /var/lib/docker/tmp/* || true
- name: Install Yarn
run: |
# https://yarnpkg.com/getting-started/install
@@ -149,7 +188,7 @@ jobs:
- name: Check HTTP request
run: |
# Need to pass environment variables:
docker run -p 22300:22300 joplin/server:$(dpkg --print-architecture)-0.0.0 node dist/app.js --env dev &
docker run --env MAX_TIME_DRIFT=0 --publish 22300:22300 joplin/server:$(dpkg --print-architecture)-0.0.0 node dist/app.js --env dev &
# Wait for server to start
sleep 120
@@ -175,5 +214,4 @@ jobs:
if [[ "$actual_body" != "$expected_body" ]]; then
echo 'Failed while checking the body response after request to /api/ping'
exit 1;
fi
fi

42
.gitignore vendored
View File

@@ -69,6 +69,7 @@ docs/**/*.mustache
packages/app-cli/app/LinkSelector.js
packages/app-cli/app/app.js
packages/app-cli/app/base-command.js
packages/app-cli/app/cli-integration-tests.js
packages/app-cli/app/command-apidoc.js
packages/app-cli/app/command-attach.js
packages/app-cli/app/command-batch.js
@@ -649,6 +650,8 @@ 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/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
@@ -695,6 +698,8 @@ packages/app-mobile/components/SearchInput.js
packages/app-mobile/components/SelectDateTimeDialog.js
packages/app-mobile/components/SideMenu.js
packages/app-mobile/components/SideMenuContentNote.js
packages/app-mobile/components/SyncWizard/JoplinCloudIcon.js
packages/app-mobile/components/SyncWizard/SyncWizard.js
packages/app-mobile/components/TagEditor.test.js
packages/app-mobile/components/TagEditor.js
packages/app-mobile/components/TextInput.js
@@ -711,6 +716,7 @@ packages/app-mobile/components/base-screen.js
packages/app-mobile/components/biometrics/BiometricPopup.js
packages/app-mobile/components/biometrics/biometricAuthenticate.js
packages/app-mobile/components/biometrics/sensorInfo.js
packages/app-mobile/components/buttons/CardButton.js
packages/app-mobile/components/buttons/FloatingActionButton.js
packages/app-mobile/components/buttons/LabelledIconButton.js
packages/app-mobile/components/buttons/MultiTouchableOpacity.js
@@ -786,7 +792,6 @@ packages/app-mobile/components/screens/ConfigScreen/plugins/buttons/InstallButto
packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/WrappedPluginStates.js
packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/mockRepositoryApiConstructor.js
packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/newRepoApi.js
packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/pluginServiceSetup.js
packages/app-mobile/components/screens/ConfigScreen/plugins/utils/openWebsiteForPlugin.js
packages/app-mobile/components/screens/ConfigScreen/plugins/utils/usePluginCallbacks.js
packages/app-mobile/components/screens/ConfigScreen/plugins/utils/usePluginItem.js
@@ -812,6 +817,7 @@ packages/app-mobile/components/screens/NoteTagsDialog.js
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/SearchScreen/SearchResults.test.js
packages/app-mobile/components/screens/SearchScreen/SearchResults.js
packages/app-mobile/components/screens/SearchScreen/index.js
packages/app-mobile/components/screens/ShareManager/AcceptedShareItem.js
@@ -874,14 +880,13 @@ packages/app-mobile/services/AlarmServiceDriver.web.js
packages/app-mobile/services/BackButtonService.js
packages/app-mobile/services/commands/stateToWhenClauseContext.js
packages/app-mobile/services/e2ee/RSA.react-native.js
packages/app-mobile/services/e2ee/RSA.react-native.web.js
packages/app-mobile/services/e2ee/crypto.js
packages/app-mobile/services/plugins/PlatformImplementation.js
packages/app-mobile/services/profiles/index.js
packages/app-mobile/services/voiceTyping/VoiceTyping.js
packages/app-mobile/services/voiceTyping/utils/unzip.android.js
packages/app-mobile/services/voiceTyping/utils/unzip.js
packages/app-mobile/services/voiceTyping/vosk.android.js
packages/app-mobile/services/voiceTyping/vosk.js
packages/app-mobile/services/voiceTyping/whisper.test.js
packages/app-mobile/services/voiceTyping/whisper.js
packages/app-mobile/setupQuickActions.js
@@ -895,6 +900,7 @@ packages/app-mobile/utils/ShareUtils.test.js
packages/app-mobile/utils/ShareUtils.js
packages/app-mobile/utils/TlsUtils.js
packages/app-mobile/utils/appDefaultState.js
packages/app-mobile/utils/appReducer.js
packages/app-mobile/utils/autodetectTheme.js
packages/app-mobile/utils/buildStartupTasks.js
packages/app-mobile/utils/checkPermissions.js
@@ -934,6 +940,7 @@ packages/app-mobile/utils/pickDocument.js
packages/app-mobile/utils/polyfills/bufferPolyfill.js
packages/app-mobile/utils/polyfills/crypto-polyfill/index.js
packages/app-mobile/utils/polyfills/index.js
packages/app-mobile/utils/polyfills/index.web.js
packages/app-mobile/utils/setupNotifications.js
packages/app-mobile/utils/shareFile.js
packages/app-mobile/utils/shareHandler.js
@@ -944,6 +951,7 @@ packages/app-mobile/utils/shim-init-react/shimInitShared.js
packages/app-mobile/utils/testing/createMockReduxStore.js
packages/app-mobile/utils/testing/getWebViewDomById.js
packages/app-mobile/utils/testing/getWebViewWindowById.js
packages/app-mobile/utils/testing/mockPluginServiceSetup.js
packages/app-mobile/utils/testing/setupGlobalStore.js
packages/app-mobile/utils/testing/testingLibrary.js
packages/app-mobile/utils/types.js
@@ -1015,6 +1023,7 @@ packages/editor/CodeMirror/extensions/rendering/types.js
packages/editor/CodeMirror/extensions/rendering/utils/makeBlockReplaceExtension.js
packages/editor/CodeMirror/extensions/rendering/utils/makeInlineReplaceExtension.js
packages/editor/CodeMirror/extensions/rendering/utils/nodeIntersectsSelection.js
packages/editor/CodeMirror/extensions/searchExtension.test.js
packages/editor/CodeMirror/extensions/searchExtension.js
packages/editor/CodeMirror/extensions/selectedNoteIdExtension.js
packages/editor/CodeMirror/getScrollFraction.js
@@ -1061,12 +1070,15 @@ packages/editor/CodeMirror/utils/markdown/renumberSelectedLists.js
packages/editor/CodeMirror/utils/markdown/stripBlockquote.js
packages/editor/CodeMirror/utils/markdown/toggleCheckboxAt.js
packages/editor/CodeMirror/utils/setupVim.js
packages/editor/CodeMirror/vendor/announceSearchMatch.js
packages/editor/ProseMirror/commands.test.js
packages/editor/ProseMirror/commands.js
packages/editor/ProseMirror/createEditor.js
packages/editor/ProseMirror/index.js
packages/editor/ProseMirror/plugins/detailsPlugin.test.js
packages/editor/ProseMirror/plugins/detailsPlugin.js
packages/editor/ProseMirror/plugins/imagePlugin.test.js
packages/editor/ProseMirror/plugins/imagePlugin.js
packages/editor/ProseMirror/plugins/inputRulesPlugin.js
packages/editor/ProseMirror/plugins/joplinEditablePlugin/createEditorDialog.js
packages/editor/ProseMirror/plugins/joplinEditablePlugin/joplinEditablePlugin.test.js
@@ -1078,12 +1090,15 @@ packages/editor/ProseMirror/plugins/linkTooltipPlugin.test.js
packages/editor/ProseMirror/plugins/linkTooltipPlugin.js
packages/editor/ProseMirror/plugins/listPlugin.js
packages/editor/ProseMirror/plugins/originalMarkupPlugin.js
packages/editor/ProseMirror/plugins/resourcePlaceholderPlugin.js
packages/editor/ProseMirror/plugins/searchPlugin.js
packages/editor/ProseMirror/plugins/utils/createExternalEditorPlugin.js
packages/editor/ProseMirror/plugins/utils/createFloatingButtonPlugin.js
packages/editor/ProseMirror/schema.js
packages/editor/ProseMirror/styles.js
packages/editor/ProseMirror/testing/createTestEditor.js
packages/editor/ProseMirror/testing/createTestEditorWithSerializer.js
packages/editor/ProseMirror/types.js
packages/editor/ProseMirror/utils/SelectableNodeView.js
packages/editor/ProseMirror/utils/UndoStackSynchronizer.js
packages/editor/ProseMirror/utils/canReplaceSelectionWith.js
packages/editor/ProseMirror/utils/computeSelectionFormatting.js
@@ -1091,6 +1106,7 @@ packages/editor/ProseMirror/utils/dom/createButton.js
packages/editor/ProseMirror/utils/dom/createTextArea.js
packages/editor/ProseMirror/utils/dom/createTextNode.js
packages/editor/ProseMirror/utils/dom/createUniqueId.js
packages/editor/ProseMirror/utils/dom/showModal.js
packages/editor/ProseMirror/utils/extractSelectedLinesTo.test.js
packages/editor/ProseMirror/utils/extractSelectedLinesTo.js
packages/editor/ProseMirror/utils/forEachHeading.js
@@ -1101,6 +1117,7 @@ packages/editor/ProseMirror/utils/postprocessEditorOutput.js
packages/editor/ProseMirror/utils/preprocessEditorInput.test.js
packages/editor/ProseMirror/utils/preprocessEditorInput.js
packages/editor/ProseMirror/utils/sanitizeHtml.js
packages/editor/ProseMirror/utils/selectFirstInstanceOfNode.js
packages/editor/ProseMirror/utils/trimEmptyParagraphs.js
packages/editor/ProseMirror/vendor/changedDescendants.js
packages/editor/ProseMirror/vendor/splitBlockAs.js
@@ -1377,14 +1394,19 @@ packages/lib/services/database/types.js
packages/lib/services/debug/populateDatabase.js
packages/lib/services/e2ee/EncryptionService.test.js
packages/lib/services/e2ee/EncryptionService.js
packages/lib/services/e2ee/RSA.node.js
packages/lib/services/e2ee/crypto.test.js
packages/lib/services/e2ee/crypto.js
packages/lib/services/e2ee/cryptoShared.js
packages/lib/services/e2ee/cryptoTestUtils.js
packages/lib/services/e2ee/ppk.test.js
packages/lib/services/e2ee/ppk.js
packages/lib/services/e2ee/ppkTestUtils.js
packages/lib/services/e2ee/ppk/RSA.node.js
packages/lib/services/e2ee/ppk/ppk.test.js
packages/lib/services/e2ee/ppk/ppk.js
packages/lib/services/e2ee/ppk/ppkTestUtils.js
packages/lib/services/e2ee/ppk/webCrypto/LongDataWrapper.js
packages/lib/services/e2ee/ppk/webCrypto/StringToBufferWrapper.js
packages/lib/services/e2ee/ppk/webCrypto/WebCryptoRsa.js
packages/lib/services/e2ee/ppk/webCrypto/buildRsaCryptoProvider.test.js
packages/lib/services/e2ee/ppk/webCrypto/buildRsaCryptoProvider.js
packages/lib/services/e2ee/types.js
packages/lib/services/e2ee/utils.test.js
packages/lib/services/e2ee/utils.js
@@ -1489,6 +1511,7 @@ packages/lib/services/plugins/utils/createViewHandle.js
packages/lib/services/plugins/utils/executeSandboxCall.js
packages/lib/services/plugins/utils/getActivePluginEditorView.js
packages/lib/services/plugins/utils/getActivePluginEditorViews.js
packages/lib/services/plugins/utils/getPluginHelpUrl.js
packages/lib/services/plugins/utils/getPluginIssueReportUrl.test.js
packages/lib/services/plugins/utils/getPluginIssueReportUrl.js
packages/lib/services/plugins/utils/getPluginNamespacedSettingKey.js
@@ -1693,6 +1716,7 @@ packages/plugin-repo-cli/lib/gitCompareUrl.test.js
packages/plugin-repo-cli/lib/gitCompareUrl.js
packages/plugin-repo-cli/lib/overrideUtils.test.js
packages/plugin-repo-cli/lib/overrideUtils.js
packages/plugin-repo-cli/lib/searchPlugins.js
packages/plugin-repo-cli/lib/types.js
packages/plugin-repo-cli/lib/updateReadme.test.js
packages/plugin-repo-cli/lib/updateReadme.js
@@ -1808,6 +1832,8 @@ packages/tools/updateMarkdownDoc.js
packages/tools/utils/discourse.test.js
packages/tools/utils/discourse.js
packages/tools/utils/loadSponsors.js
packages/tools/utils/parsePluralLocalizationForm.js
packages/tools/utils/parsePlurallLocalizationForm.test.js
packages/tools/utils/translation.js
packages/tools/validateFilenames.js
packages/tools/website/build.js

View File

@@ -0,0 +1,36 @@
# Patch to remove eval. This allows using depd in an environment with
# a strict Content-Security-Policy.
# Ref: https://github.com/dougwilson/nodejs-depd/pull/33
diff --git a/index.js b/index.js
index d758d3c8f58a60bf27ef377ad77639bf10ce7854..2bad40d4eeba553d3bcfb206873eac059067ae3b 100644
--- a/index.js
+++ b/index.js
@@ -399,19 +399,20 @@ function wrapfunction (fn, message) {
throw new TypeError('argument fn must be a function')
}
- var args = createArgumentsString(fn.length)
- var deprecate = this // eslint-disable-line no-unused-vars
var stack = getStack()
var site = callSiteLocation(stack[1])
site.name = fn.name
- // eslint-disable-next-line no-eval
- var deprecatedfn = eval('(function (' + args + ') {\n' +
- '"use strict"\n' +
- 'log.call(deprecate, message, site)\n' +
- 'return fn.apply(this, arguments)\n' +
- '})')
+ var deprecatedfn
+ var self = this
+ deprecatedfn = function () {
+ 'use strict'
+ log.call(self, message, site)
+ return fn.apply(this, arguments)
+ }
+ Object.defineProperty(deprecatedfn, 'length', { value: fn.length })
+ Object.defineProperty(deprecatedfn, 'name', { value: fn.name })
return deprecatedfn
}

View File

@@ -0,0 +1,35 @@
# Patch to remove eval. This allows using depd in an environment with
# a strict Content-Security-Policy.
# Ref: https://github.com/dougwilson/nodejs-depd/pull/33
diff --git a/index.js b/index.js
index 1bf2fcfdeffc984e5ad792eec08744c29d4a4590..1b24aa2414458bc651abfdded81b103c131efeaa 100644
--- a/index.js
+++ b/index.js
@@ -415,19 +415,19 @@ function wrapfunction (fn, message) {
throw new TypeError('argument fn must be a function')
}
- var args = createArgumentsString(fn.length)
var stack = getStack()
var site = callSiteLocation(stack[1])
site.name = fn.name
- // eslint-disable-next-line no-new-func
- var deprecatedfn = new Function('fn', 'log', 'deprecate', 'message', 'site',
- '"use strict"\n' +
- 'return function (' + args + ') {' +
- 'log.call(deprecate, message, site)\n' +
- 'return fn.apply(this, arguments)\n' +
- '}')(fn, log, this, message, site)
+ var self = this
+ var deprecatedfn = function () {
+ 'use strict'
+ log.call(self, message, site)
+ return fn.apply(this, arguments)
+ }
+ Object.defineProperty(deprecatedfn, 'length', { value: fn.length })
+ Object.defineProperty(deprecatedfn, 'name', { value: fn.name })
return deprecatedfn
}

View File

@@ -1,209 +0,0 @@
diff --git a/android/build.gradle b/android/build.gradle
index 6afcbbf0cc8ca2d69dd78077d61e59a90b2136bb..9f8d72b4ec5b2b3d290975d6a255917c95300854 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -67,19 +67,19 @@ repositories {
}
// Generate UUIDs for each models contained in android/src/main/assets/
-tasks.register('genUUID') {
- doLast {
- fileTree(dir: "$rootDir/app/src/main/assets", exclude: ['*/*']).visit { fileDetails ->
- if (fileDetails.directory) {
- def odir = file("$rootDir/app/src/main/assets/$fileDetails.relativePath")
- def ofile = file("$odir/uuid")
- mkdir odir
- ofile.text = UUID.randomUUID().toString()
- }
- }
- }
-}
-preBuild.dependsOn genUUID
+// tasks.register('genUUID') {
+// doLast {
+// fileTree(dir: "$rootDir/app/src/main/assets", exclude: ['*/*']).visit { fileDetails ->
+// if (fileDetails.directory) {
+// def odir = file("$rootDir/app/src/main/assets/$fileDetails.relativePath")
+// def ofile = file("$odir/uuid")
+// mkdir odir
+// ofile.text = UUID.randomUUID().toString()
+// }
+// }
+// }
+// }
+// preBuild.dependsOn genUUID
def kotlin_version = getExtOrDefault('kotlinVersion')
diff --git a/android/src/main/java/com/reactnativevosk/VoskModule.kt b/android/src/main/java/com/reactnativevosk/VoskModule.kt
index 0e2b6595b1b2cf1ee01c6c64239c4b0ea37fce19..5a8539b9cce8951967640dba755e29a4e3ff404a 100644
--- a/android/src/main/java/com/reactnativevosk/VoskModule.kt
+++ b/android/src/main/java/com/reactnativevosk/VoskModule.kt
@@ -19,13 +19,25 @@ class VoskModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
return "Vosk"
}
+ @ReactMethod
+ fun addListener(type: String?) {
+ // Keep: Required for RN built in Event Emitter Calls.
+ }
+
+ @ReactMethod
+ fun removeListeners(type: Int?) {
+ // Keep: Required for RN built in Event Emitter Calls.
+ }
+
override fun onResult(hypothesis: String) {
// Get text data from string object
val text = getHypothesisText(hypothesis)
// Stop recording if data found
if (text != null && text.isNotEmpty()) {
- cleanRecognizer();
+ // Don't auto-stop the recogniser - we want to do that when the user
+ // presses on "stop" only.
+ // cleanRecognizer();
sendEvent("onResult", text)
}
}
@@ -93,12 +105,11 @@ class VoskModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
@ReactMethod
fun loadModel(path: String, promise: Promise) {
cleanModel();
- StorageService.unpack(context, path, "models",
- { model: Model? ->
- this.model = model
- promise.resolve("Model successfully loaded")
- }
- ) { e: IOException ->
+
+ try {
+ this.model = Model(path);
+ promise.resolve("Model successfully loaded")
+ } catch (e: IOException) {
this.model = null
promise.reject(e)
}
@@ -153,6 +164,25 @@ class VoskModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
cleanRecognizer();
}
+ @ReactMethod
+ fun stopOnly() {
+ if (speechService != null) {
+ speechService!!.stop()
+ }
+ }
+
+ @ReactMethod
+ fun cleanup() {
+ if (speechService != null) {
+ speechService!!.shutdown();
+ speechService = null
+ }
+ if (recognizer != null) {
+ recognizer!!.close();
+ recognizer = null;
+ }
+ }
+
@ReactMethod
fun unload() {
cleanRecognizer();
diff --git a/lib/typescript/index.d.ts b/lib/typescript/index.d.ts
index 441e41cc402cca3a60b34978ef4fea976076259c..a173acebb4b314402550442ad471e0f7c706e3c4 100644
--- a/lib/typescript/index.d.ts
+++ b/lib/typescript/index.d.ts
@@ -10,6 +10,8 @@ export default class Vosk {
currentRegisteredEvents: EmitterSubscription[];
start: (grammar?: string[] | null) => Promise<String>;
stop: () => void;
+ stopOnly: () => void;
+ cleanup: () => void;
unload: () => void;
onResult: (onResult: (e: VoskEvent) => void) => EventSubscription;
onFinalResult: (onFinalResult: (e: VoskEvent) => void) => EventSubscription;
diff --git a/package.json b/package.json
index 707eddb8d68007f93071ac659c5b087c935c5f01..90ebe20f224eeec472c377df1fef9b15f2ff8200 100644
--- a/package.json
+++ b/package.json
@@ -11,12 +11,9 @@
"src",
"lib",
"android",
- "ios",
"cpp",
- "react-native-vosk.podspec",
"!lib/typescript/example",
"!android/build",
- "!ios/build",
"!**/__tests__",
"!**/__fixtures__",
"!**/__mocks__"
diff --git a/react-native-vosk.podspec b/react-native-vosk.podspec
deleted file mode 100644
index e3d41b90c5eef890c7a5108aaf16ac07d34a698b..0000000000000000000000000000000000000000
--- a/react-native-vosk.podspec
+++ /dev/null
@@ -1,41 +0,0 @@
-require "json"
-
-package = JSON.parse(File.read(File.join(__dir__, "package.json")))
-folly_version = '2021.06.28.00-v2'
-folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'
-
-Pod::Spec.new do |s|
- s.name = "react-native-vosk"
- s.version = package["version"]
- s.summary = package["description"]
- s.homepage = package["homepage"]
- s.license = package["license"]
- s.authors = package["author"]
-
- s.platforms = { :ios => "10.0" }
- s.source = { :git => "https://github.com/riderodd/react-native-vosk.git", :tag => "#{s.version}" }
-
- s.source_files = "ios/**/*.{h,m,mm,swift}"
- s.resource_bundles = { 'Vosk' => ['ios/Vosk/*'] }
-
- s.dependency "React-Core"
- s.frameworks = "Accelerate"
- s.library = "c++"
- s.vendored_frameworks = "ios/libvosk.xcframework"
- s.requires_arc = true
-
- # Don't install the dependencies when we run `pod install` in the old architecture.
- if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then
- s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1"
- s.pod_target_xcconfig = {
- "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"",
- "CLANG_CXX_LANGUAGE_STANDARD" => "c++17"
- }
-
- s.dependency "React-Codegen"
- s.dependency "RCT-Folly", folly_version
- s.dependency "RCTRequired"
- s.dependency "RCTTypeSafety"
- s.dependency "ReactCommon/turbomodule/core"
- end
-end
diff --git a/src/index.tsx b/src/index.tsx
index d9f90c921d89b1b4d85e145443ed3376546a368a..29e4068dbd7500828a73145bd25497a52c9bf638 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -69,6 +69,15 @@ export default class Vosk {
VoskModule.stop();
};
+ stopOnly = () => {
+ VoskModule.stopOnly();
+ };
+
+ cleanup = () => {
+ this.cleanListeners();
+ VoskModule.cleanup();
+ };
+
unload = () => {
this.cleanListeners();
VoskModule.unload();

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@@ -1,4 +1,77 @@
<?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, 28 Apr 2025 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Mon, 28 Apr 2025 00:00:00 GMT</pubDate><item><title><![CDATA[What's new in Joplin 3.3]]></title><description><![CDATA[<h2>Desktop application<a name="desktop-application" href="#desktop-application" class="heading-anchor">🔗</a></h2>
<?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, 22 Sep 2025 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Mon, 22 Sep 2025 00:00:00 GMT</pubDate><item><title><![CDATA[What's new in Joplin 3.4]]></title><description><![CDATA[<p>Joplin 3.4 includes many bug fixes and improvements, with a focus on the mobile app.</p>
<h2>Mobile<a name="mobile" href="#mobile" class="heading-anchor">🔗</a></h2>
<h3>Rich Text Editor<a name="rich-text-editor" href="#rich-text-editor" class="heading-anchor">🔗</a></h3>
<p>The mobile app now includes a beta <a href="https://joplinapp.org/help/apps/rich_text_editor">Rich Text Editor</a>! The new editor renders formatting/math/images within the editor:</p>
<img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20250922-mobile-rte.png" width="400" alt="screenshot: Mobile Rich Text Editor editing the welcome notes. Images, headings, etc are rendering."/>
<p>To try it, 1) open a note in the default Markdown editor 2) open the note actions menu (the three vertical dots) for the note and 3) click “Edit as Rich Text”.</p>
<p>Be aware that this editor is still in active development and <a href="https://github.com/laurent22/joplin/issues/12840">has a number of known limitations and issues</a>. The Rich Text editor is based on <a href="https://prosemirror.net/">ProseMirror</a> and will behave differently from the desktop Rich Text Editor in many cases.</p>
<h3>Support for publishing notes with Joplin Cloud and Server<a name="support-for-publishing-notes-with-joplin-cloud-and-server" href="#support-for-publishing-notes-with-joplin-cloud-and-server" class="heading-anchor">🔗</a></h3>
<p>It's now possible to <a href="https://joplinapp.org/help/apps/publish_note">publish notes</a> from the mobile app! To do so, open the “Properties” menu for a note, then click “Publish/unpublish”:</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20250922-publish-notes.png" alt="screenshot: A Publish/unpublish note action is shown in the &quot;Note properties&quot; sidebar, just below a &quot;Previous versions&quot; button"></p>
<p>Next, in the “publish note” dialog, click “Copy shareable link”. Notes can later be unpublished by clicking &quot;Unpublish&quot; in the publication dialog.</p>
<h3>Viewing note history<a name="viewing-note-history" href="#viewing-note-history" class="heading-anchor">🔗</a></h3>
<p>It is now possible to view and restore previous note versions from the mobile app. Like the &quot;publish note&quot; feature, previous note versions can be accessed from the note properties menu.</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20250922-note-history.png" alt="screenshot: The note history page"></p>
<p>As on desktop, the note history feature can be configured from the “Note History” tab in settings.</p>
<h3>Updated tag dialog<a name="updated-tag-dialog" href="#updated-tag-dialog" class="heading-anchor">🔗</a></h3>
<p>The tag dialog has been redesigned, with a new UI for adding, removing, and creating new tags:<br>
<img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20250922-tag-editor.png" width="500" alt="screenshot: Tag dialog now consists of three sections: Added tags, Add new tags, Actions."/></p>
<h3>Android: Improved voice typing<a name="android-improved-voice-typing" href="#android-improved-voice-typing" class="heading-anchor">🔗</a></h3>
<p>The voice typing feature on Android has been updated with <a href="https://github.com/laurent22/joplin/pull/12404">improved silence detection</a> and a new “<a href="https://github.com/laurent22/joplin/pull/12370">custom glossary</a>” setting. Voice typing also now <a href="https://github.com/laurent22/joplin/pull/12352">defaults to a more accurate (but somewhat slower) model</a>.</p>
<h3>Quickly creating a note from multiple photos<a name="quickly-creating-a-note-from-multiple-photos" href="#quickly-creating-a-note-from-multiple-photos" class="heading-anchor">🔗</a></h3>
<p>A “scan notebook” action has been added to the “New note” menu:</p>
<img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20250922-scan-notebook.png" width="500"/>
<p>This action allows quickly creating a new note with multiple pictures taken from the camera.</p>
<h2>Desktop<a name="desktop" href="#desktop" class="heading-anchor">🔗</a></h2>
<h3>More Markdown Editor settings<a name="more-markdown-editor-settings" href="#more-markdown-editor-settings" class="heading-anchor">🔗</a></h3>
<p>The &quot;Note&quot; tab in settings now includes new settings for the Markdown editor, including:</p>
<ul>
<li>An option to render headers, lists, and certain other formatting within the editor.</li>
<li>An option to render images in the editor.</li>
</ul>
<p>When enabled, these settings bring the Markdown editor closer to the Rich Text Editor, without <a href="https://joplinapp.org/help/apps/rich_text_editor">some of the Rich Text Editor's limitations</a>.</p>
<p>These settings are also available on mobile.</p>
<h3>Smaller application size and faster startup<a name="smaller-application-size-and-faster-startup" href="#smaller-application-size-and-faster-startup" class="heading-anchor">🔗</a></h3>
<p>We've made the desktop application roughly 33% smaller! In addition to faster application startup, this means that the desktop app should be faster to download take up less space.</p>
<table class="table">
<thead>
<tr>
<th>Joplin version</th>
<th>Previous size (v3.3.13)</th>
<th>New size (v3.4.12)</th>
</tr>
</thead>
<tbody>
<tr>
<td>Joplin for MacOS (ARM)</td>
<td>211 MB</td>
<td>141 MB</td>
</tr>
<tr>
<td>Joplin for Windows (installer)</td>
<td>321 MB</td>
<td>219 MB</td>
</tr>
<tr>
<td>Joplin for Windows (portable)</td>
<td>320 MB</td>
<td>219 MB</td>
</tr>
<tr>
<td>Joplin for Linux (AppImage)</td>
<td>219 MB</td>
<td>147 MB</td>
</tr>
</tbody>
</table>
<h2>Terminal app<a name="terminal-app" href="#terminal-app" class="heading-anchor">🔗</a></h2>
<h3>Collapsible folders<a name="collapsible-folders" href="#collapsible-folders" class="heading-anchor">🔗</a></h3>
<p>The <a href="https://joplinapp.org/help/apps/terminal/">terminal application</a> now supports expanding and collapsing folders by pressing <kbd>z</kbd>. For additional information, see <a href="https://github.com/laurent22/joplin/pull/12718">the original pull request</a>.</p>
<h3>Managing shared notebooks and published notes<a name="managing-shared-notebooks-and-published-notes" href="#managing-shared-notebooks-and-published-notes" class="heading-anchor">🔗</a></h3>
<p>New commands have been added to the terminal app, including <code>publish</code>, <code>unpublish</code>, and <code>share</code>. This allows the terminal app to manage shared folders and published notes.</p>
<h2>Bug fixes<a name="bug-fixes" href="#bug-fixes" class="heading-anchor">🔗</a></h2>
<p>For the full list of changes, see <a href="https://joplinapp.org/help/about/changelog/desktop/">the desktop changelog</a> and <a href="https://joplinapp.org/help/about/changelog/android/">the mobile changelog</a>.</p>
]]></description><link>https://joplinapp.org/news/20250922-release-3-4</link><guid isPermaLink="false">20250922-release-3-4</guid><pubDate>Mon, 22 Sep 2025 00:00:00 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[What's new in Joplin 3.3]]></title><description><![CDATA[<h2>Desktop application<a name="desktop-application" href="#desktop-application" class="heading-anchor">🔗</a></h2>
<h3>Accessibility improvements<a name="accessibility-improvements" href="#accessibility-improvements" class="heading-anchor">🔗</a></h3>
<p>The Joplin 3.3 release introduces significant accessibility enhancements designed to make the application more inclusive and user-friendly. Users can now benefit from improved keyboard navigation, thanks to newly added shortcuts and clearer labels that streamline interaction across the interface. We've also added a &quot;go to viewer&quot; menu item that moves focus from the note editor to the note viewer. Focus is moved to the location in the viewer corresponding to the location of the cursor in the editor.</p>
<p>Screen reader support has been bolstered, ensuring elements like the note list and sidebar are easier to toggle and interact with. These updates make the application more usable for individuals relying on assistive technologies.</p>
@@ -446,10 +519,4 @@ sys 0m38.013s</p>
<p>Unfortunately we cannot publish the Android version because it is based on a framework version that Google does not accept. To upgrade the app a lot of changes are needed and another round of pre-releases, and therefore there will not be a 2.9 version for Google Play. You may however download the official APK directly from there: <a href="https://github.com/laurent22/joplin-android/releases/tag/android-v2.9.8">Android 2.9 Official Release</a></p>
<p>This is the reality of app stores in general - small developers being imposed never ending new requirements by all-powerful companies, and by the time a version is finally ready we can't even publish it because yet more requirements are in place.</p>
<p>For the record the current 2.9 app works perfectly fine. It targets Android 11, which is only 2 years old and is still supported (and installed on millions of phones). Google requires us to target Android 12 which only came out last year.</p>
]]></description><link>https://joplinapp.org/news/20221216-release-2-9</link><guid isPermaLink="false">20221216-release-2-9</guid><pubDate>Fri, 16 Dec 2022 00:00:00 GMT</pubDate><twitter-text>What&apos;s new in Joplin 2.9</twitter-text></item><item><title><![CDATA[Joplin is hiring!]]></title><description><![CDATA[<p>Joplin is an open source note-taking app. Capture your thoughts and securely access them from any device.</p>
<p>We are looking to hire two JavaScript software developers to work on the desktop, mobile, and server applications. All those are built using modern technologies, including React, React Native and Electron with a strong focus on test units.</p>
<p>You need to demonstrate some experience with at least some of these technologies, and willing to learn more and touch various different projects.</p>
<p>You will be part of a small team, so you will have an opportunity for a high-impact role, targeting hundreds of thousands of users.</p>
<p>If you're interested please contact us at job-AT-joplin.cloud</p>
<p>No agencies please.</p>
]]></description><link>https://joplinapp.org/news/20221209-job</link><guid isPermaLink="false">20221209-job</guid><pubDate>Fri, 09 Dec 2022 00:00:00 GMT</pubDate><twitter-text>Joplin is hiring!</twitter-text></item></channel></rss>
]]></description><link>https://joplinapp.org/news/20221216-release-2-9</link><guid isPermaLink="false">20221216-release-2-9</guid><pubDate>Fri, 16 Dec 2022 00:00:00 GMT</pubDate><twitter-text>What&apos;s new in Joplin 2.9</twitter-text></item></channel></rss>

View File

@@ -63,11 +63,24 @@ FROM node:18-slim
ARG user=joplin
RUN useradd --create-home --shell /bin/bash $user
# Install PM2 and set home directory. Setting the PM2 data dir so modules/config persist regardless
# of user home.
RUN npm i -g pm2@5.4.3 && mkdir -p /opt/pm2 && chown -R $user:$user /opt/pm2
ENV PM2_HOME=/opt/pm2
USER $user
COPY --chown=$user:$user --from=builder /build/packages /home/$user/packages
COPY --chown=$user:$user --from=builder /usr/bin/tini /usr/local/bin/tini
# We download a specific version of the plugin to prevent pm2 from fetching the latest, since it may
# not have been properly audited (that fact was used to spread malware at some point). Ref:
# https://github.com/laurent22/joplin/issues/12754
RUN pm2 install https://registry.npmjs.org/pm2-logrotate/-/pm2-logrotate-3.0.0.tgz \
&& pm2 set pm2-logrotate:max_size 100MB \
&& pm2 set pm2-logrotate:retain 5 \
&& pm2 set pm2-logrotate:compress true
ENV NODE_ENV=production
ENV RUNNING_IN_DOCKER=1
EXPOSE ${APP_PORT}

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://essayservice.com"><img title="quick and reliable service to write my paper for me" width="256" src="https://joplinapp.org/images/sponsors/EssayService.png" alt="quick and reliable service to write my paper for me"/></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://damangameplay.in"><img title="Daman Game" width="256" src="https://joplinapp.org/images/sponsors/DamanGame.png" alt="Daman Game"/></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://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>
<!-- SPONSORS-ORG -->
* * *

View File

@@ -9,7 +9,7 @@
"vips.dev": {
"platforms": ["aarch64-darwin"],
},
"nodejs": "23.10.0",
"nodejs": "23.11.0",
"pkg-config": "latest",
"darwin.apple_sdk.frameworks.Foundation": { // satisfies missing CoreText/CoreText.h
// https://github.com/NixOS/nixpkgs/blob/master/pkgs/os-specific/darwin/apple-sdk/default.nix

View File

@@ -16,12 +16,10 @@
# SLAVE_POSTGRES_PORT=5433
# SLAVE_POSTGRES_HOST=localhost
version: '2'
services:
postgresql-master:
image: 'bitnami/postgresql:17.3.0'
image: 'bitnamilegacy/postgresql:17.4.0'
ports:
- '5432:5432'
environment:
@@ -38,7 +36,7 @@ services:
- POSTGRESQL_EXTRA_FLAGS=-c work_mem=100000 -c log_statement=all
postgresql-slave:
image: 'bitnami/postgresql:17.3.0'
image: 'bitnamilegacy/postgresql:17.4.0'
ports:
- '5433:5432'
depends_on:

View File

@@ -1,8 +1,6 @@
# This compose file can be used in development to run both the database and app
# within Docker.
version: '3'
services:
app:
build:

View File

@@ -15,8 +15,6 @@
# - This would typically be mapped to port to 443 (TLS) with a reverse proxy.
# - If Joplin Server does not need to be accessible over the internet, the port can be mapped to 22300.
version: '3'
networks:
app-network:
transcribe-network:

View File

@@ -79,17 +79,17 @@
"eslint-plugin-import": "2.31.0",
"eslint-plugin-jest": "27.9.0",
"eslint-plugin-promise": "6.6.0",
"eslint-plugin-react": "7.37.4",
"eslint-plugin-react": "7.37.5",
"execa": "5.1.1",
"fs-extra": "11.2.0",
"glob": "11.0.2",
"glob": "11.0.3",
"gulp": "4.0.2",
"husky": "9.1.7",
"lerna": "3.22.1",
"lint-staged": "15.5.2",
"madge": "8.0.0",
"npm-package-json-lint": "8.0.0",
"typescript": "5.8.2"
"typescript": "5.8.3"
},
"dependencies": {
"@types/fs-extra": "11.0.4",
@@ -101,7 +101,6 @@
"packageManager": "yarn@4.9.2",
"resolutions": {
"react-native-camera@4.2.1": "patch:react-native-camera@npm%3A4.2.1#./.yarn/patches/react-native-camera-npm-4.2.1-24b2600a7e.patch",
"react-native-vosk@0.1.12": "patch:react-native-vosk@npm%3A0.1.12#./.yarn/patches/react-native-vosk-npm-0.1.12-76b1caaae8.patch",
"eslint": "patch:eslint@8.57.1#./.yarn/patches/eslint-npm-8.39.0-d92bace04d.patch",
"app-builder-lib@24.4.0": "patch:app-builder-lib@npm%3A24.4.0#./.yarn/patches/app-builder-lib-npm-24.4.0-05322ff057.patch",
"nanoid": "patch:nanoid@npm%3A3.3.7#./.yarn/patches/nanoid-npm-3.3.7-98824ba130.patch",
@@ -118,6 +117,12 @@
"pdfjs-dist@*": "patch:pdfjs-dist@npm%3A3.11.174#./.yarn/patches/pdfjs-dist-npm-3.11.174-67f2fee6d6.patch",
"pdfjs-dist@3.11.174": "patch:pdfjs-dist@npm%3A3.11.174#./.yarn/patches/pdfjs-dist-npm-3.11.174-67f2fee6d6.patch",
"canvas@npm:^2.11.2": "link:./.yarn/joplin-empty-package/",
"node-gyp@npm:^9.0.0": "11.2.0"
"node-gyp@npm:^9.0.0": "11.2.0",
"depd@npm:^2.0.0": "patch:depd@npm%3A2.0.0#~/.yarn/patches/depd-npm-2.0.0-b6c51a4b43.patch",
"depd@npm:~2.0.0": "patch:depd@npm%3A2.0.0#~/.yarn/patches/depd-npm-2.0.0-b6c51a4b43.patch",
"depd@npm:~1.1.2": "patch:depd@npm%3A2.0.0#~/.yarn/patches/depd-npm-2.0.0-b6c51a4b43.patch",
"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"
}
}

View File

@@ -417,8 +417,10 @@ class Application extends BaseApplication {
if (argv.length) {
this.gui_ = this.dummyGui();
const initialFolder = await Folder.load(Setting.value('activeFolderId'));
await this.switchCurrentFolder(initialFolder);
await this.applySettingsSideEffects();
await this.refreshCurrentFolder();
try {
await this.execCommand(argv);
} catch (error) {
@@ -432,6 +434,7 @@ class Application extends BaseApplication {
}
await Setting.saveAll();
await this.database_.close();
// Need to call exit() explicitly, otherwise Node wait for any timeout to complete
// https://stackoverflow.com/questions/18050095

View File

@@ -2,33 +2,44 @@
/* eslint-disable no-console */
const fs = require('fs-extra');
const Logger = require('@joplin/utils/Logger').default;
const { dirname } = require('@joplin/lib/path-utils');
import * as fs from 'fs-extra';
import Logger, { TargetType } from '@joplin/utils/Logger';
import { dirname } from '@joplin/lib/path-utils';
const { DatabaseDriverNode } = require('@joplin/lib/database-driver-node.js');
const JoplinDatabase = require('@joplin/lib/JoplinDatabase').default;
const BaseModel = require('@joplin/lib/BaseModel').default;
const Folder = require('@joplin/lib/models/Folder').default;
const Note = require('@joplin/lib/models/Note').default;
const Setting = require('@joplin/lib/models/Setting').default;
import JoplinDatabase from '@joplin/lib/JoplinDatabase';
import BaseModel from '@joplin/lib/BaseModel';
import Folder from '@joplin/lib/models/Folder';
import Note from '@joplin/lib/models/Note';
import Setting from '@joplin/lib/models/Setting';
const { sprintf } = require('sprintf-js');
const exec = require('child_process').exec;
const nodeSqlite = require('sqlite3');
const { loadKeychainServiceAndSettings } = require('@joplin/lib/services/SettingUtils');
const { default: shimInitCli } = require('./utils/shimInitCli');
const baseDir = `${dirname(__dirname)}/tests/cli-integration`;
const joplinAppPath = `${__dirname}/main.js`;
shimInitCli({ nodeSqlite, appVersion: () => require('../package.json').version, keytar: null });
require('@joplin/lib/testing/test-utils');
const logger = new Logger();
logger.addTarget('console');
logger.addTarget(TargetType.Console);
logger.setLevel(Logger.LEVEL_ERROR);
const dbLogger = new Logger();
dbLogger.addTarget('console');
dbLogger.addTarget(TargetType.Console);
dbLogger.setLevel(Logger.LEVEL_INFO);
const db = new JoplinDatabase(new DatabaseDriverNode());
db.setLogger(dbLogger);
function createClient(id) {
interface Client {
id: number;
profileDir: string;
}
function createClient(id: number): Client {
return {
id: id,
profileDir: `${baseDir}/client${id}`,
@@ -37,13 +48,13 @@ function createClient(id) {
const client = createClient(1);
function execCommand(client, command) {
function execCommand(client: Client, command: string) {
const exePath = `node ${joplinAppPath}`;
const cmd = `${exePath} --update-geolocation-disabled --env dev --profile ${client.profileDir} ${command}`;
logger.info(`${client.id}: ${command}`);
return new Promise((resolve, reject) => {
exec(cmd, (error, stdout, stderr) => {
return new Promise<string>((resolve, reject) => {
exec(cmd, (error: string, stdout: string, stderr: string) => {
if (error) {
logger.error(stderr);
reject(error);
@@ -54,17 +65,17 @@ function execCommand(client, command) {
});
}
function assertTrue(v) {
function assertTrue(v: unknown) {
if (!v) throw new Error(sprintf('Expected "true", got "%s"."', v));
process.stdout.write('.');
}
function assertFalse(v) {
function assertFalse(v: unknown) {
if (v) throw new Error(sprintf('Expected "false", got "%s"."', v));
process.stdout.write('.');
}
function assertEquals(expected, real) {
function assertEquals(expected: unknown, real: unknown) {
if (expected !== real) throw new Error(sprintf('Expecting "%s", got "%s"', expected, real));
process.stdout.write('.');
}
@@ -73,7 +84,7 @@ async function clearDatabase() {
await db.transactionExecBatch(['DELETE FROM folders', 'DELETE FROM notes', 'DELETE FROM tags', 'DELETE FROM note_tags', 'DELETE FROM resources', 'DELETE FROM deleted_items']);
}
const testUnits = {};
const testUnits: Record<string, ()=> Promise<void>> = {};
testUnits.testFolders = async () => {
await execCommand(client, 'mkbook nb1');
@@ -85,10 +96,16 @@ testUnits.testFolders = async () => {
await execCommand(client, 'mkbook nb1');
folders = await Folder.all();
assertEquals(1, folders.length);
assertEquals(2, folders.length);
assertEquals('nb1', folders[0].title);
assertEquals('nb1', folders[1].title);
await execCommand(client, 'rm -r -f nb1');
await execCommand(client, 'rmbook -p -f nb1');
folders = await Folder.all();
assertEquals(1, folders.length);
await execCommand(client, 'rmbook -p -f nb1');
folders = await Folder.all();
assertEquals(0, folders.length);
@@ -102,7 +119,7 @@ testUnits.testNotes = async () => {
assertEquals(1, notes.length);
assertEquals('n1', notes[0].title);
await execCommand(client, 'rm -f n1');
await execCommand(client, 'rmnote -p -f n1');
notes = await Note.all();
assertEquals(0, notes.length);
@@ -112,12 +129,19 @@ testUnits.testNotes = async () => {
notes = await Note.all();
assertEquals(2, notes.length);
await execCommand(client, 'rm -f \'blabla*\'');
// Should fail to delete a non-existent note
let failed = false;
try {
await execCommand(client, 'rmnote -f \'blabla*\'');
} catch (error) {
failed = true;
}
assertEquals(failed, true);
notes = await Note.all();
assertEquals(2, notes.length);
await execCommand(client, 'rm -f \'n*\'');
await execCommand(client, 'rmnote -f -p \'n*\'');
notes = await Note.all();
assertEquals(0, notes.length);
@@ -140,10 +164,12 @@ testUnits.testCat = async () => {
testUnits.testConfig = async () => {
await execCommand(client, 'config editor vim');
await Setting.reset();
await Setting.load();
assertEquals('vim', Setting.value('editor'));
await execCommand(client, 'config editor subl');
await Setting.reset();
await Setting.load();
assertEquals('subl', Setting.value('editor'));
@@ -201,15 +227,47 @@ testUnits.testMv = async () => {
await execCommand(client, 'mknote note2');
await execCommand(client, 'mknote note3');
await execCommand(client, 'mknote blabla');
await execCommand(client, 'mv \'note*\' nb2');
notes1 = await Note.previews(f1.id);
notes2 = await Note.previews(f2.id);
assertEquals(4, notes1.length);
assertEquals(1, notes2.length);
await execCommand(client, 'mv \'note*\' nb2');
notes2 = await Note.previews(f2.id);
notes1 = await Note.previews(f1.id);
assertEquals(1, notes1.length);
assertEquals(4, notes2.length);
};
testUnits.testUse = async () => {
await execCommand(client, 'mkbook nb1');
await execCommand(client, 'mkbook nb2');
await execCommand(client, 'mknote n1');
await execCommand(client, 'mknote n2');
const f1 = await Folder.loadByTitle('nb1');
const f2 = await Folder.loadByTitle('nb2');
let notes1 = await Note.previews(f1.id);
let notes2 = await Note.previews(f2.id);
assertEquals(0, notes1.length);
assertEquals(2, notes2.length);
await execCommand(client, 'use nb1');
await execCommand(client, 'mknote note2');
await execCommand(client, 'mknote note3');
notes1 = await Note.previews(f1.id);
notes2 = await Note.previews(f2.id);
assertEquals(2, notes1.length);
assertEquals(2, notes2.length);
};
async function main() {
await fs.remove(baseDir);
@@ -217,7 +275,9 @@ async function main() {
await db.open({ name: `${client.profileDir}/database.sqlite` });
BaseModel.setDb(db);
await Setting.load();
Setting.setConstant('rootProfileDir', client.profileDir);
Setting.setConstant('profileDir', client.profileDir);
await loadKeychainServiceAndSettings([]);
let onlyThisTest = 'testMv';
onlyThisTest = '';
@@ -234,7 +294,7 @@ async function main() {
}
}
main(process.argv).catch(error => {
main().catch(error => {
console.info('');
logger.error(error);
});

View File

@@ -35,15 +35,15 @@
],
"owner": "Laurent Cozic"
},
"version": "3.4.0",
"version": "3.5.0",
"bin": "./main.js",
"engines": {
"node": ">=10.0.0"
},
"dependencies": {
"@joplin/lib": "~3.4",
"@joplin/renderer": "~3.4",
"@joplin/utils": "~3.4",
"@joplin/lib": "~3.5",
"@joplin/renderer": "~3.5",
"@joplin/utils": "~3.5",
"aws-sdk": "2.1340.0",
"chalk": "4.1.2",
"compare-version": "0.1.2",
@@ -57,7 +57,7 @@
"proper-lockfile": "4.1.2",
"redux": "4.2.1",
"server-destroy": "1.0.1",
"sharp": "0.34.2",
"sharp": "0.34.3",
"sprintf-js": "1.1.3",
"sqlite3": "5.1.6",
"string-padding": "1.0.2",
@@ -70,14 +70,14 @@
"yargs-parser": "21.1.1"
},
"devDependencies": {
"@joplin/tools": "~3.4",
"@joplin/tools": "~3.5",
"@types/fs-extra": "11.0.4",
"@types/jest": "29.5.14",
"@types/node": "18.19.103",
"@types/node": "18.19.119",
"@types/proper-lockfile": "^4.1.2",
"gulp": "4.0.2",
"jest": "29.7.0",
"temp": "0.9.4",
"typescript": "5.8.2"
"typescript": "5.8.3"
}
}

View File

@@ -2,7 +2,7 @@ import PluginRunner from '../../../app/services/plugins/PluginRunner';
import PluginService, { PluginSettings, defaultPluginSetting } from '@joplin/lib/services/plugins/PluginService';
import { ContentScriptType } from '@joplin/lib/services/plugins/api/types';
import MdToHtml from '@joplin/renderer/MdToHtml';
import shim from '@joplin/lib/shim';
import shim, { MobilePlatform } from '@joplin/lib/shim';
import Setting from '@joplin/lib/models/Setting';
import * as fs from 'fs-extra';
import Note from '@joplin/lib/models/Note';
@@ -310,7 +310,7 @@ describe('services_PluginService', () => {
let resetPlatformMock = () => {};
if (!isDesktop) {
resetPlatformMock = mockMobilePlatform('android').reset;
resetPlatformMock = mockMobilePlatform(MobilePlatform.Android).reset;
}
try {

View File

@@ -0,0 +1,3 @@
1. File without extension and leading `./`: [file1](./file1). Gets imported, but filename is converted to extension, like `<internal_id>.file1`
2. File without extension: [file2](file2). Not imported at all.
3. File with extension: [file3](file3.text). Gets imported properly.

View File

@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Joplin Web Clipper [DEV]",
"version": "3.4.0",
"version": "3.5.0",
"description": "Capture and save web pages and screenshots from your browser to Joplin.",
"homepage_url": "https://joplinapp.org",
"content_security_policy": {

View File

@@ -407,7 +407,17 @@ export default class ElectronAppWrapper {
isGoingToExit = true;
} else {
event.preventDefault();
this.hide();
const w = this.win_;
if (!w) return;
if (w.isFullScreen()) {
// leave fullscreen, then hide
w.once('leave-full-screen', () => w.hide());
w.setFullScreen(false);
} else {
w.hide();
}
}
} else {
const hasBackgroundWindows = this.secondaryWindows_.size > 0;
@@ -612,7 +622,11 @@ export default class ElectronAppWrapper {
console.warn('The window object was not available during the click event from tray icon');
return;
}
this.mainWindow().show();
if (!this.mainWindow().isVisible()) {
this.mainWindow().show();
} else {
this.mainWindow().hide();
}
});
} catch (error) {
console.error('Cannot create tray', error);

View File

@@ -86,8 +86,14 @@ export default class InteropServiceHelper {
// pdfs.
// https://github.com/laurent22/joplin/issues/6254.
await win.webContents.executeJavaScript('document.querySelectorAll(\'details\').forEach(el=>el.setAttribute(\'open\',\'\'))');
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const data = await win.webContents.printToPDF(options as any);
const data = await win.webContents.printToPDF({
...options,
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Partially refactored old code before rule was applied
pageSize: options.pageSize as any,
// Allows users to override the CSS page size.
// See https://github.com/laurent22/joplin/issues/13096
preferCSSPageSize: true,
});
resolve(data);
} catch (error) {
reject(error);

View File

@@ -63,6 +63,8 @@ import { refreshFolders } from '@joplin/lib/folders-screen-utils';
import initializeCommandService from './utils/initializeCommandService';
import OcrDriverBase from '@joplin/lib/services/ocr/OcrDriverBase';
import PerformanceLogger from '@joplin/lib/PerformanceLogger';
import Note from '@joplin/lib/models/Note';
import Resource from '@joplin/lib/models/Resource';
const perfLogger = PerformanceLogger.create();
@@ -683,6 +685,11 @@ class Application extends BaseApplication {
debug: new DebugService(reg.db()),
resourceService: ResourceService.instance(),
searchEngine: SearchEngine.instance(),
shim,
Note,
Folder,
Resource,
Setting,
ocrService: () => this.ocrService_,
};
});

View File

@@ -225,7 +225,7 @@ const Button = React.forwardRef(({
animation={iconAnimation}
mr={iconOnly ? '0' : '6px'}
color={color}
className={iconName}
className={`${iconName} icon`}
role='img'
/>;
}

View File

@@ -8,6 +8,7 @@ import { PluginManifest } from '@joplin/lib/services/plugins/utils/types';
import bridge from '../../../../services/bridge';
import { ItemEvent, PluginItem } from '@joplin/lib/components/shared/config/plugins/types';
import PluginService from '@joplin/lib/services/plugins/PluginService';
import getPluginHelpUrl from '@joplin/lib/services/plugins/utils/getPluginHelpUrl';
export enum InstallState {
NotInstalled = 1,
@@ -150,8 +151,7 @@ export default function(props: Props) {
const onNameClick = useCallback(() => {
const manifest = item.manifest;
if (!manifest.homepage_url) return;
void bridge().openExternal(manifest.homepage_url);
void bridge().openExternal(getPluginHelpUrl(manifest.id));
}, [item]);
const onRecommendedClick = useCallback(() => {

View File

@@ -15,7 +15,7 @@ import { connect } from 'react-redux';
import { AppState } 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';
import { PublicPrivateKeyPair } from '@joplin/lib/services/e2ee/ppk/ppk';
import ToggleAdvancedSettingsButton from '../ConfigScreen/controls/ToggleAdvancedSettingsButton';
import MacOSMissingPasswordHelpLink from '../ConfigScreen/controls/MissingPasswordHelpLink';

View File

@@ -67,6 +67,11 @@ import 'codemirror/mode/diff/diff';
import 'codemirror/mode/erlang/erlang';
import 'codemirror/mode/sql/sql';
interface ExtendedWindow {
CodeMirror?: unknown;
}
declare const window: ExtendedWindow;
export interface EditorProps {
value: string;
@@ -100,6 +105,14 @@ function Editor(props: EditorProps, ref: any) {
const editorParent = useRef(null);
const lastEditTime = useRef(NaN);
useEffect(() => {
window.CodeMirror = CodeMirror;
return () => {
window.CodeMirror = undefined;
};
}, []);
// Codemirror plugins add new commands to codemirror (or change it's behavior)
// This command adds the smartListIndent function which will be bound to tab
useListIdent(CodeMirror);

View File

@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react';
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
import bridge from '../../../../../../services/bridge';
import { contentScriptsToCodeMirrorPlugin } from '@joplin/lib/services/plugins/utils/loadContentScripts';
import { extname } from 'path';
import shim from '@joplin/lib/shim';
@@ -7,6 +8,18 @@ import uuid from '@joplin/lib/uuid';
import { reg } from '@joplin/lib/registry';
const addPluginDependency = (path: string) => {
const id = `content-script-${encodeURIComponent(path)}`;
if (document.getElementById(id)) {
return;
}
const element = document.createElement('script');
element.setAttribute('id', id);
element.setAttribute('src', path);
document.head.appendChild(element);
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
export default function useExternalPlugins(CodeMirror: any, plugins: PluginStates) {
const [options, setOptions] = useState({});
@@ -23,7 +36,14 @@ export default function useExternalPlugins(CodeMirror: any, plugins: PluginState
if (mod.codeMirrorResources) {
for (const asset of mod.codeMirrorResources) {
try {
require(`codemirror/${asset}`);
let assetPath = shim.fsDriver().resolveRelativePathWithinDir(`${bridge().vendorDir()}/lib/codemirror/`, asset);
// Compatibility with old versions of Joplin, where the file extension was automatically added by require().
if (extname(assetPath) === '') {
assetPath += '.js';
}
addPluginDependency(assetPath);
} catch (error) {
error.message = `${asset} is not a valid CodeMirror asset, keymap or mode. You can find a list of valid assets here: https://codemirror.net/doc/manual.html#addons`;
throw error;

View File

@@ -30,6 +30,7 @@ import useEditorSearchHandler from '../utils/useEditorSearchHandler';
import CommandService from '@joplin/lib/services/CommandService';
import useRefocusOnVisiblePaneChange from './utils/useRefocusOnVisiblePaneChange';
import { WindowIdContext } from '../../../../NewWindowOrIFrame';
import eventManager, { EventName, ResourceChangeEvent } from '@joplin/lib/eventManager';
const logger = Logger.create('CodeMirror6');
const logDebug = (message: string) => logger.debug(message);
@@ -272,6 +273,17 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
props.noteId, props.useCustomPdfViewer,
]);
useEffect(() => {
const listener = (event: ResourceChangeEvent) => {
editorRef.current?.onResourceChanged(event.id);
};
eventManager.on(EventName.ResourceChange, listener);
return () => {
eventManager.off(EventName.ResourceChange, listener);
};
}, [props.resourceInfos]);
useEffect(() => {
if (!webviewReady) return;

View File

@@ -110,12 +110,12 @@ const Editor = (props: Props, ref: ForwardedRef<CodeMirrorControl>) => {
const editor = createEditor(editorContainerRef.current, {
...editorProps,
resolveImageSrc: async src => {
resolveImageSrc: async (src, reloadCounter) => {
const url = parseResourceUrl(src);
if (!url.itemId) return null;
const item = await Resource.load(url.itemId);
if (!item) return null;
return `${getResourceBaseUrl()}/${resourceFilename(item)}`;
return `${getResourceBaseUrl()}/${resourceFilename(item)}${reloadCounter ? `?r=${reloadCounter}` : ''}`;
},
});
editor.addStyles({

View File

@@ -1387,16 +1387,18 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
async function onCopy(event: any) {
const copiedContent = editor.selection.getContent();
if (!copiedContent) return;
copyHtmlToClipboard(copiedContent);
event.preventDefault();
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
async function onCut(event: any) {
event.preventDefault();
const selectedContent = editor.selection.getContent();
if (!selectedContent) return;
copyHtmlToClipboard(selectedContent);
editor.insertContent('');
event.preventDefault();
onChangeHandler();
}
@@ -1444,7 +1446,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
// `compositionend` means that a user has finished entering a Chinese
// (or other languages that require IME) character.
editor.on(TinyMceEditorEvents.CompositionEnd, onChangeHandler);
editor.on(TinyMceEditorEvents.Cut, onCut);
editor.on(TinyMceEditorEvents.Cut, onCut, true);
editor.on(TinyMceEditorEvents.JoplinChange, onChangeHandler);
editor.on(TinyMceEditorEvents.Undo, onChangeHandler);
editor.on(TinyMceEditorEvents.Redo, onChangeHandler);

View File

@@ -15,6 +15,7 @@ const joplinRendererUtils = require('@joplin/renderer').utils;
const { clipboard } = require('electron');
import * as mimeUtils from '@joplin/lib/mime-utils';
import bridge from '../../../services/bridge';
import { getCollator, getCollatorLocale } from '@joplin/lib/models/utils/getCollator';
const md5 = require('md5');
const path = require('path');
@@ -43,22 +44,30 @@ export async function commandAttachFileToBody(body: string, filePaths: string[]
if (!filePaths || !filePaths.length) return null;
}
const collatorLocale = getCollatorLocale();
const collator = getCollator(collatorLocale);
filePaths = filePaths.sort((a, b) => {
return collator.compare(a, b);
});
let pos = options.position ?? 0;
for (let i = 0; i < filePaths.length; i++) {
const filePath = filePaths[i];
const beforeLen = body.length;
try {
logger.info(`Attaching ${filePath}`);
const newBody = await shim.attachFileToNoteBody(body, filePath, options.position, {
const newBody = await shim.attachFileToNoteBody(body, filePath, pos, {
createFileURL: options.createFileURL,
resizeLargeImages: Setting.value('imageResizing'),
markupLanguage: options.markupLanguage,
resourceSuffix: i > 0 ? ' ' : '',
resourcePrefix: i > 0 ? ' ' : '',
});
if (!newBody) {
logger.info('File attachment was cancelled');
return null;
}
pos += newBody.length - beforeLen;
body = newBody;
logger.info('File was attached.');
} catch (error) {
@@ -66,7 +75,6 @@ export async function commandAttachFileToBody(body: string, filePaths: string[]
bridge().showErrorMessageBox(error.message);
}
}
return body;
}

View File

@@ -13,6 +13,7 @@ import { MarkupToHtmlOptions } from '../../hooks/useMarkupToHtml';
import { ScrollbarSize } from '@joplin/lib/models/settings/builtInMetadata';
import { RefObject, SetStateAction } from 'react';
import * as React from 'react';
import { ResourceEntity, ResourceLocalStateEntity } from '@joplin/lib/services/database/types';
export interface AllAssetsOptions {
contentMaxWidthTarget?: string;
@@ -214,10 +215,8 @@ export function defaultFormNote(): FormNote {
}
export interface ResourceInfo {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
localState: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
item: any;
localState: ResourceLocalStateEntity;
item: ResourceEntity;
}
export interface ResourceInfos {

View File

@@ -251,8 +251,6 @@ export default class PromptDialog extends React.Component<Props, any> {
} else {
onClose(true);
}
} else if (event.key === 'Escape') {
onClose(false);
}
};
@@ -309,7 +307,7 @@ export default class PromptDialog extends React.Component<Props, any> {
}
return (
<Dialog className='prompt-dialog' contentStyle={styles.dialog}>
<Dialog className='prompt-dialog' contentStyle={styles.dialog} onCancel={() => onClose(false, 'cancel')}>
<label style={styles.label}>{this.props.label ? this.props.label : ''}</label>
<div style={{ display: 'inline-block', color: 'black', backgroundColor: theme.backgroundColor }}>
{inputComp}

View File

@@ -25,14 +25,13 @@ interface Props {
const SidebarComponent = (props: Props) => {
const renderSynchronizeButton = (type: string) => {
const label = type === 'sync' ? _('Synchronise') : _('Cancel');
const iconAnimation = type !== 'sync' ? 'icon-infinite-rotation 1s linear infinite' : '';
return (
<StyledSynchronizeButton
level={ButtonLevel.SidebarSecondary}
className={`sidebar-sync-button ${type === 'sync' ? '' : '-syncing'}`}
iconName="icon-sync"
key="sync_button"
iconAnimation={iconAnimation}
title={label}
onClick={() => {
void CommandService.instance().execute('synchronize', type !== 'sync');

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { useCallback } from 'react';
import { useCallback, useState } from 'react';
import { StyledHeader, StyledHeaderIcon, StyledHeaderLabel } from '../styles';
import { HeaderId, HeaderListItem } from '../types';
import bridge from '../../../services/bridge';
@@ -25,6 +25,8 @@ const HeaderItem: React.FC<Props> = props => {
const item = props.item;
const onItemClick = item.onClick;
const itemId = item.id;
const [isHovered, setIsHovered] = useState(false);
const expanded = item.expanded;
const onClick: React.MouseEventHandler<HTMLElement> = useCallback(event => {
if (onItemClick) {
@@ -44,6 +46,14 @@ const HeaderItem: React.FC<Props> = props => {
}
}, [itemId]);
const handleMouseEnter = useCallback(() => {
setIsHovered(true);
}, []);
const handleMouseLeave = useCallback(() => {
setIsHovered(false);
}, []);
return (
<ListItemWrapper
containerRef={props.anchorRef}
@@ -58,8 +68,12 @@ const HeaderItem: React.FC<Props> = props => {
{...item.extraProps}
onDrop={props.onDrop}
>
<StyledHeader onClick={onClick}>
<StyledHeaderIcon aria-hidden='true' role='img' className={item.iconName}/>
<StyledHeader
onClick={onClick}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<StyledHeaderIcon aria-hidden='true' role='img' className={isHovered ? `fas ${expanded ? 'fa-caret-down' : 'fa-caret-right'}` : item.iconName}/>
<StyledHeaderLabel>{item.label}</StyledHeaderLabel>
</StyledHeader>
</ListItemWrapper>

View File

@@ -5,4 +5,5 @@
@use 'styles/sidebar-expand-link.scss';
@use 'styles/sidebar-header-container.scss';
@use 'styles/sidebar-spacer-item.scss';
@use 'styles/sidebar-header-button.scss';
@use 'styles/sidebar-header-button.scss';
@use 'styles/sidebar-sync-button.scss';

View File

@@ -28,6 +28,11 @@ export const StyledHeader = styled.div`
user-select: none;
text-transform: uppercase;
//cursor: pointer;
cursor: default;
transition: background 0.2s;
&:hover {
background: ${(props: StyleProps) => props.theme.backgroundColorHover2};
}
`;
export const StyledHeaderIcon = styled.i`

View File

@@ -0,0 +1,16 @@
@keyframes icon-infinite-rotation {
to {
transform: rotate(360deg);
}
}
.sidebar-sync-button {
&.-syncing > .icon {
animation: icon-infinite-rotation 1s linear infinite;
@media (prefers-reduced-motion: reduce) {
animation: none;
}
}
}

View File

@@ -143,7 +143,7 @@ export default class NoteListUtils {
menu.append(new MenuItem({ type: 'separator' }));
if ([9, 10].includes(Setting.value('sync.target'))) {
if ([9, 10, 11].includes(Setting.value('sync.target'))) {
menu.append(
new MenuItem(
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied

View File

@@ -2,11 +2,19 @@
<html>
<head>
<meta charset="UTF-8">
<!--
No CPS because we need to allow everything due to some dependencies (eg. depd, which comes from maybe Node or Electron
uses 'eval'.
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' 'unsafe-eval'">
-->
<meta
http-equiv="Content-Security-Policy"
content="
default-src 'self' joplin-content://* ;
connect-src 'self' * http://* https://* joplin-content://* blob: ;
style-src 'unsafe-inline' 'self' blob: joplin-content://* https://* http://* ;
child-src 'self' joplin-content://* ;
script-src 'self' 'unsafe-inline' joplin-content://* ;
media-src 'self' * blob: data: https://* http://* joplin-content://* ;
img-src 'self' blob: data: http://* https://* joplin-content://* ;
font-src 'self' http://* https://* blob: data: joplin-content://* ;
"
/>
<title>Joplin</title>
<!-- Note: Add new dynamic CSS imports to style.scss to allow them to be included in secondary windows. -->
<link rel="stylesheet" href="style.min.css">

View File

@@ -8,6 +8,7 @@ import getMainWindow from './util/getMainWindow';
import setFilePickerResponse from './util/setFilePickerResponse';
import setMessageBoxResponse from './util/setMessageBoxResponse';
import getImageSourceSize from './util/getImageSourceSize';
import setSettingValue from './util/setSettingValue';
test.describe('main', () => {
@@ -19,6 +20,13 @@ test.describe('main', () => {
await mainPage.waitFor();
});
test('app should support French localization', async ({ mainWindow, electronApp }) => {
await setSettingValue(electronApp, mainWindow, 'locale', 'fr_FR');
// The "Notebooks" header should be localized
const localizedText = mainWindow.getByText('Carnets').first();
await expect(localizedText).toBeAttached();
});
test('should be able to create and edit a new note', async ({ mainWindow }) => {
const mainScreen = await new MainScreen(mainWindow).setup();
const editor = await mainScreen.createNewNote('Test note');

View File

@@ -72,4 +72,10 @@ export default class MainScreen {
await setFilePickerResponse(electronApp, [path]);
await activateMainMenuItem(electronApp, 'HTML - HTML document (Directory)', 'Import');
}
public async pluginPanelLocator(pluginId: string) {
return this.page.locator(
`iframe[id^=${JSON.stringify(`plugin-view-${pluginId}`)}]`,
);
}
}

View File

@@ -45,6 +45,41 @@ test.describe('pluginApi', () => {
}));
});
test('should report the correct visibility state for dialogs', async ({ startAppWithPlugins }) => {
const { app, mainWindow } = await startAppWithPlugins(['resources/test-plugins/dialogs.js']);
const mainScreen = await new MainScreen(mainWindow).setup();
await mainScreen.createNewNote('Dialog test note');
const editor = mainScreen.noteEditor;
const expectVisible = async (visible: boolean) => {
// Check UI visibility
if (visible) {
await expect(mainScreen.dialog).toBeVisible();
} else {
await expect(mainScreen.dialog).not.toBeVisible();
}
// Check visibility reported through the plugin API
await expect.poll(async () => {
await mainScreen.goToAnything.runCommand(app, 'getTestDialogVisibility');
const editorContent = await editor.contentLocator();
return editorContent.textContent();
}).toBe(JSON.stringify({
visible: visible,
active: visible,
}));
};
await expectVisible(false);
await mainScreen.goToAnything.runCommand(app, 'showTestDialog');
await expectVisible(true);
// Submitting the dialog should include form data in the output
await mainScreen.dialog.getByRole('button', { name: 'Okay' }).click();
await expectVisible(false);
});
test('should be possible to create multiple toasts with the same text from a plugin', async ({ startAppWithPlugins }) => {
const { app, mainWindow } = await startAppWithPlugins(['resources/test-plugins/showToast.js']);
const mainScreen = await new MainScreen(mainWindow).setup();
@@ -122,5 +157,30 @@ test.describe('pluginApi', () => {
await msleep(Second);
await expect(noteEditor.codeMirrorEditor).toHaveText(expectedUpdatedText);
});
test('should support hiding and showing panels', async ({ startAppWithPlugins }) => {
const { mainWindow, app } = await startAppWithPlugins(['resources/test-plugins/panels.js']);
const mainScreen = await new MainScreen(mainWindow).setup();
await mainScreen.createNewNote('Test note (panels)');
const panelLocator = await mainScreen.pluginPanelLocator('org.joplinapp.plugins.example.panels');
const noteEditor = mainScreen.noteEditor;
await mainScreen.goToAnything.runCommand(app, 'testShowPanel');
await expect(noteEditor.codeMirrorEditor).toHaveText('visible');
// Panel should be visible
await expect(panelLocator).toBeVisible();
// The panel should have the expected content
const panelContent = panelLocator.contentFrame();
await expect(
panelContent.getByRole('heading', { name: 'Panel content' }),
).toBeAttached();
await mainScreen.goToAnything.runCommand(app, 'testHidePanel');
await expect(noteEditor.codeMirrorEditor).toHaveText('hidden');
await expect(panelLocator).not.toBeVisible();
});
});

View File

@@ -47,5 +47,22 @@ joplin.plugins.register({
}));
},
});
await joplin.commands.register({
name: 'getTestDialogVisibility',
label: 'Returns the dialog visibility state',
execute: async () => {
// panels.visible should also work for dialogs.
const visible = await joplin.views.panels.visible(dialogHandle);
// For dialogs, isActive should return the visibility.
// (Prefer panels.visible for dialogs).
const active = await joplin.views.panels.isActive(dialogHandle);
await joplin.commands.execute('editor.setText', JSON.stringify({
visible,
active,
}));
},
});
},
});

View File

@@ -0,0 +1,71 @@
// Allows referencing the Joplin global:
/* eslint-disable no-undef */
// Allows the `joplin-manifest` block comment:
/* eslint-disable multiline-comment-style */
/* joplin-manifest:
{
"id": "org.joplinapp.plugins.example.panels",
"manifest_version": 1,
"app_min_version": "3.1",
"name": "JS Bundle test",
"description": "JS Bundle Test plugin",
"version": "1.0.0",
"author": "",
"homepage_url": "https://joplinapp.org"
}
*/
const waitFor = async (condition) => {
const wait = () => {
return new Promise(resolve => {
setTimeout(() => resolve(), 100);
});
};
for (let i = 0; i < 100; i++) {
if (await condition()) {
return;
}
// Pause for a brief delay
await wait();
}
throw new Error('Condition was never true');
};
joplin.plugins.register({
onStart: async function() {
const panels = joplin.views.panels;
const view = await panels.create('panelTestView');
await panels.setHtml(view, '<h1>Panel content</h1><p>Test</p>');
await panels.hide(view);
await joplin.commands.register({
name: 'testShowPanel',
label: 'Test panel visibility',
execute: async () => {
await panels.show(view);
await waitFor(async () => {
return await panels.visible(view);
});
await joplin.commands.execute('editor.setText', 'visible');
},
});
await joplin.commands.register({
name: 'testHidePanel',
label: 'Test: Hide the panel',
execute: async () => {
await panels.hide(view);
await waitFor(async () => {
return !await panels.visible(view);
});
await joplin.commands.execute('editor.setText', 'hidden');
},
});
},
});

View File

@@ -97,12 +97,6 @@ a {
border-radius: 5px;
}
@keyframes icon-infinite-rotation{
to {
transform: rotate(360deg);
}
}
.rdtPicker {
min-width: 250px;
width: auto !important;

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
"version": "3.4.8",
"version": "3.5.4",
"description": "Joplin for Desktop",
"main": "main.bundle.js",
"private": true,
@@ -46,6 +46,7 @@
"asar": true,
"asarUnpack": "./node_modules/node-notifier/vendor/**",
"win": {
"sign": "./sign.js",
"rfc3161TimeStampServer": "http://timestamp.digicert.com",
"icon": "../../Assets/ImageSources/Joplin.ico",
"target": [
@@ -131,28 +132,27 @@
"homepage": "https://github.com/laurent22/joplin#readme",
"devDependencies": {
"7zip-bin": "5.2.0",
"@axe-core/playwright": "4.10.1",
"@axe-core/playwright": "4.10.2",
"@electron/notarize": "2.5.0",
"@electron/rebuild": "3.7.2",
"@fortawesome/fontawesome-free": "5.15.4",
"@joeattardi/emoji-button": "4.6.4",
"@joplin/default-plugins": "~3.4",
"@joplin/editor": "~3.4",
"@joplin/lib": "~3.4",
"@joplin/renderer": "~3.4",
"@joplin/tools": "~3.4",
"@joplin/utils": "~3.4",
"@playwright/test": "1.52.0",
"@joplin/default-plugins": "~3.5",
"@joplin/editor": "~3.5",
"@joplin/lib": "~3.5",
"@joplin/renderer": "~3.5",
"@joplin/tools": "~3.5",
"@joplin/utils": "~3.5",
"@playwright/test": "1.53.2",
"@sentry/electron": "4.24.0",
"@testing-library/react-hooks": "8.0.1",
"@types/jest": "29.5.14",
"@types/mustache": "4.2.6",
"@types/node": "18.19.103",
"@types/node": "18.19.119",
"@types/react": "18.3.23",
"@types/react-dom": "18.3.7",
"@types/react-redux": "7.1.33",
"@types/styled-components": "5.1.32",
"@types/tesseract.js": "2.0.0",
"async-mutex": "0.5.0",
"axios": "^1.7.7",
"codemirror": "5.65.9",
@@ -160,13 +160,13 @@
"compare-versions": "6.1.1",
"countable": "3.0.1",
"debounce": "1.2.1",
"electron": "35.7.5",
"electron": "37.4.0",
"electron-builder": "24.13.3",
"electron-updater": "6.6.2",
"electron-window-state": "5.0.3",
"esbuild": "^0.25.3",
"formatcoords": "1.1.3",
"glob": "11.0.2",
"glob": "11.0.3",
"gulp": "4.0.2",
"highlight.js": "11.11.1",
"immer": "9.0.21",
@@ -187,7 +187,7 @@
"react": "18.3.1",
"react-dom": "18.3.1",
"react-redux": "8.1.3",
"react-select": "5.10.1",
"react-select": "5.10.2",
"react-test-renderer": "18.3.1",
"react-toggle-button": "2.2.0",
"react-tooltip": "4.5.1",
@@ -199,15 +199,15 @@
"styled-components": "5.3.11",
"styled-system": "5.1.5",
"taboverride": "4.0.3",
"tesseract.js": "5.1.1",
"tesseract.js": "6.0.1",
"tinymce": "6.8.5",
"ts-jest": "29.3.1",
"ts-jest": "29.3.4",
"ts-node": "10.9.2",
"typescript": "5.8.2"
"typescript": "5.8.3"
},
"dependencies": {
"@electron/remote": "2.1.2",
"@joplin/onenote-converter": "~3.4",
"@joplin/onenote-converter": "~3.5",
"fs-extra": "11.2.0",
"keytar": "7.9.0",
"node-fetch": "2.6.7",

View File

@@ -0,0 +1,109 @@
/* eslint-disable no-console */
const { execSync } = require('child_process');
const { chdir, cwd } = require('process');
const { mkdirpSync, moveSync, pathExists } = require('fs-extra');
const { readdirSync, writeFileSync } = require('fs');
const { dirname } = require('path');
const signToolName = 'CodeSignTool.bat';
const getTempDir = () => {
if (process.env.RUNNER_TEMP) return process.env.RUNNER_TEMP;
if (process.env.GITHUB_WORKSPACE) return process.env.GITHUB_WORKSPACE;
const output = `${dirname(dirname(__dirname))}/temp`;
mkdirpSync(output);
return output;
};
const tempDir = getTempDir();
const downloadSignTool = async () => {
const signToolUrl = 'https://www.ssl.com/download/codesigntool-for-windows/';
const downloadDir = `${tempDir}/signToolDownloadTemp`;
const extractDir = `${tempDir}/signToolExtractTemp`;
if (await pathExists(`${extractDir}/${signToolName}`)) {
console.info('sign.js: Sign tool has already been downloaded - skipping');
return extractDir;
}
mkdirpSync(downloadDir);
mkdirpSync(extractDir);
const response = await fetch(signToolUrl);
if (!response.ok) throw new Error(`sign.js: HTTP error ${response.status}: ${response.statusText}`);
const zipPath = `${downloadDir}/codeSignTool.zip`;
const buffer = Buffer.from(await response.arrayBuffer());
writeFileSync(zipPath, buffer);
console.info('sign.js: Downloaded sign tool zip:', readdirSync(downloadDir));
mkdirpSync(extractDir);
execSync(
`powershell -Command "Expand-Archive -Path '${zipPath}' -DestinationPath '${extractDir}' -Force"`,
{ stdio: 'inherit' },
);
console.info('sign.js: Extracted sign tool zip:', readdirSync(extractDir));
return extractDir;
};
exports.default = async (configuration) => {
const inputFilePath = configuration.path;
const {
SSL_ESIGNER_USER_NAME,
SSL_ESIGNER_USER_PASSWORD,
SSL_ESIGNER_CREDENTIAL_ID,
SSL_ESIGNER_USER_TOTP,
SIGN_APPLICATION,
} = process.env;
console.info('sign.js: File to sign:', inputFilePath);
console.info('sign.js: Using temp dir:', tempDir);
if (SIGN_APPLICATION !== '1') {
console.info('sign.js: SIGN_APPLICATION != 1 - not signing application');
return;
}
console.info('sign.js: SIGN_APPLICATION = 1 - signing application');
const signToolDir = await downloadSignTool();
const signToolOutDir = `${tempDir}/signedToolOutDir`;
mkdirpSync(signToolOutDir);
const previousDir = cwd();
chdir(signToolDir);
try {
const cmd = [
`${signToolName} sign`,
`-input_file_path="${inputFilePath}"`,
`-output_dir_path="${signToolOutDir}"`,
`-credential_id="${SSL_ESIGNER_CREDENTIAL_ID}"`,
`-username="${SSL_ESIGNER_USER_NAME}"`,
`-password="${SSL_ESIGNER_USER_PASSWORD}"`,
`-totp_secret="${SSL_ESIGNER_USER_TOTP}"`,
];
execSync(cmd.join(' '));
const createdFiles = readdirSync(signToolOutDir);
console.info('sign.js: Created files:', createdFiles);
moveSync(`${signToolOutDir}/${createdFiles[0]}`, inputFilePath, { overwrite: true });
} catch (error) {
console.error('sign.js: Could not sign file:', error);
process.exit(1);
} finally {
chdir(previousDir);
}
};

View File

@@ -82,7 +82,7 @@ async function main() {
const files = [
'@fortawesome/fontawesome-free/css/all.min.css',
'@joeattardi/emoji-button/dist/index.js',
'codemirror/addon/dialog/dialog.css',
'codemirror/addon/',
'codemirror/lib/codemirror.css',
'mark.js/dist/mark.min.js',
'roboto-fontface/css/roboto/roboto-fontface.css',

View File

@@ -89,8 +89,8 @@ android {
applicationId "net.cozic.joplin"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 2097778
versionName "3.4.5"
versionCode 2097780
versionName "3.5.0"
ndk {
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
}
@@ -100,6 +100,8 @@ android {
externalNativeBuild {
cmake {
cppFlags '-DCMAKE_BUILD_TYPE=Release'
// For 16 KB pages. This should be removable after upgrading to NDK r28
arguments "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
}
}
}

View File

@@ -38,8 +38,9 @@ add_library(${CMAKE_PROJECT_NAME} SHARED
set(WHISPER_LIB_DIR ${CMAKE_SOURCE_DIR}/../../../../vendor/whisper.cpp)
# Based on the Whisper.cpp Android example:
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O3 ")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3 -fvisibility=hidden -fvisibility-inlines-hidden -ffunction-sections -fdata-sections")
set(SHARED_FLAGS "-O3 ")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${SHARED_FLAGS} ")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${SHARED_FLAGS} -fvisibility=hidden -fvisibility-inlines-hidden -ffunction-sections -fdata-sections")
# Whisper: See https://stackoverflow.com/a/76290722
add_subdirectory(${WHISPER_LIB_DIR} ./whisper)

View File

@@ -24,29 +24,8 @@ buildscript {
allprojects {
repositories {
mavenCentral()
// Seems to be required for react-native-vosk, otherwise the lib looks for it at "https://maven.aliyun.com/repository/jcenter/com/alphacephei/vosk-android/0.3.46/vosk-android-0.3.46.aar" but it's not there. And we get this error:
//
// Execution failed for task ':app:checkDebugAarMetadata'.
// > Could not resolve all files for configuration ':app:debugRuntimeClasspath'.
// > Failed to transform vosk-android-0.3.46.aar (com.alphacephei:vosk-android:0.3.46) to match attributes {artifactType=android-aar-metadata, org.gradle.status=release}.
// > Could not find vosk-android-0.3.46.aar (com.alphacephei:vosk-android:0.3.46).
// Searched in the following locations:
// https://maven.aliyun.com/repository/jcenter/com/alphacephei/vosk-android/0.3.46/vosk-android-0.3.46.aar
//
// But according to this page, the lib is on the Apache repository:
//
// https://search.maven.org/artifact/com.alphacephei/vosk-android/0.3.46/aar
maven { url "https://maven.apache.org" }
// Also required for react-native-vosk?
maven { url "https://maven.google.com" }
// Maybe still needed to fetch above package?
google()
maven { url 'https://www.jitpack.io' }
mavenCentral()
maven {
// expo-camera bundles a custom com.google.android:cameraview

View File

@@ -54,6 +54,14 @@ const Camera = (props: Props, ref: ForwardedRef<CameraRef>) => {
logger.error(message);
}, []);
const isReadyRef = useRef(false);
const onCameraReady = useCallback(() => {
if (isReadyRef.current) return; // Already emitted
isReadyRef.current = true;
props.onCameraReady();
}, [props.onCameraReady]);
useAsyncEffect(async (event) => {
// iOS issue workaround: Since upgrading to Expo SDK 52, closing and reopening the camera on iOS
// never emits onCameraReady. As a workaround, call .resumePreview and wait for it to resolve,
@@ -63,16 +71,16 @@ const Camera = (props: Props, ref: ForwardedRef<CameraRef>) => {
// Instead, wait for the preview to start using resumePreview:
await camera.resumePreview();
if (event.cancelled) return;
props.onCameraReady();
onCameraReady();
}
}, [camera, props.onCameraReady]);
}, [camera, onCameraReady]);
return hasPermission?.granted ? <CameraView
ref={setCamera}
style={props.style}
facing={props.cameraType === CameraDirection.Front ? 'front' : 'back'}
ratio={props.ratio as CameraRatio}
onCameraReady={Platform.OS === 'android' ? props.onCameraReady : undefined}
onCameraReady={onCameraReady}
onMountError={onMountError}
animateShutter={false}
barcodeScannerSettings={barcodeScannerSettings}

View File

@@ -4,7 +4,7 @@ import { Platform, ScrollView, StyleSheet, View } from 'react-native';
import { BarcodeScanner } from './utils/useBarcodeScanner';
import { LinkButton, PrimaryButton } from '../buttons';
import { _ } from '@joplin/lib/locale';
import DismissibleDialog, { DialogSize } from '../DismissibleDialog';
import DismissibleDialog, { DialogVariant } from '../DismissibleDialog';
import { Chip, Text } from 'react-native-paper';
import { isCallbackUrl, parseCallbackUrl } from '@joplin/lib/callbackUrlUtils';
import CommandService from '@joplin/lib/services/CommandService';
@@ -84,7 +84,7 @@ const ScannedBarcodes: React.FC<Props> = props => {
visible={dialogVisible}
onDismiss={onHideDialog}
themeId={props.themeId}
size={DialogSize.Small}
size={DialogVariant.Small}
>
<ScrollView>
<Text variant='titleMedium' role='heading'>{_('Scanned code')}</Text>

View File

@@ -66,12 +66,12 @@ describe('ComboBox', () => {
unmount();
});
test('changing the search query should limit which items are visible', () => {
test('changing the search query should limit which items are visible and be case insensitive', () => {
const testItems = [
{ title: 'a' },
{ title: 'b' },
{ title: 'c' },
{ title: 'aa' },
{ title: 'Aa' },
];
const { unmount } = render(
<WrappedComboBox items={testItems}/>,
@@ -82,7 +82,7 @@ describe('ComboBox', () => {
const updatedResults = getSearchResults();
expect(updatedResults[0]).toHaveTextContent('a');
expect(updatedResults[1]).toHaveTextContent('aa');
expect(updatedResults[1]).toHaveTextContent('Aa');
expect(updatedResults).toHaveLength(2);
unmount();

View File

@@ -12,7 +12,7 @@ import focusView from '../utils/focusView';
import AsyncActionQueue from '@joplin/lib/AsyncActionQueue';
import NestableFlatList, { NestableFlatListControl } from './NestableFlatList';
import useKeyboardState from '../utils/hooks/useKeyboardState';
const naturalCompare = require('string-natural-compare');
import { getCollator, getCollatorLocale } from '@joplin/lib/models/utils/getCollator';
export interface Option {
@@ -64,17 +64,20 @@ interface UseSearchResultsOptions {
const useSearchResults = ({
search, setSearch, options, onAddItem, canAddItem,
}: UseSearchResultsOptions) => {
const collatorLocale = getCollatorLocale();
const results = useMemo(() => {
const collator = getCollator(collatorLocale);
const lowerSearch = search?.toLowerCase();
return options
.filter(option => option.title.startsWith(search))
.filter(option => option.title.toLowerCase().includes(lowerSearch))
.sort((a, b) => {
if (a.title === b.title) return 0;
// Full matches should go first
if (a.title === search) return -1;
if (b.title === search) return 1;
return naturalCompare(a.title, b.title);
if (a.title.toLowerCase() === lowerSearch) return -1;
if (b.title.toLowerCase() === lowerSearch) return 1;
return collator.compare(a.title, b.title);
});
}, [search, options]);
}, [search, options, collatorLocale]);
const canAdd = (
!!onAddItem
@@ -254,6 +257,8 @@ const SearchResult: React.FC<SearchResultProps> = ({
<View style={[styles.optionContent, selected && styles.optionContentSelected]}>
{icon}
<Text
ellipsizeMode='tail'
numberOfLines={1}
style={styles.optionLabel}
>{text}</Text>
</View>
@@ -458,10 +463,11 @@ const useInputEventHandlers = ({
} else if (key === 'ArrowUp') {
selectedIndexControl.onPreviousResult();
event.preventDefault();
} else if (key === 'Enter') {
} else if (key === 'Enter' && Platform.OS === 'web') {
// This case is necessary on web to prevent the
// search input from becoming defocused after
// pressing "enter".
// pressing "enter". Enter key behavior is handled
// elsewhere for other platforms.
event.preventDefault();
onSubmit();
setSearch('');
@@ -584,6 +590,7 @@ const ComboBox: React.FC<Props> = ({
onChangeText={setSearch}
onKeyPress={onKeyPress}
onSubmitEditing={onSubmit}
submitBehavior='submit'
placeholder={placeholder}
aria-activedescendant={showSearchResults ? activeId : undefined}
aria-controls={`menuBox-${baseId}`}

View File

@@ -6,7 +6,10 @@ import { themeStyle } from './global-style';
import Modal from './Modal';
import { _ } from '@joplin/lib/locale';
export enum DialogSize {
export enum DialogVariant {
// Small width, auto-determined height
SmallResize = 'small-resize',
Small = 'small',
// Ideal for panels and dialogs that should be fullscreen even on large devices
@@ -20,34 +23,58 @@ interface Props {
containerStyle?: ViewStyle;
children: React.ReactNode;
heading?: string;
scrollOverflow?: boolean;
size: DialogSize;
size: DialogVariant;
}
const useStyles = (themeId: number, containerStyle: ViewStyle, size: DialogSize) => {
const useStyles = (themeId: number, containerStyle: ViewStyle, size: DialogVariant) => {
const windowSize = useWindowDimensions();
return useMemo(() => {
const theme = themeStyle(themeId);
const maxWidth = size === DialogSize.Large ? windowSize.width : 500;
const maxHeight = size === DialogSize.Large ? windowSize.height : 700;
const maxWidth = size === DialogVariant.Large ? windowSize.width : 500;
const maxHeight = size === DialogVariant.Large ? windowSize.height : 700;
const dialogSizing: ViewStyle = {
width: '100%',
...(size !== DialogVariant.SmallResize ? {
height: '100%',
} : { }),
};
return StyleSheet.create({
closeButtonContainer: {
closeButtonRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignContent: 'center',
marginBottom: 8,
},
closeButtonRowWithHeading: {
marginBottom: 16,
},
closeButton: {
margin: 0,
},
// Ensure that the close button is aligned with the center of the header:
// Make its container smaller and center it.
closeButtonWrapper: {
height: 18,
flexDirection: 'column',
justifyContent: 'center',
alignSelf: 'center',
},
heading: {
alignSelf: 'center',
},
modalBackground: {
justifyContent: 'center',
},
dialogContainer: {
maxHeight,
maxWidth,
width: '100%',
height: '100%',
flexShrink: 1,
...dialogSizing,
// Center
marginLeft: 'auto',
@@ -58,11 +85,11 @@ const useStyles = (themeId: number, containerStyle: ViewStyle, size: DialogSize)
...containerStyle,
},
dialogSurface: {
borderRadius: 12,
borderRadius: 24,
backgroundColor: theme.backgroundColor,
padding: 10,
width: '100%',
height: '100%',
paddingHorizontal: 16,
paddingVertical: 24,
...dialogSizing,
},
});
}, [themeId, windowSize.width, windowSize.height, containerStyle, size]);
@@ -76,13 +103,16 @@ const DismissibleDialog: React.FC<Props> = props => {
<Text variant='headlineSmall' role='heading' style={styles.heading}>{props.heading}</Text>
) : null;
const closeButtonRow = (
<View style={styles.closeButtonContainer}>
<View style={[styles.closeButtonRow, !!props.heading && styles.closeButtonRowWithHeading]}>
{heading ?? <View/>}
<IconButton
icon='close'
accessibilityLabel={_('Close')}
onPress={props.onDismiss}
/>
<View style={styles.closeButtonWrapper}>
<IconButton
icon='close'
accessibilityLabel={_('Close')}
onPress={props.onDismiss}
style={styles.closeButton}
/>
</View>
</View>
);
@@ -92,9 +122,13 @@ const DismissibleDialog: React.FC<Props> = props => {
onDismiss={props.onDismiss}
onRequestClose={props.onDismiss}
containerStyle={styles.dialogContainer}
modalBackgroundStyle={styles.modalBackground}
animationType='fade'
backgroundColor={theme.backgroundColorTransparent2}
transparent={true}
scrollOverflow={props.scrollOverflow}
// Allows the modal background to extend under the statusbar
statusBarTranslucent
>
<Surface style={styles.dialogSurface} elevation={1}>
{closeButtonRow}

View File

@@ -1,7 +1,8 @@
import * as React from 'react';
import { TouchableOpacity, TouchableWithoutFeedback, Dimensions, Text, Modal, View, LayoutRectangle, ViewStyle, TextStyle, FlatList } from 'react-native';
import { TouchableOpacity, TouchableWithoutFeedback, Dimensions, Text, Modal, View, LayoutRectangle, ViewStyle, TextStyle, FlatList, Platform } from 'react-native';
import { Component, ReactElement } from 'react';
import { _ } from '@joplin/lib/locale';
import { EdgeInsets, SafeAreaInsetsContext } from 'react-native-safe-area-context';
type ValueType = string;
export interface DropdownListItem {
@@ -56,25 +57,43 @@ class Dropdown extends Component<DropdownProps, DropdownState> {
};
}
private updateHeaderCoordinates = () => {
private updateHeaderCoordinates = (insets: EdgeInsets) => {
if (!this.headerRef) return;
// https://stackoverflow.com/questions/30096038/react-native-getting-the-position-of-an-element
this.headerRef.measure((_fx, _fy, width, height, px, py) => {
const lastLayout = this.state.headerSize;
let offsetX = 0;
let offsetY = 0;
// The opening position of the dropdown must be offset to cater for insets, on newer versions of Android which use edge to edge by default
// If the dropdown fills the full height of the screen, the offset gets ignored and does not cause anything to be truncated
if (Platform.OS === 'android' && Platform.Version >= 35) {
const windowHeight = Dimensions.get('window').height;
const windowWidth = Dimensions.get('window').width;
const isLandscape = windowWidth > windowHeight;
if (isLandscape) {
offsetX = insets.left;
offsetY = insets.top;
} else {
offsetY = insets.top;
}
}
if (px !== lastLayout.x || py !== lastLayout.y || width !== lastLayout.width || height !== lastLayout.height) {
this.setState({
headerSize: { x: px, y: py, width: width, height: height },
headerSize: { x: px - offsetX, y: py - offsetY, width: width, height: height },
});
}
});
};
private onOpenList = () => {
private onOpenList = (insets: EdgeInsets) => {
// On iOS, we need to re-measure just before opening the list. Measurements from just after
// onLayout can be inaccurate in some cases (in the past, this had caused the menu to be
// drawn far offscreen).
this.updateHeaderCoordinates();
this.updateHeaderCoordinates(insets);
this.setState({ listVisible: true });
};
private onCloseList = () => {
@@ -92,10 +111,16 @@ class Dropdown extends Component<DropdownProps, DropdownState> {
}
};
public render() {
private renderWithInsets(insets: EdgeInsets) {
let offsetHeight = 0;
if (Platform.OS === 'android' && Platform.Version >= 35) {
offsetHeight = insets.bottom;
}
const items = this.props.items;
const itemHeight = 60;
const windowHeight = Dimensions.get('window').height - 50;
const windowHeight = Dimensions.get('window').height - 50 - offsetHeight;
const windowWidth = Dimensions.get('window').width;
// Dimensions doesn't return quite the right dimensions so leave an extra gap to make
@@ -205,13 +230,13 @@ class Dropdown extends Component<DropdownProps, DropdownState> {
<View style={{ flex: 1, flexDirection: 'column' }}>
<View
style={{ flexDirection: 'row', flex: 1, alignItems: 'center' }}
onLayout={this.updateHeaderCoordinates}
onLayout={() => this.updateHeaderCoordinates(insets)}
ref={ref => { this.headerRef = ref; } }
>
<TouchableOpacity
style={headerWrapperStyle}
disabled={this.props.disabled}
onPress={this.onOpenList}
onPress={() => this.onOpenList(insets)}
accessibilityRole='button'
accessibilityHint={[this.props.accessibilityHint, _('Opens dropdown')].join(' ')}
>
@@ -268,6 +293,14 @@ class Dropdown extends Component<DropdownProps, DropdownState> {
</View>
);
}
public render() {
return (
<SafeAreaInsetsContext.Consumer>
{(insets) => this.renderWithInsets(insets)}
</SafeAreaInsetsContext.Consumer>
);
}
}
export default Dropdown;

View File

@@ -12,7 +12,7 @@ import { AppState } from '../../utils/types';
import CommandService from '@joplin/lib/services/CommandService';
import allToolbarCommandNamesFromState from './utils/allToolbarCommandNamesFromState';
import Setting from '@joplin/lib/models/Setting';
import DismissibleDialog, { DialogSize } from '../DismissibleDialog';
import DismissibleDialog, { DialogVariant } from '../DismissibleDialog';
import selectedCommandNamesFromState from './utils/selectedCommandNamesFromState';
import stateToWhenClauseContext from '../../services/commands/stateToWhenClauseContext';
import { DeleteButton } from '../buttons';
@@ -158,7 +158,7 @@ const ToolbarEditorScreen: React.FC<EditorDialogProps> = props => {
return (
<DismissibleDialog
size={DialogSize.Small}
size={DialogVariant.Small}
themeId={props.themeId}
visible={props.visible}
onDismiss={props.onDismiss}

View File

@@ -0,0 +1,133 @@
import * as React from 'react';
import { Store } from 'redux';
import { AppState } from '../utils/types';
import TestProviderStack from './testing/TestProviderStack';
import { switchClient, setupDatabase, mockMobilePlatform, mockFetch, waitFor } from '@joplin/lib/testing/test-utils';
import createMockReduxStore from '../utils/testing/createMockReduxStore';
import setupGlobalStore from '../utils/testing/setupGlobalStore';
import { act, fireEvent, render, screen } from '@testing-library/react-native';
import FeedbackBanner from './FeedbackBanner';
import { MobilePlatform } from '@joplin/lib/shim';
interface WrapperProps { }
let store: Store<AppState>;
const WrappedFeedbackBanner: React.FC<WrapperProps> = () => {
return <TestProviderStack store={store}>
<FeedbackBanner/>
</TestProviderStack>;
};
const getFeedbackButton = (positive: boolean) => {
return screen.getByRole('button', { name: positive ? 'Useful' : 'Not useful' });
};
const getSurveyLink = () => {
return screen.getByRole('button', { name: 'Take survey' });
};
const mockFeedbackServer = (surveyName = 'web-app-test') => {
let helpfulCount = 0;
let unhelpfulCount = 0;
const { reset } = mockFetch((request) => {
const surveyBaseUrls = [
'https://objects.joplinusercontent.com/',
'http://localhost:3430/',
];
const isSurveyRequest = surveyBaseUrls.some(url => request.url.startsWith(url));
if (!isSurveyRequest) {
return null;
}
const url = new URL(request.url);
if (url.pathname === `/r/survey--${surveyName}--helpful`) {
helpfulCount ++;
} else if (url.pathname === `/r/survey--${surveyName}--unhelpful`) {
unhelpfulCount ++;
} else {
return new Response('Not found', { status: 404 });
}
// The feedback server always redirects to another URL after a
// successful request. Mock this by always redirecting to the
// same URL.
return new Response('', {
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/302
status: 302,
statusText: 'Found',
headers: [
['location', 'https://joplinapp.org'],
],
});
});
return {
reset,
get helpfulCount() {
return helpfulCount;
},
get unhelpfulCount() {
return unhelpfulCount;
},
};
};
describe('FeedbackBanner', () => {
const resetMobilePlatform = ()=>{};
beforeEach(async () => {
await setupDatabase(0);
await switchClient(0);
store = createMockReduxStore();
setupGlobalStore(store);
jest.useFakeTimers({ advanceTimers: true });
mockMobilePlatform(MobilePlatform.Web);
});
afterEach(() => {
screen.unmount();
resetMobilePlatform();
});
test.each([
{ platform: MobilePlatform.Android, shouldShow: false },
{ platform: MobilePlatform.Web, shouldShow: true },
{ platform: MobilePlatform.Ios, shouldShow: false },
])('should correctly show/hide the feedback banner on %s', ({ platform, shouldShow }) => {
mockMobilePlatform(platform);
render(<WrappedFeedbackBanner />);
const header = screen.queryByRole('header', { name: 'Feedback' });
if (shouldShow) {
expect(header).toBeVisible();
} else {
expect(header).toBeNull();
}
});
test('clicking the "Useful" button should submit the response and show the "take survey" link', async () => {
const feedbackServerMock = mockFeedbackServer();
render(<WrappedFeedbackBanner />);
try {
const usefulButton = getFeedbackButton(true);
fireEvent.press(usefulButton);
await act(() => waitFor(async () => {
expect(getSurveyLink()).toBeVisible();
}));
expect(feedbackServerMock).toMatchObject({
helpfulCount: 1,
unhelpfulCount: 0,
});
} finally {
feedbackServerMock.reset();
}
});
});

View File

@@ -0,0 +1,216 @@
import { _ } from '@joplin/lib/locale';
import * as React from 'react';
import { View, StyleSheet, useWindowDimensions, TextStyle, Linking } from 'react-native';
import { Portal, Text } from 'react-native-paper';
import IconButton from './IconButton';
import { useCallback, useMemo } from 'react';
import shim from '@joplin/lib/shim';
import { Dispatch } from 'redux';
import { themeStyle } from './global-style';
import { AppState } from '../utils/types';
import { connect } from 'react-redux';
import Setting from '@joplin/lib/models/Setting';
import { LinkButton } from './buttons';
import Logger from '@joplin/utils/Logger';
import { SurveyProgress } from '@joplin/lib/models/settings/builtInMetadata';
const logger = Logger.create('FeedbackBanner');
interface Props {
dispatch: Dispatch;
progress: SurveyProgress;
surveyKey: string;
themeId: number;
}
const useStyles = (themeId: number, sentFeedback: boolean) => {
const { width: windowWidth } = useWindowDimensions();
return useMemo(() => {
const theme = themeStyle(themeId);
const iconBaseStyle: TextStyle = {
fontSize: 24,
color: theme.color3,
};
return StyleSheet.create({
container: {
backgroundColor: theme.backgroundColor3,
borderTopRightRadius: 16,
display: 'flex',
flexGrow: 1,
flexWrap: 'wrap',
flexDirection: 'row',
position: 'absolute',
bottom: 0,
left: 0,
maxWidth: windowWidth - 50,
gap: 18,
padding: 12,
},
contentRight: {
display: sentFeedback ? 'none' : 'flex',
flexDirection: 'row',
alignItems: 'center',
gap: 16,
},
header: {
fontWeight: 'bold',
},
iconUseful: {
...iconBaseStyle,
color: theme.colorCorrect,
},
iconNotUseful: {
...iconBaseStyle,
color: theme.colorWarn,
},
dismissButtonIcon: {
fontSize: 16,
color: theme.color2,
marginLeft: 'auto',
marginRight: 'auto',
},
dismissButton: {
backgroundColor: theme.backgroundColor2,
borderColor: theme.backgroundColor,
borderWidth: 2,
width: 29,
height: 29,
borderRadius: 14,
position: 'absolute',
top: -16,
right: -16,
justifyContent: 'center',
},
dismissButtonContent: {
flexShrink: 1,
},
});
}, [themeId, windowWidth, sentFeedback]);
};
const useSurveyUrl = (surveyKey: string) => {
return useMemo(() => {
let baseUrl = 'https://objects.joplinusercontent.com/';
// For testing with a locally-hosted server:
const useLocalServer = false;
if (Setting.value('env') === 'dev' && useLocalServer) {
baseUrl = 'http://localhost:3430/';
}
return `${baseUrl}r/survey--${encodeURIComponent(surveyKey)}`;
}, [surveyKey]);
};
const setProgress = (progress: SurveyProgress) => {
Setting.setValue('survey.webClientEval2025.progress', progress);
};
const onDismiss = () => {
setProgress(SurveyProgress.Dismissed);
};
const FeedbackBanner: React.FC<Props> = props => {
const surveyUrl = useSurveyUrl(props.surveyKey);
const sentFeedback = props.progress === SurveyProgress.Started;
const sendSurveyResponse = useCallback(async (surveyResponse: string) => {
const fetchUrl = `${surveyUrl}--${encodeURIComponent(surveyResponse)}`;
logger.debug('sending response to', fetchUrl);
const showError = (message: string) => {
logger.error('Error', message);
void shim.showErrorDialog(
_('An error occurred while sending the response. This can happen if the app is offline or cannot connect to the server.\nError: %s', message),
);
};
try {
const response = await shim.fetch(fetchUrl);
// The server currently redirects (status 302) in response
// to many survey-related requests. This may be returned by
// the web app service worker as a 200 OK response, however. Support both:
if (response.ok || response.status === 302) {
setProgress(SurveyProgress.Started);
} else {
const body = await response.text();
showError(`Server error: ${response.status} ${body}`);
}
} catch (error) {
showError(error);
}
}, [surveyUrl]);
const onSurveyLinkClick = useCallback(() => {
void Linking.openURL(surveyUrl);
onDismiss();
}, [surveyUrl]);
const onNotUsefulClick = useCallback(() => {
void sendSurveyResponse('unhelpful');
}, [sendSurveyResponse]);
const onUsefulClick = useCallback(() => {
void sendSurveyResponse('helpful');
}, [sendSurveyResponse]);
const styles = useStyles(props.themeId, sentFeedback);
const renderStatusMessage = () => {
if (sentFeedback) {
return <View>
<Text>{_('Thank you for the feedback!\nDo you have time to complete a short survey?')}</Text>
<LinkButton onPress={onSurveyLinkClick}>{_('Take survey')}</LinkButton>
</View>;
} else {
return <Text>{_('Do you find the Joplin web app useful?')}</Text>;
}
};
if (shim.mobilePlatform() !== 'web' || props.progress === SurveyProgress.Dismissed) return null;
return <Portal>
<View style={styles.container} role='complementary'>
<View>
<Text
accessibilityRole='header'
variant='titleMedium'
style={styles.header}
>{_('Feedback')}</Text>
<Text>{renderStatusMessage()}</Text>
</View>
<View style={styles.contentRight}>
<IconButton
iconName='fas times'
themeId={props.themeId}
onPress={onNotUsefulClick}
description={_('Not useful')}
iconStyle={styles.iconNotUseful}
/>
<IconButton
iconName='fas check'
themeId={props.themeId}
onPress={onUsefulClick}
description={_('Useful')}
iconStyle={styles.iconUseful}
/>
</View>
<IconButton
iconName='fas times'
themeId={props.themeId}
onPress={onDismiss}
description={_('Dismiss')}
iconStyle={styles.dismissButtonIcon}
contentWrapperStyle={styles.dismissButtonContent}
containerStyle={styles.dismissButton}
/>
</View>
</Portal>;
};
export default connect((state: AppState) => ({
themeId: state.settings.theme,
surveyKey: 'web-app-test',
progress: state.settings['survey.webClientEval2025.progress'],
}))(FeedbackBanner);

View File

@@ -87,6 +87,14 @@ const IconButton = (props: ButtonProps) => {
props.preventKeyboardDismiss, props.onPress, props.disabled,
);
let icon = <Icon
name={props.iconName}
style={props.iconStyle}
accessibilityLabel={null}
/>;
// Include browser-provided tooltips on web.
icon = Platform.OS === 'web' ? <span title={props.description}>{icon}</span> : icon;
const button = (
<Pressable
ref={props.pressableRef}
@@ -115,11 +123,7 @@ const IconButton = (props: ButtonProps) => {
opacity: fadeAnim,
...props.contentWrapperStyle,
}}>
<Icon
name={props.iconName}
style={props.iconStyle}
accessibilityLabel={null}
/>
{icon}
</Animated.View>
</Pressable>
);

View File

@@ -27,11 +27,23 @@ export interface ModalElementProps extends ModalProps {
const useStyles = (hasScrollView: boolean, backgroundColor: string|undefined) => {
const safeAreaPadding = useSafeAreaPadding();
return useMemo(() => {
// On Android, the top-level container seems to need to be absolutely positioned
// to prevent it from being larger than the screen size:
const absoluteFill = {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
} satisfies ViewStyle;
return StyleSheet.create({
modalBackground: {
...safeAreaPadding,
flexGrow: 1,
flexShrink: 1,
...(hasScrollView ? {
flexGrow: 1,
flexShrink: 1,
} : absoluteFill),
// When hasScrollView, the modal background is wrapped in a ScrollView. In this case, it's
// possible to scroll content outside the background into view. To prevent the edge of the
@@ -40,6 +52,7 @@ const useStyles = (hasScrollView: boolean, backgroundColor: string|undefined) =>
backgroundColor: hasScrollView ? null : backgroundColor,
},
keyboardAvoidingView: {
...absoluteFill,
flex: 1,
},
modalScrollView: {

View File

@@ -84,7 +84,7 @@ const NestableFlatList = function<T>({
}, []);
const bufferSize = 10;
const visibleStartIndex = Math.floor(scroll / itemHeight);
const visibleStartIndex = Math.min(Math.floor(scroll / itemHeight), data.length);
const visibleEndIndex = Math.ceil((scroll + listHeight) / itemHeight);
const startIndex = Math.max(0, visibleStartIndex - bufferSize);
const maximumIndex = data.length - 1;

View File

@@ -16,6 +16,12 @@ import { ResourceInfo } from './hooks/useRerenderHandler';
import getWebViewDomById from '../../utils/testing/getWebViewDomById';
import TestProviderStack from '../testing/TestProviderStack';
import createMockReduxStore from '../../utils/testing/createMockReduxStore';
import Plugin from '@joplin/lib/services/plugins/Plugin';
import { Store } from 'redux';
import { ContentScriptType } from '@joplin/lib/services/plugins/api/types';
import { basename, dirname, join } from 'path';
import PluginService from '@joplin/lib/services/plugins/PluginService';
import mockPluginServiceSetup from '../../utils/testing/mockPluginServiceSetup';
interface WrapperProps {
noteBody: string;
@@ -28,7 +34,7 @@ interface WrapperProps {
const emptyObject = {};
const emptyArray: string[] = [];
const noOpFunction = () => {};
const testStore = createMockReduxStore();
let testStore: Store;
const WrappedNoteViewer: React.FC<WrapperProps> = (
{
noteBody,
@@ -58,10 +64,34 @@ const getNoteViewerDom = async () => {
return await getWebViewDomById('NoteBodyViewer');
};
const loadTestContentScript = async (path: string, pluginId: string) => {
const plugin = new Plugin(
dirname(path),
{
manifest_version: 1,
id: pluginId,
name: 'Test plugin',
version: '1',
app_min_version: '1',
},
'',
testStore.dispatch,
'',
);
await PluginService.instance().runPlugin(plugin);
await plugin.registerContentScript(
ContentScriptType.MarkdownItPlugin,
`${pluginId}-markdown-it`,
basename(path),
);
};
describe('NoteBodyViewer', () => {
beforeEach(async () => {
await setupDatabaseAndSynchronizer(0);
await switchClient(0);
testStore = createMockReduxStore();
mockPluginServiceSetup(testStore);
});
afterEach(() => {
@@ -85,6 +115,17 @@ describe('NoteBodyViewer', () => {
await expectHeaderToBe('Test 3');
});
it('should support basic renderer plugins', async () => {
await loadTestContentScript(join(supportDir, 'plugins', 'markdownItTestPlugin.js'), 'test-plugin');
render(<WrappedNoteViewer noteBody={'```justtesting\ntest\n```\n'}/>);
const noteViewer = await getNoteViewerDom();
await waitFor(async () => {
expect(noteViewer.querySelector('div.just-testing')).toBeTruthy();
});
});
it.each([
{ keywords: ['match'], body: 'A match and another match. Both should be highlighted.', expectedMatchCount: 2 },
{ keywords: ['test'], body: 'No match.', expectedMatchCount: 0 },

View File

@@ -65,7 +65,7 @@ function NoteBodyViewer(props: Props) {
onResourceLongPress,
});
const { api: renderer, pageSetup, webViewEventHandlers } = useWebViewSetup({
const { api: renderer, pageSetup, webViewEventHandlers, hasPluginScripts } = useWebViewSetup({
webviewRef,
onBodyScroll: onScroll,
onPostMessage,
@@ -106,6 +106,7 @@ function NoteBodyViewer(props: Props) {
mixedContentMode="always"
onLoadEnd={onLoadEnd}
onMessage={webViewEventHandlers.onMessage}
hasPluginScripts={hasPluginScripts}
/>
</View>
);

View File

@@ -33,6 +33,9 @@ import { toFileExtension } from '@joplin/lib/mime-utils';
import { MarkupLanguage } from '@joplin/renderer';
import WarningBanner from './WarningBanner';
import useIsScreenReaderEnabled from '../../utils/hooks/useIsScreenReaderEnabled';
import Logger from '@joplin/utils/Logger';
const logger = Logger.create('NoteEditor');
type ChangeEventHandler = (event: ChangeEvent)=> void;
type UndoRedoDepthChangeHandler = (event: UndoRedoDepthChangeEvent)=> void;
@@ -87,6 +90,8 @@ function editorTheme(themeId: number) {
};
}
const noteEditorSearchChangeSource = 'joplin.noteEditor.setSearchState';
type OnSetVisibleCallback = (visible: boolean)=> void;
type OnSearchStateChangeCallback = (state: SearchState)=> void;
const useEditorControl = (
@@ -101,7 +106,7 @@ const useEditorControl = (
};
const setSearchStateCallback = (state: SearchState) => {
editorRef.current.setSearchState(state);
editorRef.current.setSearchState(state, noteEditorSearchChangeSource);
setSearchState(state);
};
@@ -111,6 +116,7 @@ const useEditorControl = (
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
execCommand(command, ...args: any[]) {
logger.debug('execCommand', command);
return editorRef.current.execCommand(command, ...args);
},
@@ -230,8 +236,8 @@ const useEditorControl = (
setSearchState: setSearchStateCallback,
},
onResourceDownloaded: (id: string) => {
editorRef.current.onResourceDownloaded(id);
onResourceChanged: (id: string) => {
editorRef.current.onResourceChanged(id);
},
remove: () => {
@@ -306,15 +312,26 @@ function NoteEditor(props: Props) {
case EditorEventType.FollowLink:
void CommandService.instance().execute('openItem', event.link);
break;
case EditorEventType.UpdateSearchDialog:
setSearchState(event.searchState);
case EditorEventType.UpdateSearchDialog: {
const hasExternalChange = (
event.changeSources.length !== 1
|| event.changeSources[0] !== noteEditorSearchChangeSource
);
if (event.searchState.dialogVisible) {
editorControl.searchControl.showSearch();
} else {
editorControl.searchControl.hideSearch();
// If the change to the search was done by this editor, it was already applied to the
// search state. Skipping the update in this case also helps avoid overwriting the
// search state with an older value.
if (hasExternalChange) {
setSearchState(event.searchState);
if (event.searchState.dialogVisible) {
editorControl.searchControl.showSearch();
} else {
editorControl.searchControl.hideSearch();
}
}
break;
}
case EditorEventType.Remove:
case EditorEventType.Scroll:
// Not handled
@@ -342,10 +359,18 @@ function NoteEditor(props: Props) {
const isDownloaded = (resourceInfos: ResourceInfos, resourceId: string) => {
return resourceInfos[resourceId]?.localState?.fetch_status === Resource.FETCH_STATUS_DONE;
};
const isEncrypted = (resourceInfos: ResourceInfos, resourceId: string) => {
return resourceInfos[resourceId]?.item?.encryption_blob_encrypted === 1;
};
for (const key in props.noteResources) {
const wasDownloaded = isDownloaded(lastNoteResources.current, key);
if (!wasDownloaded && isDownloaded(props.noteResources, key)) {
editorControl.onResourceDownloaded(key);
const hasDownloaded = !wasDownloaded && isDownloaded(props.noteResources, key);
const wasEncrypted = isEncrypted(lastNoteResources.current, key);
const hasDecrypted = wasEncrypted && !isEncrypted(props.noteResources, key);
if (hasDownloaded || hasDecrypted) {
editorControl.onResourceChanged(key);
}
}
}, [props.noteResources, editorControl]);

View File

@@ -23,6 +23,7 @@ import { MarkupLanguage } from '@joplin/renderer';
import { NoteEntity } from '@joplin/lib/services/database/types';
import { EditorSettings } from './types';
import { pregQuote } from '@joplin/lib/string-utils';
import { join } from 'path';
interface WrapperProps {
@@ -103,8 +104,8 @@ const mockTyping = (window: EditorWindow, text: string) => {
}
};
const mockSelectionMovement = (window: EditorWindow, position: number) => {
getEditorControl(window).select(position, position);
const mockSelectionMovement = (window: EditorWindow, from: number, to?: number) => {
getEditorControl(window).select(from, to ?? from);
};
const findElement = async function<ElementType extends Element = Element>(selector: string) {
@@ -257,7 +258,7 @@ describe('RichTextEditor', () => {
ref={editorRef}
/>,
);
editorRef.current.onResourceDownloaded(localResource.id);
editorRef.current.onResourceChanged(localResource.id);
expect(
await findElement(`img[data-resource-id=${JSON.stringify(localResource.id)}]`),
@@ -333,7 +334,7 @@ describe('RichTextEditor', () => {
const editorContent = body.trim();
if (markupLanguage === MarkupLanguage.Html) {
expect(editorContent).toMatch(
new RegExp(`^<p><img src=":/${pregQuote(resource.id)}" alt="${pregQuote(renderedImage.alt)}"[^>]*> test</p>$`),
new RegExp(`^<p><img[^>]* src=":/${pregQuote(resource.id)}" alt="${pregQuote(renderedImage.alt)}"[^>]*> test</p>$`),
);
} else {
expect(editorContent).toBe(`![${renderedImage.alt}](:/${resource.id}) test`);
@@ -341,6 +342,29 @@ describe('RichTextEditor', () => {
});
});
it('should preserve non-image attachments on edit', async () => {
const { note, resource } = await createNoteAndResource({ path: join(supportDir, 'sample.txt') });
let body = note.body;
const resources = await attachedResources(body);
render(<WrappedEditor
noteBody={note.body}
note={note}
onBodyChange={newBody => { body = newBody; }}
noteResources={resources}
/>);
const window = await getEditorWindow();
mockTyping(window, ' test');
await waitFor(async () => {
const editorContent = body.trim();
// TODO: At present, the resource title may be included in the final Markdown
// (e.g. as [sample.txt](:/id-here "sample.txt")).
expect(editorContent).toMatch(new RegExp(`^\\[sample\\.txt\\]\\(:/${pregQuote(resource.id)}.*\\) test$`));
});
});
it.each([
{ useValidSyntax: false },
{ useValidSyntax: true },
@@ -390,14 +414,18 @@ describe('RichTextEditor', () => {
});
});
it('should be possible show an editor for math blocks', async () => {
it('should be possible to show an editor for math blocks', async () => {
let body = 'Test:\n\n$$3^2 + 4^2 = 5^2$$';
render(<WrappedEditor
noteBody={body}
onBodyChange={newBody => { body = newBody; }}
/>);
const editButton = await findElement<HTMLButtonElement>('button.edit');
const window = await getEditorWindow();
// Select the math block to show the "edit" button.
mockSelectionMovement(window, '<Test:>'.length, '<Test:>$'.length);
const editButton = await findElement<HTMLButtonElement>('button.edit-button');
editButton.click();
const editor = await findElement('dialog .cm-editor');
@@ -452,6 +480,8 @@ describe('RichTextEditor', () => {
'==highlight==ed',
'<sup>Super</sup>script',
'<sub>Sub</sub>script',
'![image](data:image/svg+xml;utf8,test)',
'<img width="120" src="data:image/svg+xml;utf8,test">',
])('should preserve inline markup on edit (case %#)', async (initialBody) => {
initialBody += 'test'; // Ensure that typing will add new content outside the formatting
let body = initialBody;

View File

@@ -44,6 +44,13 @@ function useCss(themeId: number, editorCss: string): string {
font-size: 13pt;
font-family: ${JSON.stringify(theme.fontFamily)}, sans-serif;
}
.RichTextEditor {
/* Relatively positioning the editor container causes absolutely-positioned
elements to be positioned relative to Rich Text Editor's container,
rather than the body. This fixes an alignment issue involving button overlays. */
position: relative;
}
`;
}, [themeId, editorCss]);
}

View File

@@ -158,8 +158,7 @@ export const SearchPanel = (props: SearchPanelProps) => {
const state = props.searchState;
const control = props.searchControl;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const updateSearchState = (changedData: any) => {
const updateSearchState = (changedData: Partial<SearchState>) => {
const newState = { ...state, ...changedData };
control.setSearchState(newState);
};

View File

@@ -37,6 +37,7 @@ const useStyles = (themeId: number) => {
const listItemPressable: ViewStyle = {
flexGrow: 1,
flexShrink: 1,
alignSelf: 'stretch',
};
const listItemPressableWithCheckbox: ViewStyle = {

View File

@@ -11,7 +11,7 @@ import { saveProfileConfig, switchProfile } from '../../services/profiles';
import { themeStyle } from '../global-style';
import shim from '@joplin/lib/shim';
import { DialogContext } from '../DialogManager';
import { FAB, List, Portal } from 'react-native-paper';
import { FAB, List } from 'react-native-paper';
import { TextStyle } from 'react-native';
import useOnLongPressProps from '../../utils/hooks/useOnLongPressProps';
import { Dispatch } from 'redux';
@@ -206,19 +206,17 @@ export default (props: Props) => {
extraData={extraListItemData}
/>
</View>
<Portal>
<FAB
icon="plus"
accessibilityLabel={_('New profile')}
style={style.fab}
onPress={() => {
props.dispatch({
type: 'NAV_GO',
routeName: 'ProfileEditor',
});
}}
/>
</Portal>
<FAB
icon="plus"
accessibilityLabel={_('New profile')}
style={style.fab}
onPress={() => {
props.dispatch({
type: 'NAV_GO',
routeName: 'ProfileEditor',
});
}}
/>
</View>
);
};

View File

@@ -7,6 +7,8 @@ import AccessibleView from '../accessibility/AccessibleView';
import debounce from '../../utils/debounce';
import FocusControl from '../accessibility/FocusControl/FocusControl';
import { ModalState } from '../accessibility/FocusControl/types';
import useKeyboardState from '../../utils/hooks/useKeyboardState';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
interface MenuOptionDivider {
isDivider: true;
@@ -29,7 +31,9 @@ interface Props {
}
const useStyles = (themeId: number) => {
const { height: windowHeight } = useWindowDimensions();
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
const safeAreaInsets = useSafeAreaInsets();
const { dockedKeyboardHeight: keyboardHeight } = useKeyboardState();
return useMemo(() => {
const theme = themeStyle(themeId);
@@ -46,6 +50,20 @@ const useStyles = (themeId: number) => {
fontSize: theme.fontSize,
};
const isLandscape = windowWidth > windowHeight;
const extraPadding = isLandscape ? 25 : 50;
// When a docked on-screen keyboard is showing, we want to maximise the height of the menu as much as possible, due to the limited available space.
// However, when the on-screen keyboard is hidden or floating in either portrait or landscape orientation, it is less of an issue to have excess in the amount
// of padding, to ensure nothing is cut off on all varieties of supported mobile platforms with different input and navigation bar settings. In particular,
// on Android it is not possible to distinguish between a floating keyboard and a horizontal input bar which is docked, but the latter requires a larger
// reduction in height. For this reason we use a fixed value for insetOrExtraFullscreenPadding when the keyboard height is zero. However, Android versions
// earlier than 15 have an IME toolbar in addition to the input toolbar when using an external keyboard, so to cater for this scenario, we can use the fixed
// value if the keyboardHeight is <= 25 (as any proper on-screen keyboard would be much taller than this). If the keyboard height is larger than this, we can assume
// a docked keyboard is visible, so we only need cater for the insets in addition to the fixed extraPadding required for compatibility across Android versions
const insetOrExtraFullscreenPadding = keyboardHeight <= 25 ? 70 : safeAreaInsets.top + safeAreaInsets.bottom;
const maxMenuHeight = windowHeight - keyboardHeight - extraPadding - insetOrExtraFullscreenPadding;
return StyleSheet.create({
divider: {
borderBottomWidth: 1,
@@ -66,13 +84,13 @@ const useStyles = (themeId: number) => {
opacity: 0.5,
},
menuContentScroller: {
maxHeight: windowHeight - 50,
maxHeight: maxMenuHeight,
},
contextMenuButton: {
padding: 0,
},
});
}, [themeId, windowHeight]);
}, [themeId, windowWidth, windowHeight, safeAreaInsets, keyboardHeight]);
};
const MenuComponent: React.FC<Props> = props => {

View File

@@ -1,12 +1,15 @@
import * as React from 'react';
import { Linking, TextStyle, View, ViewStyle } from 'react-native';
import { Linking, StyleSheet, TextStyle, View, ViewStyle } from 'react-native';
import { Text } from 'react-native-paper';
import IconButton from '../IconButton';
import { _ } from '@joplin/lib/locale';
import { useCallback, useState } from 'react';
import DismissibleDialog, { DialogSize } from '../DismissibleDialog';
import { LinkButton } from '../buttons';
import NavService from '@joplin/lib/services/NavService';
import DismissibleDialog, { DialogVariant } from '../DismissibleDialog';
import { LinkButton, PrimaryButton } from '../buttons';
import makeDiscourseDebugUrl from '@joplin/lib/makeDiscourseDebugUrl';
import getPackageInfo from '../../utils/getPackageInfo';
import PluginService from '@joplin/lib/services/plugins/PluginService';
import Setting from '@joplin/lib/models/Setting';
interface Props {
wrapperStyle: ViewStyle;
@@ -15,10 +18,27 @@ interface Props {
}
const onLeaveFeedback = () => {
void Linking.openURL('https://discourse.joplinapp.org/t/web-client-running-joplin-mobile-in-a-web-browser-with-react-native-web/38749');
void Linking.openURL('https://forms.gle/B5YGDNzsUYBnoPx19');
};
const feedbackContainerStyles: ViewStyle = { flexGrow: 1, justifyContent: 'flex-end' };
const onReportBug = () => {
void Linking.openURL(
makeDiscourseDebugUrl('', '', [], getPackageInfo(), PluginService.instance(), Setting.value('plugins.states')),
);
};
const styles = StyleSheet.create({
feedbackContainer: {
flexGrow: 1,
flexDirection: 'row',
gap: 16,
justifyContent: 'flex-end',
flexWrap: 'wrap',
},
paragraph: {
paddingBottom: 7,
},
});
const WebBetaButton: React.FC<Props> = props => {
const [dialogVisible, setDialogVisible] = useState(false);
@@ -31,6 +51,10 @@ const WebBetaButton: React.FC<Props> = props => {
setDialogVisible(false);
}, []);
const renderParagraph = (content: string) => {
return <Text variant='bodyLarge' style={styles.paragraph}>{content}</Text>;
};
return (
<>
<IconButton
@@ -44,15 +68,18 @@ const WebBetaButton: React.FC<Props> = props => {
/>
<DismissibleDialog
heading={_('Beta')}
size={DialogSize.Small}
size={DialogVariant.SmallResize}
themeId={props.themeId}
visible={dialogVisible}
onDismiss={onHideDialog}
>
<Text>{'At present, the web client is in beta. In the future, it may change significantly, or be removed.'}</Text>
<View style={feedbackContainerStyles}>
<LinkButton onPress={onLeaveFeedback}>{'Give feedback'}</LinkButton>
<LinkButton onPress={() => NavService.go('DocumentScanner')}>{'Test work-in-progress feature: Document scanner'}</LinkButton>
{renderParagraph('Welcome to the beta version of the Joplin Web App!')}
{renderParagraph('Thank you for participating in the beta version of the Joplin Web App.')}
{renderParagraph('The Joplin Web App is available for a limited time in open beta and may later join the Joplin Cloud plans.')}
{renderParagraph('Feel free to use it and let us know if have any questions or notice any issues!')}
<View style={styles.feedbackContainer}>
<LinkButton onPress={onReportBug}>{'Report bug'}</LinkButton>
<PrimaryButton onPress={onLeaveFeedback}>{'Give feedback'}</PrimaryButton>
</View>
</DismissibleDialog>
</>

View File

@@ -101,12 +101,24 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const styleObject: any = {
container: {
outerContainer: {
flexDirection: 'column',
},
innerContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: theme.backgroundColor2,
shadowColor: '#000000',
elevation: 5,
},
// A small border above the header: Covers the part of the shadow that would otherwise
// be shown above the header on Android.
aboveHeader: {
backgroundColor: theme.backgroundColor2,
paddingBottom: 6,
marginTop: -6,
zIndex: 2,
},
sideMenuButton: {
flex: 1,
alignItems: 'center',
@@ -678,8 +690,9 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
);
return (
<View style={this.styles().container}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<View style={this.styles().outerContainer}>
<View style={this.styles().aboveHeader}/>
<View style={this.styles().innerContainer}>
{sideMenuComp}
{backButtonComp}
{renderUndoButton()}

View File

@@ -0,0 +1,37 @@
import * as React from 'react';
import Svg, { SvgProps, G, Path, Defs, LinearGradient, Stop, ClipPath, Rect } from 'react-native-svg';
const JoplinCloudIcon: React.FC<SvgProps> = props => {
return <Svg
viewBox='0 0 84 84'
fill='none'
{...props}
>
<G clipPath='url(#a)'>
<Path fill='url(#b)' d='M0 0h84v84H0z'/>
<Path
fill='#fff'
d='M73.706 49.825c0 3.732-1.534 7.148-4.007 9.592a13.714 13.714 0 0 1-9.675 3.973h-8.199v-7.065h8.2c1.818 0 3.436-.723 4.635-1.904 1.19-1.188 1.92-2.784 1.92-4.596 0-1.804-.73-3.408-1.92-4.597a6.539 6.539 0 0 0-4.636-1.903h-6.933l.386-3.882c.042-.4.059-.79.059-1.197 0-3.3-1.342-6.251-3.513-8.412a11.964 11.964 0 0 0-8.484-3.483c-3.328 0-6.304 1.33-8.484 3.483-2.18 2.16-3.513 5.112-3.513 8.412 0 .399.017.798.06 1.197l.385 3.882h-6.154c-1.819 0-3.437.723-4.636 1.903-1.19 1.189-1.92 2.785-1.92 4.597 0 1.803.73 3.408 1.92 4.596a6.539 6.539 0 0 0 4.636 1.904h9.935a8.854 8.854 0 0 0 4.82-2.452 8.705 8.705 0 0 0 2.59-6.201v-7.523h-7.217v-2.726c0-2.32 1.903-4.215 4.25-4.215h9.968v14.464c0 4.14-1.685 8.187-4.636 11.105-2.6 2.585-6.078 4.19-9.733 4.53l-.923.083h-9.062c-3.764 0-7.21-1.52-9.674-3.973a13.483 13.483 0 0 1 0-19.185 13.673 13.673 0 0 1 8.35-3.906c.452-4.464 2.48-8.487 5.507-11.488a19.154 19.154 0 0 1 13.523-5.552 19.16 19.16 0 0 1 13.522 5.552 18.842 18.842 0 0 1 5.5 11.446c3.554.141 6.782 1.613 9.129 3.948a13.428 13.428 0 0 1 4.024 9.593z'
strokeWidth={1.3}
/>
</G>
<Defs>
<LinearGradient
id='b'
x1={3}
x2={78}
y1={4}
y2={79}
gradientUnits='userSpaceOnUse'
>
<Stop offset={0.14} stopColor='#3873DB'/>
<Stop offset={0.974} stopColor='#163467'/>
</LinearGradient>
<ClipPath id='a'>
<Rect width={84} height={84} fill='#fff' rx={20} />
</ClipPath>
</Defs>
</Svg>;
};
export default JoplinCloudIcon;

View File

@@ -0,0 +1,138 @@
import * as React from 'react';
import DismissibleDialog, { DialogVariant } from '../DismissibleDialog';
import { AppState } from '../../utils/types';
import { connect } from 'react-redux';
import { Dispatch } from 'redux';
import { useCallback } from 'react';
import { Icon, Text } from 'react-native-paper';
import { _ } from '@joplin/lib/locale';
import JoplinCloudIcon from './JoplinCloudIcon';
import NavService from '@joplin/lib/services/NavService';
import { StyleSheet, View } from 'react-native';
import CardButton from '../buttons/CardButton';
interface Props {
dispatch: Dispatch;
visible: boolean;
themeId: number;
}
const iconSize = 24;
const styles = StyleSheet.create({
titleContainer: {
flexDirection: 'row',
gap: 8,
paddingBottom: 6,
alignItems: 'center',
},
subheading: {
marginBottom: 24,
},
cardContent: {
padding: 12,
borderRadius: 14,
},
syncProviderList: {
gap: 8,
},
featuresList: {
marginTop: 4,
},
listItem: {
flexDirection: 'row',
gap: 8,
marginVertical: 6,
verticalAlign: 'middle',
},
});
interface SyncProviderProps {
title: string;
icon: ()=> React.ReactNode;
description: string;
onPress: ()=> void;
featuresList: string[];
disabled: boolean;
}
const SyncProvider: React.FC<SyncProviderProps> = props => {
return <CardButton
disabled={props.disabled}
onPress={props.onPress}
testID='sync-provider-card'
>
<View style={styles.cardContent}>
<View style={styles.titleContainer}>
{props.icon()}
<Text variant='titleMedium'>{props.title}{props.disabled ? ' (Not supported)' : ''}</Text>
</View>
{props.description && <Text variant='bodyMedium'>{props.description}</Text>}
<View style={styles.featuresList}>
{props.featuresList.map((feature, index) => (
<View key={`feature-${index}`} style={styles.listItem}>
<Icon size={14} source='check'/><Text>{feature}</Text>
</View>
))}
</View>
</View>
</CardButton>;
};
const SyncWizard: React.FC<Props> = ({ themeId, visible, dispatch }) => {
const onDismiss = useCallback(() => {
dispatch({
type: 'SYNC_WIZARD_VISIBLE_CHANGE',
visible: false,
});
}, [dispatch]);
const onSelectJoplinCloud = useCallback(async () => {
onDismiss();
await NavService.go('JoplinCloudLogin');
}, [onDismiss]);
const onSelectOtherTarget = useCallback(async () => {
onDismiss();
await NavService.go('Config', { sectionName: 'sync' });
}, [onDismiss]);
return <DismissibleDialog
themeId={themeId}
visible={visible}
onDismiss={onDismiss}
size={DialogVariant.SmallResize}
scrollOverflow={true}
heading={_('Sync')}
>
<Text variant='bodyLarge' role='heading' style={styles.subheading}>{
_('Joplin can synchronise your notes using various providers. Select one from the list below.')
}</Text>
<View style={styles.syncProviderList}>
<SyncProvider
title={_('Joplin Cloud')}
description={_('Joplin\'s own sync service. Also gives access to Joplin-specific features such as publishing notes or collaborating on notebooks with others.')}
featuresList={[
_('Sync your notes'),
_('Publish notes to the internet'),
_('Collaborate on notebooks with others'),
]}
icon={() => <JoplinCloudIcon width={iconSize} height={iconSize}/>}
onPress={onSelectJoplinCloud}
disabled={false}
/>
<SyncProvider
title={_('Other')}
description={_('Select one of the other supported sync targets.')}
icon={() => <Icon size={iconSize} source='dots-horizontal-circle'/>}
featuresList={[]}
onPress={onSelectOtherTarget}
disabled={false}
/>
</View>
</DismissibleDialog>;
};
export default connect((state: AppState) => ({
visible: state.syncWizardVisible,
themeId: state.settings.theme,
}))(SyncWizard);

View File

@@ -89,7 +89,7 @@ describe('TagEditor', () => {
const searchResult = screen.getByRole('button', { name: 'new tag 1' });
fireEvent.press(searchResult);
expect(currentTags).toEqual(['test', 'new tag 1']);
expect(currentTags).toEqual(['new tag 1', 'test']);
// Manually unmount to prevent warnings
unmount();
@@ -115,7 +115,7 @@ describe('TagEditor', () => {
const addNewButton = screen.getByRole('button', { name: 'Add new' });
fireEvent.press(addNewButton);
expect(currentTags).toEqual(['test', 'create']);
expect(currentTags).toEqual(['create', 'test']);
unmount();
});

View File

@@ -10,6 +10,7 @@ import { TagEntity } from '@joplin/lib/services/database/types';
import { Divider } from 'react-native-paper';
import focusView from '../utils/focusView';
import { msleep } from '@joplin/utils/time';
import { getCollator, getCollatorLocale } from '@joplin/lib/models/utils/getCollator';
export enum TagEditorMode {
Large,
@@ -38,11 +39,13 @@ const useStyles = (themeId: number, headerStyle: TextStyle|undefined) => {
color: theme.color3,
flexDirection: 'row',
alignItems: 'center',
maxWidth: '100%',
gap: 4,
},
tagText: {
color: theme.color3,
fontSize: theme.fontSize,
flexShrink: 1,
},
removeTagButton: {
color: theme.color3,
@@ -122,7 +125,11 @@ const TagCard: React.FC<TagChipProps> = props => {
style={props.styles.tag}
role='listitem'
>
<Text style={props.styles.tagText}>{props.title}</Text>
<Text
ellipsizeMode='tail'
numberOfLines={1}
style={props.styles.tagText}
>{props.title}</Text>
<IconButton
pressableRef={removeButtonRef}
themeId={props.themeId}
@@ -144,23 +151,32 @@ interface TagsBoxProps {
}
const TagsBox: React.FC<TagsBoxProps> = props => {
const collatorLocale = getCollatorLocale();
const collator = useMemo(() => {
return getCollator(collatorLocale);
}, [collatorLocale]);
const onRemoveTag = useCallback((tag: string) => {
props.onRemoveTag(tag);
}, [props.onRemoveTag]);
const renderContent = () => {
if (props.tags.length) {
return props.tags.map(tag => (
<TagCard
key={`tag-${tag}`}
title={tag}
styles={props.styles}
themeId={props.themeId}
onRemove={onRemoveTag}
autofocus={props.autofocusTag === tag}
onAutoFocusComplete={props.onAutoFocusComplete}
/>
));
return props.tags
.sort((a, b) => {
return collator.compare(a, b);
})
.map(tag => (
<TagCard
key={`tag-${tag}`}
title={tag}
styles={props.styles}
themeId={props.themeId}
onRemove={onRemoveTag}
autofocus={props.autofocusTag === tag}
onAutoFocusComplete={props.onAutoFocusComplete}
/>
));
} else {
return <Text
style={props.styles.noTagsLabel}
@@ -189,15 +205,13 @@ const TagsBox: React.FC<TagsBoxProps> = props => {
</View>;
};
const normalizeTag = (tagText: string) => tagText.trim().toLowerCase();
const TagEditor: React.FC<Props> = props => {
const styles = useStyles(props.themeId, props.headerStyle);
const comboBoxItems = useMemo(() => {
return props.allTags
// Exclude tags already associated with the note
.filter(tag => !props.tags.includes(tag.title))
.filter(tag => !props.tags.some(o => o.toLowerCase() === tag.title?.toLowerCase()))
.map((tag): Option => {
const title = tag.title ?? 'Untitled';
return {
@@ -217,11 +231,13 @@ const TagEditor: React.FC<Props> = props => {
const onAddTag = useCallback((title: string) => {
AccessibilityInfo.announceForAccessibility(_('Added tag: %s', title));
props.onTagsChange([...props.tags, normalizeTag(title)]);
props.onTagsChange([...props.tags, title.trim()]);
}, [props.tags, props.onTagsChange]);
const onRemoveTag = useCallback(async (title: string) => {
const previousTagIndex = props.tags.indexOf(title);
if (!title) return;
const lowercaseTitle = title.toLowerCase();
const previousTagIndex = props.tags.findIndex(item => item.toLowerCase() === lowercaseTitle);
const targetTag = props.tags[previousTagIndex + 1] ?? props.tags[previousTagIndex - 1];
setAutofocusTag(targetTag);
@@ -229,7 +245,7 @@ const TagEditor: React.FC<Props> = props => {
// prevent focus from occasionally jumping away from the tag box.
await msleep(100);
AccessibilityInfo.announceForAccessibility(_('Removed tag: %s', title));
props.onTagsChange(props.tags.filter(tag => tag !== title));
props.onTagsChange(props.tags.filter(tag => tag.toLowerCase() !== lowercaseTitle));
}, [props.tags, props.onTagsChange]);
const onComboBoxSelect = useCallback((item: { title: string }) => {
@@ -237,16 +253,16 @@ const TagEditor: React.FC<Props> = props => {
return { willRemove: true };
}, [onAddTag]);
const allTagsSet = useMemo(() => {
const allTagsSetNormalized = useMemo(() => {
return new Set([
...props.allTags.map(tag => tag.title),
...props.tags,
...props.allTags.map(tag => tag.title?.trim()?.toLowerCase()),
...props.tags.map(tag => tag.trim().toLowerCase()),
]);
}, [props.allTags, props.tags]);
const onCanAddTag = useCallback((tag: string) => {
return !allTagsSet.has(normalizeTag(tag));
}, [allTagsSet]);
return !allTagsSetNormalized.has(tag.trim().toLowerCase());
}, [allTagsSetNormalized]);
const showAssociatedTags = props.mode === TagEditorMode.Large || props.tags.length > 0;

View File

@@ -8,6 +8,7 @@ import { themeStyle } from './global-style';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import useKeyboardState from '../utils/hooks/useKeyboardState';
import usePrevious from '@joplin/lib/hooks/usePrevious';
import FeedbackBanner from './FeedbackBanner';
interface Props {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
@@ -67,6 +68,7 @@ const AppNavComponent: React.FC<Props> = (props) => {
<NotesScreen visible={notesScreenVisible} />
{searchScreenLoaded && <SearchScreen visible={searchScreenVisible} />}
{!notesScreenVisible && !searchScreenVisible && <Screen navigation={{ state: route }} themeId={props.themeId} dispatch={props.dispatch} />}
{notesScreenVisible ? <FeedbackBanner/> : null}
<View style={{ height: autocompletionBarPadding }} />
</KeyboardAvoidingView>
);

View File

@@ -0,0 +1,72 @@
import * as React from 'react';
import { Card, TouchableRipple } from 'react-native-paper';
import { useMemo } from 'react';
import { StyleSheet, View, ViewStyle } from 'react-native';
export enum InstallState {
NotInstalled,
Installing,
Installed,
}
interface Props {
onPress: ()=> void;
disabled: boolean;
children: React.ReactNode;
style?: ViewStyle;
testID?: string;
}
const useStyles = (disabled: boolean) => {
return useMemo(() => {
// For the TouchableRipple to work on Android, the card needs a transparent background.
const baseCard = { backgroundColor: 'transparent' };
return StyleSheet.create({
cardOuterWrapper: {
margin: 0,
padding: 0,
borderRadius: 12,
overflow: 'hidden',
},
cardInnerWrapper: {
width: '100%',
},
card: disabled ? {
...baseCard,
opacity: 0.7,
} : baseCard,
content: {
gap: 5,
},
});
}, [disabled]);
};
const CardButton: React.FC<Props> = props => {
const containerIsButton = !!props.onPress;
const styles = useStyles(props.disabled);
const CardWrapper = containerIsButton ? TouchableRipple : View;
return (
<View style={[styles.cardOuterWrapper, props.style]}>
<CardWrapper
accessibilityRole={containerIsButton ? 'button' : null}
accessible={containerIsButton}
onPress={props.onPress}
disabled={props.disabled}
style={styles.cardInnerWrapper}
testID={props.testID}
>
<Card
mode='outlined'
style={styles.card}
>
{props.children}
</Card>
</CardWrapper>
</View>
);
};
export default CardButton;

View File

@@ -6,11 +6,12 @@ import createMockReduxStore from '../../utils/testing/createMockReduxStore';
import setupGlobalStore from '../../utils/testing/setupGlobalStore';
import PluginRunnerWebView from './PluginRunnerWebView';
import TestProviderStack from '../testing/TestProviderStack';
import { render, waitFor } from '../../utils/testing/testingLibrary';
import { act, render, screen, waitFor } from '../../utils/testing/testingLibrary';
import createTestPlugin from '@joplin/lib/testing/plugins/createTestPlugin';
import getWebViewDomById from '../../utils/testing/getWebViewDomById';
import Setting from '@joplin/lib/models/Setting';
import PluginService from '@joplin/lib/services/plugins/PluginService';
import CommandService from '@joplin/lib/services/CommandService';
let store: Store<AppState>;
@@ -30,6 +31,16 @@ const defaultManifestProperties = {
name: 'Some plugin name',
};
type PluginSlice = { manifest: { id: string } };
const waitForPluginToLoad = (plugin: PluginSlice) => {
return waitFor(async () => {
expect(PluginService.instance().pluginById(plugin.manifest.id)).toBeTruthy();
});
};
const webViewId = 'joplin__PluginDialogWebView';
const getUserWebViewDom = () => getWebViewDomById(webViewId);
describe('PluginRunnerWebView', () => {
beforeEach(async () => {
await setupDatabaseAndSynchronizer(0);
@@ -56,16 +67,68 @@ describe('PluginRunnerWebView', () => {
`,
});
render(<WrappedPluginRunnerWebView/>);
// Should load the plugin
await waitFor(async () => {
expect(PluginService.instance().pluginById(testPlugin.manifest.id)).toBeTruthy();
});
await waitForPluginToLoad(testPlugin);
// Should show the dialog
await waitFor(async () => {
const dom = await getWebViewDomById('joplin__PluginDialogWebView');
const dom = await getUserWebViewDom();
expect(dom.querySelector('h1').textContent).toBe('Test!');
});
});
test('should load a plugin that adds a panel', async () => {
const testPlugin = await createTestPlugin({
...defaultManifestProperties,
id: 'org.joplinapp.panel-test',
}, {
onStart: `
const panels = joplin.views.panels;
const handle = await panels.create('test-panel');
await panels.setHtml(
handle,
'<h1>Panel content</h1><p>Test</p>',
);
const commands = joplin.commands;
await commands.register({
name: 'hideTestPanel',
label: 'Hide the test plugin panel',
execute: async () => {
await panels.hide(handle);
},
});
await commands.register({
name: 'showTestPanel',
execute: async () => {
await panels.show(handle);
},
});
`,
});
render(<WrappedPluginRunnerWebView/>);
await waitForPluginToLoad(testPlugin);
act(() => {
store.dispatch({ type: 'SET_PLUGIN_PANELS_DIALOG_VISIBLE', visible: true });
});
const expectPanelVisible = async () => {
const dom = await getUserWebViewDom();
await waitFor(async () => {
expect(dom.querySelector('h1').textContent).toBe('Panel content');
});
};
await expectPanelVisible();
// Should hide the panel
await act(() => CommandService.instance().execute('hideTestPanel'));
await waitFor(() => {
expect(screen.queryByTestId('webViewId')).toBeNull();
});
// Should show the panel again
await act(() => CommandService.instance().execute('showTestPanel'));
await expectPanelVisible();
});
});

View File

@@ -12,7 +12,7 @@ import PluginUserWebView from './PluginUserWebView';
import { View, StyleSheet, AccessibilityInfo } from 'react-native';
import { _ } from '@joplin/lib/locale';
import Setting from '@joplin/lib/models/Setting';
import DismissibleDialog, { DialogSize } from '../../../components/DismissibleDialog';
import DismissibleDialog, { DialogVariant } from '../../../components/DismissibleDialog';
import CommandService from '@joplin/lib/services/CommandService';
interface Props {
@@ -120,7 +120,7 @@ const PluginPanelViewer: React.FC<Props> = props => {
}
return (
<View style={styles.webViewContainer}>
<View style={styles.webViewContainer} testID='plugin-tab-content'>
<PluginUserWebView
key={selectedTabId}
themeId={props.themeId}
@@ -164,7 +164,7 @@ const PluginPanelViewer: React.FC<Props> = props => {
<DismissibleDialog
themeId={props.themeId}
visible={props.visible}
size={DialogSize.Large}
size={DialogVariant.Large}
onDismiss={onClose}
>
{renderTabContent()}

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