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

Compare commits

...

189 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
281 changed files with 7036 additions and 3970 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
@@ -724,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
@@ -740,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
@@ -840,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
@@ -909,8 +914,6 @@ 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
@@ -1047,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
@@ -1093,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
@@ -1110,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
@@ -1123,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
@@ -1133,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
@@ -1526,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
@@ -1730,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
@@ -1845,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

@@ -59,10 +59,10 @@ jobs:
fi
# The build-tools/ directory contains different subdirectories
# for each build tools version. As a result, there may be multiple
# zipalign tools. Select one of them:
ZIPALIGN_PATH="$(find $BUILD_TOOLS_PATH -name "zipalign" -print | head -n1)"
# 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"
"$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

22
.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
@@ -697,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
@@ -713,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
@@ -813,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
@@ -882,8 +887,6 @@ 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
@@ -1020,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
@@ -1066,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
@@ -1083,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
@@ -1096,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
@@ -1106,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
@@ -1499,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
@@ -1703,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
@@ -1818,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: 109 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

@@ -73,8 +73,10 @@ 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
# Install pm2-logrotate and default settings as the runtime user
RUN pm2 install pm2-logrotate \
# 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

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://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://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

@@ -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.1",
"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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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.11",
"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",
@@ -166,7 +166,7 @@
"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

@@ -90,7 +90,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 2097780
versionName "3.4.7"
versionName "3.5.0"
ndk {
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
}

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

@@ -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.toLowerCase().includes(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

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

@@ -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

@@ -7,6 +7,7 @@ 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 { }
@@ -84,7 +85,7 @@ describe('FeedbackBanner', () => {
setupGlobalStore(store);
jest.useFakeTimers({ advanceTimers: true });
mockMobilePlatform('web');
mockMobilePlatform(MobilePlatform.Web);
});
afterEach(() => {
@@ -93,9 +94,9 @@ describe('FeedbackBanner', () => {
});
test.each([
{ platform: 'android', shouldShow: false },
{ platform: 'web', shouldShow: true },
{ platform: 'ios', shouldShow: false },
{ 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);

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);
},
@@ -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

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) {
@@ -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

@@ -4,8 +4,8 @@ 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 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';
@@ -30,7 +30,10 @@ const onReportBug = () => {
const styles = StyleSheet.create({
feedbackContainer: {
flexGrow: 1,
flexDirection: 'row',
gap: 16,
justifyContent: 'flex-end',
flexWrap: 'wrap',
},
paragraph: {
paddingBottom: 7,
@@ -65,7 +68,7 @@ const WebBetaButton: React.FC<Props> = props => {
/>
<DismissibleDialog
heading={_('Beta')}
size={DialogSize.Small}
size={DialogVariant.SmallResize}
themeId={props.themeId}
visible={dialogVisible}
onDismiss={onHideDialog}
@@ -76,7 +79,7 @@ const WebBetaButton: React.FC<Props> = props => {
{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>
<LinkButton onPress={onLeaveFeedback}>{'Give feedback'}</LinkButton>
<PrimaryButton onPress={onLeaveFeedback}>{'Give feedback'}</PrimaryButton>
</View>
</DismissibleDialog>
</>

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,
@@ -150,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}
@@ -195,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 {
@@ -223,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);
@@ -235,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 }) => {
@@ -243,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

@@ -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

@@ -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 {
@@ -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()}

View File

@@ -9,6 +9,7 @@ import { reg } from '@joplin/lib/registry';
import { State } from '@joplin/lib/reducer';
import BackButtonService from '../../../services/BackButtonService';
import { connect } from 'react-redux';
import { Dispatch } from 'redux';
import ScreenHeader from '../../ScreenHeader';
import { _ } from '@joplin/lib/locale';
import BaseScreenComponent from '../../base-screen';
@@ -60,6 +61,7 @@ interface ConfigScreenProps {
themeId: number;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
navigation: any;
dispatch: Dispatch;
}
class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, ConfigScreenState> {
@@ -126,6 +128,13 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
void NavService.go('EncryptionConfig');
};
private onShowSyncWizard_ = () => {
this.props.dispatch({
type: 'SYNC_WIZARD_VISIBLE_CHANGE',
visible: true,
});
};
private saveButton_press = async () => {
if (this.state.changedSettingKeys.includes('sync.target') && this.state.settings['sync.target'] === SyncTargetRegistry.nameToId('filesystem')) {
if (Platform.OS === 'android') {
@@ -231,11 +240,11 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
// Not implemented yet
return true;
}
return await checkPermissions(PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, {
return await checkPermissions(PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, { rationale: {
title: _('Information'),
message: _('In order to use file system synchronisation your permission to write to external storage is required.'),
buttonPositive: _('OK'),
});
} });
}
public UNSAFE_componentWillMount() {
@@ -545,6 +554,7 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
}
if (section.name === 'sync') {
addSettingButton('sync_wizard_button', _('Open Sync Wizard...'), this.onShowSyncWizard_);
addSettingButton('e2ee_config_button', _('Encryption Config'), this.e2eeConfig_);
}

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { Card, Text, TouchableRipple } from 'react-native-paper';
import { Card, Text } from 'react-native-paper';
import { _ } from '@joplin/lib/locale';
import { PluginItem } from '@joplin/lib/components/shared/config/plugins/types';
import ActionButton from '../buttons/ActionButton';
@@ -7,11 +7,12 @@ import { ButtonType } from '../../../../buttons/TextButton';
import PluginChips from './PluginChips';
import { UpdateState } from '../utils/useUpdateState';
import { PluginCallback } from '../utils/usePluginCallbacks';
import { useCallback, useMemo } from 'react';
import { useCallback } from 'react';
import { StyleSheet, View } from 'react-native';
import InstallButton from '../buttons/InstallButton';
import PluginTitle from './PluginTitle';
import RecommendedBadge from './RecommendedBadge';
import CardButton from '../../../../buttons/CardButton';
export enum InstallState {
NotInstalled,
@@ -38,28 +39,14 @@ interface Props {
onShowPluginInfo?: PluginCallback;
}
const useStyles = (compatible: boolean) => {
return useMemo(() => {
// For the TouchableRipple to work on Android, the card needs a transparent background.
const baseCard = { backgroundColor: 'transparent' };
return StyleSheet.create({
cardContainer: {
margin: 0,
marginTop: 8,
padding: 0,
borderRadius: 14,
},
card: !compatible ? {
...baseCard,
opacity: 0.7,
} : baseCard,
content: {
gap: 5,
},
});
}, [compatible]);
};
const styles = StyleSheet.create({
content: {
gap: 5,
},
cardContainer: {
marginTop: 8,
},
});
const PluginBox: React.FC<Props> = props => {
const manifest = props.item.manifest;
@@ -78,46 +65,36 @@ const PluginBox: React.FC<Props> = props => {
props.onShowPluginInfo?.({ item: props.item });
}, [props.onShowPluginInfo, props.item]);
const styles = useStyles(props.isCompatible);
const CardWrapper = props.onShowPluginInfo ? TouchableRipple : View;
const containerIsButton = !!props.onShowPluginInfo;
return (
<CardWrapper
accessibilityRole={containerIsButton ? 'button' : null}
accessible={containerIsButton}
onPress={props.onShowPluginInfo ? onPress : null}
<CardButton
style={styles.cardContainer}
onPress={props.onShowPluginInfo ? onPress : null}
testID='plugin-card'
disabled={!props.isCompatible}
>
<Card
mode='outlined'
style={styles.card}
testID='plugin-card'
>
<Card.Content style={styles.content}>
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
<View style={{ flexShrink: 1 }}>
<PluginTitle manifest={item.manifest} />
<Text numberOfLines={2}>{manifest.description}</Text>
</View>
<RecommendedBadge manifest={item.manifest} isCompatible={props.isCompatible} themeId={props.themeId} />
<Card.Content style={styles.content}>
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
<View style={{ flexShrink: 1 }}>
<PluginTitle manifest={item.manifest} />
<Text numberOfLines={2}>{manifest.description}</Text>
</View>
<PluginChips
themeId={props.themeId}
item={props.item}
showInstalledChip={props.showInstalledChip}
hasErrors={props.hasErrors}
canUpdate={props.updateState === UpdateState.CanUpdate}
onShowPluginLog={props.onShowPluginLog}
isCompatible={props.isCompatible}
/>
</Card.Content>
<Card.Actions>
{props.onAboutPress ? aboutButton : null}
{props.onInstall ? installButton : null}
</Card.Actions>
</Card>
</CardWrapper>
<RecommendedBadge manifest={item.manifest} isCompatible={props.isCompatible} themeId={props.themeId} />
</View>
<PluginChips
themeId={props.themeId}
item={props.item}
showInstalledChip={props.showInstalledChip}
hasErrors={props.hasErrors}
canUpdate={props.updateState === UpdateState.CanUpdate}
onShowPluginLog={props.onShowPluginLog}
isCompatible={props.isCompatible}
/>
</Card.Content>
<Card.Actions>
{props.onAboutPress ? aboutButton : null}
{props.onInstall ? installButton : null}
</Card.Actions>
</CardButton>
);
};

View File

@@ -5,7 +5,7 @@ import { useCallback, useMemo } from 'react';
import { Card, Divider, List, Portal, Switch, Text } from 'react-native-paper';
import getPluginIssueReportUrl from '@joplin/lib/services/plugins/utils/getPluginIssueReportUrl';
import { Linking, ScrollView, StyleSheet, View, ViewStyle } from 'react-native';
import DismissibleDialog, { DialogSize } from '../../../DismissibleDialog';
import DismissibleDialog, { DialogVariant } from '../../../DismissibleDialog';
import openWebsiteForPlugin from './utils/openWebsiteForPlugin';
import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/PluginService';
import PluginTitle from './PluginBox/PluginTitle';
@@ -253,7 +253,7 @@ const PluginInfoModal: React.FC<Props> = props => {
<DismissibleDialog
themeId={props.themeId}
visible={props.visible}
size={DialogSize.Small}
size={DialogVariant.Small}
onDismiss={props.onModalDismiss}
>
{ props.item ? <PluginInfoModalContent {...props}/> : null }

View File

@@ -6,7 +6,7 @@ import { act, fireEvent, render, screen, userEvent, waitFor } from '../../../../
import PluginService, { PluginSettings, defaultPluginSetting } from '@joplin/lib/services/plugins/PluginService';
import { writeFile } from 'fs-extra';
import { join } from 'path';
import shim from '@joplin/lib/shim';
import shim, { MobilePlatform } from '@joplin/lib/shim';
import { resetRepoApi } from './utils/useRepoApi';
import { Store } from 'redux';
import { AppState } from '../../../../utils/types';
@@ -59,7 +59,7 @@ describe('PluginStates.installed', () => {
mockPluginServiceSetup(reduxStore);
resetRepoApi();
await mockMobilePlatform('android');
await mockMobilePlatform(MobilePlatform.Android);
await mockRepositoryApiConstructor();
// Fake timers are necessary to prevent a warning.
@@ -73,8 +73,8 @@ describe('PluginStates.installed', () => {
});
it.each([
'android',
'ios',
MobilePlatform.Android,
MobilePlatform.Ios,
])('should not allow updating a plugin that is not recommended on iOS, but should on Android (on %s)', async (platform) => {
await mockMobilePlatform(platform);
expect(shim.mobilePlatform()).toBe(platform);

View File

@@ -10,6 +10,7 @@ import { Store } from 'redux';
import mockRepositoryApiConstructor from './testUtils/mockRepositoryApiConstructor';
import { resetRepoApi } from './utils/useRepoApi';
import mockPluginServiceSetup from '../../../../utils/testing/mockPluginServiceSetup';
import { MobilePlatform } from '@joplin/lib/shim';
const expectSearchResultCountToBe = async (count: number) => {
await waitFor(() => {
@@ -38,7 +39,7 @@ describe('PluginStates.search', () => {
await switchClient(0);
reduxStore = createMockReduxStore();
mockPluginServiceSetup(reduxStore);
mockMobilePlatform('android');
mockMobilePlatform(MobilePlatform.Android);
resetRepoApi();
await mockRepositoryApiConstructor();
@@ -70,7 +71,7 @@ describe('PluginStates.search', () => {
it('should only show recommended plugin search results on iOS-like environments', async () => {
// iOS uses restricted install mode
mockMobilePlatform('ios');
mockMobilePlatform(MobilePlatform.Ios);
await mockRepositoryApiConstructor();
const wrapper = render(<WrappedPluginStates initialPluginSettings={{}} store={reduxStore}/>);

View File

@@ -1,8 +1,9 @@
import { ItemEvent } from '@joplin/lib/components/shared/config/plugins/types';
import { Linking } from 'react-native';
import getPluginHelpUrl from '@joplin/lib/services/plugins/utils/getPluginHelpUrl';
const openWebsiteForPlugin = ({ item }: ItemEvent) => {
return Linking.openURL(`https://joplinapp.org/plugins/plugin/${item.manifest.id}`);
return Linking.openURL(getPluginHelpUrl(item.manifest.id));
};
export default openWebsiteForPlugin;

View File

@@ -17,6 +17,7 @@ import { Portal, ProgressBar, Snackbar } from 'react-native-paper';
import useBackHandler from '../../../utils/hooks/useBackHandler';
import Logger from '@joplin/utils/Logger';
import NavService from '@joplin/lib/services/NavService';
import { ResourceOcrDriverId, ResourceOcrStatus } from '@joplin/lib/services/database/types';
const logger = Logger.create('DocumentScanner');
@@ -77,13 +78,24 @@ const DocumentScanner: React.FC<Props> = ({ themeId, dispatch }) => {
const onCreateNote = useCallback(async (event: CreateNoteEvent) => {
setSnackbarMessage(_('Creating note "%s"...', event.title));
setCreatingNote(true);
logger.info('Creating note', event.queueForTranscription ? '(with transcription)' : '');
try {
const resources = [];
for (const image of photos) {
resources.push(await shim.createResourceFromPath(
image.uri,
{ title: event.title, mime: image.type },
{
...(event.queueForTranscription ? {
ocr_status: ResourceOcrStatus.Todo,
ocr_driver_id: ResourceOcrDriverId.HandwrittenText,
ocr_details: '',
ocr_error: '',
ocr_text: '',
} : {}),
title: event.title,
mime: image.type,
},
));
}

View File

@@ -1,7 +1,7 @@
import * as React from 'react';
import { useMemo, useState, useCallback, useEffect } from 'react';
import { themeStyle } from '../../global-style';
import { ScrollView, StyleSheet, View } from 'react-native';
import { ScrollView, StyleSheet, TextStyle, View } from 'react-native';
import { CameraResult } from '../../CameraView/types';
import TextInput from '../../TextInput';
import PhotoPreview from '../../CameraView/PhotoPreview';
@@ -15,11 +15,14 @@ import Folder from '@joplin/lib/models/Folder';
import Setting from '@joplin/lib/models/Setting';
import { formatMsToLocal } from '@joplin/utils/time';
import { PrimaryButton } from '../../buttons';
import { Switch, Text } from 'react-native-paper';
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
export interface CreateNoteEvent {
title: string;
tags: string[];
parentId: string;
queueForTranscription: boolean;
}
type OnCreateNote = (event: CreateNoteEvent)=> void;
@@ -31,6 +34,8 @@ interface Props {
allTags: TagEntity[];
allFolders: FolderEntity[];
selectedFolderId: string;
isJoplinServer: boolean;
queueForTranscriptionDefault: boolean;
onCreateNote: null|OnCreateNote;
}
@@ -38,6 +43,11 @@ interface Props {
const useStyles = (themeId: number) => {
return useMemo(() => {
const theme = themeStyle(themeId);
const headerStyle: TextStyle = {
...theme.headerStyle,
fontSize: theme.fontSize,
fontWeight: 'normal',
};
return StyleSheet.create({
titleInput: {
color: theme.color,
@@ -58,9 +68,7 @@ const useStyles = (themeId: number) => {
tagEditor: {
marginHorizontal: theme.margin,
},
tagEditorHeader: {
fontWeight: 'normal',
},
tagEditorHeader: headerStyle,
folderPickerLine: {
flexDirection: 'row',
justifyContent: 'space-between',
@@ -75,6 +83,24 @@ const useStyles = (themeId: number) => {
alignSelf: 'flex-end',
margin: theme.margin,
},
transcriptionCheckboxContainer: {
display: 'flex',
flexDirection: 'column',
marginHorizontal: theme.margin,
marginTop: theme.margin * 2,
marginBottom: theme.margin * 2,
gap: 6,
},
transcriptionCheckbox: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
},
transcriptionLabel: headerStyle,
transcriptionHelp: {
color: theme.colorFaded,
gap: 8,
},
});
}, [themeId]);
};
@@ -85,12 +111,13 @@ const tagSearchResultsProps = {
};
const NotePreview: React.FC<Props> = ({
themeId, lastImage, imageCount, allTags, onCreateNote, allFolders, selectedFolderId: propsSelectedFolderId,
themeId, lastImage, imageCount, allTags, onCreateNote, allFolders, selectedFolderId: propsSelectedFolderId, isJoplinServer, queueForTranscriptionDefault,
}) => {
const styles = useStyles(themeId);
const [title, setTitle] = useState('');
const [tags, setTags] = useState([]);
const [selectedFolderId, setSelectedFolderId] = useState(propsSelectedFolderId);
const [queueForTranscription, setQueueForTranscription] = useState(queueForTranscriptionDefault);
const realFolders = useMemo(() => {
return Folder.getRealFolders(allFolders);
@@ -115,18 +142,33 @@ const NotePreview: React.FC<Props> = ({
const onNewNote = useCallback(() => {
if (!onCreateNote) return;
Setting.setValue('scanner.requestTranscription', queueForTranscription);
onCreateNote({
tags,
title,
parentId: selectedFolderId ?? '',
queueForTranscription,
});
}, [onCreateNote, tags, title, selectedFolderId]);
}, [onCreateNote, tags, title, selectedFolderId, queueForTranscription]);
const onNewFolder = useCallback(async (title: string) => {
const folder = await Folder.save({ title });
setSelectedFolderId(folder.id);
}, []);
const transcriptionCheckbox = <View style={styles.transcriptionCheckboxContainer}>
<View style={styles.transcriptionCheckbox}>
<Text nativeID='transcriptionLabel' style={styles.transcriptionLabel}>{_('Recognise text:')}</Text>
<Switch accessibilityLabelledBy='transcriptionLabel' value={queueForTranscription} onValueChange={setQueueForTranscription} />
</View>
<View>
<Text style={styles.transcriptionHelp}>{
_('When enabled, requests that the images in the note be transcribed with a higher-quality on-server transcription service. Requires sync with a copy of the desktop app.')
}</Text>
</View>
</View>;
return <ScrollView style={styles.rootScrollView}>
<TextInput
style={styles.titleInput}
@@ -162,6 +204,7 @@ const NotePreview: React.FC<Props> = ({
headerStyle={styles.tagEditorHeader}
searchResultProps={tagSearchResultsProps}
/>
{isJoplinServer ? transcriptionCheckbox : null}
<PrimaryButton
onPress={onNewNote}
style={styles.actionButton}
@@ -174,5 +217,7 @@ export default connect((state: AppState) => ({
allTags: state.tags,
allFolders: state.folders,
selectedFolderId: state.selectedFolderId,
isJoplinServer: SyncTargetRegistry.isJoplinServerOrCloud(state.settings['sync.target']),
themeId: state.settings.theme,
queueForTranscriptionDefault: state.settings['scanner.requestTranscription'],
}))(NotePreview);

View File

@@ -73,6 +73,9 @@ import { defaultWindowId } from '@joplin/lib/reducer';
import useVisiblePluginEditorViewIds from '@joplin/lib/hooks/plugins/useVisiblePluginEditorViewIds';
import { SelectionRange } from '../../../contentScripts/markdownEditorBundle/types';
import { EditorType } from '../../NoteEditor/types';
import { IconButton } from 'react-native-paper';
import { writeTextToCacheFile } from '../../../utils/ShareUtils';
import shareFile from '../../../utils/shareFile';
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const emptyArray: any[] = [];
@@ -148,6 +151,7 @@ interface State {
};
showSpeechToTextDialog: boolean;
multiline: boolean;
}
class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> implements BaseNoteScreenComponent<State> {
@@ -219,6 +223,7 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
},
showSpeechToTextDialog: false,
multiline: false,
};
this.titleTextFieldRef = React.createRef();
@@ -508,7 +513,6 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
flexDirection: 'row',
flexBasis: 'auto',
paddingLeft: theme.marginLeft,
paddingRight: theme.marginRight,
borderBottomColor: theme.dividerColor,
borderBottomWidth: 1,
};
@@ -541,8 +545,17 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
if (Platform.OS === 'web') return;
const response = await checkPermissions(PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION, {
message: _('In order to associate a geo-location with the note, the app needs your permission to access your location.\n\nYou may turn off this option at any time in the Configuration screen.'),
title: _('Permission needed'),
onRequestConfirmation: async () => {
const yesIndex = 0;
const result = await shim.showMessageBox(
_('Joplin supports saving the location at which notes are saved or created. Do you want to enable it? This can be changed at any time in settings.'),
{
buttons: [_('Yes'), _('No')],
title: _('Save geolocation?'),
},
);
return result === yesIndex;
},
});
// If the user simply pressed "Deny", we don't automatically switch it off because they might accept
@@ -701,7 +714,13 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
}
private title_changeText(text: string) {
shared.noteComponent_change(this, 'title', text);
let newText = text;
if (Platform.OS !== 'web') {
// Manipulating the underlying text inside of onChangeText causes issues with the cursor position jumping to the end while typing
// when the Web app is being used on a desktop OS, so providing a toggle to expand the title field can only be done on mobile platforms
newText = text.replace(/(\r\n|\n|\r)/gm, ' ');
}
shared.noteComponent_change(this, 'title', newText);
this.setState({ newAndNoTitleChangeNoteId: null });
}
@@ -1049,10 +1068,33 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
}
private async share_onPress() {
await Share.share({
message: `${this.state.note.title}\n\n${this.state.note.body}`,
title: this.state.note.title,
});
const shareText = `${this.state.note.title}\n\n${this.state.note.body}`;
const filename = this.state.note.id ?? uuid.create();
if (shareText.length > 100000) {
let fileToShare;
try {
// Using a .txt file extension causes a "No valid provider found from URL" error
// and blank share sheet on iOS for larger log files (around 200 KiB).
fileToShare = await writeTextToCacheFile(shareText, `${filename}.md`);
await shareFile(fileToShare, 'text/plain');
} catch (e) {
logger.error('Unable to share note data:', e);
// Display a message to the user (e.g. in the case where the user is out of disk space).
void shim.showErrorDialog(_('Unable to share note data. Reason: %s', e.toString()));
} finally {
if (fileToShare) {
await shim.fsDriver().remove(fileToShare);
}
}
} else {
// A txt extension is automatically appended to the title when shared to a file via this route
await Share.share({
message: shareText,
title: filename,
});
}
}
private properties_onPress() {
@@ -1172,69 +1214,6 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
await CommandService.instance().execute('attachFile', filePath);
};
// private vosk_:Vosk;
// private async getVosk() {
// if (this.vosk_) return this.vosk_;
// this.vosk_ = new Vosk();
// await this.vosk_.loadModel('model-fr-fr');
// return this.vosk_;
// }
// private async voiceRecording_onPress() {
// logger.info('Vosk: Getting instance...');
// const vosk = await this.getVosk();
// this.voskResult_ = [];
// const eventHandlers: any[] = [];
// eventHandlers.push(vosk.onResult(e => {
// logger.info('Vosk: result', e.data);
// this.voskResult_.push(e.data);
// }));
// eventHandlers.push(vosk.onError(e => {
// logger.warn('Vosk: error', e.data);
// }));
// eventHandlers.push(vosk.onTimeout(e => {
// logger.warn('Vosk: timeout', e.data);
// }));
// eventHandlers.push(vosk.onFinalResult(e => {
// logger.info('Vosk: final result', e.data);
// }));
// logger.info('Vosk: Starting recording...');
// void vosk.start();
// const buttonId = await dialogs.pop(this, 'Voice recording in progress...', [
// { text: 'Stop recording', id: 'stop' },
// { text: _('Cancel'), id: 'cancel' },
// ]);
// logger.info('Vosk: Stopping recording...');
// vosk.stop();
// for (const eventHandler of eventHandlers) {
// eventHandler.remove();
// }
// logger.info('Vosk: Recording stopped:', this.voskResult_);
// if (buttonId === 'cancel') return;
// const newNote: NoteEntity = { ...this.state.note };
// newNote.body = `${newNote.body} ${this.voskResult_.join(' ')}`;
// this.setState({ note: newNote });
// this.scheduleSave();
// }
public menuOptions() {
const note = this.state.note;
const isTodo = note && !!note.is_todo;
@@ -1711,6 +1690,15 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
const dueDate = Note.dueDateObject(note);
const titleToggleButton = Platform.OS === 'web' ? null :
<IconButton
icon={(!this.state.multiline && 'menu-down') || (this.state.multiline && 'menu-up')}
accessibilityLabel={(!this.state.multiline && _('Expand title')) || (this.state.multiline && _('Collapse title'))}
onPress={() => this.setState({ multiline: !this.state.multiline })}
size={30}
style={{ width: 30, height: 30, alignSelf: 'center' }}
/>;
const titleComp = (
<View style={titleContainerStyle}>
{isTodo && <Checkbox style={this.styles().checkbox} checked={!!Number(note.todo_completed)} onChange={this.todoCheckbox_change} />}
@@ -1726,7 +1714,10 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
placeholder={_('Add title')}
placeholderTextColor={theme.colorFaded}
editable={!this.state.readOnly}
multiline={this.state.multiline}
submitBehavior = "blurAndSubmit"
/>
{ titleToggleButton }
</View>
);

View File

@@ -1,6 +1,6 @@
import * as React from 'react';
import { connect } from 'react-redux';
import { View, StyleSheet, SafeAreaView, ScrollView } from 'react-native';
import { View, StyleSheet, TextInput, Platform } from 'react-native';
import { AppState } from '../../utils/types';
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
import Revision from '@joplin/lib/models/Revision';
@@ -102,15 +102,12 @@ const useStyles = (themeId: number) => {
root: {
...theme.rootStyle,
},
titleContainer: {
titleViewContainer: {
paddingLeft: theme.marginLeft,
paddingRight: theme.marginRight,
borderTopColor: theme.dividerColor,
borderTopWidth: 1,
borderBottomColor: theme.dividerColor,
borderBottomWidth: 1,
},
titleViewContainer: {
flex: 0,
flexDirection: 'row',
flexBasis: 'auto',
@@ -139,6 +136,7 @@ const NoteRevisionViewer: React.FC<Props> = props => {
const { note, resources } = useRevisionNote(revisions, currentRevisionId);
const [initialScroll, setInitialScroll] = useState(0);
const [hasRevisions, setHasRevisions] = useState(false);
const [multiline, setMultiline] = useState(false);
const options = useMemo(() => {
const result = [];
@@ -201,6 +199,9 @@ const NoteRevisionViewer: React.FC<Props> = props => {
const onHelpPress = useCallback(() => {
void dialogs.info(helpMessageText);
}, [helpMessageText, dialogs]);
const onToggleTitlePress = useCallback(() => {
void setMultiline(!multiline);
}, [multiline]);
const styles = useStyles(props.themeId);
const dropdownLabelText = _('Revision:');
@@ -212,14 +213,25 @@ const NoteRevisionViewer: React.FC<Props> = props => {
>{restoreButtonTitle}</PrimaryButton>
);
const titleToggleButton = Platform.OS === 'web' ? null :
<IconButton
icon={(!multiline && 'menu-down') || (multiline && 'menu-up')}
accessibilityLabel={(!multiline && _('Expand title')) || (multiline && _('Collapse title'))}
onPress={onToggleTitlePress}
size={30}
style={{ width: 30, height: 30, alignSelf: 'center' }}
/>;
const titleComponent = (
<SafeAreaView style={styles.titleContainer}>
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
<View style={styles.titleViewContainer}>
<Text style={styles.titleText}>{note?.title ?? ''}</Text>
</View>
</ScrollView>
</SafeAreaView>
<View style={styles.titleViewContainer}>
<TextInput
style={styles.titleText}
value={note?.title ?? ''}
editable={false}
multiline={multiline}
/>
{ titleToggleButton }
</View>
);
return <View style={styles.root}>

View File

@@ -0,0 +1,50 @@
import * as React from 'react';
import { AppState } from '../../../utils/types';
import { Store } from 'redux';
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
import createMockReduxStore from '../../../utils/testing/createMockReduxStore';
import setupGlobalStore from '../../../utils/testing/setupGlobalStore';
import Note from '@joplin/lib/models/Note';
import { render, screen } from '../../../utils/testing/testingLibrary';
import SearchResults from './SearchResults';
import SearchEngine from '@joplin/lib/services/search/SearchEngine';
import Folder from '@joplin/lib/models/Folder';
import TestProviderStack from '../../testing/TestProviderStack';
const createNotes = async (count: number) => {
const folder = await Folder.save({ title: 'Test Note' });
for (let i = 0; i < count; i++) {
await Note.save({ title: `abcd ${i}`, body: 'body', parent_id: folder.id });
}
await SearchEngine.instance().syncTables();
};
let store: Store<AppState>;
interface WrapperProps {
query: string;
paused: boolean;
}
const WrappedSearchResults: React.FC<WrapperProps> = props => (
<TestProviderStack store={store}>
<SearchResults paused={props.paused} query={props.query} onHighlightedWordsChange={() => { }} ftsEnabled={1} />
</TestProviderStack>
);
describe('SearchResult', () => {
beforeEach(async () => {
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
store = createMockReduxStore();
setupGlobalStore(store);
});
test('should show results when unpaused', async () => {
const noteCount = 8;
await createNotes(noteCount);
render(<WrappedSearchResults query='abcd' paused={false}/>);
const items = await screen.findAllByText(/abcd \d\d?\d?/);
expect(items.length).toBe(noteCount);
});
});

View File

@@ -13,6 +13,7 @@ import shim from '@joplin/lib/shim';
interface Props {
query: string;
paused: boolean;
onHighlightedWordsChange: (highlightedWords: (ComplexTerm | string)[])=> void;
ftsEnabled: number;
@@ -28,7 +29,7 @@ const useResults = (props: Props) => {
let notes: NoteEntity[] = [];
setIsProcessing(true);
try {
if (query) {
if (query && !props.paused) {
if (ftsEnabled) {
const r = await SearchEngineUtils.notesForQuery(query, true, { appendWildCards: true });
notes = r.notes;
@@ -57,7 +58,7 @@ const useResults = (props: Props) => {
} finally {
setIsProcessing(false);
}
}, [query, ftsEnabled], { interval: 200 });
}, [query, props.paused, ftsEnabled], { interval: 200 });
return {
notes,

View File

@@ -53,11 +53,36 @@ const useStyles = (theme: ThemeStyle, visible: boolean) => {
}, [theme, visible]);
};
// Workaround for https://github.com/laurent22/joplin/issues/12823:
// Disable search-as-you-type for short 0-2 character searches that
// are likely to match the start of a large number of words.
const useSearchPaused = (query: string) => {
const [pauseDisabled, setPauseDisabled] = useState(false);
// Only disable search-as-you-type for a subset of all characters.
// This is, for example, to ensure that search-as-you-type remains
// enabled for CJK characters (e.g. U+6570 has length 1).
const paused = query.match(/^[a-z0-9]{0,2}$/i);
const onOverridePause = useCallback(() => {
setPauseDisabled(true);
}, []);
useEffect(() => {
setPauseDisabled(false);
}, [query]);
return {
paused: paused && !pauseDisabled,
onOverridePause,
};
};
const SearchScreenComponent: React.FC<Props> = props => {
const theme = themeStyle(props.themeId);
const styles = useStyles(theme, props.visible);
const [query, setQuery] = useState(props.query);
const { paused, onOverridePause } = useSearchPaused(query);
const globalQueryRef = useRef(props.query);
globalQueryRef.current = props.query;
@@ -99,6 +124,7 @@ const SearchScreenComponent: React.FC<Props> = props => {
autoFocus={props.visible}
underlineColorAndroid="#ffffff00"
onChangeText={setQuery}
onSubmitEditing={onOverridePause}
value={query}
selectionColor={theme.textSelectionColor}
keyboardAppearance={theme.keyboardAppearance}
@@ -114,6 +140,7 @@ const SearchScreenComponent: React.FC<Props> = props => {
<SearchResults
query={query}
paused={paused}
ftsEnabled={props.ftsEnabled}
onHighlightedWordsChange={onHighlightedWordsChange}
/>

View File

@@ -15,7 +15,7 @@ import useEncryptionWarningMessage from '@joplin/lib/components/shared/ShareNote
import { SharingStatus } from '@joplin/lib/components/shared/ShareNoteDialog/types';
import { AppState } from '../../utils/types';
import { connect } from 'react-redux';
import DismissibleDialog, { DialogSize } from '../DismissibleDialog';
import DismissibleDialog, { DialogVariant } from '../DismissibleDialog';
import { _, _n } from '@joplin/lib/locale';
import { LinkButton, PrimaryButton } from '../buttons';
import { themeStyle } from '../global-style';
@@ -191,7 +191,7 @@ const ShareNoteDialog: React.FC<Props> = props => {
themeId={props.themeId}
visible={props.visible}
onDismiss={props.onClose}
size={DialogSize.Small}
size={DialogVariant.Small}
heading={_('Publish Note')}
>
{props.visible ? <ShareNoteDialogContent {...props}/> : null}

View File

@@ -11,6 +11,7 @@ import { TagEntity } from '@joplin/lib/services/database/types';
import { useCallback, useMemo, useState } from 'react';
import { Dispatch } from 'redux';
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
import { getCollator, getCollatorLocale } from '@joplin/lib/models/utils/getCollator';
interface Props {
dispatch: Dispatch;
@@ -46,13 +47,17 @@ const useStyles = (themeId: number) => {
const TagsScreenComponent: React.FC<Props> = props => {
const [tags, setTags] = useState<TagEntity[]>([]);
const styles = useStyles(props.themeId);
const collatorLocale = getCollatorLocale();
const collator = useMemo(() => {
return getCollator(collatorLocale);
}, [collatorLocale]);
type TagItemPressEvent = { id: string };
useAsyncEffect(async () => {
const tags = await Tag.allWithNotes();
tags.sort((a, b) => {
return a.title.toLowerCase() < b.title.toLowerCase() ? -1 : +1;
return collator.compare(a.title, b.title);
});
setTags(tags);
}, []);

View File

@@ -96,6 +96,10 @@ const useStyles = (themeId: number) => {
...buttonStyle,
flex: 0,
};
const folderButtonTextStyle: ViewStyle = {
...buttonTextStyle,
paddingLeft: 0,
};
const styles = StyleSheet.create({
menu: {
@@ -113,9 +117,10 @@ const useStyles = (themeId: number) => {
},
sidebarIcon: sidebarIconStyle,
folderButton: folderButtonStyle,
folderButtonText: {
...buttonTextStyle,
paddingLeft: 0,
folderButtonText: folderButtonTextStyle,
conflictFolderButtonText: {
...folderButtonTextStyle,
color: theme.colorError,
},
folderButtonSelected: {
...folderButtonStyle,
@@ -264,6 +269,7 @@ const FolderItem: React.FC<FolderItemProps> = props => {
// depth is specified with an accessibilityLabel:
const folderDepthDescription = props.depth > 0 ? _('(level %d)', props.depth) : '';
const accessibilityLabel = `${folderTitle} ${folderDepthDescription}`.trim();
const folderButtonTextStyle = props.folder.id === Folder.conflictFolderId() ? baseStyles.conflictFolderButtonText : baseStyles.folderButtonText;
return (
<View key={props.folder.id} style={styles.buttonWrapper}>
<TouchableRipple
@@ -279,7 +285,7 @@ const FolderItem: React.FC<FolderItemProps> = props => {
{renderFolderIcon(props.folder.id, folderIcon)}
<Text
numberOfLines={1}
style={baseStyles.folderButtonText}
style={folderButtonTextStyle}
accessibilityLabel={accessibilityLabel}
>
{folderTitle}
@@ -502,9 +508,8 @@ const SideMenuContentComponent = (props: Props) => {
});
props.dispatch({
type: 'NAV_GO',
routeName: 'Config',
sectionName: 'sync',
type: 'SYNC_WIZARD_VISIBLE_CHANGE',
visible: true,
});
return 'init';

View File

@@ -5,9 +5,6 @@ import { _, languageName } from '@joplin/lib/locale';
import useAsyncEffect, { AsyncEffectEvent } from '@joplin/lib/hooks/useAsyncEffect';
import VoiceTyping, { OnTextCallback, VoiceTypingSession } from '../../services/voiceTyping/VoiceTyping';
import whisper from '../../services/voiceTyping/whisper';
import vosk from '../../services/voiceTyping/vosk';
import { AppState } from '../../utils/types';
import { connect } from 'react-redux';
import { RecorderState } from './types';
import RecordingControls from './RecordingControls';
import { PrimaryButton } from '../buttons';
@@ -16,19 +13,17 @@ import shim from '@joplin/lib/shim';
interface Props {
locale: string;
provider: string;
onDismiss: ()=> void;
onText: (text: string)=> void;
}
interface UseVoiceTypingProps {
locale: string;
provider: string;
onSetPreview: OnTextCallback;
onText: OnTextCallback;
}
const useVoiceTyping = ({ locale, provider, onSetPreview, onText }: UseVoiceTypingProps) => {
const useVoiceTyping = ({ locale, onSetPreview, onText }: UseVoiceTypingProps) => {
const [voiceTyping, setVoiceTyping] = useState<VoiceTypingSession>(null);
const [error, setError] = useState<Error|null>(null);
const [mustDownloadModel, setMustDownloadModel] = useState<boolean | null>(null);
@@ -43,8 +38,8 @@ const useVoiceTyping = ({ locale, provider, onSetPreview, onText }: UseVoiceTypi
voiceTypingRef.current = voiceTyping;
const builder = useMemo(() => {
return new VoiceTyping(locale, provider?.startsWith('whisper') ? [whisper] : [vosk]);
}, [locale, provider]);
return new VoiceTyping(locale, [whisper]);
}, [locale]);
const [redownloadCounter, setRedownloadCounter] = useState(0);
@@ -121,7 +116,6 @@ const SpeechToTextComponent: React.FC<Props> = props => {
locale: props.locale,
onSetPreview: setPreview,
onText: props.onText,
provider: props.provider,
});
useEffect(() => {
@@ -209,6 +203,4 @@ const SpeechToTextComponent: React.FC<Props> = props => {
/>;
};
export default connect((state: AppState) => ({
provider: state.settings['voiceTyping.preferredProvider'],
}))(SpeechToTextComponent);
export default SpeechToTextComponent;

View File

@@ -56,13 +56,11 @@ const useWebViewSetup = ({
})})
` : '';
const injectedJavaScript = useMemo(() => `
if (typeof markdownEditorBundle === 'undefined') {
${shim.injectedJs('markdownEditorBundle')};
window.markdownEditorBundle = markdownEditorBundle;
markdownEditorBundle.setUpLogger();
}
const afterLoadFinishedJs = useRef((): string => '');
// Store as a callback to avoid rebuilding the string on each content change.
// Since the editor content is included in editorOptions, for large documents,
// creating the initial injected JS is potentially expensive.
afterLoadFinishedJs.current = () => `
if (!window.cm) {
const parentClassName = ${JSON.stringify(editorOptions?.parentElementOrClassName)};
const foundParent = !!parentClassName && document.getElementsByClassName(parentClassName).length > 0;
@@ -74,6 +72,7 @@ const useWebViewSetup = ({
window.cm = markdownEditorBundle.createMainEditor(${JSON.stringify(editorOptions)});
${jumpToHashJs}
// Set the initial selection after jumping to the header -- the initial selection,
// if specified, should take precedence.
${setInitialSelectionJs}
@@ -86,7 +85,15 @@ const useWebViewSetup = ({
console.log('No parent element found with class name ', parentClassName);
}
}
`, [jumpToHashJs, setInitialSearchJs, setInitialSelectionJs, editorOptions]);
`;
const injectedJavaScript = useMemo(() => `
if (typeof markdownEditorBundle === 'undefined') {
${shim.injectedJs('markdownEditorBundle')};
window.markdownEditorBundle = markdownEditorBundle;
markdownEditorBundle.setUpLogger();
}
`, []);
// Scroll to the new hash, if it changes.
const isFirstScrollRef = useRef(true);
@@ -158,13 +165,14 @@ const useWebViewSetup = ({
const webViewEventHandlers = useMemo(() => {
return {
onLoadEnd: () => {
webviewRef.current?.injectJS(afterLoadFinishedJs.current());
editorMessenger.onWebViewLoaded();
},
onMessage: (event: OnMessageEvent) => {
editorMessenger.onWebViewMessage(event);
},
};
}, [editorMessenger]);
}, [editorMessenger, webviewRef]);
const api = useMemo(() => {
return editorMessenger.remoteApi;

View File

@@ -137,7 +137,11 @@ const useTempDirPath = () => {
return tempDirPath;
};
const useWebViewSetup = (props: Props): SetUpResult<RendererControl> => {
type Result = SetUpResult<RendererControl> & {
hasPluginScripts: boolean;
};
const useWebViewSetup = (props: Props): Result => {
const tempDirPath = useTempDirPath();
const { css, injectedJs } = useSource(tempDirPath);
const { editPopupCss, createEditPopupSyntax, destroyEditPopupSyntax } = useEditPopup(props.themeId);
@@ -269,6 +273,7 @@ const useWebViewSetup = (props: Props): SetUpResult<RendererControl> => {
};
}, [createEditPopupSyntax, destroyEditPopupSyntax, messenger]);
const hasPluginScripts = contentScripts.length > 0;
return useMemo(() => {
return {
api: rendererControl,
@@ -280,8 +285,9 @@ const useWebViewSetup = (props: Props): SetUpResult<RendererControl> => {
onLoadEnd: messenger.onWebViewLoaded,
onMessage: messenger.onWebViewMessage,
},
hasPluginScripts,
};
}, [css, injectedJs, messenger, editPopupCss, rendererControl]);
}, [css, injectedJs, messenger, editPopupCss, rendererControl, hasPluginScripts]);
};
export default useWebViewSetup;

View File

@@ -98,7 +98,7 @@ export const initialize = async (
},
});
});
editor.setSearchState(initialSearch);
editor.setSearchState(initialSearch, 'initialSearch');
messenger.setLocalInterface({
editor,

View File

@@ -44,11 +44,14 @@ const useMessenger = (props: UseMessengerProps) => {
onAttachRef.current = props.onAttachFile;
const markupRenderingSettings = useRef<RenderOptions>(null);
const baseTheme = props.settings.themeData;
markupRenderingSettings.current = {
themeId: props.themeId,
highlightedKeywords: [],
resources: props.noteResources,
themeOverrides: {},
themeOverrides: {
noteViewerFontSize: `${baseTheme.fontSize}${baseTheme.fontSizeUnits ?? 'px'}`,
},
noteHash: '',
initialScroll: 0,
pluginAssetContainerSelector: null,

View File

@@ -341,6 +341,7 @@
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RNDeviceInfo/RNDeviceInfoPrivacyInfo.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/AntDesign.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Entypo.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/EvilIcons.ttf",
@@ -363,7 +364,6 @@
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/react-native-image-picker/RNImagePickerPrivacyInfo.bundle",
);
name = "[CP] Copy Pods Resources";
@@ -373,6 +373,7 @@
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNDeviceInfoPrivacyInfo.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AntDesign.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Entypo.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EvilIcons.ttf",
@@ -395,7 +396,6 @@
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNImagePickerPrivacyInfo.bundle",
);
runOnlyForDeploymentPostprocessing = 0;
@@ -544,7 +544,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 13.4.3;
MARKETING_VERSION = 13.5.0;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -578,7 +578,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 13.4.3;
MARKETING_VERSION = 13.5.0;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -782,7 +782,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 13.4.3;
MARKETING_VERSION = 13.5.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_LDFLAGS = (
@@ -825,7 +825,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 13.4.3;
MARKETING_VERSION = 13.5.0;
MTL_FAST_MATH = YES;
OTHER_LDFLAGS = (
"$(inherited)",

View File

@@ -1406,7 +1406,7 @@ PODS:
- React-jsiexecutor
- React-RCTFBReactNativeSpec
- ReactCommon/turbomodule/core
- react-native-alarm-notification (3.4.0):
- react-native-alarm-notification (3.5.0):
- React
- react-native-document-picker (10.1.3):
- DoubleConversion
@@ -1458,7 +1458,7 @@ PODS:
- Yoga
- react-native-get-random-values (1.11.0):
- React-Core
- react-native-image-picker (8.0.0):
- react-native-image-picker (8.2.1):
- DoubleConversion
- glog
- hermes-engine
@@ -1486,7 +1486,7 @@ PODS:
- React-Core
- react-native-netinfo (11.4.1):
- React-Core
- react-native-quick-crypto (0.7.13):
- react-native-quick-crypto (0.7.17):
- DoubleConversion
- glog
- hermes-engine
@@ -1514,7 +1514,7 @@ PODS:
- Yoga
- react-native-rsa-native (2.0.5):
- React
- react-native-saf-x (3.4.1):
- react-native-saf-x (3.5.0):
- React-Core
- react-native-safe-area-context (5.4.1):
- React-Core
@@ -1522,7 +1522,7 @@ PODS:
- React-Core
- react-native-version-info (1.1.1):
- React-Core
- react-native-webview (13.13.5):
- react-native-webview (13.14.2):
- DoubleConversion
- glog
- hermes-engine
@@ -1870,11 +1870,11 @@ PODS:
- React-utils (= 0.79.2)
- rn-fetch-blob (0.12.0):
- React-Core
- RNCClipboard (1.16.2):
- RNCClipboard (1.16.3):
- React-Core
- RNCPushNotificationIOS (1.11.0):
- React-Core
- RNDateTimePicker (8.3.0):
- RNDateTimePicker (8.4.2):
- React-Core
- RNDeviceInfo (14.0.4):
- React-Core
@@ -1884,7 +1884,7 @@ PODS:
- React-Core
- RNFS (2.20.0):
- React-Core
- RNLocalize (3.4.1):
- RNLocalize (3.4.2):
- React-Core
- RNQuickAction (0.3.13):
- React
@@ -1914,6 +1914,8 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- RNSVG (15.13.0):
- React-Core
- RNVectorIcons (10.2.0):
- DoubleConversion
- glog
@@ -2055,6 +2057,7 @@ DEPENDENCIES:
- RNQuickAction (from `../node_modules/react-native-quick-actions`)
- RNSecureRandom (from `../node_modules/react-native-securerandom`)
- RNShare (from `../node_modules/react-native-share`)
- RNSVG (from `../node_modules/react-native-svg`)
- RNVectorIcons (from `../node_modules/react-native-vector-icons`)
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
@@ -2278,6 +2281,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-securerandom"
RNShare:
:path: "../node_modules/react-native-share"
RNSVG:
:path: "../node_modules/react-native-svg"
RNVectorIcons:
:path: "../node_modules/react-native-vector-icons"
Yoga:
@@ -2285,7 +2290,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90
DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb
DoubleConversion: 76ab83afb40bddeeee456813d9c04f67f78771b5
EXAV: ae28256069c4cdde93d185c007d8f68d92902c2e
EXConstants: 98bcf0f22b820f9b28f9fee55ff2daededadd2f8
Expo: 4b1c6de7c441e1caa1918671ae0aa34d51f019a5
@@ -2298,7 +2303,7 @@ SPEC CHECKSUMS:
fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6
FBLazyVector: 84b955f7b4da8b895faf5946f73748267347c975
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2
hermes-engine: 314be5250afa5692b57b4dd1705959e1973a8ebe
JoplinCommonShareExtension: a8b60b02704d85a7305627912c0240e94af78db7
JoplinRNShareExtension: e158a4b53ee0aa9cd3037a16221dc8adbd6f7860
@@ -2334,20 +2339,20 @@ SPEC CHECKSUMS:
React-logger: 8edfcedc100544791cd82692ca5a574240a16219
React-Mapbuffer: c3f4b608e4a59dd2f6a416ef4d47a14400194468
React-microtasksnativemodule: 054f34e9b82f02bd40f09cebd4083828b5b2beb6
react-native-alarm-notification: fd7c73a3dc15ce2d5bd9b28dfaa5aa2e25850c7b
react-native-alarm-notification: a4326a743df72a94d361a4c3a21515556f650341
react-native-document-picker: da39c5e4f279d39c0356dca157b98f9dc349e5bb
react-native-geolocation: ec15ffebc53790314885eb9e5f2132132fbc2600
react-native-get-random-values: d16467cf726c618e9c7a8c3c39c31faa2244bbba
react-native-image-picker: 922b9ba90f144b5866d07d04b0fb2b4e9ab0ed75
react-native-image-picker: 7babe45e727db306b3f00d08c72eda3586d6e9c1
react-native-image-resizer: 24c5d06fae2176dc0caed4b6396e02befb44064a
react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187
react-native-quick-crypto: 988d8d57cd720dbe218272b60775a8e0210d0b80
react-native-quick-crypto: b475b71e7fa4dbf3446be55e8ad4ef2c58ac4f7f
react-native-rsa-native: a7931cdda1f73a8576a46d7f431378c5550f0c38
react-native-saf-x: 3f8b52fb8160d7322161dec02a564271cc8f4138
react-native-saf-x: 8a349c8348f43ff7c14770da4b0d618d62593346
react-native-safe-area-context: dde2052b903c11d677c320b599c3244021c34ce8
react-native-sqlite-storage: 0c84826214baaa498796c7e46a5ccc9a82e114ed
react-native-version-info: f0b04e16111c4016749235ff6d9a757039189141
react-native-webview: 1b5778b306d4ed09d13829a6e7a6550e3c1a644a
react-native-webview: 2d9ffd72b87cf905cdf8821d7d27d551188bac70
React-NativeModulesApple: 2c4377e139522c3d73f5df582e4f051a838ff25e
React-oscompat: ef5df1c734f19b8003e149317d041b8ce1f7d29c
React-perflogger: 9a151e0b4c933c9205fd648c246506a83f31395d
@@ -2380,17 +2385,18 @@ SPEC CHECKSUMS:
ReactCodegen: c63eda03ba1d94353fb97b031fc84f75a0d125ba
ReactCommon: 76d2dc87136d0a667678668b86f0fca0c16fdeb0
rn-fetch-blob: 25612b6d6f6e980c6f17ed98ba2f58f5696a51ca
RNCClipboard: e1d17c9d093d8129ef50b39b63a17a0e8ccd0ade
RNCClipboard: f6679d470d0da2bce2a37b0af7b9e0bf369ecda5
RNCPushNotificationIOS: 6c4ca3388c7434e4a662b92e4dfeeee858e6f440
RNDateTimePicker: 29264364ea7b8cc0fb355b3843cf276a4ff78966
RNDateTimePicker: 392bdc0d6863b5de2fe9b957c82c25b6a038db29
RNDeviceInfo: d863506092aef7e7af3a1c350c913d867d795047
RNExitApp: 4432b9b7cc5ccec9f91c94e507849891282befd4
RNFileViewer: 4b5d83358214347e4ab2d4ca8d5c1c90d869e251
RNFS: 89de7d7f4c0f6bafa05343c578f61118c8282ed8
RNLocalize: 15463c4d79c7da45230064b4adcf5e9bb984667e
RNLocalize: 6a87f0490f1793d7a70042e4c55eb9a1ba6dd5b4
RNQuickAction: c2c8f379e614428be0babe4d53a575739667744d
RNSecureRandom: b64d263529492a6897e236a22a2c4249aa1b53dc
RNShare: 675e8e4a84f0137baf33057cac8f7334b0bb4b98
RNSVG: 295a96bc43f2baa5958d64aeec9847a1d8ca7a3d
RNVectorIcons: d53917643fddb261b22bd6d889776f336893622b
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
Yoga: c758bfb934100bb4bf9cbaccb52557cee35e8bdf

View File

@@ -105,6 +105,13 @@ jest.mock('react-native-zip-archive', () => {
jest.mock('@react-native-documents/picker', () => ({ default: { } }));
// This is one of the icon libraries that react-native-paper attempts to use.
// Throwing an Error causes react-native-paper to select a different icon library
// that better supports our automated testing environment.
jest.doMock('@expo/vector-icons/MaterialCommunityIcons', () => {
throw new Error('Not supported in testing environments.');
});
// Used by the renderer
jest.doMock('react-native-vector-icons/Ionicons', () => {
return {

View File

@@ -2,7 +2,7 @@
"name": "@joplin/app-mobile",
"description": "Joplin for Mobile",
"license": "AGPL-3.0-or-later",
"version": "3.4.0",
"version": "3.5.0",
"private": true,
"scripts": {
"start": "BROWSERSLIST_IGNORE_OLD_DATA=true react-native start --reset-cache",
@@ -22,18 +22,18 @@
},
"dependencies": {
"@bam.tech/react-native-image-resizer": "3.0.11",
"@joplin/editor": "~3.4",
"@joplin/lib": "~3.4",
"@joplin/react-native-alarm-notification": "~3.4",
"@joplin/react-native-saf-x": "~3.4",
"@joplin/renderer": "~3.4",
"@joplin/utils": "~3.4",
"@react-native-clipboard/clipboard": "1.16.2",
"@react-native-community/datetimepicker": "8.3.0",
"@joplin/editor": "~3.5",
"@joplin/lib": "~3.5",
"@joplin/react-native-alarm-notification": "~3.5",
"@joplin/react-native-saf-x": "~3.5",
"@joplin/renderer": "~3.5",
"@joplin/utils": "~3.5",
"@react-native-clipboard/clipboard": "1.16.3",
"@react-native-community/datetimepicker": "8.4.2",
"@react-native-community/geolocation": "3.4.0",
"@react-native-community/netinfo": "11.4.1",
"@react-native-community/push-notification-ios": "1.11.0",
"@react-native-documents/picker": "10.1.3",
"@react-native-documents/picker": "10.1.5",
"assert-browserify": "2.0.0",
"buffer": "6.0.3",
"color": "3.2.1",
@@ -41,9 +41,9 @@
"crypto-browserify": "3.12.1",
"deprecated-react-native-prop-types": "5.0.0",
"events": "3.3.0",
"expo": "53.0.19",
"expo": "53.0.20",
"expo-av": "15.1.7",
"expo-camera": "16.1.10",
"expo-camera": "16.1.11",
"expo-local-authentication": "16.0.5",
"lodash": "4.17.21",
"md5": "2.3.0",
@@ -58,10 +58,10 @@
"react-native-file-viewer": "2.1.5",
"react-native-fs": "2.20.0",
"react-native-get-random-values": "1.11.0",
"react-native-image-picker": "8.0.0",
"react-native-localize": "3.4.1",
"react-native-image-picker": "8.2.1",
"react-native-localize": "3.4.2",
"react-native-modal-datetime-picker": "18.0.0",
"react-native-paper": "5.13.5",
"react-native-paper": "5.14.5",
"react-native-popup-menu": "0.17.0",
"react-native-quick-actions": "0.3.13",
"react-native-quick-crypto": "0.7.17",
@@ -70,12 +70,12 @@
"react-native-securerandom": "1.0.1",
"react-native-share": "12.0.11",
"react-native-sqlite-storage": "6.0.1",
"react-native-svg": "15.13.0",
"react-native-url-polyfill": "2.0.0",
"react-native-vector-icons": "10.2.0",
"react-native-version-info": "1.1.1",
"react-native-vosk": "0.1.12",
"react-native-webview": "13.13.5",
"react-native-zip-archive": "7.0.1",
"react-native-webview": "13.15.0",
"react-native-zip-archive": "7.0.2",
"react-redux": "8.1.3",
"redux": "4.2.1",
"rn-fetch-blob": "0.12.0",
@@ -88,41 +88,41 @@
},
"devDependencies": {
"@babel/core": "7.26.0",
"@babel/plugin-transform-export-namespace-from": "7.25.9",
"@babel/plugin-transform-export-namespace-from": "7.27.1",
"@babel/preset-env": "7.25.3",
"@babel/runtime": "7.25.0",
"@joplin/tools": "~3.4",
"@joplin/tools": "~3.5",
"@joplin/turndown": "~4.0.80",
"@joplin/turndown-plugin-gfm": "~1.0.62",
"@js-draw/material-icons": "1.30.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
"@js-draw/material-icons": "1.30.1",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.0",
"@react-native-community/cli": "16.0.3",
"@react-native-community/cli-platform-android": "16.0.3",
"@react-native-community/cli-platform-ios": "16.0.3",
"@react-native/babel-preset": "0.79.2",
"@react-native/metro-config": "0.79.2",
"@react-native/typescript-config": "0.79.2",
"@react-native/babel-preset": "0.80.1",
"@react-native/metro-config": "0.79.5",
"@react-native/typescript-config": "0.79.5",
"@sqlite.org/sqlite-wasm": "3.46.0-build2",
"@testing-library/react-native": "13.2.0",
"@types/fs-extra": "11.0.4",
"@types/jest": "29.5.14",
"@types/node": "18.19.103",
"@types/node": "18.19.119",
"@types/react": "19.0.14",
"@types/react-redux": "7.1.33",
"@types/serviceworker": "0.0.135",
"@types/tar-stream": "3.1.3",
"@types/serviceworker": "0.0.142",
"@types/tar-stream": "3.1.4",
"babel-jest": "29.7.0",
"babel-loader": "9.1.3",
"babel-plugin-module-resolver": "4.1.0",
"babel-plugin-react-native-web": "0.20.0",
"esbuild": "0.25.5",
"esbuild": "0.25.6",
"fast-deep-equal": "3.1.3",
"fs-extra": "11.2.0",
"gulp": "4.0.2",
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"jetifier": "2.0.0",
"js-draw": "1.30.0",
"js-draw": "1.30.1",
"jsdom": "26.1.0",
"nodemon": "3.1.10",
"punycode": "2.3.1",
@@ -130,17 +130,17 @@
"react-native-web": "0.20.0",
"react-refresh": "0.17.0",
"react-test-renderer": "19.0.0",
"sharp": "0.34.2",
"sharp": "0.34.3",
"sqlite3": "5.1.6",
"timers-browserify": "2.0.12",
"ts-jest": "29.3.1",
"ts-jest": "29.3.4",
"ts-loader": "9.5.2",
"ts-node": "10.9.2",
"typescript": "5.8.3",
"url-loader": "4.1.1",
"webpack": "5.97.1",
"webpack-cli": "5.1.4",
"webpack-dev-server": "5.2.1"
"webpack-dev-server": "5.2.2"
},
"engines": {
"node": ">=18"

View File

@@ -107,6 +107,7 @@ import DocumentScanner from './components/screens/DocumentScanner/DocumentScanne
import buildStartupTasks from './utils/buildStartupTasks';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import appReducer from './utils/appReducer';
import SyncWizard from './components/SyncWizard/SyncWizard';
const logger = Logger.create('root');
const perfLogger = PerformanceLogger.create();
@@ -143,7 +144,7 @@ const generalMiddleware = (store: any) => (next: any) => async (action: any) =>
if (action.type === 'NAV_GO') Keyboard.dismiss();
if (['NOTE_UPDATE_ONE', 'NOTE_DELETE', 'FOLDER_UPDATE_ONE', 'FOLDER_DELETE'].indexOf(action.type) >= 0) {
if (!await reg.syncTarget().syncStarted()) void reg.scheduleSync(1000, { syncSteps: ['update_remote', 'delete_remote'] }, true);
if (!await reg.syncTarget().syncStarted()) void reg.scheduleSync(reg.syncAsYouTypeInterval(), { syncSteps: ['update_remote', 'delete_remote'] }, true);
SearchEngine.instance().scheduleSyncTables();
}
@@ -762,6 +763,7 @@ class AppComponent extends React.Component<AppComponentProps, AppComponentState>
</View>
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied */}
<DropdownAlert alert={(func: any) => (this.dropdownAlert_ = func)} />
<SyncWizard/>
</SafeAreaView>
</View>
</SideMenu>

View File

@@ -1,197 +0,0 @@
import { languageCodeOnly } from '@joplin/lib/locale';
import Logger from '@joplin/utils/Logger';
import Setting from '@joplin/lib/models/Setting';
import { rtrimSlashes } from '@joplin/lib/path-utils';
import shim from '@joplin/lib/shim';
import Vosk from 'react-native-vosk';
import RNFetchBlob from 'rn-fetch-blob';
import { VoiceTypingProvider, VoiceTypingSession } from './VoiceTyping';
import { join } from 'path';
const logger = Logger.create('voiceTyping/vosk');
enum State {
Idle = 0,
Recording,
Completing,
}
interface StartOptions {
onResult: (text: string)=> void;
}
let vosk_: Record<string, Vosk> = {};
let state_: State = State.Idle;
const defaultSupportedLanguages = {
'en': 'https://alphacephei.com/vosk/models/vosk-model-small-en-us-0.15.zip',
'zh': 'https://alphacephei.com/vosk/models/vosk-model-small-cn-0.22.zip',
'ru': 'https://alphacephei.com/vosk/models/vosk-model-small-ru-0.22.zip',
'fr': 'https://alphacephei.com/vosk/models/vosk-model-small-fr-0.22.zip',
'de': 'https://alphacephei.com/vosk/models/vosk-model-small-de-0.15.zip',
'es': 'https://alphacephei.com/vosk/models/vosk-model-small-es-0.42.zip',
'pt': 'https://alphacephei.com/vosk/models/vosk-model-small-pt-0.3.zip',
'tr': 'https://alphacephei.com/vosk/models/vosk-model-small-tr-0.3.zip',
'vn': 'https://alphacephei.com/vosk/models/vosk-model-small-vn-0.4.zip',
'it': 'https://alphacephei.com/vosk/models/vosk-model-small-it-0.22.zip',
'nl': 'https://alphacephei.com/vosk/models/vosk-model-small-nl-0.22.zip',
'uk': 'https://alphacephei.com/vosk/models/vosk-model-small-uk-v3-small.zip',
'ja': 'https://alphacephei.com/vosk/models/vosk-model-small-ja-0.22.zip',
'hi': 'https://alphacephei.com/vosk/models/vosk-model-small-hi-0.22.zip',
'cs': 'https://alphacephei.com/vosk/models/vosk-model-small-cs-0.4-rhasspy.zip',
'pl': 'https://alphacephei.com/vosk/models/vosk-model-small-pl-0.22.zip',
'uz': 'https://alphacephei.com/vosk/models/vosk-model-small-uz-0.22.zip',
'ko': 'https://alphacephei.com/vosk/models/vosk-model-small-ko-0.22.zip',
};
export const isSupportedLanguage = (locale: string) => {
const l = languageCodeOnly(locale).toLowerCase();
return Object.keys(defaultSupportedLanguages).includes(l);
};
// Where all the models files for all the languages are
const getModelRootDir = () => {
return `${RNFetchBlob.fs.dirs.DocumentDir}/vosk-models`;
};
// Where we unzip a model after downloading it
const getUnzipDir = (locale: string) => {
return `${getModelRootDir()}/${locale}`;
};
// Where the model for a particular language is
const getModelDir = (locale: string) => {
return `${getUnzipDir(locale)}/model`;
};
const languageModelUrl = (locale: string): string => {
const lang = languageCodeOnly(locale).toLowerCase();
if (!(lang in defaultSupportedLanguages)) throw new Error(`No language file for: ${locale}`);
const urlTemplate = rtrimSlashes(Setting.value('voiceTypingBaseUrl').trim());
if (urlTemplate) {
let url = rtrimSlashes(urlTemplate);
if (!url.includes('{lang}')) url += '/{lang}.zip';
return url.replace(/\{lang\}/g, lang);
} else {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
return (defaultSupportedLanguages as any)[lang];
}
};
export const getVosk = async (modelDir: string, locale: string) => {
if (vosk_[locale]) return vosk_[locale];
const vosk = new Vosk();
logger.info(`Loading model from ${modelDir}`);
await shim.fsDriver().readDirStats(modelDir);
const result = await vosk.loadModel(modelDir);
logger.info('getVosk:', result);
vosk_ = { [locale]: vosk };
return vosk;
};
export const startRecording = (vosk: Vosk, options: StartOptions): VoiceTypingSession => {
if (state_ !== State.Idle) throw new Error('Vosk is already recording');
state_ = State.Recording;
const result: string[] = [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const eventHandlers: any[] = [];
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
const finalResultPromiseResolve: Function = null;
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
const finalResultPromiseReject: Function = null;
const finalResultTimeout = false;
const completeRecording = (finalResult: string, error: Error) => {
logger.info(`Complete recording. Final result: ${finalResult}. Error:`, error);
for (const eventHandler of eventHandlers) {
eventHandler.remove();
}
vosk.cleanup();
state_ = State.Idle;
if (error) {
if (finalResultPromiseReject) finalResultPromiseReject(error);
} else {
if (finalResultPromiseResolve) finalResultPromiseResolve(finalResult);
}
};
eventHandlers.push(vosk.onResult(e => {
const text = e.data;
logger.info('Result', text);
result.push(text);
options.onResult(text);
}));
eventHandlers.push(vosk.onError(e => {
logger.warn('Error', e.data);
}));
eventHandlers.push(vosk.onTimeout(e => {
logger.warn('Timeout', e.data);
}));
eventHandlers.push(vosk.onFinalResult(e => {
logger.info('Final result', e.data);
if (finalResultTimeout) {
logger.warn('Got final result - but already timed out. Not doing anything.');
return;
}
completeRecording(e.data, null);
}));
const stopOrCancel = () => {
if (state_ === State.Recording) {
logger.info('Cancelling...');
state_ = State.Completing;
vosk.stopOnly();
completeRecording('', null);
}
};
return {
start: async () => {
logger.info('Starting recording...');
await vosk.start();
},
stop: async () => {
stopOrCancel();
},
cancel: async () => {
stopOrCancel();
},
};
};
const vosk: VoiceTypingProvider = {
supported: () => true,
modelLocalFilepath: (locale: string) => getModelDir(locale),
deleteCachedModels: async (locale: string) => {
const path = getModelDir(locale);
await shim.fsDriver().remove(path, { recursive: true });
},
getDownloadUrl: (locale) => languageModelUrl(locale),
getUuidPath: (locale: string) => join(getModelDir(locale), 'uuid'),
build: async ({ callbacks, locale, modelPath }) => {
const vosk = await getVosk(modelPath, locale);
return startRecording(vosk, { onResult: callbacks.onFinalize });
},
modelName: 'vosk',
};
export default vosk;

View File

@@ -1,15 +0,0 @@
import { VoiceTypingProvider } from './VoiceTyping';
const vosk: VoiceTypingProvider = {
supported: () => false,
modelLocalFilepath: () => null,
getDownloadUrl: () => null,
getUuidPath: () => null,
deleteCachedModels: () => null,
build: async () => {
throw new Error('Unsupported!');
},
modelName: 'vosk',
};
export default vosk;

View File

@@ -17,6 +17,7 @@ const appDefaultState: AppState = {
disableSideMenuGestures: false,
showPanelsDialog: false,
noteEditorVisible: false,
syncWizardVisible: false,
...defaultState,
// On mobile, it's possible to select notes that aren't in the selected folder/tag/etc.

View File

@@ -195,6 +195,10 @@ const appReducer = (state = appDefaultState, action: any) => {
case 'NOTE_EDITOR_VISIBLE_CHANGE':
newState = { ...state, noteEditorVisible: action.visible };
break;
case 'SYNC_WIZARD_VISIBLE_CHANGE':
newState = { ...state, syncWizardVisible: action.visible };
break;
}
} catch (error) {
error.message = `In reducer: ${error.message} Action: ${JSON.stringify(action)}`;

View File

@@ -1,25 +1,36 @@
import Logger from '@joplin/utils/Logger';
const { Platform, PermissionsAndroid } = require('react-native');
import { Platform, PermissionsAndroid, Permission } from 'react-native';
const logger = Logger.create('checkPermissions');
type rationale = {
type Rationale = {
title: string;
message: string;
buttonPositive?: string;
buttonPositive: string;
buttonNegative?: string;
buttonNeutral?: string;
};
export default async (permissions: string, rationale?: rationale) => {
interface Options {
rationale?: Rationale;
onRequestConfirmation?: ()=> Promise<boolean>;
}
export default async (permissions: Permission, { rationale, onRequestConfirmation }: Options = {}) => {
// On iOS, permissions are prompted for by the system, so here we assume it's granted.
if (Platform.OS !== 'android') return PermissionsAndroid.RESULTS.GRANTED;
let result = await PermissionsAndroid.check(permissions);
logger.info('Checked permission:', result);
if (result !== PermissionsAndroid.RESULTS.GRANTED) {
result = await PermissionsAndroid.request(permissions, rationale);
const granted = await PermissionsAndroid.check(permissions);
logger.info('Checked permission:', granted);
if (granted) {
return PermissionsAndroid.RESULTS.GRANTED;
} else {
if (onRequestConfirmation && !await onRequestConfirmation()) {
return PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN;
}
const result = await PermissionsAndroid.request(permissions, rationale);
logger.info('Requested permission:', result);
return result;
}
return result;
};

View File

@@ -3,6 +3,7 @@ import { Platform } from 'react-native';
const lockToSingleInstance = async () => {
if (Platform.OS !== 'web') return;
if (__DEV__) return;
const channel = new BroadcastChannel('single-instance-lock');
channel.postMessage('app-opened');

View File

@@ -1,6 +1,6 @@
import shimInitShared from './shimInitShared';
import shim from '@joplin/lib/shim';
import shim, { MobilePlatform } from '@joplin/lib/shim';
const { GeolocationReact } = require('../geolocation-react.js');
import RNFetchBlob from 'rn-fetch-blob';
import { generateSecureRandom } from 'react-native-securerandom';
@@ -165,7 +165,7 @@ export default function shimInit() {
};
shim.mobilePlatform = () => {
return Platform.OS;
return Platform.OS as MobilePlatform;
};
shim.isAppleSilicon = () => {

View File

@@ -9,7 +9,7 @@ import * as mimeUtils from '@joplin/lib/mime-utils';
import Resource from '@joplin/lib/models/Resource';
import { getLocales } from 'react-native-localize';
import type Setting from '@joplin/lib/models/Setting';
import shim from '@joplin/lib/shim';
import shim, { MobilePlatform } from '@joplin/lib/shim';
import { closestSupportedLocale, defaultLocale, setLocale } from '@joplin/lib/locale';
const shimInitShared = () => {
@@ -76,7 +76,7 @@ const shimInitShared = () => {
};
shim.mobilePlatform = () => {
return Platform.OS;
return Platform.OS as MobilePlatform;
};
shim.platformArch = () => {
@@ -116,8 +116,7 @@ const shimInitShared = () => {
const resourceId = defaultProps.id ? defaultProps.id : uuid.create();
const ext = fileExtension(filePath);
let mimeType = mimeUtils.fromFileExtension(ext);
if (!mimeType) mimeType = 'image/jpeg';
const mimeType = defaultProps.mime ?? mimeUtils.fromFileExtension(ext) ?? 'image/jpeg';
let resource = Resource.new();
resource.id = resourceId;

View File

@@ -11,4 +11,5 @@ export interface AppState extends State {
noteSideMenuOptions: any;
disableSideMenuGestures: boolean;
noteEditorVisible: boolean;
syncWizardVisible: boolean;
}

View File

@@ -0,0 +1 @@
throw new Error('Failed to load');

View File

@@ -45,6 +45,7 @@ const buildSharedConfig = (hotReload: boolean): webpack.Configuration => {
};
const emptyLibraryMock = path.resolve(__dirname, 'mocks/empty.js');
const throwOnLoadLibraryMock = path.resolve(__dirname, 'mocks/throwOnLoad.js');
return {
output: {
@@ -78,6 +79,8 @@ const buildSharedConfig = (hotReload: boolean): webpack.Configuration => {
'@react-native-documents/picker': emptyLibraryMock,
'react-native-exit-app': emptyLibraryMock,
'expo-camera': emptyLibraryMock,
// Remove this after upgrading react-native-vector-icons.
'@react-native-vector-icons/material-design-icons': throwOnLoadLibraryMock,
// Workaround for applying serviceworker types to a single file.
// See https://joshuatz.com/posts/2021/strongly-typed-service-workers/.

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