1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-27 20:29:45 +02:00

Compare commits

...

179 Commits

Author SHA1 Message Date
Laurent Cozic
a2069df3e0 Android 3.1.6 2024-10-17 23:14:16 +01:00
Laurent Cozic
1ad150c1bf Desktop release v3.1.19 2024-10-17 23:05:46 +01:00
Henry Heino
41b251d67a Linux: Move keychain support behind an off-by-default feature flag (#11227) 2024-10-17 22:58:03 +01:00
Henry Heino
2c40cec639 Chore: Desktop: Fix incorrect log tag (#11215) 2024-10-17 22:49:50 +01:00
Henry Heino
efb58c5f40 Desktop: Fix error screen shown on opening settings when an incompatible plugin is installed (#11223) 2024-10-17 22:49:29 +01:00
Henry Heino
9d8cd1d707 Desktop: Security: Open more target="_blank" links in a browser (#11212) 2024-10-15 16:38:33 +01:00
Henry Heino
591c458a4f Desktop: Security: Improve KaTeX error handling (#11207) 2024-10-15 16:37:15 +01:00
Laurent Cozic
f9b1a32ae7 Tools: Update script to test plugins 2024-10-14 17:52:31 +01:00
Henry Heino
1a195e23dd Desktop: Security: Improve Markdown viewer link handling (#11201) 2024-10-14 17:51:28 +01:00
Joplin Bot
26ae3f853e Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-10-12 00:47:59 +00:00
Laurent Cozic
e84e9a58e1 iOS 13.1.5 2024-10-11 23:29:52 +01:00
Laurent Cozic
3b8da5023d Tools: Always run pod install 2024-10-11 23:28:45 +01:00
Laurent Cozic
548d41d0d4 lock files 2024-10-11 23:28:29 +01:00
Laurent Cozic
d6c921249f iOS 13.1.4 2024-10-11 23:21:27 +01:00
Laurent Cozic
e044c50b03 Android 3.1.5 2024-10-11 23:16:57 +01:00
Laurent Cozic
beec74d792 Desktop release v3.1.18 2024-10-11 23:05:38 +01:00
pedr
8b4e163b28 Server: Fixes #10532: Fix PostgreSQL version check failing on Windows Server because wrong regex (#11038) 2024-10-11 22:26:01 +01:00
Henry Heino
b61467097d Mobile: Fixes #11134: Fix automatic resource download mode (#11144) 2024-10-11 22:14:18 +01:00
Matthew Moore
447e4638d1 add typecript information regarding turndown-plugin-gfm (#11153)
Co-authored-by: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com>
2024-10-11 22:14:05 +01:00
ScriptInfra
b831525b20 Update faq.md (#11169)
Co-authored-by: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2024-10-11 22:12:05 +01:00
Henry Heino
e05be832d5 Desktop, Mobile: Downgrade CodeMirror packages to fix various Android regressions (#11170) 2024-10-11 22:08:17 +01:00
Henry Heino
64c9c3179f Chore: Update KaTeX asset files (#11172) 2024-10-11 22:08:07 +01:00
Henry Heino
0ea61f26eb Desktop: Accessibility: Fix context menu button doesn't open the note list context menu (regression) (#11175) 2024-10-11 22:07:56 +01:00
Henry Heino
349fa426ea Mobile: Fixes #11183: Fix new note/edit buttons only work if pressed quickly (#11185) 2024-10-11 22:04:29 +01:00
Henry Heino
e3d5f0c9cf Chore: Desktop: Use SCSS instead of styled-components for plugin dialogs and toolbar buttons (#11189) 2024-10-11 22:03:41 +01:00
Joplin Bot
e63d545ed8 Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-10-11 18:20:26 +00:00
Laurent Cozic
ab3058612d Doc: Add sponsor 2024-10-11 19:00:43 +01:00
Laurent Cozic
715abcce32 Plugins: Add support for joplin.settings.values and deprecate joplin.settings.value 2024-10-11 18:56:04 +01:00
renovate[bot]
f165b3f870 Update dependency @types/serviceworker to v0.0.89 (#11191) 2024-10-11 03:01:01 +00:00
renovate[bot]
8895d745e7 Update dependency glob to v10.4.5 (#11192) 2024-10-11 03:01:00 +00:00
renovate[bot]
33a9b96a31 Update dependency pm2 to v5.4.2 (#11193) 2024-10-11 03:00:52 +00:00
github-actions[bot]
d1ac3d415e @moorage has signed the CLA in laurent22/joplin#11153 2024-10-05 22:34:48 +00:00
Henry Heino
432fac8fda Chore: Fix CI (#11173) 2024-10-05 12:22:44 -07:00
renovate[bot]
0f23882d47 Update dependency nodemon to v3.1.7 (#11162) 2024-10-01 02:49:36 +00:00
renovate[bot]
693c0f22c8 Update eslint (#11163) 2024-10-01 02:49:34 +00:00
renovate[bot]
e2db7a6b61 Update types (#11164) 2024-10-01 02:49:27 +00:00
renovate[bot]
2a74f60812 Update dependency katex to v0.16.11 (#11159) 2024-10-01 02:19:59 +00:00
renovate[bot]
2419291976 Update dependency nodemon to v3.1.4 (#11160) 2024-10-01 02:19:57 +00:00
renovate[bot]
733845eb95 Update eslint (#11161) 2024-10-01 02:19:50 +00:00
Joplin Bot
b3315aeb03 Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-10-01 00:52:32 +00:00
renovate[bot]
d88c522d96 Update dependency @react-native/babel-preset to v0.74.85 (#11156) 2024-09-30 02:46:33 +00:00
renovate[bot]
c0cefc30f4 Update dependency @react-native/metro-config to v0.74.85 (#11157) 2024-09-30 02:17:39 +00:00
renovate[bot]
0dc3589661 Update dependency nodemon to v3.1.2 (#11140)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-28 16:24:50 +01:00
Henry Heino
f64c3d5484 Desktop: Remove Math Mode from the list of plugins incompatible with the new editor (#11143) 2024-09-28 16:21:59 +01:00
Henry Heino
5fceb5a3c9 Chore: Reduce mobile note screen test flakiness (#11145) 2024-09-28 16:20:46 +01:00
renovate[bot]
916b3f6f69 Update dependency rate-limiter-flexible to v5.0.3 (#11148) 2024-09-28 11:15:33 +00:00
renovate[bot]
0c4e8eeafc Update dependency react-native-document-picker to v9.3.0 (#11141)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-28 11:30:30 +01:00
renovate[bot]
b27e0ff1f4 Update dependency rate-limiter-flexible to v5 (#11147)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-28 11:29:30 +01:00
Laurent Cozic
59ffb0f265 Update renovate.json5 2024-09-28 11:29:10 +01:00
renovate[bot]
20b4fd85c1 Update dependency react-native-safe-area-context to v4.10.7 (#11138) 2024-09-27 20:52:29 +00:00
renovate[bot]
fc2da05ba6 Update dependency stream to v0.0.3 (#11139) 2024-09-27 20:52:22 +00:00
Henry Heino
948ca605b0 Mobile,Desktop: Fixes #11135: Fix incorrect list switching behavior (#11137) 2024-09-27 21:28:56 +01:00
Henry Heino
eda2c69334 Desktop: Fixes #11129: Improve performance by allowing note list background timers to be cancelled (#11133) 2024-09-27 15:25:55 +01:00
Henry Heino
42ab9ecd95 Mobile: Fixes #11130: Fix regression: Search screen not hidden when cached for search result navigation (#11131) 2024-09-27 15:23:31 +01:00
Henry Heino
5935c9c147 Chore: Mobile: Improve note screen tests and fix CI warning (#11126) 2024-09-27 15:23:02 +01:00
Joplin Bot
90640e590e Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-09-26 12:25:12 +00:00
Laurent Cozic
75b8caf816 Desktop, Mobile: Plugins: Name webview root attribute so that it can be styled 2024-09-26 11:40:13 +01:00
Laurent Cozic
3ea403d004 Desktop release v3.1.17 2024-09-26 11:36:01 +01:00
Laurent Cozic
058a559de4 Desktop: Enable again auto-updates 2024-09-26 11:35:42 +01:00
Laurent Cozic
ac43c62ce8 Chore: Disable custom protocol debug logging 2024-09-26 11:35:42 +01:00
Henry Heino
c4a7749f2a Desktop: Fixes #11105: Plugin API: Save changes made with editor.setText (#11117) 2024-09-26 11:35:32 +01:00
Joplin Bot
e6c09da639 Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-09-24 18:20:50 +00:00
Laurent Cozic
2d05b5f43e Android 3.1.4 2024-09-24 15:23:16 +01:00
Laurent Cozic
63d0855a59 Desktop release v3.1.16 2024-09-24 15:15:43 +01:00
Henry Heino
3d42485315 Mobile: Show loading indicator while loading search results (#11104) 2024-09-24 15:12:02 +01:00
renovate[bot]
f772cc500c Update dependency jsdom to v24.1.0 (#11114)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-23 20:16:50 +01:00
renovate[bot]
ad8bcacbca Update dependency glob to v10.4.2 (#11112) 2024-09-23 17:59:26 +00:00
renovate[bot]
fbab549a1c Update dependency pm2 to v5.4.1 (#11113) 2024-09-23 17:59:19 +00:00
pedr
817f3bc121 Cli: Fixes #10992: Disable deprecation warning when running Joplin from CLI (#11074) 2024-09-23 18:20:13 +01:00
pedr
e3576683b0 Desktop,Mobile,Cli: Fixes #10608: WebDAV synchronisation not working because of URL encoding differences (#11076) 2024-09-23 18:19:51 +01:00
renovate[bot]
85c2eb43dd Update dependency turndown to v7.2.0 (#11103)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-22 01:45:21 +01:00
renovate[bot]
0f2b2b1e7b Update dependency pm2 to v5.4.0 (#11102)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-22 01:45:05 +01:00
renovate[bot]
8fd2eeaea5 Update dependency glob to v10.4.1 (#11101)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-22 01:44:56 +01:00
renovate[bot]
b97a14c559 Update dependency nodemailer to v6.9.14 (#11098) 2024-09-21 17:43:23 +00:00
renovate[bot]
bbb97bcb02 Update dependency react-native-webview to v13.10.4 (#11099) 2024-09-21 17:43:21 +00:00
renovate[bot]
8a51ed892a Update dependency sass to v1.77.6 (#11100) 2024-09-21 17:43:14 +00:00
Henry Heino
0cac69c2fa Chore: Migrate back-button.js to TypeScript (#11087) 2024-09-21 18:28:41 +01:00
Henry Heino
feb946acfb Chore: Mobile: Search screen: Use stronger types, try to prevent multiple concurrent attempts to update the result list (#11075) 2024-09-21 18:28:33 +01:00
Henry Heino
220f867814 Mobile: Resolves #10763: Support permanent note deletion on mobile (#10786) 2024-09-21 13:05:27 +01:00
Henry Heino
050a896c8b Android,Web: Fix scroll issues and incorrect main content height (#11071) 2024-09-21 13:05:17 +01:00
Henry Heino
d13e7b32c3 Desktop,Mobile,Cli: Fixes #11017: Delete revisions on the sync target when deleted locally (#11035) 2024-09-21 13:04:54 +01:00
Alice
a56f104fe8 Desktop: Seamless-Updates: triggering updates (#11079) 2024-09-21 13:02:22 +01:00
Henry Heino
99696637b9 Chore: Desktop: Add extra check to try to prevent duplicate setting key warning (#11084) 2024-09-21 13:01:08 +01:00
Henry Heino
be5a6c189a Desktop: New Markdown editor: Fix horizontal rule button when cursor is not on a new line (#11085) 2024-09-21 13:00:43 +01:00
Henry Heino
a01f519131 Mobile: Resolves #11082: Make pressing "back" navigate to the previous note after following a link (#11086) 2024-09-21 13:00:31 +01:00
Dave Goldberg
a71ee1d0b8 Doc: Add Backblaze to S3 Providers (#11088)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2024-09-21 12:59:38 +01:00
Henry Heino
a40bb77feb Mobile: Use fade animation for edit link dialog (#11090) 2024-09-21 12:58:20 +01:00
Henry Heino
5c23765458 Mobile: Scroll dropdown to selected value when first opened (#11091) 2024-09-21 12:58:01 +01:00
Henry Heino
d023ce592c Desktop: Fix ctrl/cmd-n can create new notes while the trash folder is selected (#11092) 2024-09-21 12:57:48 +01:00
Henry Heino
8c4bf057d6 Chore: Mobile: Improve Notes screen type safety (#11093) 2024-09-21 12:57:38 +01:00
Henry Heino
b9dc226031 Chore: Mobile: Migrate NoteItem and Checkbox to TypeScript (#11094) 2024-09-21 12:57:26 +01:00
Henry Heino
a81c1ff663 Chore: Mobile: Convert side-menu-content-note.js to a TypeScript function component (#11095) 2024-09-21 12:57:08 +01:00
Jonatan
c909d85acc Update Swedish translation (#11096) 2024-09-21 12:56:55 +01:00
Henry Heino
0965c6d257 Desktop,Mobile: Fixes #11065: Improve performance when there are many selected items (#11067) 2024-09-21 12:53:16 +01:00
Laurent Cozic
5beb80bf61 Desktop, Mobile: Automatically detect and use operating system theme by default 2024-09-20 19:31:35 +01:00
renovate[bot]
1b2f5e5cd8 Update dependency @types/node to v18.19.39 (#11089) 2024-09-20 17:52:37 +00:00
github-actions[bot]
2db82ac732 @dhgoldberg has signed the CLA in laurent22/joplin#11088 2024-09-20 14:29:03 +00:00
renovate[bot]
3f1ec682b9 Update dependency @types/node to v18.19.38 (#11080) 2024-09-19 05:27:44 +00:00
renovate[bot]
59b3030e45 Update dependency @types/node to v18.19.37 (#11078) 2024-09-18 11:52:31 +00:00
Joplin Bot
54d223a721 Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-09-17 12:24:41 +00:00
Laurent Cozic
e5771a36bb Desktop release v3.1.15 2024-09-17 09:37:02 +01:00
Laurent Cozic
31a5ee20df Tools: Trying to fix autoupdate CI script 2024-09-17 09:35:39 +01:00
Laurent Cozic
efd9ada977 Desktop release v3.1.14 2024-09-17 07:37:06 +01:00
Alice
b4450ae4ef Desktop: Seamless-Updates: used axios for download function (#11068) 2024-09-17 07:36:53 +01:00
github-actions[bot]
73076bd4b7 @mrjo118 has signed the CLA in laurent22/joplin#11069 2024-09-17 00:13:07 +00:00
Laurent Cozic
0ba0550baf Desktop release v3.1.13 2024-09-16 22:24:40 +01:00
Laurent Cozic
41b03f9356 macOS: Fixed shortcut for permanent note deletion 2024-09-16 22:22:27 +01:00
et al.
95f1992b8a Update Simplified Chinese translation (#11061) 2024-09-16 22:22:08 +01:00
Henry Heino
11c1c0638d Desktop: Resolves #11063: Improve the performance of GoToAnything (#11064) 2024-09-16 22:20:44 +01:00
Henry Heino
e0daf807a6 Mobile: Fixes #11028: Accessibility: Fix sidebar broken in right-to-left mode, improve screen reader accessibility (#11056) 2024-09-16 22:17:12 +01:00
Alice
2594c1edb1 Desktop: Seamless-Updates: used download function from tool-utils (#11066) 2024-09-16 22:13:24 +01:00
github-actions[bot]
e80bede7b7 @howfool has signed the CLA in laurent22/joplin#11061 2024-09-16 13:37:59 +00:00
renovate[bot]
1eb721c717 Update dependency @types/node to v18.19.36 (#11057) 2024-09-16 03:03:06 +00:00
renovate[bot]
38b6484f12 Update dependency @types/node to v18.19.35 (#11055) 2024-09-15 19:42:31 +00:00
Laurent Cozic
a0163ba793 Desktop release v3.1.12 2024-09-15 20:17:57 +01:00
Alice
e2e589e907 Desktop: Seamless-Updates: added debugging logs for createReleaseAssets (#11053) 2024-09-15 20:17:44 +01:00
renovate[bot]
93f96c03b1 Update dependency react-native-safe-area-context to v4.10.5 (#11051) 2024-09-15 14:52:44 +00:00
Laurent Cozic
77f09a4408 Desktop release v3.1.11 2024-09-15 15:32:08 +01:00
Alice
faf30306da Desktop: Seamless-Updates: updated asset name locally after update call to GitHub API (#11050) 2024-09-15 15:31:41 +01:00
Laurent Cozic
c1c02204fa Desktop release v3.1.10 2024-09-15 11:02:44 +01:00
Alice
017480eb45 Desktop: Seamless-Updates: used url instead of browser url and added api heade… (#11049) 2024-09-15 11:02:02 +01:00
Laurent Cozic
8931a68ec8 Desktop release v3.1.9 2024-09-14 22:24:02 +01:00
Henry Heino
3c6a419cad Desktop: Accessibility: Add "Move to" context menu action for notebooks (#11039) 2024-09-14 22:18:07 +01:00
renovate[bot]
dce4c715e3 Update dependency markdown-it-sup to v2 (#11048)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-14 22:16:57 +01:00
Alice
5763de3b26 Desktop: Seamless-Updates: generated and uploaded latest-mac-arm64.yml to GitHub Releases (#11042) 2024-09-14 22:16:42 +01:00
renovate[bot]
4fa61e443f Update dependency markdown-it-ins to v4 (#11045)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-14 18:39:39 +01:00
renovate[bot]
84e312563a Update dependency react-native-device-info to v10.14.0 (#11044)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-14 18:38:33 +01:00
renovate[bot]
707c21a2fe Update dependency sass to v1.77.5 (#11043) 2024-09-14 15:22:02 +00:00
renovate[bot]
d0057ae838 Update dependency lint-staged to v15.2.7 (#11041) 2024-09-14 14:52:40 +00:00
pedr
8d3ac630c5 Desktop: Fixes #10560: Fix table column and rows not being resizable on RTE (#11037) 2024-09-13 18:07:52 +01:00
pedr
b5f06b6958 Desktop: Fixes #10561: Table options not visible on dark theme (#11036) 2024-09-13 18:07:03 +01:00
Henry Heino
5a07b795d3 Desktop, Mobile: Upgrade CodeMirror packages (#11034) 2024-09-13 13:24:37 +01:00
Alice
bfab4426ca Seamless-Updates: Auto Updater Service is architecture dependent (#11015) 2024-09-13 11:03:14 +01:00
Henry Heino
bcb5218e1a Desktop: Fix editor/viewer loses focus when visible panels are changed with ctrl-l (#11029) 2024-09-12 17:54:10 +01:00
Henry Heino
c897cc1582 Desktop: Update plugin compatibility layer to allow more legacy plugins (e.g. Markdown Prettier) to run (#11033) 2024-09-12 17:51:38 +01:00
Henry Heino
ea61bfc498 Mobile: Fixes #10253: Move accessibility focus to the first note action menu item on open (#11031) 2024-09-12 09:04:23 +01:00
Henry Heino
ca5d35339f Desktop: Accessibility: Improve text read by screen readers when focusing the note viewer (#11030) 2024-09-12 09:00:35 +01:00
Laurent Cozic
5c00ea93c2 Doc: Move next release 3.1 dates by 10 days 2024-09-11 20:59:05 +01:00
Henry Heino
f005977ce0 Desktop: Encryption screen: Fix "invalid password" border shown for some correct passwords (#11027) 2024-09-11 19:02:31 +01:00
Henry Heino
79773dab95 Mobile,Desktop: Fix unable to change incorrect decryption password if the same as the master password (#11026) 2024-09-11 19:01:57 +01:00
Henry Heino
69168f1ec2 Desktop: Fixes #11020: Fix clicking on most non-media resource links opens them inline (#11022) 2024-09-11 16:49:35 +01:00
Henry Heino
147a66d64e Desktop: Accessibility: Fix multi-note selection menu not tab-focusable (#11018) 2024-09-10 19:29:17 +01:00
Joplin Bot
ec36847de0 Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-09-09 00:48:14 +00:00
renovate[bot]
d7bef7e923 Update dependency jsdom to v24 (#11007)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-08 22:56:53 +01:00
Laurent Cozic
55faab25b5 Desktop release v3.1.8 2024-09-08 20:50:06 +01:00
Alice
4da8060e62 Desktop: Seamless-Updates - rename latest-mac.yml to latest-mac-arm64.yml (#10985) 2024-09-08 20:49:29 +01:00
Laurent Cozic
821cfc5bd8 Update renovate.json5 2024-09-08 16:27:44 +01:00
renovate[bot]
9956caea1b Update dependency react-native-share to v10.2.1 (#11003) 2024-09-08 12:09:18 +00:00
renovate[bot]
f95b663f28 Update dependency react-native-webview to v13.10.3 (#11004) 2024-09-08 12:09:17 +00:00
renovate[bot]
dd990e7cf6 Update dependency sass to v1.77.4 (#11005) 2024-09-08 12:09:09 +00:00
davidsmoot
23dec124dd Doc: Update nextcloud.md (#10987)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2024-09-08 12:49:39 +01:00
renovate[bot]
2b6cb908fa Update dependency react-native-webview to v13.10.0 (#11000)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-08 03:57:35 +01:00
renovate[bot]
40475d60fb Update dependency react-native-share to v10.2.0 (#10999)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-08 03:57:19 +01:00
renovate[bot]
48e96a055f Update dependency sass to v1.77.1 (#11001)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-08 02:50:44 +01:00
renovate[bot]
3dbc9a5723 Update dependency tesseract.js to v5.1.0 (#11002)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-08 02:50:24 +01:00
renovate[bot]
9832af0d3a Update dependency react-native-localize to v3.1.0 (#10997)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-07 21:22:17 +01:00
renovate[bot]
26caf2a4c6 Update dependency @playwright/test to v1.44.1 (#10998) 2024-09-07 19:23:22 +00:00
renovate[bot]
29d7804ffd Update dependency react-native-document-picker to v9.2.0 (#10996)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-07 20:16:35 +01:00
renovate[bot]
6fe0104483 Update dependency @playwright/test to v1.44.0 (#10995)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-07 20:15:19 +01:00
Henry Heino
04f5433839 Mobile: Drawing: Fix clicking "cancel" after starting a new drawing in editing mode creates an empty resource (#10986) 2024-09-07 15:11:08 +01:00
Henry Heino
0bfa28d795 Mobile,Desktop,CLI: Fixes #10856: Decrypt master keys only as needed (#10990) 2024-09-07 11:56:13 +01:00
Laurent Cozic
ac2258769a Desktop: Fixes #10865: Removed unneeded electron-log dependency 2024-09-06 18:06:04 +01:00
github-actions[bot]
7cd0ed1714 @davidsmoot has signed the CLA in laurent22/joplin#10987 2024-09-05 11:49:13 +00:00
Laurent Cozic
803d508c69 Desktop release v3.1.7 2024-09-04 12:14:55 +01:00
Henry Heino
3c13568107 Chore: Mobile tests: Fix CodeMirror-related warnings (#10978) 2024-09-04 12:14:23 +01:00
Henry Heino
e41394b57f Chore: Tests: Improve Playwright test reliability (#10981) 2024-09-04 12:14:12 +01:00
Henry Heino
0b13dbddd8 Chore: Mobile tests: Fix warning: "A worker process ... has been force exited" (#10980) 2024-09-04 12:14:05 +01:00
Henry Heino
2a2dd96c02 Chore: Mobile tests: Hide debug logging (#10977) 2024-09-04 12:13:48 +01:00
renovate[bot]
2f7b2fb948 Update dependency css-loader to v6.11.0 (#10976)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-04 12:13:42 +01:00
Alice
4e8316a6ee Desktop: Seamless-Updates - generated latest-mac-arm64.yml (#10982) 2024-09-04 12:11:17 +01:00
renovate[bot]
01412b4500 Update dependency @rollup/plugin-replace to v5.0.7 (#10983) 2024-09-04 02:35:06 +00:00
renovate[bot]
2df8137281 Update dependency js-draw to v1.20.3 (#10973) 2024-09-03 03:43:16 +00:00
renovate[bot]
f24e229a4e Update dependency pg to v8.11.6 (#10974) 2024-09-03 03:43:14 +00:00
renovate[bot]
fa6060d6d2 Update dependency react-native-safe-area-context to v4.10.4 (#10975) 2024-09-03 03:43:07 +00:00
renovate[bot]
2d6796db16 Update dependency @react-native/babel-preset to v0.74.84 (#10970) 2024-09-03 03:07:48 +00:00
renovate[bot]
82be4f566a Update dependency @js-draw/material-icons to v1.20.3 (#10969) 2024-09-03 03:00:56 +00:00
renovate[bot]
f353686166 Update dependency @react-native/metro-config to v0.74.84 (#10971) 2024-09-03 03:00:20 +00:00
renovate[bot]
a6dbe4b67a Update dependency @rollup/plugin-replace to v5.0.6 (#10972) 2024-09-03 03:00:13 +00:00
Joplin Bot
b597d5f9d1 Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-09-02 18:20:26 +00:00
220 changed files with 13089 additions and 9878 deletions

View File

@@ -209,7 +209,6 @@ packages/app-desktop/gui/MainScreen/MainScreen.js
packages/app-desktop/gui/MainScreen/commands/addProfile.js
packages/app-desktop/gui/MainScreen/commands/commandPalette.js
packages/app-desktop/gui/MainScreen/commands/deleteFolder.js
packages/app-desktop/gui/MainScreen/commands/deleteNote.js
packages/app-desktop/gui/MainScreen/commands/duplicateNote.js
packages/app-desktop/gui/MainScreen/commands/editAlarm.js
packages/app-desktop/gui/MainScreen/commands/exportPdf.js
@@ -228,7 +227,6 @@ packages/app-desktop/gui/MainScreen/commands/openItem.js
packages/app-desktop/gui/MainScreen/commands/openNote.js
packages/app-desktop/gui/MainScreen/commands/openPdfViewer.js
packages/app-desktop/gui/MainScreen/commands/openTag.js
packages/app-desktop/gui/MainScreen/commands/permanentlyDeleteNote.js
packages/app-desktop/gui/MainScreen/commands/print.js
packages/app-desktop/gui/MainScreen/commands/renameFolder.js
packages/app-desktop/gui/MainScreen/commands/renameTag.js
@@ -287,6 +285,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/CodeMirror.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/Editor.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/useEditorCommands.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useKeymap.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useRefocusOnVisiblePaneChange.js
packages/app-desktop/gui/NoteEditor/NoteBody/PlainEditor/PlainEditor.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js
@@ -375,6 +374,7 @@ packages/app-desktop/gui/NoteListItem/utils/useItemElement.js
packages/app-desktop/gui/NoteListItem/utils/useItemEventHandlers.js
packages/app-desktop/gui/NoteListItem/utils/useOnContextMenu.js
packages/app-desktop/gui/NoteListItem/utils/useRenderedNote.js
packages/app-desktop/gui/NoteListItem/utils/useRootElement.test.js
packages/app-desktop/gui/NoteListItem/utils/useRootElement.js
packages/app-desktop/gui/NoteListWrapper/NoteListWrapper.js
packages/app-desktop/gui/NotePropertiesDialog.js
@@ -442,7 +442,6 @@ packages/app-desktop/gui/ToggleEditorsButton/ToggleEditorsButton.js
packages/app-desktop/gui/ToggleEditorsButton/styles/index.js
packages/app-desktop/gui/ToolbarBase.js
packages/app-desktop/gui/ToolbarButton/ToolbarButton.js
packages/app-desktop/gui/ToolbarButton/styles/index.js
packages/app-desktop/gui/ToolbarSpace.js
packages/app-desktop/gui/TrashNotification/TrashNotification.js
packages/app-desktop/gui/UpdateNotification/UpdateNotification.js
@@ -476,6 +475,7 @@ packages/app-desktop/integration-tests/models/NoteList.js
packages/app-desktop/integration-tests/models/SettingsScreen.js
packages/app-desktop/integration-tests/models/Sidebar.js
packages/app-desktop/integration-tests/noteList.spec.js
packages/app-desktop/integration-tests/pluginApi.spec.js
packages/app-desktop/integration-tests/richTextEditor.spec.js
packages/app-desktop/integration-tests/settings.spec.js
packages/app-desktop/integration-tests/sidebar.spec.js
@@ -517,8 +517,10 @@ packages/app-desktop/services/sortOrder/notesSortOrderUtils.test.js
packages/app-desktop/services/sortOrder/notesSortOrderUtils.js
packages/app-desktop/services/spellChecker/SpellCheckerServiceDriverNative.js
packages/app-desktop/tools/copy7Zip.js
packages/app-desktop/tools/generateLatestArm64Yml.js
packages/app-desktop/tools/githubReleasesUtils.js
packages/app-desktop/tools/modifyReleaseAssets.js
packages/app-desktop/tools/notarizeMacApp.js
packages/app-desktop/tools/renameReleaseAssets.js
packages/app-desktop/utils/7zip/getPathToExecutable7Zip.js
packages/app-desktop/utils/7zip/pathToBundled7Zip.js
packages/app-desktop/utils/checkForUpdatesUtils.test.js
@@ -545,6 +547,7 @@ packages/app-mobile/commands/util/showResource.js
packages/app-mobile/components/BackButtonDialogBox.js
packages/app-mobile/components/BetaChip.js
packages/app-mobile/components/CameraView.js
packages/app-mobile/components/Checkbox.js
packages/app-mobile/components/DialogManager.js
packages/app-mobile/components/DismissibleDialog.js
packages/app-mobile/components/Dropdown.test.js
@@ -608,10 +611,12 @@ packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.test.js
packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.js
packages/app-mobile/components/NoteEditor/hooks/useKeyboardVisible.js
packages/app-mobile/components/NoteEditor/types.js
packages/app-mobile/components/NoteItem.js
packages/app-mobile/components/NoteList.js
packages/app-mobile/components/ProfileSwitcher/ProfileEditor.js
packages/app-mobile/components/ProfileSwitcher/ProfileSwitcher.js
packages/app-mobile/components/ProfileSwitcher/useProfileConfig.js
packages/app-mobile/components/ScreenHeader/Menu.js
packages/app-mobile/components/ScreenHeader/WarningBanner.test.js
packages/app-mobile/components/ScreenHeader/WarningBanner.js
packages/app-mobile/components/ScreenHeader/WarningBox.js
@@ -619,6 +624,7 @@ packages/app-mobile/components/ScreenHeader/WebBetaButton.js
packages/app-mobile/components/ScreenHeader/index.js
packages/app-mobile/components/SelectDateTimeDialog.js
packages/app-mobile/components/SideMenu.js
packages/app-mobile/components/SideMenuContentNote.js
packages/app-mobile/components/TextInput.js
packages/app-mobile/components/accessibility/AccessibleModalMenu.js
packages/app-mobile/components/accessibility/AccessibleView.js
@@ -707,13 +713,14 @@ packages/app-mobile/components/screens/Note.test.js
packages/app-mobile/components/screens/Note.js
packages/app-mobile/components/screens/NoteTagsDialog.js
packages/app-mobile/components/screens/Notes.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
packages/app-mobile/components/screens/ShareManager/IncomingShareItem.js
packages/app-mobile/components/screens/ShareManager/index.test.js
packages/app-mobile/components/screens/ShareManager/index.js
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
packages/app-mobile/components/screens/encryption-config.js
packages/app-mobile/components/screens/search.js
packages/app-mobile/components/screens/status.js
packages/app-mobile/components/side-menu-content.js
packages/app-mobile/components/voiceTyping/VoiceTypingDialog.js
@@ -723,6 +730,7 @@ packages/app-mobile/root.js
packages/app-mobile/services/AlarmServiceDriver.android.js
packages/app-mobile/services/AlarmServiceDriver.ios.js
packages/app-mobile/services/AlarmServiceDriver.web.js
packages/app-mobile/services/BackButtonService.js
packages/app-mobile/services/e2ee/RSA.react-native.js
packages/app-mobile/services/plugins/PlatformImplementation.js
packages/app-mobile/services/profiles/index.js
@@ -757,6 +765,7 @@ packages/app-mobile/utils/fs-driver/testUtil/createFilesFromPathRecord.js
packages/app-mobile/utils/fs-driver/testUtil/verifyDirectoryMatches.js
packages/app-mobile/utils/getPackageInfo.js
packages/app-mobile/utils/getVersionInfoText.js
packages/app-mobile/utils/hooks/useReduceMotionEnabled.js
packages/app-mobile/utils/image/fileToImage.web.js
packages/app-mobile/utils/image/getImageDimensions.js
packages/app-mobile/utils/image/resizeImage.js
@@ -778,6 +787,7 @@ packages/app-mobile/utils/shim-init-react/injectedJs.js
packages/app-mobile/utils/shim-init-react/shimInitShared.js
packages/app-mobile/utils/testing/createMockReduxStore.js
packages/app-mobile/utils/testing/getWebViewDomById.js
packages/app-mobile/utils/testing/getWebViewWindowById.js
packages/app-mobile/utils/types.js
packages/app-mobile/web/serviceWorker.js
packages/default-plugins/build.js
@@ -883,6 +893,7 @@ packages/lib/ArrayUtils.js
packages/lib/AsyncActionQueue.test.js
packages/lib/AsyncActionQueue.js
packages/lib/BaseApplication.js
packages/lib/BaseModel.test.js
packages/lib/BaseModel.js
packages/lib/BaseSyncTarget.js
packages/lib/ClipperServer.js
@@ -912,11 +923,14 @@ packages/lib/array.js
packages/lib/callbackUrlUtils.test.js
packages/lib/callbackUrlUtils.js
packages/lib/clipperUtils.js
packages/lib/commands/deleteNote.js
packages/lib/commands/historyBackward.js
packages/lib/commands/historyForward.js
packages/lib/commands/index.js
packages/lib/commands/openMasterPasswordDialog.js
packages/lib/commands/permanentlyDeleteNote.js
packages/lib/commands/synchronize.js
packages/lib/components/EncryptionConfigScreen/utils.test.js
packages/lib/components/EncryptionConfigScreen/utils.js
packages/lib/components/shared/NoteList/getEmptyFolderMessage.js
packages/lib/components/shared/config/config-shared.js
@@ -942,6 +956,7 @@ packages/lib/eventManager.js
packages/lib/file-api-driver-joplinServer.js
packages/lib/file-api-driver-local.js
packages/lib/file-api-driver-memory.js
packages/lib/file-api-driver-webdav.test.js
packages/lib/file-api-driver.test.js
packages/lib/file-api.test.js
packages/lib/file-api.js
@@ -955,6 +970,8 @@ packages/lib/hooks/useElementSize.js
packages/lib/hooks/useEventListener.js
packages/lib/hooks/usePlugin.js
packages/lib/hooks/usePrevious.js
packages/lib/hooks/useQueuedAsyncEffect.test.js
packages/lib/hooks/useQueuedAsyncEffect.js
packages/lib/htmlUtils.test.js
packages/lib/htmlUtils.js
packages/lib/htmlUtils2.test.js

View File

@@ -44,6 +44,14 @@ jobs:
with:
python-version: '3.11'
- name: Set Publish Flag
run: |
if [[ $GIT_TAG_NAME = v* ]]; then
echo "PUBLISH_ENABLED=true" >> $GITHUB_ENV
else
echo "PUBLISH_ENABLED=false" >> $GITHUB_ENV
fi
- name: Build macOS M1 app
env:
APPLE_ASC_PROVIDER: ${{ secrets.APPLE_ASC_PROVIDER }}
@@ -56,6 +64,7 @@ jobs:
GH_REPO: ${{ github.repository }}
IS_CONTINUOUS_INTEGRATION: 1
BUILD_SEQUENCIAL: 1
PUBLISH_ENABLED: ${{ env.PUBLISH_ENABLED }}
run: |
export npm_config_arch=arm64
export npm_config_target_arch=arm64
@@ -67,11 +76,11 @@ jobs:
npm pkg set 'build.mac.target[1].target'='zip'
npm pkg set 'build.mac.target[1].arch[0]'='arm64'
if [[ $GIT_TAG_NAME = v* ]]; then
if [[ "$PUBLISH_ENABLED" == "true" ]]; then
echo "Building and publishing desktop application..."
PYTHON_PATH=$(which python) USE_HARD_LINKS=false yarn dist --mac --arm64
yarn renameReleaseAssets --repo="$GH_REPO" --tag="$GIT_TAG_NAME" --token="$GITHUB_TOKEN"
yarn modifyReleaseAssets --repo="$GH_REPO" --tag="$GIT_TAG_NAME" --token="$GITHUB_TOKEN"
else
echo "Building but *not* publishing desktop application..."

27
.gitignore vendored
View File

@@ -186,7 +186,6 @@ packages/app-desktop/gui/MainScreen/MainScreen.js
packages/app-desktop/gui/MainScreen/commands/addProfile.js
packages/app-desktop/gui/MainScreen/commands/commandPalette.js
packages/app-desktop/gui/MainScreen/commands/deleteFolder.js
packages/app-desktop/gui/MainScreen/commands/deleteNote.js
packages/app-desktop/gui/MainScreen/commands/duplicateNote.js
packages/app-desktop/gui/MainScreen/commands/editAlarm.js
packages/app-desktop/gui/MainScreen/commands/exportPdf.js
@@ -205,7 +204,6 @@ packages/app-desktop/gui/MainScreen/commands/openItem.js
packages/app-desktop/gui/MainScreen/commands/openNote.js
packages/app-desktop/gui/MainScreen/commands/openPdfViewer.js
packages/app-desktop/gui/MainScreen/commands/openTag.js
packages/app-desktop/gui/MainScreen/commands/permanentlyDeleteNote.js
packages/app-desktop/gui/MainScreen/commands/print.js
packages/app-desktop/gui/MainScreen/commands/renameFolder.js
packages/app-desktop/gui/MainScreen/commands/renameTag.js
@@ -264,6 +262,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/CodeMirror.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/Editor.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/useEditorCommands.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useKeymap.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useRefocusOnVisiblePaneChange.js
packages/app-desktop/gui/NoteEditor/NoteBody/PlainEditor/PlainEditor.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js
@@ -352,6 +351,7 @@ packages/app-desktop/gui/NoteListItem/utils/useItemElement.js
packages/app-desktop/gui/NoteListItem/utils/useItemEventHandlers.js
packages/app-desktop/gui/NoteListItem/utils/useOnContextMenu.js
packages/app-desktop/gui/NoteListItem/utils/useRenderedNote.js
packages/app-desktop/gui/NoteListItem/utils/useRootElement.test.js
packages/app-desktop/gui/NoteListItem/utils/useRootElement.js
packages/app-desktop/gui/NoteListWrapper/NoteListWrapper.js
packages/app-desktop/gui/NotePropertiesDialog.js
@@ -419,7 +419,6 @@ packages/app-desktop/gui/ToggleEditorsButton/ToggleEditorsButton.js
packages/app-desktop/gui/ToggleEditorsButton/styles/index.js
packages/app-desktop/gui/ToolbarBase.js
packages/app-desktop/gui/ToolbarButton/ToolbarButton.js
packages/app-desktop/gui/ToolbarButton/styles/index.js
packages/app-desktop/gui/ToolbarSpace.js
packages/app-desktop/gui/TrashNotification/TrashNotification.js
packages/app-desktop/gui/UpdateNotification/UpdateNotification.js
@@ -453,6 +452,7 @@ packages/app-desktop/integration-tests/models/NoteList.js
packages/app-desktop/integration-tests/models/SettingsScreen.js
packages/app-desktop/integration-tests/models/Sidebar.js
packages/app-desktop/integration-tests/noteList.spec.js
packages/app-desktop/integration-tests/pluginApi.spec.js
packages/app-desktop/integration-tests/richTextEditor.spec.js
packages/app-desktop/integration-tests/settings.spec.js
packages/app-desktop/integration-tests/sidebar.spec.js
@@ -494,8 +494,10 @@ packages/app-desktop/services/sortOrder/notesSortOrderUtils.test.js
packages/app-desktop/services/sortOrder/notesSortOrderUtils.js
packages/app-desktop/services/spellChecker/SpellCheckerServiceDriverNative.js
packages/app-desktop/tools/copy7Zip.js
packages/app-desktop/tools/generateLatestArm64Yml.js
packages/app-desktop/tools/githubReleasesUtils.js
packages/app-desktop/tools/modifyReleaseAssets.js
packages/app-desktop/tools/notarizeMacApp.js
packages/app-desktop/tools/renameReleaseAssets.js
packages/app-desktop/utils/7zip/getPathToExecutable7Zip.js
packages/app-desktop/utils/7zip/pathToBundled7Zip.js
packages/app-desktop/utils/checkForUpdatesUtils.test.js
@@ -522,6 +524,7 @@ packages/app-mobile/commands/util/showResource.js
packages/app-mobile/components/BackButtonDialogBox.js
packages/app-mobile/components/BetaChip.js
packages/app-mobile/components/CameraView.js
packages/app-mobile/components/Checkbox.js
packages/app-mobile/components/DialogManager.js
packages/app-mobile/components/DismissibleDialog.js
packages/app-mobile/components/Dropdown.test.js
@@ -585,10 +588,12 @@ packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.test.js
packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.js
packages/app-mobile/components/NoteEditor/hooks/useKeyboardVisible.js
packages/app-mobile/components/NoteEditor/types.js
packages/app-mobile/components/NoteItem.js
packages/app-mobile/components/NoteList.js
packages/app-mobile/components/ProfileSwitcher/ProfileEditor.js
packages/app-mobile/components/ProfileSwitcher/ProfileSwitcher.js
packages/app-mobile/components/ProfileSwitcher/useProfileConfig.js
packages/app-mobile/components/ScreenHeader/Menu.js
packages/app-mobile/components/ScreenHeader/WarningBanner.test.js
packages/app-mobile/components/ScreenHeader/WarningBanner.js
packages/app-mobile/components/ScreenHeader/WarningBox.js
@@ -596,6 +601,7 @@ packages/app-mobile/components/ScreenHeader/WebBetaButton.js
packages/app-mobile/components/ScreenHeader/index.js
packages/app-mobile/components/SelectDateTimeDialog.js
packages/app-mobile/components/SideMenu.js
packages/app-mobile/components/SideMenuContentNote.js
packages/app-mobile/components/TextInput.js
packages/app-mobile/components/accessibility/AccessibleModalMenu.js
packages/app-mobile/components/accessibility/AccessibleView.js
@@ -684,13 +690,14 @@ packages/app-mobile/components/screens/Note.test.js
packages/app-mobile/components/screens/Note.js
packages/app-mobile/components/screens/NoteTagsDialog.js
packages/app-mobile/components/screens/Notes.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
packages/app-mobile/components/screens/ShareManager/IncomingShareItem.js
packages/app-mobile/components/screens/ShareManager/index.test.js
packages/app-mobile/components/screens/ShareManager/index.js
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
packages/app-mobile/components/screens/encryption-config.js
packages/app-mobile/components/screens/search.js
packages/app-mobile/components/screens/status.js
packages/app-mobile/components/side-menu-content.js
packages/app-mobile/components/voiceTyping/VoiceTypingDialog.js
@@ -700,6 +707,7 @@ packages/app-mobile/root.js
packages/app-mobile/services/AlarmServiceDriver.android.js
packages/app-mobile/services/AlarmServiceDriver.ios.js
packages/app-mobile/services/AlarmServiceDriver.web.js
packages/app-mobile/services/BackButtonService.js
packages/app-mobile/services/e2ee/RSA.react-native.js
packages/app-mobile/services/plugins/PlatformImplementation.js
packages/app-mobile/services/profiles/index.js
@@ -734,6 +742,7 @@ packages/app-mobile/utils/fs-driver/testUtil/createFilesFromPathRecord.js
packages/app-mobile/utils/fs-driver/testUtil/verifyDirectoryMatches.js
packages/app-mobile/utils/getPackageInfo.js
packages/app-mobile/utils/getVersionInfoText.js
packages/app-mobile/utils/hooks/useReduceMotionEnabled.js
packages/app-mobile/utils/image/fileToImage.web.js
packages/app-mobile/utils/image/getImageDimensions.js
packages/app-mobile/utils/image/resizeImage.js
@@ -755,6 +764,7 @@ packages/app-mobile/utils/shim-init-react/injectedJs.js
packages/app-mobile/utils/shim-init-react/shimInitShared.js
packages/app-mobile/utils/testing/createMockReduxStore.js
packages/app-mobile/utils/testing/getWebViewDomById.js
packages/app-mobile/utils/testing/getWebViewWindowById.js
packages/app-mobile/utils/types.js
packages/app-mobile/web/serviceWorker.js
packages/default-plugins/build.js
@@ -860,6 +870,7 @@ packages/lib/ArrayUtils.js
packages/lib/AsyncActionQueue.test.js
packages/lib/AsyncActionQueue.js
packages/lib/BaseApplication.js
packages/lib/BaseModel.test.js
packages/lib/BaseModel.js
packages/lib/BaseSyncTarget.js
packages/lib/ClipperServer.js
@@ -889,11 +900,14 @@ packages/lib/array.js
packages/lib/callbackUrlUtils.test.js
packages/lib/callbackUrlUtils.js
packages/lib/clipperUtils.js
packages/lib/commands/deleteNote.js
packages/lib/commands/historyBackward.js
packages/lib/commands/historyForward.js
packages/lib/commands/index.js
packages/lib/commands/openMasterPasswordDialog.js
packages/lib/commands/permanentlyDeleteNote.js
packages/lib/commands/synchronize.js
packages/lib/components/EncryptionConfigScreen/utils.test.js
packages/lib/components/EncryptionConfigScreen/utils.js
packages/lib/components/shared/NoteList/getEmptyFolderMessage.js
packages/lib/components/shared/config/config-shared.js
@@ -919,6 +933,7 @@ packages/lib/eventManager.js
packages/lib/file-api-driver-joplinServer.js
packages/lib/file-api-driver-local.js
packages/lib/file-api-driver-memory.js
packages/lib/file-api-driver-webdav.test.js
packages/lib/file-api-driver.test.js
packages/lib/file-api.test.js
packages/lib/file-api.js
@@ -932,6 +947,8 @@ packages/lib/hooks/useElementSize.js
packages/lib/hooks/useEventListener.js
packages/lib/hooks/usePlugin.js
packages/lib/hooks/usePrevious.js
packages/lib/hooks/useQueuedAsyncEffect.test.js
packages/lib/hooks/useQueuedAsyncEffect.js
packages/lib/htmlUtils.test.js
packages/lib/htmlUtils.js
packages/lib/htmlUtils2.test.js

File diff suppressed because one or more lines are too long

875
.yarn/releases/yarn-3.8.3.cjs vendored Executable file

File diff suppressed because one or more lines are too long

View File

@@ -6,7 +6,7 @@ plugins:
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
spec: "@yarnpkg/plugin-workspace-tools"
yarnPath: .yarn/releases/yarn-3.6.4.cjs
yarnPath: .yarn/releases/yarn-3.8.3.cjs
logFilters:

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

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://www.hosting.de/nextcloud/?mtm_campaign=managed-nextcloud&amp;mtm_kwd=joplinapp&amp;mtm_source=joplinapp-webseite&amp;mtm_medium=banner"><img title="Hosting.de" width="256" src="https://joplinapp.org/images/sponsors/HostingDe.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://buyyoutubviews.com"><img title="BYTV" width="256" src="https://joplinapp.org/images/sponsors/BYTV.png"/></a> <a href="https://www.famegear.com"><img title="Famegear" width="256" src="https://joplinapp.org/images/sponsors/Famegear.png"/></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://www.hosting.de/nextcloud/?mtm_campaign=managed-nextcloud&amp;mtm_kwd=joplinapp&amp;mtm_source=joplinapp-webseite&amp;mtm_medium=banner"><img title="Hosting.de" width="256" src="https://joplinapp.org/images/sponsors/HostingDe.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://buyyoutubviews.com"><img title="BYTV" width="256" src="https://joplinapp.org/images/sponsors/BYTV.png"/></a> <a href="https://www.famegear.com"><img title="Famegear" width="256" src="https://joplinapp.org/images/sponsors/Famegear.png"/></a> <a href="https://casinoreviews.net"><img title="Casino Reviews" width="256" src="https://joplinapp.org/images/sponsors/CasinoReviews.png"/></a>
<!-- SPONSORS-ORG -->
* * *

View File

@@ -72,38 +72,38 @@
"@crowdin/cli": "3",
"@joplin/utils": "~2.12",
"@seiyab/eslint-plugin-react-hooks": "4.5.1-beta.0",
"@typescript-eslint/eslint-plugin": "6.8.0",
"@typescript-eslint/parser": "6.8.0",
"@typescript-eslint/eslint-plugin": "6.21.0",
"@typescript-eslint/parser": "6.21.0",
"cspell": "5.21.2",
"eslint": "8.52.0",
"eslint": "8.57.0",
"eslint-interactive": "10.8.0",
"eslint-plugin-import": "2.28.1",
"eslint-plugin-jest": "27.4.3",
"eslint-plugin-promise": "6.1.1",
"eslint-plugin-react": "7.33.2",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-jest": "27.9.0",
"eslint-plugin-promise": "6.2.0",
"eslint-plugin-react": "7.34.3",
"execa": "5.1.1",
"fs-extra": "11.2.0",
"glob": "10.3.16",
"glob": "10.4.5",
"gulp": "4.0.2",
"husky": "3.1.0",
"lerna": "3.22.1",
"lint-staged": "15.2.5",
"lint-staged": "15.2.7",
"madge": "6.1.0",
"npm-package-json-lint": "7.1.0",
"typescript": "5.2.2"
"typescript": "5.4.5"
},
"dependencies": {
"@types/fs-extra": "11.0.4",
"eslint-plugin-github": "4.10.1",
"eslint-plugin-github": "4.10.2",
"http-server": "14.1.1",
"node-gyp": "9.4.1",
"nodemon": "3.0.3"
"nodemon": "3.1.7"
},
"packageManager": "yarn@3.6.4",
"packageManager": "yarn@3.8.3",
"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.52.0#./.yarn/patches/eslint-npm-8.39.0-d92bace04d.patch",
"eslint": "patch:eslint@8.57.0#./.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",
"pdfjs-dist": "patch:pdfjs-dist@npm%3A3.11.174#./.yarn/patches/pdfjs-dist-npm-3.11.174-67f2fee6d6.patch",

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env node
#!/usr/bin/env -S NODE_OPTIONS=--no-deprecation node
// Use njstrace to find out what Node.js might be spending time on
// var njstrace = require('njstrace').inject();

View File

@@ -72,12 +72,12 @@
"devDependencies": {
"@joplin/tools": "~3.1",
"@types/fs-extra": "11.0.4",
"@types/jest": "29.5.8",
"@types/node": "18.19.34",
"@types/jest": "29.5.12",
"@types/node": "18.19.39",
"@types/proper-lockfile": "^4.1.2",
"gulp": "4.0.2",
"jest": "29.7.0",
"temp": "0.9.4",
"typescript": "5.2.2"
"typescript": "5.4.5"
}
}

View File

@@ -1,4 +1,4 @@
import MdToHtml from '@joplin/renderer/MdToHtml';
import MdToHtml, { LinkRenderingType } from '@joplin/renderer/MdToHtml';
const { filename } = require('@joplin/lib/path-utils');
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
import shim from '@joplin/lib/shim';
@@ -218,6 +218,9 @@ describe('MdToHtml', () => {
const mdToHtmlLinkifyOn = newTestMdToHtml({
pluginOptions: {
linkify: { enabled: true },
link_open: {
linkRenderingType: LinkRenderingType.HrefHandler,
},
},
});
@@ -227,29 +230,52 @@ describe('MdToHtml', () => {
},
});
const renderOptions = {
bodyOnly: true,
plainResourceRendering: true,
linkRenderingType: LinkRenderingType.HrefHandler,
};
for (const testCase of testCases) {
const [input, expectedLinkifyOff, expectedLinkifyOn] = testCase;
{
const actual = await mdToHtmlLinkifyOn.render(input, null, {
bodyOnly: true,
plainResourceRendering: true,
});
const actual = await mdToHtmlLinkifyOn.render(input, null, renderOptions);
expect(actual.html).toBe(expectedLinkifyOn);
}
{
const actual = await mdToHtmlLinkifyOff.render(input, null, {
bodyOnly: true,
plainResourceRendering: true,
});
const actual = await mdToHtmlLinkifyOff.render(input, null, renderOptions);
expect(actual.html).toBe(expectedLinkifyOff);
}
}
}));
it.each([
'[test](http://example.com/)',
'[test](mailto:test@example.com)',
])('should add onclick handlers to links when linkRenderingType is JavaScriptHandler (%j)', async (markdown) => {
const mdToHtml = newTestMdToHtml();
const renderWithoutOnClickOptions = {
bodyOnly: true,
linkRenderingType: LinkRenderingType.HrefHandler,
};
expect(
(await mdToHtml.render(markdown, undefined, renderWithoutOnClickOptions)).html,
).not.toContain('onclick');
const renderWithOnClickOptions = {
bodyOnly: true,
linkRenderingType: LinkRenderingType.JavaScriptHandler,
};
expect(
(await mdToHtml.render(markdown, undefined, renderWithOnClickOptions)).html,
).toMatch(/<a data-from-md .*onclick=['"].*['"].*>/);
});
it('should return attributes of line numbers', (async () => {
const mdToHtml = newTestMdToHtml();
@@ -326,4 +352,12 @@ describe('MdToHtml', () => {
expect(html).toContain('Inline</span>');
expect(html).toContain('Block</span>');
});
it('should sanitize KaTeX errors', async () => {
const markdown = '$\\a<svg>$';
const renderResult = await newTestMdToHtml().render(markdown, null, { bodyOnly: true });
// Should not contain the HTML in unsanitized form
expect(renderResult.html).not.toContain('<svg>');
});
});

View File

@@ -24,3 +24,4 @@ build/defaultPlugins/
build/7zip/7za
build/7zip/7za.exe
sentry.properties
downloads/

View File

@@ -5,7 +5,7 @@ import type ShimType from '@joplin/lib/shim';
const shim: typeof ShimType = require('@joplin/lib/shim').default;
import { isCallbackUrl } from '@joplin/lib/callbackUrlUtils';
import { BrowserWindow, Tray, screen } from 'electron';
import { BrowserWindow, Tray, WebContents, screen } from 'electron';
import bridge from './bridge';
const url = require('url');
const path = require('path');
@@ -232,14 +232,35 @@ export default class ElectronAppWrapper {
}, 3000);
}
// will-frame-navigate is fired by clicking on a link within the BrowserWindow.
this.win_.webContents.on('will-frame-navigate', event => {
// If the link changes the URL of the browser window,
if (event.isMainFrame) {
event.preventDefault();
void bridge().openExternal(event.url);
}
});
const addWindowEventHandlers = (webContents: WebContents) => {
// will-frame-navigate is fired by clicking on a link within the BrowserWindow.
webContents.on('will-frame-navigate', event => {
// If the link changes the URL of the browser window,
if (event.isMainFrame) {
event.preventDefault();
void bridge().openExternal(event.url);
}
});
// Override calls to window.open and links with target="_blank": Open most in a browser instead
// of Electron:
webContents.setWindowOpenHandler((event) => {
if (event.url === 'about:blank') {
// Script-controlled pages: Used for opening notes in new windows
return {
action: 'allow',
};
} else if (event.url.match(/^https?:\/\//)) {
void bridge().openExternal(event.url);
}
return { action: 'deny' };
});
webContents.on('did-create-window', (event) => {
addWindowEventHandlers(event.webContents);
});
};
addWindowEventHandlers(this.win_.webContents);
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
this.win_.on('close', (event: any) => {
@@ -332,6 +353,10 @@ export default class ElectronAppWrapper {
this.updaterService_.updateApp();
});
ipcMain.on('check-for-updates', () => {
void this.updaterService_.checkForUpdates(true);
});
// Let us register listeners on the window, so we can update the state
// automatically (the listeners will be removed when the window is closed)
// and restore the maximized or full screen state
@@ -470,7 +495,7 @@ export default class ElectronAppWrapper {
}
// Electron's autoUpdater has to be init from the main process
public async initializeAutoUpdaterService(logger: LoggerWrapper, devMode: boolean, includePreReleases: boolean) {
public initializeAutoUpdaterService(logger: LoggerWrapper, devMode: boolean, includePreReleases: boolean) {
if (shim.isWindows() || shim.isMac()) {
if (!this.updaterService_) {
this.updaterService_ = new AutoUpdaterService(this.win_, logger, devMode, includePreReleases);
@@ -482,7 +507,7 @@ export default class ElectronAppWrapper {
private startPeriodicUpdateCheck = (updateInterval: number = defaultUpdateInterval): void => {
this.stopPeriodicUpdateCheck();
this.updatePollInterval_ = setInterval(() => {
void this.updaterService_.checkForUpdates();
void this.updaterService_.checkForUpdates(false);
}, updateInterval);
setTimeout(this.updaterService_.checkForUpdates, initialUpdateStartup);
};
@@ -491,6 +516,7 @@ export default class ElectronAppWrapper {
if (this.updatePollInterval_) {
clearInterval(this.updatePollInterval_);
this.updatePollInterval_ = null;
this.updaterService_ = null;
}
};

View File

@@ -7,7 +7,7 @@ const distPath = path.join(__dirname, distDirName);
const generateChecksumFile = () => {
if (os.platform() !== 'linux') {
return []; // SHA-512 is only for AppImage
return; // SHA-512 is only for AppImage
}
let appImageName = '';
@@ -28,11 +28,13 @@ const generateChecksumFile = () => {
const sha512FileName = `${appImageName}.sha512`;
const sha512FilePath = path.join(distPath, sha512FileName);
fs.writeFileSync(sha512FilePath, checksum);
return [sha512FilePath];
return sha512FilePath;
};
const mainHook = () => {
generateChecksumFile();
const sha512FilePath = generateChecksumFile();
const outputFiles = [sha512FilePath].filter(item => item);
return outputFiles;
};
exports.default = mainHook;

View File

@@ -404,9 +404,9 @@ class Application extends BaseApplication {
eventManager.on(EventName.ResourceChange, handleResourceChange);
}
private async setupAutoUpdaterService() {
private setupAutoUpdaterService() {
if (Setting.value('featureFlag.autoUpdaterServiceEnabled')) {
await bridge().electronApp().initializeAutoUpdaterService(
bridge().electronApp().initializeAutoUpdaterService(
Logger.create('AutoUpdaterService'),
Setting.value('env') === 'dev',
Setting.value('autoUpdate.includePreReleases'),
@@ -449,6 +449,8 @@ class Application extends BaseApplication {
// Loads app-wide styles. (Markdown preview-specific styles loaded in app.js)
await injectCustomStyles('appStyles', Setting.customCssFilePath(Setting.customCssFilenames.JOPLIN_APP));
this.setupAutoUpdaterService();
AlarmService.setDriver(new AlarmServiceDriverNode({ appName: packageInfo.build.appId }));
AlarmService.setLogger(reg.logger());
@@ -698,8 +700,6 @@ class Application extends BaseApplication {
SearchEngine.instance().scheduleSyncTables();
});
await this.setupAutoUpdaterService();
// setTimeout(() => {
// void populateDatabase(reg.db(), {
// clearDatabase: true,

View File

@@ -1,25 +0,0 @@
const fs = require('fs');
const path = require('path');
const os = require('os');
const distDirName = 'dist';
const distPath = path.join(__dirname, distDirName);
const renameLatestYmlFile = () => {
if (os.platform() === 'darwin' && process.arch === 'arm64') {
const latestMacFilePath = path.join(distPath, 'latest-mac.yml');
const renamedMacFilePath = path.join(distPath, 'latest-mac-arm64.yml');
if (fs.existsSync(latestMacFilePath)) {
fs.renameSync(latestMacFilePath, renamedMacFilePath);
return [renamedMacFilePath];
} else {
throw new Error('latest-mac.yml not found!');
}
}
};
const mainHook = () => {
renameLatestYmlFile();
};
exports.default = mainHook;

View File

@@ -234,7 +234,7 @@ export default function(props: Props) {
return (
<CellFooter>
<NeedUpgradeMessage>
{PluginService.instance().describeIncompatibility(props.manifest)}
{PluginService.instance().describeIncompatibility(item.manifest)}
</NeedUpgradeMessage>
</CellFooter>
);

View File

@@ -116,7 +116,7 @@ const EncryptionConfigScreen = (props: Props) => {
);
} else {
return (
<td style={missingPasswordCellStyle}>
<td style={passwordChecks[masterKeyId] ? theme.textStyle : missingPasswordCellStyle}>
<input
type="password"
placeholder={_('Enter password')}

View File

@@ -2,7 +2,6 @@
import * as addProfile from './addProfile';
import * as commandPalette from './commandPalette';
import * as deleteFolder from './deleteFolder';
import * as deleteNote from './deleteNote';
import * as duplicateNote from './duplicateNote';
import * as editAlarm from './editAlarm';
import * as exportPdf from './exportPdf';
@@ -20,7 +19,6 @@ import * as openItem from './openItem';
import * as openNote from './openNote';
import * as openPdfViewer from './openPdfViewer';
import * as openTag from './openTag';
import * as permanentlyDeleteNote from './permanentlyDeleteNote';
import * as print from './print';
import * as renameFolder from './renameFolder';
import * as renameTag from './renameTag';
@@ -52,7 +50,6 @@ const index: any[] = [
addProfile,
commandPalette,
deleteFolder,
deleteNote,
duplicateNote,
editAlarm,
exportPdf,
@@ -70,7 +67,6 @@ const index: any[] = [
openNote,
openPdfViewer,
openTag,
permanentlyDeleteNote,
print,
renameFolder,
renameTag,

View File

@@ -1,7 +1,13 @@
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
import { _ } from '@joplin/lib/locale';
import Folder from '@joplin/lib/models/Folder';
import Folder, { FolderEntityWithChildren } from '@joplin/lib/models/Folder';
import Note from '@joplin/lib/models/Note';
import BaseItem from '@joplin/lib/models/BaseItem';
import { ModelType } from '@joplin/lib/BaseModel';
import Logger from '@joplin/utils/Logger';
import shim from '@joplin/lib/shim';
const logger = Logger.create('commands/moveToFolder');
export const declaration: CommandDeclaration = {
name: 'moveToFolder',
@@ -11,19 +17,44 @@ export const declaration: CommandDeclaration = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
export const runtime = (comp: any): CommandRuntime => {
return {
execute: async (context: CommandContext, noteIds: string[] = null) => {
noteIds = noteIds || context.state.selectedNoteIds;
execute: async (context: CommandContext, itemIds: string[] = null) => {
itemIds = itemIds || context.state.selectedNoteIds;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const folders: any[] = await Folder.sortFolderTree();
let allAreFolders = true;
const itemIdToType = new Map<string, ModelType>();
for (const id of itemIds) {
const item = await BaseItem.loadItemById(id);
itemIdToType.set(id, item.type_);
if (item.type_ !== ModelType.Folder) {
allAreFolders = false;
}
}
const folders = await Folder.sortFolderTree();
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const startFolders: any[] = [];
const maxDepth = 15;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const addOptions = (folders: any[], depth: number) => {
// It's okay for folders (but not notes) to have no parent folder:
if (allAreFolders) {
startFolders.push({
key: '',
value: '',
label: _('None'),
indentDepth: 0,
});
}
const addOptions = (folders: FolderEntityWithChildren[], depth: number) => {
for (let i = 0; i < folders.length; i++) {
const folder = folders[i];
// Disallow making a folder a subfolder of itself.
if (itemIdToType.has(folder.id)) {
continue;
}
startFolders.push({ key: folder.id, value: folder.id, label: folder.title, indentDepth: depth });
if (folder.children) addOptions(folder.children, (depth + 1) < maxDepth ? depth + 1 : maxDepth);
}
@@ -40,8 +71,25 @@ export const runtime = (comp: any): CommandRuntime => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
onClose: async (answer: any) => {
if (answer) {
for (let i = 0; i < noteIds.length; i++) {
await Note.moveToFolder(noteIds[i], answer.value);
try {
const targetFolderId = answer.value;
for (const id of itemIds) {
if (id === targetFolderId) {
continue;
}
const itemType = itemIdToType.get(id);
if (itemType === ModelType.Note) {
await Note.moveToFolder(id, targetFolderId);
} else if (itemType === ModelType.Folder) {
await Folder.moveToFolder(id, targetFolderId);
} else {
throw new Error(`Cannot move item with type ${itemType}`);
}
}
} catch (error) {
logger.error('Error moving items', error);
void shim.showMessageBox(`Error: ${error}`);
}
}
comp.setState({ promptOptions: null });

View File

@@ -26,6 +26,7 @@ import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/Plug
import { getListRendererById, getListRendererIds } from '@joplin/lib/services/noteList/renderers';
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
import { EventName } from '@joplin/lib/eventManager';
import { ipcRenderer } from 'electron';
const packageInfo: PackageInfo = require('../packageInfo.js');
const { clipboard } = require('electron');
const Menu = bridge().Menu;
@@ -575,7 +576,12 @@ function useMenu(props: Props) {
toolsItems.push(SpellCheckerService.instance().spellCheckerConfigMenuItem(props['spellChecker.languages'], props['spellChecker.enabled']));
function _checkForUpdates() {
void checkForUpdates(false, bridge().window(), { includePreReleases: Setting.value('autoUpdate.includePreReleases') });
if (Setting.value('featureFlag.autoUpdaterServiceEnabled')) {
ipcRenderer.send('check-for-updates');
} else {
void checkForUpdates(false, bridge().window(), { includePreReleases: Setting.value('autoUpdate.includePreReleases') });
}
}
function _showAbout() {

View File

@@ -28,6 +28,7 @@ import useWebviewIpcMessage from '../utils/useWebviewIpcMessage';
import Toolbar from '../Toolbar';
import useEditorSearchHandler from '../utils/useEditorSearchHandler';
import CommandService from '@joplin/lib/services/CommandService';
import useRefocusOnVisiblePaneChange from './utils/useRefocusOnVisiblePaneChange';
const logger = Logger.create('CodeMirror6');
const logDebug = (message: string) => logger.debug(message);
@@ -318,6 +319,8 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
return output;
}, [styles.cellViewer, props.visiblePanes]);
useRefocusOnVisiblePaneChange({ editorRef, webviewRef, visiblePanes: props.visiblePanes });
useEditorSearchHandler({
setLocalSearchResultCount: props.setLocalSearchResultCount,
searchMarkers: props.searchMarkers,

View File

@@ -88,7 +88,7 @@ const useEditorCommands = (props: Props) => {
editorRef.current.updateBody(newBody);
}
},
textHorizontalRule: () => editorRef.current.insertText('* * *'),
textHorizontalRule: () => editorRef.current.execCommand(EditorCommandType.InsertHorizontalRule),
'editor.execCommand': (value: CommandValue) => {
if (!('args' in value)) value.args = [];

View File

@@ -27,6 +27,10 @@ const useKeymap = (editorControl: CodeMirrorControl) => {
binding.accelerator, CodeMirrorVersion.CodeMirror6,
),
run: () => {
if (!CommandService.instance().isEnabled(binding.command)) {
return false;
}
void CommandService.instance().execute(binding.command);
return true;
},

View File

@@ -0,0 +1,44 @@
import { RefObject, useRef, useEffect } from 'react';
import { focus } from '@joplin/lib/utils/focusHandler';
import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
import NoteTextViewer from '../../../../../NoteTextViewer';
interface Props {
editorRef: RefObject<CodeMirrorControl>;
webviewRef: RefObject<NoteTextViewer>;
visiblePanes: string[];
}
const useRefocusOnVisiblePaneChange = ({ editorRef, webviewRef, visiblePanes }: Props) => {
const lastVisiblePanes = useRef(visiblePanes);
useEffect(() => {
const editorHasFocus = editorRef.current?.cm6?.dom?.contains(document.activeElement);
const viewerHasFocus = webviewRef.current?.hasFocus();
const lastHadViewer = lastVisiblePanes.current.includes('viewer');
const hasViewer = visiblePanes.includes('viewer');
const lastHadEditor = lastVisiblePanes.current.includes('editor');
const hasEditor = visiblePanes.includes('editor');
const viewerJustHidden = lastHadViewer && !hasViewer;
if (viewerJustHidden && viewerHasFocus) {
focus('CodeMirror/refocusEditor1', editorRef.current);
}
// Jump focus to the editor just after showing it -- this assumes that the user
// shows the editor to start editing the note.
const editorJustShown = !lastHadEditor && hasEditor;
if (editorJustShown && viewerHasFocus) {
focus('CodeMirror/refocusEditor2', editorRef.current);
}
const editorJustHidden = lastHadEditor && !hasEditor;
if (editorJustHidden && editorHasFocus) {
focus('CodeMirror/refocusViewer', webviewRef.current);
}
lastVisiblePanes.current = visiblePanes;
}, [visiblePanes, editorRef, webviewRef]);
};
export default useRefocusOnVisiblePaneChange;

View File

@@ -411,6 +411,20 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
color: ${theme.color};
}
.tox .tox-dialog__body-nav-item {
color: ${theme.color};
}
.tox .tox-dialog__body-nav-item[aria-selected=true] {
color: ${theme.color3};
border-color: ${theme.color3};
background-color: ${theme.backgroundColor3};
}
.tox .tox-checkbox__icons .tox-checkbox-icon__unchecked svg {
fill: ${theme.color};
}
.tox .tox-collection--list .tox-collection__item--active {
color: ${theme.backgroundColor};
}
@@ -655,7 +669,6 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
// Handle the first table row as table header.
// https://www.tiny.cloud/docs/plugins/table/#table_header_type
table_header_type: 'sectionCells',
table_resize_bars: false,
language_url: ['en_US', 'en_GB'].includes(language) ? undefined : `${bridge().vendorDir()}/lib/tinymce/langs/${language}`,
toolbar: toolbar.join(' '),
localization_function: _,

View File

@@ -218,15 +218,6 @@ function NoteEditor(props: NoteEditorProps) {
}
}, [handleProvisionalFlag, formNote, setFormNote, isNewNote, titleHasBeenManuallyChanged, scheduleNoteListResort, scheduleSaveNote]);
useWindowCommandHandler({
dispatch: props.dispatch,
setShowLocalSearch,
noteSearchBarRef,
editorRef,
titleInputRef,
setFormNote,
});
const onDrop = useDropHandler({ editorRef });
const onBodyChange = useCallback((event: OnChangeEvent) => onFieldChange('body', event.content, event.changeId), [onFieldChange]);
@@ -234,6 +225,15 @@ function NoteEditor(props: NoteEditorProps) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const onTitleChange = useCallback((event: any) => onFieldChange('title', event.target.value), [onFieldChange]);
useWindowCommandHandler({
dispatch: props.dispatch,
setShowLocalSearch,
noteSearchBarRef,
editorRef,
titleInputRef,
onBodyChange,
});
// const onTitleKeydown = useCallback((event:any) => {
// const keyCode = event.keyCode;

View File

@@ -1,34 +1,10 @@
import * as React from 'react';
import { _ } from '@joplin/lib/locale';
import CommandService from '@joplin/lib/services/CommandService';
import { ChangeEvent, useCallback } from 'react';
import { ChangeEvent, useCallback, useRef } from 'react';
import NoteToolbar from '../../NoteToolbar/NoteToolbar';
import { buildStyle } from '@joplin/lib/theme';
import time from '@joplin/lib/time';
import styled from 'styled-components';
const StyledRoot = styled.div`
display: flex;
flex-direction: row;
align-items: center;
padding-left: ${props => props.theme.editorPaddingLeft}px;
@media (max-width: 800px) {
flex-direction: column;
align-items: flex-start;
}
`;
const InfoGroup = styled.div`
display: flex;
flex-direction: row;
align-items: center;
@media (max-width: 800px) {
border-top: 1px solid ${props => props.theme.dividerColor};
width: 100%;
}
`;
interface Props {
themeId: number;
@@ -75,6 +51,33 @@ function styles_(props: Props) {
});
}
const useReselectHandlers = () => {
const lastTitleFocus = useRef([0, 0]);
const lastTitleValue = useRef('');
const onTitleBlur: React.FocusEventHandler<HTMLInputElement> = useCallback((event) => {
const titleElement = event.currentTarget;
lastTitleFocus.current = [titleElement.selectionStart, titleElement.selectionEnd];
lastTitleValue.current = titleElement.value;
}, []);
const onTitleFocus: React.FocusEventHandler<HTMLInputElement> = useCallback((event) => {
const titleElement = event.currentTarget;
// By default, focusing the note title bar can cause its content to become selected. We override
// this with a more reasonable default:
if (titleElement.selectionStart === 0 && titleElement.selectionEnd === titleElement.value.length) {
if (lastTitleValue.current !== titleElement.value) {
titleElement.selectionStart = titleElement.value.length;
} else {
titleElement.selectionStart = lastTitleFocus.current[0];
titleElement.selectionEnd = lastTitleFocus.current[1];
}
}
}, []);
return { onTitleBlur, onTitleFocus };
};
export default function NoteTitleBar(props: Props) {
const styles = styles_(props);
@@ -88,6 +91,8 @@ export default function NoteTitleBar(props: Props) {
}
}, []);
const { onTitleFocus, onTitleBlur } = useReselectHandlers();
function renderTitleBarDate() {
return <span className="updated-time-label" style={styles.titleDate}>{time.formatMsToLocal(props.noteUserUpdatedTime)}</span>;
}
@@ -101,7 +106,7 @@ export default function NoteTitleBar(props: Props) {
}
return (
<StyledRoot>
<div className='note-title-wrapper'>
<input
className="title-input"
type="text"
@@ -111,12 +116,14 @@ export default function NoteTitleBar(props: Props) {
readOnly={props.disabled}
onChange={props.onTitleChange}
onKeyDown={onTitleKeydown}
onFocus={onTitleFocus}
onBlur={onTitleBlur}
value={props.noteTitle}
/>
<InfoGroup>
<div className='note-title-info-group'>
{renderTitleBarDate()}
{renderNoteToolbar()}
</InfoGroup>
</StyledRoot>
</div>
</div>
);
}

View File

@@ -38,7 +38,6 @@ const incompatiblePluginIds = [
'ylc395.noteLinkSystem',
'outline',
'joplin.plugin.cmoptions',
'plugin.calebjohn.MathMode',
'com.ckant.joplin-plugin-better-code-blocks',
// cSpell:enable
];

View File

@@ -1,3 +1,5 @@
@use "./styles/warning-banner.scss";
@use "./styles/warning-banner-link.scss";
@use "./styles/note-title-info-group.scss";
@use "./styles/note-title-wrapper.scss";

View File

@@ -0,0 +1,11 @@
.note-title-info-group {
display: flex;
flex-direction: row;
align-items: center;
@media (max-width: 800px) {
border-top: 1px solid var(--joplin-divider-color);
width: 100%;
}
}

View File

@@ -0,0 +1,12 @@
.note-title-wrapper {
display: flex;
flex-direction: row;
align-items: center;
padding-left: var(--joplin-editor-padding-left);
@media (max-width: 800px) {
flex-direction: column;
align-items: flex-start;
}
}

View File

@@ -1,3 +1,4 @@
import { LinkRenderingType } from '@joplin/renderer/MdToHtml';
import { MarkupToHtmlOptions } from './types';
export default (override: MarkupToHtmlOptions = null): MarkupToHtmlOptions => {
@@ -7,7 +8,7 @@ export default (override: MarkupToHtmlOptions = null): MarkupToHtmlOptions => {
checkboxRenderingType: 2,
},
link_open: {
linkRenderingType: 2,
linkRenderingType: LinkRenderingType.HrefHandler,
},
},
replaceResourceInternalToExternalLinks: true,

View File

@@ -1,5 +1,5 @@
import { RefObject, useEffect } from 'react';
import { FormNote, NoteBodyEditorRef, ScrollOptionTypes } from './types';
import { NoteBodyEditorRef, OnChangeEvent, ScrollOptionTypes } from './types';
import editorCommandDeclarations, { enabledCondition } from '../editorCommandDeclarations';
import CommandService, { CommandDeclaration, CommandRuntime, CommandContext } from '@joplin/lib/services/CommandService';
import time from '@joplin/lib/time';
@@ -12,7 +12,7 @@ const commandsWithDependencies = [
require('../commands/pasteAsText'),
];
type SetFormNoteCallback = (callback: (prev: FormNote)=> FormNote)=> void;
type OnBodyChange = (event: OnChangeEvent)=> void;
interface HookDependencies {
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
@@ -23,13 +23,13 @@ interface HookDependencies {
noteSearchBarRef: any;
editorRef: RefObject<NoteBodyEditorRef>;
titleInputRef: RefObject<HTMLInputElement>;
setFormNote: SetFormNoteCallback;
onBodyChange: OnBodyChange;
}
function editorCommandRuntime(
declaration: CommandDeclaration,
editorRef: RefObject<NoteBodyEditorRef>,
setFormNote: SetFormNoteCallback,
onBodyChange: OnBodyChange,
): CommandRuntime {
return {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
@@ -55,9 +55,7 @@ function editorCommandRuntime(
value: args[0],
});
} else if (declaration.name === 'editor.setText') {
setFormNote((prev: FormNote) => {
return { ...prev, body: args[0] };
});
onBodyChange({ content: args[0], changeId: 0 });
} else {
return editorRef.current.execCommand({
name: declaration.name,
@@ -78,11 +76,11 @@ function editorCommandRuntime(
}
export default function useWindowCommandHandler(dependencies: HookDependencies) {
const { setShowLocalSearch, noteSearchBarRef, editorRef, titleInputRef, setFormNote } = dependencies;
const { setShowLocalSearch, noteSearchBarRef, editorRef, titleInputRef, onBodyChange } = dependencies;
useEffect(() => {
for (const declaration of editorCommandDeclarations) {
CommandService.instance().registerRuntime(declaration.name, editorCommandRuntime(declaration, editorRef, setFormNote));
CommandService.instance().registerRuntime(declaration.name, editorCommandRuntime(declaration, editorRef, onBodyChange));
}
const dependencies = {
@@ -105,5 +103,5 @@ export default function useWindowCommandHandler(dependencies: HookDependencies)
CommandService.instance().unregisterRuntime(command.declaration.name);
}
};
}, [editorRef, setShowLocalSearch, noteSearchBarRef, titleInputRef, setFormNote]);
}, [editorRef, setShowLocalSearch, noteSearchBarRef, titleInputRef, onBodyChange]);
}

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { useMemo, useRef, useEffect } from 'react';
import { useMemo, useRef, useEffect, useCallback } from 'react';
import { AppState } from '../../app.reducer';
import BaseModel, { ModelType } from '@joplin/lib/BaseModel';
import { Props } from './utils/types';
@@ -275,6 +275,12 @@ const NoteList = (props: Props) => {
return output;
}, [listRenderer.flow]);
const onContainerContextMenu = useCallback((event: React.MouseEvent) => {
const isFromKeyboard = event.button === -1;
if (event.isDefaultPrevented() || !isFromKeyboard) return;
onItemContextMenu({ itemId: activeNoteId });
}, [onItemContextMenu, activeNoteId]);
return (
<div
role='listbox'
@@ -293,6 +299,7 @@ const NoteList = (props: Props) => {
onKeyDown={onKeyDown}
onKeyUp={onKeyUp}
onDrop={onDrop}
onContextMenu={onContainerContextMenu}
>
{renderEmptyList()}
{renderFiller('top', topFillerStyle)}

View File

@@ -122,7 +122,7 @@ const useOnKeyDown = (
}
if (noteIds.length) {
if (key === 'Delete' && event.shiftKey) {
if (key === 'Delete' && event.shiftKey || (key === 'Backspace' && event.metaKey && event.altKey)) {
event.preventDefault();
if (CommandService.instance().isEnabled('permanentlyDeleteNote')) {
void CommandService.instance().execute('permanentlyDeleteNote', noteIds);
@@ -153,14 +153,9 @@ const useOnKeyDown = (
announceForAccessibility(!wasCompleted ? _('Complete') : _('Incomplete'));
}
if (key === 'Tab') {
if (key === 'Tab' && event.shiftKey) {
event.preventDefault();
if (event.shiftKey) {
void CommandService.instance().execute('focusElement', 'sideBar');
} else {
void CommandService.instance().execute('focusElement', 'noteTitle');
}
void CommandService.instance().execute('focusElement', 'sideBar');
}
if (key.toUpperCase() === 'A' && (event.ctrlKey || event.metaKey)) {

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import Folder from '@joplin/lib/models/Folder';
import { NoteEntity } from '@joplin/lib/services/database/types';
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
@@ -6,6 +7,13 @@ import { Dispatch } from 'redux';
import bridge from '../../../services/bridge';
import NoteListUtils from '../../utils/NoteListUtils';
interface CustomContextMenuEvent {
itemId: string;
currentTarget?: undefined;
preventDefault?: undefined;
}
type ContextMenuEvent = React.MouseEvent|CustomContextMenuEvent;
const useOnContextMenu = (
selectedNoteIds: string[],
selectedFolderId: string,
@@ -15,10 +23,14 @@ const useOnContextMenu = (
plugins: PluginStates,
customCss: string,
) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
return useCallback((event: any) => {
const currentNoteId = event.currentTarget.getAttribute('data-id');
return useCallback((event: ContextMenuEvent) => {
let currentNoteId = event.currentTarget?.getAttribute('data-id');
if ('itemId' in event) {
currentNoteId = event.itemId;
}
if (!currentNoteId) return;
event.preventDefault?.();
let noteIds = [];
if (selectedNoteIds.indexOf(currentNoteId) < 0) {

View File

@@ -0,0 +1,55 @@
import { act, renderHook } from '@testing-library/react-hooks';
import useRootElement from './useRootElement';
describe('useRootElement', () => {
beforeEach(() => {
jest.useFakeTimers({ advanceTimers: true });
});
test('should find an element with a matching ID', async () => {
const testElement = document.createElement('div');
testElement.id = 'test-element-id';
document.body.appendChild(testElement);
const { result } = renderHook(useRootElement, {
initialProps: testElement.id,
});
await act(async () => {
await jest.advanceTimersByTimeAsync(100);
});
expect(result.current).toBe(testElement);
testElement.remove();
});
test('should redo the element search when the elementId prop changes', async () => {
const testElement = document.createElement('div');
document.body.appendChild(testElement);
const { rerender, result } = renderHook(useRootElement, {
initialProps: 'some-id-here',
});
await jest.advanceTimersByTimeAsync(100);
expect(result.current).toBe(null);
// Searching for another non-existent ID: Should not match
rerender('updated-id');
await jest.advanceTimersByTimeAsync(100);
expect(result.current).toBe(null);
// Should not match the first element if its ID is set to the original (search
// should be cancelled).
testElement.id = 'some-id-here';
await jest.advanceTimersByTimeAsync(100);
expect(result.current).toBe(null);
// Should match if the element ID changes to the updated ID.
await act(async () => {
testElement.id = 'updated-id';
await jest.advanceTimersByTimeAsync(100);
});
expect(result.current).toBe(testElement);
testElement.remove();
});
});

View File

@@ -6,7 +6,7 @@ const useRootElement = (elementId: string) => {
const [rootElement, setRootElement] = useState<HTMLDivElement>(null);
useAsyncEffect(async (event) => {
const element = await waitForElement(document, elementId);
const element = await waitForElement(document, elementId, event);
if (event.cancelled) return;
setRootElement(element);
}, [document, elementId]);

View File

@@ -26,8 +26,7 @@ export default class NoteTextViewerComponent extends React.Component<Props, any>
private initialized_ = false;
private domReady_ = false;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
private webviewRef_: any;
private webviewRef_: React.RefObject<HTMLIFrameElement>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
private webviewListeners_: any = null;
private removePluginAssetsCallback_: RemovePluginAssetsCallback|null = null;
@@ -131,10 +130,21 @@ export default class NoteTextViewerComponent extends React.Component<Props, any>
public focus() {
if (this.webviewRef_.current) {
// Calling focus on webviewRef_ seems to be necessary when NoteTextViewer.focus
// is called outside of a user event (e.g. in a setTimeout) or during automated
// tests:
focus('NoteTextViewer::focus', this.webviewRef_.current);
// Calling .focus on this.webviewRef.current isn't sufficient.
// To allow arrow-key scrolling, focus must also be set within the iframe:
this.send('focus');
}
}
public hasFocus() {
return this.webviewRef_.current?.contains(document.activeElement);
}
public tryInit() {
if (!this.initialized_ && this.webviewRef_.current) {
this.initWebview();

View File

@@ -151,6 +151,13 @@ const useOnRenderItem = (props: Props) => {
}
if (itemType === BaseModel.TYPE_FOLDER && !item.encryption_applied) {
menu.append(new MenuItem({
...menuUtils.commandToStatefulMenuItem('moveToFolder', [itemId]),
// By default, enabled is based on the selected folder. However, the right-click
// menu can be shown for unselected folders.
enabled: true,
}));
menu.append(new MenuItem(menuUtils.commandToStatefulMenuItem('openFolderDialog', { folderId: itemId })));
menu.append(new MenuItem({ type: 'separator' }));

View File

@@ -1,6 +1,5 @@
import * as React from 'react';
import { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
import { StyledIconSpan, StyledIconI } from './styles';
interface Props {
readonly themeId: number;
@@ -36,8 +35,12 @@ export default function ToolbarButton(props: Props) {
let icon = null;
const iconName = getProp(props, 'iconName');
if (iconName) {
const IconClass = isFontAwesomeIcon(iconName) ? StyledIconI : StyledIconSpan;
icon = <IconClass className={iconName} aria-label='' hasTitle={!!title} role='img'/>;
const iconProps: React.HTMLProps<HTMLDivElement> = {
'aria-label': '',
role: 'img',
className: `toolbar-icon ${title ? '-has-title' : ''} ${iconName}`,
};
icon = isFontAwesomeIcon(iconName) ? <i {...iconProps} /> : <span {...iconProps} />;
}
// Keep this for legacy compatibility but for consistency we should use "disabled" prop

View File

@@ -1,19 +0,0 @@
import { ThemeStyle } from '@joplin/lib/theme';
const styled = require('styled-components').default;
const { css } = require('styled-components');
interface IconProps {
readonly theme: ThemeStyle;
readonly hasTitle: boolean;
}
const iconStyle = css<IconProps>`
font-size: ${(props: IconProps) => props.theme.toolbarIconSize}px;
color: ${(props: IconProps) => props.theme.color3};
margin-right: ${(props: IconProps) => props.hasTitle ? 5 : 0}px;
pointer-events: none; /* Need this to get button tooltip to work */
`;
export const StyledIconI = styled.i`${iconStyle}`;
export const StyledIconSpan = styled.span`${iconStyle}`;

View File

@@ -1,4 +1,4 @@
import { useContext, useCallback, useMemo } from 'react';
import { useContext, useCallback, useMemo, useRef } from 'react';
import { StateLastDeletion } from '@joplin/lib/reducer';
import { _, _n } from '@joplin/lib/locale';
import NotyfContext from '../NotyfContext';
@@ -9,6 +9,7 @@ import restoreItems from '@joplin/lib/services/trash/restoreItems';
import { ModelType } from '@joplin/lib/BaseModel';
import { themeStyle } from '@joplin/lib/theme';
import { Dispatch } from 'redux';
import { NotyfNotification } from 'notyf';
interface Props {
lastDeletion: StateLastDeletion;
@@ -19,6 +20,7 @@ interface Props {
export default (props: Props) => {
const notyfContext = useContext(NotyfContext);
const notificationRef = useRef<NotyfNotification | null>(null);
const theme = useMemo(() => {
return themeStyle(props.themeId);
@@ -39,7 +41,8 @@ export default (props: Props) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const onCancelClick = useCallback(async (event: any) => {
notyf.dismissAll();
notyf.dismiss(notificationRef.current);
notificationRef.current = null;
const lastDeletion: StateLastDeletion = JSON.parse(event.currentTarget.getAttribute('data-lastDeletion'));
@@ -70,7 +73,8 @@ export default (props: Props) => {
const linkId = `deletion-notification-cancel-${Math.floor(Math.random() * 1000000)}`;
const cancelLabel = _('Cancel');
notyf.success(`${msg} <a href="#" class="cancel" data-lastDeletion="${htmlentities(JSON.stringify(props.lastDeletion))}" id="${linkId}">${cancelLabel}</a>`);
const notification = notyf.success(`${msg} <a href="#" class="cancel" data-lastDeletion="${htmlentities(JSON.stringify(props.lastDeletion))}" id="${linkId}">${cancelLabel}</a>`);
notificationRef.current = notification;
const element: HTMLAnchorElement = await waitForElement(document, linkId);
if (event.cancelled) return;

View File

@@ -5,7 +5,7 @@ import NotyfContext from '../NotyfContext';
import { UpdateInfo } from 'electron-updater';
import { ipcRenderer, IpcRendererEvent } from 'electron';
import { AutoUpdaterEvents } from '../../services/autoUpdater/AutoUpdaterService';
import { NotyfNotification } from 'notyf';
import { NotyfEvent, NotyfNotification } from 'notyf';
import { _ } from '@joplin/lib/locale';
import { htmlentities } from '@joplin/utils/html';
import shim from '@joplin/lib/shim';
@@ -16,6 +16,7 @@ interface UpdateNotificationProps {
export enum UpdateNotificationEvents {
ApplyUpdate = 'apply-update',
UpdateNotAvailable = 'update-not-available',
Dismiss = 'dismiss-update-notification',
}
@@ -86,17 +87,46 @@ const UpdateNotification = ({ themeId }: UpdateNotificationProps) => {
notificationRef.current = notification;
}, [notyf, theme]);
const handleUpdateNotAvailable = useCallback(() => {
if (notificationRef.current) return;
const noUpdateMessageHtml = htmlentities(_('No updates available'));
const messageHtml = `
<div class="update-notification" style="color: ${theme.color2};">
${noUpdateMessageHtml}
</div>
`;
const notification: NotyfNotification = notyf.open({
type: 'success',
message: messageHtml,
position: {
x: 'right',
y: 'bottom',
},
duration: 5000,
});
notification.on(NotyfEvent.Dismiss, () => {
notificationRef.current = null;
});
notificationRef.current = notification;
}, [notyf, theme]);
useEffect(() => {
ipcRenderer.on(AutoUpdaterEvents.UpdateDownloaded, handleUpdateDownloaded);
ipcRenderer.on(AutoUpdaterEvents.UpdateNotAvailable, handleUpdateNotAvailable);
document.addEventListener(UpdateNotificationEvents.ApplyUpdate, handleApplyUpdate);
document.addEventListener(UpdateNotificationEvents.Dismiss, handleDismissNotification);
return () => {
ipcRenderer.removeListener(AutoUpdaterEvents.UpdateDownloaded, handleUpdateDownloaded);
ipcRenderer.removeListener(AutoUpdaterEvents.UpdateNotAvailable, handleUpdateNotAvailable);
document.removeEventListener(UpdateNotificationEvents.ApplyUpdate, handleApplyUpdate);
document.removeEventListener(UpdateNotificationEvents.Dismiss, handleDismissNotification);
};
}, [handleApplyUpdate, handleDismissNotification, handleUpdateDownloaded]);
}, [handleApplyUpdate, handleDismissNotification, handleUpdateDownloaded, handleUpdateNotAvailable]);
return (

View File

@@ -369,7 +369,7 @@
ipc.focus = (event) => {
const dummyID = 'joplin-content-focus-dummy';
if (! document.getElementById(dummyID)) {
const focusDummy = '<div style="width: 0; height: 0; overflow: hidden"><a id="' + dummyID + '" href="#">focus dummy</a></div>';
const focusDummy = '<div style="width: 0; height: 0; overflow: hidden"><a id="' + dummyID + '" href="#">Note viewer top</a></div>';
contentElement.insertAdjacentHTML("afterbegin", focusDummy);
}
const scrollTop = contentElement.scrollTop;
@@ -733,6 +733,13 @@
}));
document.addEventListener('click', webviewLib.logEnabledEventHandler(e => {
// Links should all have custom click handlers. Allowing Electron to load custom links
// can cause security issues, particularly if these links have the same domain as the
// top-level page.
if (e.target.hasAttribute('href')) {
e.preventDefault();
}
document.querySelectorAll('.media-pdf').forEach(element => {
if(!!element.contentWindow){
element.contentWindow.postMessage({

View File

@@ -0,0 +1,4 @@
.dialog-anchor-node {
display: none;
}

View File

@@ -5,4 +5,7 @@
@use './flat-button.scss';
@use './help-text.scss';
@use './toolbar-button.scss';
@use './toolbar-icon.scss';
@use './editor-toolbar.scss';
@use './user-webview-dialog-container.scss';
@use './dialog-anchor-node.scss';

View File

@@ -0,0 +1,11 @@
.toolbar-icon {
font-size: var(--joplin-toolbar-icon-size);
color: var(--joplin-color3);
margin-right: 0px;
pointer-events: none; /* Need this to get button tooltip to work */
&.-has-title {
margin-right: 5px;
}
}

View File

@@ -0,0 +1,11 @@
.user-webview-dialog-container {
display: flex;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1000;
box-sizing: border-box;
}

View File

@@ -32,7 +32,7 @@ export default class NoteListUtils {
const menuUtils = new MenuUtils(cmdService);
const notes: NoteEntity[] = noteIds.map(id => BaseModel.byId(props.notes, id));
const notes: NoteEntity[] = BaseModel.modelsByIds(props.notes, noteIds);
const singleNoteId = noteIds.length === 1 ? noteIds[0] : null;

View File

@@ -36,7 +36,7 @@ test.describe('main', () => {
await mainWindow.keyboard.type('New note content!');
// Should render
const viewerFrame = editor.getNoteViewerIframe();
const viewerFrame = editor.getNoteViewerFrameLocator();
await expect(viewerFrame.locator('h1')).toHaveText('Test note!');
});
@@ -78,7 +78,7 @@ test.describe('main', () => {
}
// Should render mermaid
const viewerFrame = editor.getNoteViewerIframe();
const viewerFrame = editor.getNoteViewerFrameLocator();
await expect(
viewerFrame.locator('pre.mermaid text', { hasText: testCommitId }),
).toBeVisible();
@@ -115,7 +115,7 @@ test.describe('main', () => {
await setMessageBoxResponse(electronApp, /^No/i);
await editor.attachFileButton.click();
const viewerFrame = editor.getNoteViewerIframe();
const viewerFrame = editor.getNoteViewerFrameLocator();
const renderedImage = viewerFrame.getByAltText(filename);
const fullSize = await getImageSourceSize(renderedImage);
@@ -136,50 +136,55 @@ test.describe('main', () => {
expect(fullSize[0] / resizedSize[0]).toBeCloseTo(fullSize[1] / resizedSize[1]);
});
test('clicking on an external link should try to launch a browser', async ({ electronApp, mainWindow }) => {
const mainScreen = new MainScreen(mainWindow);
await mainScreen.waitFor();
for (const target of ['', '_blank']) {
test(`clicking on an external link with target=${JSON.stringify(target)} should try to launch a browser`, async ({ electronApp, mainWindow }) => {
const mainScreen = new MainScreen(mainWindow);
await mainScreen.waitFor();
// Mock openExternal
const nextExternalUrlPromise = electronApp.evaluate(({ shell }) => {
return new Promise<string>(resolve => {
const openExternal = async (url: string) => {
resolve(url);
};
shell.openExternal = openExternal;
// Mock openExternal
const nextExternalUrlPromise = electronApp.evaluate(({ shell }) => {
return new Promise<string>(resolve => {
const openExternal = async (url: string) => {
resolve(url);
};
shell.openExternal = openExternal;
});
});
// Create a test link
const testLinkTitle = 'This is a test link!';
const linkHref = 'https://joplinapp.org/';
await mainWindow.evaluate(({ testLinkTitle, linkHref, target }) => {
const testLink = document.createElement('a');
testLink.textContent = testLinkTitle;
testLink.onclick = () => {
// We need to navigate by setting location.href -- clicking on a link
// directly within the main window (i.e. not in a PDF viewer) doesn't
// navigate.
location.href = linkHref;
};
testLink.href = '#';
// Display on top of everything
testLink.style.zIndex = '99999';
testLink.style.position = 'fixed';
testLink.style.top = '0';
testLink.style.left = '0';
if (target) {
testLink.target = target;
}
document.body.appendChild(testLink);
}, { testLinkTitle, linkHref, target });
const testLink = mainWindow.getByText(testLinkTitle);
await expect(testLink).toBeVisible();
await testLink.click({ noWaitAfter: true });
expect(await nextExternalUrlPromise).toBe(linkHref);
});
// Create a test link
const testLinkTitle = 'This is a test link!';
const linkHref = 'https://joplinapp.org/';
await mainWindow.evaluate(({ testLinkTitle, linkHref }) => {
const testLink = document.createElement('a');
testLink.textContent = testLinkTitle;
testLink.onclick = () => {
// We need to navigate by setting location.href -- clicking on a link
// directly within the main window (i.e. not in a PDF viewer) doesn't
// navigate.
location.href = linkHref;
};
testLink.href = '#';
// Display on top of everything
testLink.style.zIndex = '99999';
testLink.style.position = 'fixed';
testLink.style.top = '0';
testLink.style.left = '0';
document.body.appendChild(testLink);
}, { testLinkTitle, linkHref });
const testLink = mainWindow.getByText(testLinkTitle);
await expect(testLink).toBeVisible();
await testLink.click({ noWaitAfter: true });
expect(await nextExternalUrlPromise).toBe(linkHref);
});
}
test('should start in safe mode if profile-dir/force-safe-mode-on-next-start exists', async ({ profileDirectory }) => {
await writeFile(join(profileDirectory, 'force-safe-mode-on-next-start'), 'true', 'utf8');

View File

@@ -3,6 +3,7 @@ import MainScreen from './models/MainScreen';
import { join } from 'path';
import getImageSourceSize from './util/getImageSourceSize';
import setFilePickerResponse from './util/setFilePickerResponse';
import activateMainMenuItem from './util/activateMainMenuItem';
test.describe('markdownEditor', () => {
@@ -13,13 +14,18 @@ test.describe('markdownEditor', () => {
await mainScreen.importHtmlDirectory(electronApp, join(__dirname, 'resources', 'html-import'));
const importedFolder = mainScreen.sidebar.container.getByText('html-import');
await importedFolder.waitFor();
await importedFolder.click();
await mainScreen.noteList.focusContent(electronApp);
const importedHtmlFileItem = mainScreen.noteList.getNoteItemByTitle('test-html-file-with-image');
await importedHtmlFileItem.click();
// Retry -- focusing the imported-folder may fail in some cases
await expect(async () => {
await importedFolder.click();
const viewerFrame = mainScreen.noteEditor.getNoteViewerIframe();
await mainScreen.noteList.focusContent(electronApp);
const importedHtmlFileItem = mainScreen.noteList.getNoteItemByTitle('test-html-file-with-image');
await importedHtmlFileItem.click({ timeout: 300 });
}).toPass();
const viewerFrame = mainScreen.noteEditor.getNoteViewerFrameLocator();
// Should render headers
await expect(viewerFrame.locator('h1')).toHaveText('Test HTML file!');
@@ -39,7 +45,7 @@ test.describe('markdownEditor', () => {
await setFilePickerResponse(electronApp, [join(__dirname, 'resources', 'small-pdf.pdf')]);
await editor.attachFileButton.click();
const viewerFrame = mainScreen.noteEditor.getNoteViewerIframe();
const viewerFrame = mainScreen.noteEditor.getNoteViewerFrameLocator();
const pdfLink = viewerFrame.getByText('small-pdf.pdf');
await expect(pdfLink).toBeVisible();
@@ -72,6 +78,24 @@ test.describe('markdownEditor', () => {
await mainScreen.noteEditor.toggleEditorsButton.click();
await expectToBeRendered();
// Clicking on the PDF link should attempt to open it in a viewer
await expect(pdfLink).toBeVisible();
const nextOpenFilePromise = electronApp.evaluate(({ shell }) => {
return new Promise<string>(resolve => {
const openPath = async (url: string) => {
resolve(url);
return '';
};
shell.openPath = openPath;
});
});
await pdfLink.click();
expect(await nextOpenFilePromise).toMatch(/\.pdf$/);
// Should not have rendered something else in the viewer frame
await expectToBeRendered();
});
test('preview pane should render video attachments', async ({ mainWindow, electronApp }) => {
@@ -83,7 +107,7 @@ test.describe('markdownEditor', () => {
await setFilePickerResponse(electronApp, [join(__dirname, 'resources', 'video.mp4')]);
await editor.attachFileButton.click();
const videoLocator = editor.getNoteViewerIframe().locator('video');
const videoLocator = editor.getNoteViewerFrameLocator().locator('video');
const expectVideoToRender = async () => {
await expect(videoLocator).toBeSeekableMediaElement(6.9, 7);
};
@@ -149,7 +173,7 @@ test.describe('markdownEditor', () => {
await mainWindow.keyboard.press('Enter');
await mainWindow.keyboard.type('This is a test of search. `Test inline code`');
const viewer = noteEditor.getNoteViewerIframe();
const viewer = noteEditor.getNoteViewerFrameLocator();
await expect(viewer.locator('h1')).toHaveText('Testing');
const matches = viewer.locator('mark');
@@ -190,5 +214,39 @@ test.describe('markdownEditor', () => {
await expect(noteEditor.codeMirrorEditor).toBeVisible();
await expect(noteEditor.editorSearchInput).not.toBeVisible();
});
test('should move focus when the visible editor panes change', async ({ mainWindow, electronApp }) => {
const mainScreen = new MainScreen(mainWindow);
await mainScreen.waitFor();
const noteEditor = mainScreen.noteEditor;
await mainScreen.createNewNote('Note');
await noteEditor.focusCodeMirrorEditor();
await mainWindow.keyboard.type('test');
const focusInMarkdownEditor = noteEditor.codeMirrorEditor.locator(':focus');
await expect(focusInMarkdownEditor).toBeAttached();
const toggleEditorLayout = () => activateMainMenuItem(electronApp, 'Toggle editor layout');
// Editor only
await toggleEditorLayout();
await expect(noteEditor.noteViewerContainer).not.toBeVisible();
// Markdown editor should be focused
await expect(focusInMarkdownEditor).toBeAttached();
// Viewer only
await toggleEditorLayout();
await expect(noteEditor.codeMirrorEditor).not.toBeVisible();
// Viewer should be focused
await expect(noteEditor.noteViewerContainer).toBeFocused();
// Viewer and editor
await toggleEditorLayout();
await expect(noteEditor.noteViewerContainer).toBeAttached();
await expect(noteEditor.codeMirrorEditor).toBeVisible();
// Editor should be focused
await expect(focusInMarkdownEditor).toBeAttached();
});
});

View File

@@ -14,10 +14,7 @@ export default class GoToAnything {
public async open(electronApp: ElectronApplication) {
await this.mainScreen.waitFor();
if (!await activateMainMenuItem(electronApp, 'Goto Anything...')) {
throw new Error('Menu item for opening Goto Anything not found');
}
await activateMainMenuItem(electronApp, 'Goto Anything...');
return this.waitFor();
}
@@ -33,4 +30,16 @@ export default class GoToAnything {
public async expectToBeOpen() {
await expect(this.containerLocator).toBeAttached();
}
public async runCommand(electronApp: ElectronApplication, command: string) {
if (!command.startsWith(':')) {
command = `:${command}`;
}
await this.open(electronApp);
await this.inputLocator.fill(command);
await this.containerLocator.locator('.match-highlight').first().waitFor();
await this.inputLocator.press('Enter');
await this.expectToBeClosed();
}
}

View File

@@ -46,12 +46,7 @@ export default class MainScreen {
public async openSettings(electronApp: ElectronApplication) {
// Check both labels so this works on MacOS
const openedWithPreferences = await activateMainMenuItem(electronApp, 'Preferences...');
const openedWithOptions = await activateMainMenuItem(electronApp, 'Options');
if (!openedWithOptions && !openedWithPreferences) {
throw new Error('Unable to find settings menu item in application menus.');
}
await activateMainMenuItem(electronApp, /^(Preferences\.\.\.|Options)$/);
}
public async search(text: string) {
@@ -61,10 +56,6 @@ export default class MainScreen {
public async importHtmlDirectory(electronApp: ElectronApplication, path: string) {
await setFilePickerResponse(electronApp, [path]);
const startedImport = await activateMainMenuItem(electronApp, 'HTML - HTML document (Directory)', 'Import');
if (!startedImport) {
throw new Error('Unable to find HTML directory import menu item.');
}
await activateMainMenuItem(electronApp, 'HTML - HTML document (Directory)', 'Import');
}
}

View File

@@ -1,8 +1,10 @@
import { Locator, Page } from '@playwright/test';
import { expect } from '../util/test';
export default class NoteEditorPage {
public readonly codeMirrorEditor: Locator;
public readonly noteViewerContainer: Locator;
public readonly richTextEditor: Locator;
public readonly noteTitleInput: Locator;
public readonly attachFileButton: Locator;
@@ -12,7 +14,7 @@ export default class NoteEditorPage {
public readonly viewerSearchInput: Locator;
private readonly containerLocator: Locator;
public constructor(private readonly page: Page) {
public constructor(page: Page) {
this.containerLocator = page.locator('.rli-editor');
this.codeMirrorEditor = this.containerLocator.locator('.cm-editor');
this.richTextEditor = this.containerLocator.locator('iframe[title="Rich Text Area"]');
@@ -20,6 +22,7 @@ export default class NoteEditorPage {
this.attachFileButton = this.containerLocator.getByRole('button', { name: 'Attach file' });
this.toggleEditorsButton = this.containerLocator.getByRole('button', { name: 'Toggle editors' });
this.toggleEditorLayoutButton = this.containerLocator.getByRole('button', { name: 'Toggle editor layout' });
this.noteViewerContainer = this.containerLocator.locator('iframe[src$="note-viewer/index.html"]');
// The editor and viewer have slightly different search UI
this.editorSearchInput = this.containerLocator.getByPlaceholder('Find');
this.viewerSearchInput = this.containerLocator.getByPlaceholder('Search...');
@@ -29,14 +32,39 @@ export default class NoteEditorPage {
return this.containerLocator.getByRole('button', { name: title });
}
public getNoteViewerIframe() {
public async contentLocator() {
const richTextBody = this.getRichTextFrameLocator().locator('body');
const markdownEditor = this.codeMirrorEditor;
// Work around an issue where .or doesn't work with frameLocators.
// See https://github.com/microsoft/playwright/issues/27688#issuecomment-1771403495
await Promise.race([
richTextBody.waitFor({ state: 'visible' }).catch(()=>{}),
markdownEditor.waitFor({ state: 'visible' }).catch(()=>{}),
]);
if (await richTextBody.isVisible()) {
return richTextBody;
} else {
return markdownEditor;
}
}
public async expectToHaveText(content: string) {
// expect(...).toHaveText can fail in the Rich Text Editor (perhaps due to frame locators).
// Using expect.poll refreshes the locator on each attempt, which seems to prevent flakiness.
await expect.poll(
async () => (await this.contentLocator()).textContent(),
).toBe(content);
}
public getNoteViewerFrameLocator() {
// The note viewer can change content when the note re-renders. As such,
// a new locator needs to be created after re-renders (and this can't be a
// static property).
return this.page.frameLocator('[src$="note-viewer/index.html"]');
return this.noteViewerContainer.frameLocator(':scope');
}
public getTinyMCEFrameLocator() {
public getRichTextFrameLocator() {
// We use frameLocator(':scope') to convert the richTextEditor Locator into
// a FrameLocator. (:scope selects the locator itself).
// https://playwright.dev/docs/api/class-framelocator
@@ -51,4 +79,10 @@ export default class NoteEditorPage {
await this.noteTitleInput.waitFor();
await this.toggleEditorsButton.waitFor();
}
public async goBack() {
const backButton = this.toolbarButtonLocator('Back');
await expect(backButton).not.toBeDisabled();
await backButton.click();
}
}

View File

@@ -3,9 +3,11 @@ import { ElectronApplication, Locator, Page, expect } from '@playwright/test';
export default class NoteList {
public readonly container: Locator;
public readonly sortOrderButton: Locator;
public constructor(page: Page) {
this.container = page.locator('.rli-noteList');
this.sortOrderButton = this.container.getByRole('button', { name: 'Toggle sort order' });
}
public waitFor() {
@@ -13,14 +15,12 @@ export default class NoteList {
}
private async sortBy(electronApp: ElectronApplication, sortMethod: string) {
const success = await activateMainMenuItem(electronApp, sortMethod, 'Sort notes by');
if (!success) {
throw new Error(`Unable to find sorting menu item: ${sortMethod}`);
}
await activateMainMenuItem(electronApp, sortMethod, 'Sort notes by');
}
public async sortByTitle(electronApp: ElectronApplication) {
return this.sortBy(electronApp, 'Title');
await this.sortBy(electronApp, 'Title');
await expect(this.sortOrderButton).toHaveAttribute('title', /Toggle sort order field:[\n ]*title ->/);
}
public async focusContent(electronApp: ElectronApplication) {

View File

@@ -23,10 +23,7 @@ export default class Sidebar {
}
private async sortBy(electronApp: ElectronApplication, option: string) {
const success = await activateMainMenuItem(electronApp, option, 'Sort notebooks by');
if (!success) {
throw new Error(`Failed to find menu item: ${option}`);
}
await activateMainMenuItem(electronApp, option, 'Sort notebooks by');
}
public async sortByDate(electronApp: ElectronApplication) {

View File

@@ -32,7 +32,7 @@ test.describe('noteList', () => {
await mainWindow.keyboard.type('[Testing...](http://example.com/)');
// Wait to render
await expect(editor.getNoteViewerIframe().locator('a', { hasText: 'Testing...' })).toBeVisible();
await expect(editor.getNoteViewerFrameLocator().locator('a', { hasText: 'Testing...' })).toBeVisible();
// Updating the title should force the sidebar to update sooner
await expect(editor.noteTitleInput).toHaveValue('note-1');
@@ -91,7 +91,14 @@ test.describe('noteList', () => {
await noteList.focusContent(electronApp);
// The most recently-created note should be visible
const note4Item = noteList.getNoteItemByTitle('note_4');
const note3Item = noteList.getNoteItemByTitle('note_3');
const note2Item = noteList.getNoteItemByTitle('note_2');
const note1Item = noteList.getNoteItemByTitle('note_1');
await expect(note4Item).toBeVisible();
await expect(note3Item).toBeVisible();
await expect(note2Item).toBeVisible();
await expect(note1Item).toBeVisible();
await noteList.expectNoteToBeSelected('note_4');
await noteList.container.press('ArrowUp');

View File

@@ -0,0 +1,32 @@
import { test } from './util/test';
import MainScreen from './models/MainScreen';
test.describe('pluginApi', () => {
for (const richTextEditor of [false, true]) {
test(`the editor.setText command should update the current note (use RTE: ${richTextEditor})`, async ({ startAppWithPlugins }) => {
const { app, mainWindow } = await startAppWithPlugins(['resources/test-plugins/execCommand.js']);
const mainScreen = new MainScreen(mainWindow);
await mainScreen.createNewNote('First note');
const editor = mainScreen.noteEditor;
await editor.focusCodeMirrorEditor();
await mainWindow.keyboard.type('This content should be overwritten.');
if (richTextEditor) {
await editor.toggleEditorsButton.click();
await editor.richTextEditor.click();
}
await mainScreen.goToAnything.runCommand(app, 'testUpdateEditorText');
await editor.expectToHaveText('PASS');
// Should still have the same text after switching notes:
await mainScreen.createNewNote('Second note');
await editor.goBack();
await editor.expectToHaveText('PASS');
});
}
});

View File

@@ -0,0 +1,31 @@
// Allows referencing the Joplin global:
/* eslint-disable no-undef */
// Allows the `joplin-manifest` block comment:
/* eslint-disable multiline-comment-style */
/* joplin-manifest:
{
"id": "org.joplinapp.plugins.example.execCommand",
"manifest_version": 1,
"app_min_version": "3.1",
"name": "JS Bundle test",
"description": "JS Bundle Test plugin",
"version": "1.0.0",
"author": "",
"homepage_url": "https://joplinapp.org"
}
*/
joplin.plugins.register({
onStart: async function() {
await joplin.commands.register({
name: 'testUpdateEditorText',
label: 'Test setting the editor\'s text with editor.setText',
iconName: 'fas fa-drum',
execute: async () => {
await joplin.commands.execute('editor.setText', 'PASS');
},
});
},
});

View File

@@ -18,7 +18,7 @@ test.describe('richTextEditor', () => {
await editor.attachFileButton.click();
// Wait to render
const viewerFrame = editor.getNoteViewerIframe();
const viewerFrame = editor.getNoteViewerFrameLocator();
await viewerFrame.locator('a[data-from-md]').waitFor();
// Should have an attached resource
@@ -38,7 +38,7 @@ test.describe('richTextEditor', () => {
await editor.richTextEditor.waitFor();
// Edit the note to cause the original content to update
await editor.getTinyMCEFrameLocator().locator('a').click();
await editor.getRichTextFrameLocator().locator('a').click();
await mainWindow.keyboard.type('Test...');
await editor.toggleEditorsButton.click();
@@ -70,7 +70,7 @@ test.describe('richTextEditor', () => {
// Click on the attached file URL
const openPathResult = waitForNextOpenPath(electronApp);
const targetLink = editor.getTinyMCEFrameLocator().getByRole('link', { name: basename(pathToAttach) });
const targetLink = editor.getRichTextFrameLocator().getByRole('link', { name: basename(pathToAttach) });
if (process.platform === 'darwin') {
await targetLink.click({ modifiers: ['Meta'] });
} else {

View File

@@ -8,7 +8,7 @@ test.describe('settings', () => {
await mainScreen.waitFor();
// Sort order buttons should be visible by default
const sortOrderLocator = mainScreen.noteList.container.getByRole('button', { name: 'Toggle sort order' });
const sortOrderLocator = mainScreen.noteList.sortOrderButton;
await expect(sortOrderLocator).toBeVisible();
await mainScreen.openSettings(electronApp);

View File

@@ -1,4 +1,4 @@
import { test, expect } from './util/test';
import { test } from './util/test';
import MainScreen from './models/MainScreen';
import SettingsScreen from './models/SettingsScreen';
import activateMainMenuItem from './util/activateMainMenuItem';
@@ -28,7 +28,7 @@ test.describe('simpleBackup', () => {
await mainScreen.waitFor();
// Backups should work
expect(await activateMainMenuItem(electronApp, 'Create backup')).toBe(true);
await activateMainMenuItem(electronApp, 'Create backup');
const successDialog = mainWindow.locator('iframe[id$=backup-backupDialog]');
await successDialog.waitFor();

View File

@@ -1,5 +1,5 @@
import type { ElectronApplication } from '@playwright/test';
import { expect, type ElectronApplication } from '@playwright/test';
import type { MenuItem } from 'electron';
@@ -7,35 +7,45 @@ import type { MenuItem } from 'electron';
// https://github.com/spaceagetv/electron-playwright-helpers/blob/main/src/menu_helpers.ts
// If given, `parentMenuLabel` should be the label of the menu containing the target item.
const activateMainMenuItem = (
const activateMainMenuItem = async (
electronApp: ElectronApplication,
targetItemLabel: string,
targetItemLabel: string|RegExp,
parentMenuLabel?: string,
) => {
return electronApp.evaluate(async ({ Menu }, [targetItemLabel, parentMenuLabel]) => {
const activateItemInSubmenu = (submenu: MenuItem[], parentLabel: string) => {
for (const item of submenu) {
const matchesParent = !parentMenuLabel || parentLabel === parentMenuLabel;
if (item.label === targetItemLabel && matchesParent && item.visible) {
// Found!
item.click();
return true;
} else if (item.submenu) {
const foundItem = activateItemInSubmenu(item.submenu.items, item.label);
await expect.poll(() => {
return electronApp.evaluate(async ({ Menu }, [targetItemLabel, parentMenuLabel]) => {
const activateItemInSubmenu = (submenu: MenuItem[], parentLabel: string) => {
for (const item of submenu) {
const matchesParent = !parentMenuLabel || parentLabel === parentMenuLabel;
const matchesLabel = typeof targetItemLabel === 'string' ? (
targetItemLabel === item.label
) : (
item.label.match(targetItemLabel)
);
if (foundItem) {
if (matchesLabel && matchesParent && item.visible) {
// Found!
item.click();
return true;
} else if (item.submenu) {
const foundItem = activateItemInSubmenu(item.submenu.items, item.label);
if (foundItem) {
return true;
}
}
}
}
// No item found
return false;
};
// No item found
return false;
};
const appMenu = Menu.getApplicationMenu();
return activateItemInSubmenu(appMenu.items, '');
}, [targetItemLabel, parentMenuLabel]);
const appMenu = Menu.getApplicationMenu();
return activateItemInSubmenu(appMenu.items, '');
}, [targetItemLabel, parentMenuLabel]);
}, {
message: `should find and activate menu item with label ${JSON.stringify(targetItemLabel)}`,
}).toBe(true);
};
export default activateMainMenuItem;

View File

@@ -6,10 +6,12 @@ import createStartupArgs from './createStartupArgs';
import firstNonDevToolsWindow from './firstNonDevToolsWindow';
type StartWithPluginsResult = { app: ElectronApplication; mainWindow: Page };
type JoplinFixtures = {
profileDirectory: string;
electronApp: ElectronApplication;
startAppWithPlugins: (pluginPaths: string[])=> Promise<StartWithPluginsResult>;
startupPluginsLoaded: Promise<void>;
mainWindow: Page;
};
@@ -17,6 +19,20 @@ type JoplinFixtures = {
// A custom fixture that loads an electron app. See
// https://playwright.dev/docs/test-fixtures
const getAndResizeMainWindow = async (electronApp: ElectronApplication) => {
const mainWindow = await firstNonDevToolsWindow(electronApp);
// Setting the viewport size helps keep test environments consistent.
await mainWindow.setViewportSize({
width: 1200,
height: 800,
});
return mainWindow;
};
const testDir = dirname(__dirname);
export const test = base.extend<JoplinFixtures>({
// Playwright fails if we don't use the object destructuring
// pattern in the first argument.
@@ -25,7 +41,7 @@ export const test = base.extend<JoplinFixtures>({
//
// eslint-disable-next-line no-empty-pattern
profileDirectory: async ({ }, use) => {
const profilePath = resolve(join(dirname(__dirname), 'test-profile'));
const profilePath = resolve(join(testDir, 'test-profile'));
const profileSubdir = join(profilePath, uuid.createNano());
await mkdirp(profileSubdir);
@@ -44,6 +60,34 @@ export const test = base.extend<JoplinFixtures>({
await electronApp.close();
},
startAppWithPlugins: async ({ profileDirectory }, use) => {
const startupArgs = createStartupArgs(profileDirectory);
let electronApp: ElectronApplication;
await use(async (pluginPaths: string[]) => {
if (electronApp) {
throw new Error('Electron app already created');
}
electronApp = await electron.launch({
args: [
...startupArgs,
'--dev-plugins',
pluginPaths.map(path => resolve(testDir, path)).join(','),
],
});
return {
app: electronApp,
mainWindow: await getAndResizeMainWindow(electronApp),
};
});
if (electronApp) {
await electronApp.firstWindow();
await electronApp.close();
}
},
startupPluginsLoaded: async ({ electronApp }, use) => {
const startupPluginsLoadedPromise = electronApp.evaluate(({ ipcMain }) => {
return new Promise<void>(resolve => {
@@ -55,8 +99,7 @@ export const test = base.extend<JoplinFixtures>({
},
mainWindow: async ({ electronApp }, use) => {
const mainWindow = await firstNonDevToolsWindow(electronApp);
await use(mainWindow);
await use(await getAndResizeMainWindow(electronApp));
},
});

View File

@@ -24,9 +24,9 @@ jest.mock('@electron/remote', () => {
// Import after mocking problematic libraries
const { afterEachCleanUp, afterAllCleanUp } = require('@joplin/lib/testing/test-utils.js');
const React = require('react');
shimInit({ nodeSqlite: sqlite3 });
shimInit({ nodeSqlite: sqlite3, React });
afterEach(async () => {
await afterEachCleanUp();

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
"version": "3.1.6",
"version": "3.1.19",
"description": "Joplin for Desktop",
"main": "main.js",
"private": true,
@@ -15,7 +15,7 @@
"test": "jest",
"test-ui": "playwright test",
"test-ci": "yarn test && sh ./integration-tests/run-ci.sh",
"renameReleaseAssets": "node tools/renameReleaseAssets.js"
"modifyReleaseAssets": "node tools/modifyReleaseAssets.js"
},
"repository": {
"type": "git",
@@ -124,33 +124,34 @@
"homepage": "https://github.com/laurent22/joplin#readme",
"devDependencies": {
"7zip-bin": "5.2.0",
"@electron/rebuild": "3.3.0",
"@electron/rebuild": "3.6.0",
"@joplin/default-plugins": "~3.1",
"@joplin/tools": "~3.1",
"@playwright/test": "1.43.1",
"@playwright/test": "1.44.1",
"@testing-library/react-hooks": "8.0.1",
"@types/jest": "29.5.8",
"@types/node": "18.19.34",
"@types/jest": "29.5.12",
"@types/node": "18.19.39",
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"@types/react-redux": "7.1.33",
"@types/styled-components": "5.1.32",
"@types/tesseract.js": "2.0.0",
"axios": "^1.7.7",
"electron": "29.4.5",
"electron-builder": "24.13.3",
"glob": "10.3.16",
"glob": "10.4.5",
"gulp": "4.0.2",
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"js-sha512": "0.9.0",
"nan": "2.19.0",
"react-test-renderer": "18.3.1",
"ts-jest": "29.1.1",
"ts-jest": "29.1.5",
"ts-node": "10.9.2",
"typescript": "5.2.2"
"typescript": "5.4.5"
},
"dependencies": {
"@electron/notarize": "2.1.0",
"@electron/notarize": "2.3.2",
"@electron/remote": "2.1.2",
"@fortawesome/fontawesome-free": "5.15.4",
"@joeattardi/emoji-button": "4.6.4",
@@ -158,7 +159,7 @@
"@joplin/lib": "~3.1",
"@joplin/renderer": "~3.1",
"@joplin/utils": "~3.1",
"@sentry/electron": "4.17.0",
"@sentry/electron": "4.24.0",
"@types/mustache": "4.2.5",
"async-mutex": "0.5.0",
"codemirror": "5.65.9",
@@ -166,7 +167,6 @@
"compare-versions": "6.1.0",
"countable": "3.0.1",
"debounce": "1.2.1",
"electron-log": "5.1.6",
"electron-updater": "6.2.1",
"electron-window-state": "5.0.3",
"formatcoords": "1.1.3",
@@ -200,7 +200,7 @@
"styled-components": "5.3.11",
"styled-system": "5.1.5",
"taboverride": "4.0.3",
"tesseract.js": "5.0.5",
"tesseract.js": "5.1.0",
"tinymce": "5.10.6"
}
}

View File

@@ -2,11 +2,10 @@ import * as React from 'react';
import { AppState } from '../app.reducer';
import CommandService, { SearchResult as CommandSearchResult } from '@joplin/lib/services/CommandService';
import KeymapService from '@joplin/lib/services/KeymapService';
import shim from '@joplin/lib/shim';
const { connect } = require('react-redux');
import { _ } from '@joplin/lib/locale';
import { themeStyle } from '@joplin/lib/theme';
import SearchEngine from '@joplin/lib/services/search/SearchEngine';
import SearchEngine, { ComplexTerm } from '@joplin/lib/services/search/SearchEngine';
import gotoAnythingStyleQuery from '@joplin/lib/services/search/gotoAnythingStyleQuery';
import BaseModel, { ModelType } from '@joplin/lib/BaseModel';
import Tag from '@joplin/lib/models/Tag';
@@ -14,7 +13,7 @@ import Folder from '@joplin/lib/models/Folder';
import Note from '@joplin/lib/models/Note';
import ItemList from '../gui/ItemList';
import HelpButton from '../gui/HelpButton';
const { surroundKeywords, nextWhitespaceIndex, removeDiacritics } = require('@joplin/lib/string-utils.js');
import { surroundKeywords, nextWhitespaceIndex, removeDiacritics } from '@joplin/lib/string-utils';
import { mergeOverlappingIntervals } from '@joplin/lib/ArrayUtils';
import markupLanguageUtils from '../utils/markupLanguageUtils';
import focusEditorIfEditorCommand from '@joplin/lib/services/commands/focusEditorIfEditorCommand';
@@ -23,6 +22,7 @@ import { MarkupLanguage, MarkupToHtml } from '@joplin/renderer';
import Resource from '@joplin/lib/models/Resource';
import { NoteEntity, ResourceEntity } from '@joplin/lib/services/database/types';
import Dialog from '../gui/Dialog';
import AsyncActionQueue from '@joplin/lib/AsyncActionQueue';
const logger = Logger.create('GotoAnything');
@@ -129,8 +129,7 @@ class DialogComponent extends React.PureComponent<Props, State> {
private inputRef: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
private itemListRef: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
private listUpdateIID_: any;
private listUpdateQueue_: AsyncActionQueue;
private markupToHtml_: MarkupToHtml;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
private userCallback_: any = null;
@@ -141,6 +140,7 @@ class DialogComponent extends React.PureComponent<Props, State> {
const startString = props?.userData?.startString ? props?.userData?.startString : '';
this.userCallback_ = props?.userData?.callback;
this.listUpdateQueue_ = new AsyncActionQueue(100);
this.state = {
query: startString,
@@ -235,7 +235,7 @@ class DialogComponent extends React.PureComponent<Props, State> {
}
public componentWillUnmount() {
if (this.listUpdateIID_) shim.clearTimeout(this.listUpdateIID_);
void this.listUpdateQueue_.reset();
this.props.dispatch({
type: 'VISIBLE_DIALOGS_REMOVE',
@@ -263,12 +263,7 @@ class DialogComponent extends React.PureComponent<Props, State> {
}
public scheduleListUpdate() {
if (this.listUpdateIID_) shim.clearTimeout(this.listUpdateIID_);
this.listUpdateIID_ = shim.setTimeout(async () => {
await this.updateList();
this.listUpdateIID_ = null;
}, 100);
this.listUpdateQueue_.push(() => this.updateList());
}
public async keywords(searchQuery: string) {
@@ -360,7 +355,6 @@ class DialogComponent extends React.PureComponent<Props, State> {
}
} else {
const limit = 20;
const searchKeywords = await this.keywords(searchQuery);
// Note: any filtering must be done **before** fetching the notes, because we're
// going to apply a limit to the number of fetched notes.
@@ -381,6 +375,10 @@ class DialogComponent extends React.PureComponent<Props, State> {
results = results.filter(r => !!notesById[r.id])
.map(r => ({ ...r, title: notesById[r.id].title }));
const normalizedKeywords = (await this.keywords(searchQuery)).map(
({ valueRegex }: ComplexTerm) => new RegExp(removeDiacritics(valueRegex), 'ig'),
);
for (let i = 0; i < results.length; i++) {
const row = results[i];
const path = Folder.folderPathString(this.props.folders, row.parent_id);
@@ -388,21 +386,14 @@ class DialogComponent extends React.PureComponent<Props, State> {
if (row.fields.includes('body')) {
let fragments = '...';
if (i < limit) { // Display note fragments of search keyword matches
const { markupLanguage, content } = getContentMarkupLanguageAndBody(
row,
notesById,
resources,
);
const loadFragments = (markupLanguage: MarkupLanguage, content: string) => {
const indices = [];
const body = this.markupToHtml().stripMarkup(markupLanguage, content, { collapseWhiteSpaces: true });
const normalizedBody = removeDiacritics(body);
// Iterate over all matches in the body for each search keyword
for (let { valueRegex } of searchKeywords) {
valueRegex = removeDiacritics(valueRegex);
for (const match of removeDiacritics(body).matchAll(new RegExp(valueRegex, 'ig'))) {
for (const keywordRegex of normalizedKeywords) {
for (const match of normalizedBody.matchAll(keywordRegex)) {
// Populate 'indices' with [begin index, end index] of each note fragment
// Begins at the regex matching index, ends at the next whitespace after seeking 15 characters to the right
indices.push([match.index, nextWhitespaceIndex(body, match.index + match[0].length + 15)]);
@@ -418,6 +409,19 @@ class DialogComponent extends React.PureComponent<Props, State> {
fragments = mergedIndices.map((f: any) => body.slice(f[0], f[1])).join(' ... ');
// Add trailing ellipsis if the final fragment doesn't end where the note is ending
if (mergedIndices.length && mergedIndices[mergedIndices.length - 1][1] !== body.length) fragments += ' ...';
};
if (i < limit) { // Display note fragments of search keyword matches
const { markupLanguage, content } = getContentMarkupLanguageAndBody(
row,
notesById,
resources,
);
// Don't load fragments for long notes -- doing so can lead to UI freezes.
if (content.length < 100_000) {
loadFragments(markupLanguage, content);
}
}
results[i] = { ...row, path, fragments };
@@ -539,11 +543,22 @@ class DialogComponent extends React.PureComponent<Props, State> {
const resultId = getResultId(item);
const isSelected = resultId === this.state.selectedItemId;
const rowStyle = isSelected ? style.rowSelected : style.row;
const wrapKeywordMatches = (unescapedContent: string) => {
return surroundKeywords(
this.state.keywords,
unescapedContent,
`<span class="match-highlight" style="font-weight: bold; color: ${theme.searchMarkerColor}; background-color: ${theme.searchMarkerBackgroundColor}">`,
'</span>',
{ escapeHtml: true },
);
};
const titleHtml = item.fragments
? `<span style="font-weight: bold; color: ${theme.color};">${item.title}</span>`
: surroundKeywords(this.state.keywords, item.title, `<span style="font-weight: bold; color: ${theme.searchMarkerColor}; background-color: ${theme.searchMarkerBackgroundColor}">`, '</span>', { escapeHtml: true });
: wrapKeywordMatches(item.title);
const fragmentsHtml = !item.fragments ? null : surroundKeywords(this.state.keywords, item.fragments, `<span style="color: ${theme.searchMarkerColor}; background-color: ${theme.searchMarkerBackgroundColor}">`, '</span>', { escapeHtml: true });
const fragmentsHtml = !item.fragments ? null : wrapKeywordMatches(item.fragments);
const folderIcon = <i style={{ fontSize: theme.fontSize, marginRight: 2 }} className="fa fa-book" role='img' aria-label={_('Notebook')} />;
const pathComp = !item.path ? null : <div style={style.rowPath}>{folderIcon} {item.path}</div>;

View File

@@ -46,23 +46,37 @@ describe('AutoUpdaterService', () => {
expect(release.tag_name).toBe('v3.1.2');
});
it('should return the correct download URL for Windows', async () => {
it('should return the correct download URL for Windows x32', async () => {
const release = await service.fetchLatestRelease(true);
expect(release).toBeDefined();
const url = service.getDownloadUrlForPlatform(release, 'win32');
const url = service.getDownloadUrlForPlatform(release, 'win32', 'ia32');
expect(url).toBe('https://github.com/laurent22/joplin/releases/download/v3.1.3/latest.yml');
});
it('should return the correct download URL for Mac', async () => {
it('should return the correct download URL for Windows x64', async () => {
const release = await service.fetchLatestRelease(true);
expect(release).toBeDefined();
const url = service.getDownloadUrlForPlatform(release, 'darwin');
const url = service.getDownloadUrlForPlatform(release, 'win32', 'x64');
expect(url).toBe('https://github.com/laurent22/joplin/releases/download/v3.1.3/latest.yml');
});
it('should return the correct download URL for Mac x64', async () => {
const release = await service.fetchLatestRelease(true);
expect(release).toBeDefined();
const url = service.getDownloadUrlForPlatform(release, 'darwin', 'x64');
expect(url).toBe('https://github.com/laurent22/joplin/releases/download/v3.1.3/latest-mac.yml');
});
it('should return the correct download URL for Mac arm64', async () => {
const release = await service.fetchLatestRelease(true);
expect(release).toBeDefined();
const url = service.getDownloadUrlForPlatform(release, 'darwin', 'arm64');
expect(url).toBe('https://github.com/laurent22/joplin/releases/download/v3.1.3/latest-mac-arm64.yml');
});
it('should throw an error for Linux', async () => {
const release = await service.fetchLatestRelease(true);
expect(release).toBeDefined();
expect(() => service.getDownloadUrlForPlatform(release, 'linux')).toThrow('The AutoUpdaterService does not support the following platform: linux');
expect(() => service.getDownloadUrlForPlatform(release, 'linux', 'amd64')).toThrow('The AutoUpdaterService does not support the following platform: linux');
});
});

View File

@@ -19,16 +19,28 @@ export enum AutoUpdaterEvents {
export const defaultUpdateInterval = 12 * 60 * 60 * 1000;
export const initialUpdateStartup = 5 * 1000;
const releasesLink = 'https://objects.joplinusercontent.com/r/releases';
const supportedPlatformAssets: { [key in string]: string } = {
'darwin': 'latest-mac.yml',
'win32': 'latest.yml',
export type Architecture = typeof process.arch;
interface PlatformAssets {
[platform: string]: {
[arch in Architecture]?: string;
};
}
const supportedPlatformAssets: PlatformAssets = {
'darwin': {
'x64': 'latest-mac.yml',
'arm64': 'latest-mac-arm64.yml',
},
'win32': {
'x64': 'latest.yml',
'ia32': 'latest.yml',
},
};
export interface AutoUpdaterServiceInterface {
checkForUpdates(): void;
checkForUpdates(isManualCheck: boolean): void;
updateApp(): void;
fetchLatestRelease(includePreReleases: boolean): Promise<GitHubRelease>;
getDownloadUrlForPlatform(release: GitHubRelease, platform: string): string;
getDownloadUrlForPlatform(release: GitHubRelease, platform: string, arch: string): string;
}
export default class AutoUpdaterService implements AutoUpdaterServiceInterface {
@@ -36,10 +48,11 @@ export default class AutoUpdaterService implements AutoUpdaterServiceInterface {
private logger_: LoggerWrapper;
private devMode_: boolean;
private enableDevMode = true; // force the updater to work in "dev" mode
private enableAutoDownload = false; // automatically download an update when it is found
private enableAutoDownload = true; // automatically download an update when it is found
private autoInstallOnAppQuit = false; // automatically install the downloaded update once the user closes the application
private includePreReleases_ = false;
private allowDowngrade = false;
private isManualCheckInProgress = false;
public constructor(mainWindow: BrowserWindow, logger: LoggerWrapper, devMode: boolean, includePreReleases: boolean) {
this.window_ = mainWindow;
@@ -49,8 +62,9 @@ export default class AutoUpdaterService implements AutoUpdaterServiceInterface {
this.configureAutoUpdater();
}
public checkForUpdates = async (): Promise<void> => {
public checkForUpdates = async (isManualCheck = false): Promise<void> => {
try {
this.isManualCheckInProgress = isManualCheck;
await this.checkForLatestRelease();
} catch (error) {
this.logger_.error('Failed to check for updates:', error);
@@ -76,15 +90,20 @@ export default class AutoUpdaterService implements AutoUpdaterServiceInterface {
};
public getDownloadUrlForPlatform(release: GitHubRelease, platform: string): string {
const assetName: string = supportedPlatformAssets[platform];
if (!assetName) {
public getDownloadUrlForPlatform(release: GitHubRelease, platform: string, arch: string): string {
if (!supportedPlatformAssets[platform]) {
throw new Error(`The AutoUpdaterService does not support the following platform: ${platform}`);
}
const platformAssets = supportedPlatformAssets[platform];
const assetName: string | undefined = platformAssets ? platformAssets[arch as Architecture] : undefined;
if (!assetName) {
throw new Error(`The AutoUpdaterService does not support the architecture: ${arch} for platform: ${platform}`);
}
const asset: GitHubReleaseAsset = release.assets.find(a => a.name === assetName);
if (!asset) {
throw new Error('No suitable update asset found for this platform.');
throw new Error(`Yml file: ${assetName} not found for version: ${release.tag_name} platform: ${platform} and architecture: ${arch}`);
}
return asset.browser_download_url;
@@ -110,11 +129,12 @@ export default class AutoUpdaterService implements AutoUpdaterServiceInterface {
const release: GitHubRelease = await this.fetchLatestRelease(this.includePreReleases_);
try {
let assetUrl = this.getDownloadUrlForPlatform(release, shim.platformName());
let assetUrl = this.getDownloadUrlForPlatform(release, shim.platformName(), process.arch);
// electron's autoUpdater appends automatically the platform's yml file to the link so we should remove it
assetUrl = assetUrl.substring(0, assetUrl.lastIndexOf('/'));
autoUpdater.setFeedURL({ provider: 'generic', url: assetUrl });
await autoUpdater.checkForUpdates();
this.isManualCheckInProgress = false;
} catch (error) {
this.logger_.error(`Update download url failed: ${error.message}`);
}
@@ -150,6 +170,10 @@ export default class AutoUpdaterService implements AutoUpdaterServiceInterface {
};
private onUpdateNotAvailable = (_info: UpdateInfo): void => {
if (this.isManualCheckInProgress) {
this.window_.webContents.send(AutoUpdaterEvents.UpdateNotAvailable);
}
this.logger_.info('Update not available.');
};

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { useRef, useImperativeHandle, forwardRef, useEffect } from 'react';
import { useRef, useImperativeHandle, forwardRef, useEffect, useMemo } from 'react';
import useViewIsReady from './hooks/useViewIsReady';
import useThemeCss from './hooks/useThemeCss';
import useContentSize from './hooks/useContentSize';
@@ -8,14 +8,10 @@ import useHtmlLoader from './hooks/useHtmlLoader';
import useWebviewToPluginMessages from './hooks/useWebviewToPluginMessages';
import useScriptLoader from './hooks/useScriptLoader';
import Logger from '@joplin/utils/Logger';
import styled from 'styled-components';
import { focus } from '@joplin/lib/utils/focusHandler';
const logger = Logger.create('UserWebview');
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
type StyleProps = any;
export interface Props {
html: string;
scripts: string[];
@@ -36,15 +32,6 @@ export interface Props {
onReady?: Function;
}
const StyledFrame = styled.iframe<{ fitToContent: boolean; borderBottom: boolean }>`
padding: 0;
margin: 0;
width: ${(props: StyleProps) => props.fitToContent ? `${props.width}px` : '100%'};
height: ${(props: StyleProps) => props.fitToContent ? `${props.height}px` : '100%'};
border: none;
border-bottom: ${(props: StyleProps) => props.borderBottom ? `1px solid ${props.theme.dividerColor}` : 'none'};
`;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
function serializeForm(form: any) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
@@ -153,15 +140,18 @@ function UserWebview(props: Props, ref: any) {
cssFilePath,
);
return <StyledFrame
const style = useMemo(() => ({
'--content-width': `${contentSize.width}px`,
'--content-height': `${contentSize.height}px`,
} as React.CSSProperties), [contentSize.width, contentSize.height]);
return <iframe
id={props.viewId}
width={contentSize.width}
height={contentSize.height}
fitToContent={props.fitToContent}
style={style}
className={`plugin-user-webview ${props.fitToContent ? '-fit-to-content' : ''} ${props.borderBottom ? '-border-bottom' : ''}`}
ref={viewRef}
src="services/plugins/UserWebviewIndex.html"
borderBottom={props.borderBottom}
></StyledFrame>;
></iframe>;
}
export default forwardRef(UserWebview);

View File

@@ -7,18 +7,12 @@ import UserWebview, { Props as UserWebviewProps } from './UserWebview';
import UserWebviewDialogButtonBar from './UserWebviewDialogButtonBar';
import { focus } from '@joplin/lib/utils/focusHandler';
import Dialog from '../../gui/Dialog';
const styled = require('styled-components').default;
interface Props extends UserWebviewProps {
buttons: ButtonSpec[];
fitToContent: boolean;
}
const UserWebViewWrapper = styled.div`
display: flex;
flex: 1;
`;
function defaultButtons(): ButtonSpec[] {
return [
{
@@ -84,7 +78,7 @@ export default function UserWebviewDialog(props: Props) {
return (
<Dialog className={`user-webview-dialog ${props.fitToContent ? '-fit' : ''}`}>
<UserWebViewWrapper>
<div className='user-dialog-wrapper'>
<UserWebview
ref={webviewRef}
html={props.html}
@@ -98,7 +92,7 @@ export default function UserWebviewDialog(props: Props) {
onDismiss={onDismiss}
onReady={onReady}
/>
</UserWebViewWrapper>
</div>
<UserWebviewDialogButtonBar buttons={buttons}/>
</Dialog>
);

View File

@@ -5,20 +5,10 @@ import { ButtonSpec } from '@joplin/lib/services/plugins/api/types';
const styled = require('styled-components').default;
const { space } = require('styled-system');
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
type StyleProps = any;
interface Props {
buttons: ButtonSpec[];
}
const StyledRoot = styled.div`
display: flex;
width: 100%;
box-sizing: border-box;
justify-content: flex-end;
padding-top: ${(props: StyleProps) => props.theme.mainPadding}px;
`;
const StyledButton = styled(Button)`${space}`;
@@ -48,8 +38,8 @@ export default function UserWebviewDialogButtonBar(props: Props) {
}
return (
<StyledRoot>
<div className='user-dialog-button-bar'>
{renderButtons()}
</StyledRoot>
</div>
);
}

View File

@@ -51,6 +51,7 @@ const webviewApi = {
docReady(() => {
const rootElement = document.createElement('div');
rootElement.setAttribute('id', 'joplin-plugin-content-root');
document.getElementsByTagName('body')[0].appendChild(rootElement);
const contentElement = document.createElement('div');

View File

@@ -0,0 +1,3 @@
@use './plugin-user-webview.scss';
@use './user-dialog-wrapper.scss';
@use './user-dialog-button-bar.scss';

View File

@@ -0,0 +1,17 @@
.plugin-user-webview {
padding: 0;
margin: 0;
border: none;
width: 100%;
height: 100%;
&.-border-bottom {
border-bottom: 1px solid var(--joplin-divider-color);
}
&.-fit-to-content {
width: var(--content-width);
height: var(--content-height);
}
}

View File

@@ -0,0 +1,8 @@
.user-dialog-button-bar {
display: flex;
width: 100%;
box-sizing: border-box;
justify-content: flex-end;
padding-top: var(--joplin-main-padding);
}

View File

@@ -0,0 +1,5 @@
.user-dialog-wrapper {
display: flex;
flex: 1;
}

View File

@@ -11,6 +11,7 @@
@use 'gui/UpdateNotification/style.scss' as update-notification;
@use 'gui/TrashNotification/style.scss' as trash-notification;
@use 'gui/Sidebar/style.scss' as sidebar-styles;
@use 'gui/styles/index.scss';
@use 'gui/NoteEditor/style.scss';
@use 'gui/NoteEditor/style.scss' as note-editor-styles;
@use 'services/plugins/styles/index.scss' as plugins-styles;
@use 'gui/styles/index.scss' as gui-styles;
@use 'main.scss' as main;

View File

@@ -16,9 +16,13 @@ if [[ $NEED_COMPILING == 1 ]]; then
echo "Copying from: $PLUGIN_PATH"
echo "To: $TEMP_PLUGIN_PATH"
rsync -a --delete "$PLUGIN_PATH/" "$TEMP_PLUGIN_PATH/"
rsync -a --exclude "cache/" --exclude "node_modules" --delete "$PLUGIN_PATH/" "$TEMP_PLUGIN_PATH/"
NODE_OPTIONS=--openssl-legacy-provider npm install --prefix="$TEMP_PLUGIN_PATH" && yarn start --dev-plugins "$TEMP_PLUGIN_PATH"
cd "$TEMP_PLUGIN_PATH/"
NODE_OPTIONS=--openssl-legacy-provider npm install
cd "$SCRIPT_DIR"
yarn start --dev-plugins "$TEMP_PLUGIN_PATH"
else
yarn start --dev-plugins "$PLUGIN_PATH"
fi

View File

@@ -0,0 +1,69 @@
import * as fs from 'fs';
import * as path from 'path';
import * as crypto from 'crypto';
export interface GenerateInfo {
version: string;
dmgPath: string;
zipPath: string;
releaseDate: string;
}
const calculateHash = (filePath: string): string => {
const fileBuffer = fs.readFileSync(filePath);
const hashSum = crypto.createHash('sha512');
hashSum.update(fileBuffer);
return hashSum.digest('base64');
};
const getFileSize = (filePath: string): number => {
return fs.statSync(filePath).size;
};
export const generateLatestArm64Yml = (info: GenerateInfo, destinationPath: string): string | undefined => {
if (!fs.existsSync(info.dmgPath) || !fs.existsSync(info.zipPath)) {
throw new Error(`One or both executable files do not exist: ${info.dmgPath}, ${info.zipPath}`);
}
if (!info.version) {
throw new Error('Version is empty');
}
if (!destinationPath) {
throw new Error('Destination path is empty');
}
console.info('Calculating hash of files...');
const dmgHash: string = calculateHash(info.dmgPath);
const zipHash: string = calculateHash(info.zipPath);
console.info('Calculating size of files...');
const dmgSize: number = getFileSize(info.dmgPath);
const zipSize: number = getFileSize(info.zipPath);
console.info('Generating content of latest-mac-arm64.yml file...');
if (!fs.existsSync(destinationPath)) {
fs.mkdirSync(destinationPath);
}
const yamlFilePath: string = path.join(destinationPath, 'latest-mac-arm64.yml');
const yamlContent = `version: ${info.version}
files:
- url: ${path.basename(info.zipPath)}
sha512: ${zipHash}
size: ${zipSize}
- url: ${path.basename(info.dmgPath)}
sha512: ${dmgHash}
size: ${dmgSize}
path: ${path.basename(info.zipPath)}
sha512: ${zipHash}
releaseDate: '${info.releaseDate}'
`;
fs.writeFileSync(yamlFilePath, yamlContent);
console.log(`YML file for version ${info.version} was generated successfully at ${destinationPath} for arm64.`);
const fileContent: string = fs.readFileSync(yamlFilePath, 'utf8');
console.log('Generated YML Content:\n', fileContent);
return yamlFilePath;
};

View File

@@ -0,0 +1,118 @@
import * as fs from 'fs';
import { createWriteStream } from 'fs';
import * as path from 'path';
import { pipeline } from 'stream/promises';
import axios from 'axios';
import { GitHubRelease, GitHubReleaseAsset } from '../utils/checkForUpdatesUtils';
export interface Context {
repo: string; // {owner}/{repo}
githubToken: string;
targetTag: string;
}
const apiBaseUrl = 'https://api.github.com/repos/';
const defaultApiHeaders = (context: Context) => ({
'User-Agent': 'Joplin',
'Authorization': `token ${context.githubToken}`,
'X-GitHub-Api-Version': '2022-11-28',
'Accept': 'application/vnd.github+json',
});
export const getTargetRelease = async (context: Context, targetTag: string): Promise<GitHubRelease> => {
console.log('Fetching releases...');
// Note: We need to fetch all releases, not just /releases/tag/tag-name-here.
// The latter doesn't include draft releases.
const result = await fetch(`${apiBaseUrl}${context.repo}/releases`, {
method: 'GET',
headers: defaultApiHeaders(context),
});
const releases = await result.json();
if (!result.ok) {
throw new Error(`Error fetching release: ${JSON.stringify(releases)}`);
}
for (const release of releases) {
if (release.tag_name === targetTag) {
return release;
}
}
throw new Error(`No release with tag ${targetTag} found!`);
};
// Download a file from Joplin Desktop releases
export const downloadFileFromGitHub = async (context: Context, asset: GitHubReleaseAsset, destinationDir: string) => {
const downloadPath = path.join(destinationDir, asset.name);
if (!fs.existsSync(destinationDir)) {
fs.mkdirSync(destinationDir);
}
/* eslint-disable no-console */
console.log(`Downloading ${asset.name} from ${asset.url} to ${downloadPath}`);
try {
const response = await axios({
method: 'get',
url: asset.url,
responseType: 'stream',
headers: {
...defaultApiHeaders(context),
'Accept': 'application/octet-stream',
},
});
if (response.status < 200 || response.status >= 300) {
throw new Error(`Failed to download file: Status Code ${response.status}`);
}
await pipeline(response.data, createWriteStream(downloadPath));
console.log('Download successful!');
/* eslint-enable no-console */
return downloadPath;
} catch (error) {
throw new Error('Download not successful.');
}
};
export const updateReleaseAsset = async (context: Context, assetUrl: string, newName: string) => {
console.log('Updating asset with URL', assetUrl, 'to have name, ', newName);
// See https://docs.github.com/en/rest/releases/assets?apiVersion=2022-11-28#update-a-release-asset
const result = await fetch(assetUrl, {
method: 'PATCH',
headers: defaultApiHeaders(context),
body: JSON.stringify({
name: newName,
}),
});
if (!result.ok) {
throw new Error(`Unable to update release asset: ${await result.text()}`);
}
};
export const uploadReleaseAsset = async (context: Context, release: GitHubRelease, filePath: string): Promise<void> => {
console.log(`Uploading file from ${filePath} to release ${release.tag_name}`);
const fileContent = fs.readFileSync(filePath);
const fileName = path.basename(filePath);
const uploadUrl = `https://uploads.github.com/repos/${context.repo}/releases/${release.id}/assets?name=${encodeURIComponent(fileName)}`;
const response = await fetch(uploadUrl, {
method: 'POST',
headers: {
...defaultApiHeaders(context),
'Content-Type': 'application/octet-stream',
},
body: fileContent,
});
if (!response.ok) {
throw new Error(`Failed to upload asset: ${await response.text()}`);
} else {
console.log(`${fileName} uploaded successfully.`);
}
};

View File

@@ -0,0 +1,104 @@
import path = require('path');
import { parseArgs } from 'util';
import { Context, downloadFileFromGitHub, getTargetRelease, updateReleaseAsset, uploadReleaseAsset } from './githubReleasesUtils';
import { GitHubRelease } from '../utils/checkForUpdatesUtils';
import { GenerateInfo, generateLatestArm64Yml } from './generateLatestArm64Yml';
const basePath = path.join(__dirname, '..');
const downloadDir = path.join(basePath, 'downloads');
// Renames release assets in Joplin Desktop releases
const renameReleaseAssets = async (context: Context, release: GitHubRelease) => {
// Patterns used to rename releases
const renamePatterns: [RegExp, string][] = [
[/-arm64\.dmg$/, '-arm64.DMG'],
];
for (const asset of release.assets) {
for (const [pattern, replacement] of renamePatterns) {
if (asset.name.match(pattern)) {
const newName = asset.name.replace(pattern, replacement);
await updateReleaseAsset(context, asset.url, newName);
asset.name = newName;
// Only rename a release once.
break;
}
}
}
};
// Creates release assets in Joplin Desktop releases
const createReleaseAssets = async (context: Context, release: GitHubRelease) => {
// Create latest-mac-arm64.yml file and publish
let dmgPath: string;
let zipPath: string;
for (const asset of release.assets) {
console.log(`Checking asset: ${asset.name}`);
if (asset.name.endsWith('arm64.zip')) {
zipPath = await downloadFileFromGitHub(context, asset, downloadDir);
} else if (asset.name.endsWith('arm64.DMG')) {
dmgPath = await downloadFileFromGitHub(context, asset, downloadDir);
}
}
if (!zipPath || !dmgPath) {
const formattedAssets = release.assets.map(asset => ({
name: asset.name,
url: asset.url,
}));
throw new Error(`Zip path: ${zipPath} and/or dmg path: ${dmgPath} are not defined. Logging assets of release: ${JSON.stringify(formattedAssets, null, 2)}`);
}
const info: GenerateInfo = {
version: release.tag_name.slice(1),
dmgPath: dmgPath,
zipPath: zipPath,
releaseDate: new Date().toISOString(),
};
const latestArm64FilePath = generateLatestArm64Yml(info, downloadDir);
await uploadReleaseAsset(context, release, latestArm64FilePath);
};
const modifyReleaseAssets = async () => {
const args = parseArgs({
options: {
tag: { type: 'string' },
token: { type: 'string' },
repo: { type: 'string' },
},
});
if (!args.values.tag || !args.values.token || !args.values.repo) {
throw new Error([
'Required arguments: --tag, --token, --repo',
' --tag should be a git tag with an associated release (e.g. v12.12.12)',
' --token should be a GitHub API token',
' --repo should be a string in the form user/reponame (e.g. laurent22/joplin)',
].join('\n'));
}
const context: Context = {
repo: args.values.repo,
githubToken: args.values.token,
targetTag: args.values.tag,
};
const release = await getTargetRelease(context, context.targetTag);
if (!release.assets) {
console.log(release);
throw new Error(`Release ${release.tag_name} missing assets!`);
}
console.log('Renaming release assets for tag', context.targetTag, context.repo);
await renameReleaseAssets(context, release);
console.log('Creating latest-mac-arm64.yml asset for tag', context.targetTag, context.repo);
await createReleaseAssets(context, release);
};
void modifyReleaseAssets();

View File

@@ -1,109 +0,0 @@
import { parseArgs } from 'util';
interface Context {
repo: string; // {owner}/{repo}
githubToken: string;
}
const apiBaseUrl = 'https://api.github.com/repos/';
const defaultApiHeaders = (context: Context) => ({
'Authorization': `token ${context.githubToken}`,
'X-GitHub-Api-Version': '2022-11-28',
'Accept': 'application/vnd.github+json',
});
const getTargetRelease = async (context: Context, targetTag: string) => {
console.log('Fetching releases...');
// Note: We need to fetch all releases, not just /releases/tag/tag-name-here.
// The latter doesn't include draft releases.
const result = await fetch(`${apiBaseUrl}${context.repo}/releases`, {
method: 'GET',
headers: defaultApiHeaders(context),
});
const releases = await result.json();
if (!result.ok) {
throw new Error(`Error fetching release: ${JSON.stringify(releases)}`);
}
for (const release of releases) {
if (release.tag_name === targetTag) {
return release;
}
}
throw new Error(`No release with tag ${targetTag} found!`);
};
const updateReleaseAsset = async (context: Context, assetUrl: string, newName: string) => {
console.log('Updating asset with URL', assetUrl, 'to have name, ', newName);
// See https://docs.github.com/en/rest/releases/assets?apiVersion=2022-11-28#update-a-release-asset
const result = await fetch(assetUrl, {
method: 'PATCH',
headers: defaultApiHeaders(context),
body: JSON.stringify({
name: newName,
}),
});
if (!result.ok) {
throw new Error(`Unable to update release asset: ${await result.text()}`);
}
};
// Renames release assets in Joplin Desktop releases
const renameReleaseAssets = async () => {
const args = parseArgs({
options: {
tag: { type: 'string' },
token: { type: 'string' },
repo: { type: 'string' },
},
});
if (!args.values.tag || !args.values.token || !args.values.repo) {
throw new Error([
'Required arguments: --tag, --token, --repo',
' --tag should be a git tag with an associated release (e.g. v12.12.12)',
' --token should be a GitHub API token',
' --repo should be a string in the form user/reponame (e.g. laurent22/joplin)',
].join('\n'));
}
const context: Context = {
repo: args.values.repo,
githubToken: args.values.token,
};
console.log('Renaming release assets for tag', args.values.tag, context.repo);
const release = await getTargetRelease(context, args.values.tag);
if (!release.assets) {
console.log(release);
throw new Error(`Release ${release.name} missing assets!`);
}
// Patterns used to rename releases
const renamePatterns = [
[/-arm64\.dmg$/, '-arm64.DMG'],
];
for (const asset of release.assets) {
for (const [pattern, replacement] of renamePatterns) {
if (asset.name.match(pattern)) {
const newName = asset.name.replace(pattern, replacement);
await updateReleaseAsset(context, asset.url, newName);
// Only rename a release once.
break;
}
}
}
};
void renameReleaseAssets();

View File

@@ -7,9 +7,11 @@ export interface CheckForUpdateOptions {
export interface GitHubReleaseAsset {
name: string;
browser_download_url: string;
url?: string;
}
export interface GitHubRelease {
id?: string;
tag_name: string;
prerelease: boolean;
body: string;

View File

@@ -5717,6 +5717,41 @@ export const releases3: any = [
'updated_at': '2024-08-17T12:40:54Z',
'browser_download_url': 'https://github.com/laurent22/joplin/releases/download/v3.1.3/latest-mac.yml',
},
{
'url': 'https://api.github.com/repos/laurent22/joplin/releases/assets/186557908',
'id': 186557908,
'node_id': 'RA_kwDOBLftOs4LHqXU',
'name': 'latest-mac-arm64.yml',
'label': '',
'uploader':
{
'login': 'laurent22',
'id': 1285584,
'node_id': 'MDQ6VXNlcjEyODU1ODQ=',
'avatar_url': 'https://avatars.githubusercontent.com/u/1285584?v=4',
'gravatar_id': '',
'url': 'https://api.github.com/users/laurent22',
'html_url': 'https://github.com/laurent22',
'followers_url': 'https://api.github.com/users/laurent22/followers',
'following_url': 'https://api.github.com/users/laurent22/following{/other_user}',
'gists_url': 'https://api.github.com/users/laurent22/gists{/gist_id}',
'starred_url': 'https://api.github.com/users/laurent22/starred{/owner}{/repo}',
'subscriptions_url': 'https://api.github.com/users/laurent22/subscriptions',
'organizations_url': 'https://api.github.com/users/laurent22/orgs',
'repos_url': 'https://api.github.com/users/laurent22/repos',
'events_url': 'https://api.github.com/users/laurent22/events{/privacy}',
'received_events_url': 'https://api.github.com/users/laurent22/received_events',
'type': 'User',
'site_admin': false,
},
'content_type': 'text/yaml',
'state': 'uploaded',
'size': 484,
'download_count': 9,
'created_at': '2024-08-17T12:40:54Z',
'updated_at': '2024-08-17T12:40:54Z',
'browser_download_url': 'https://github.com/laurent22/joplin/releases/download/v3.1.3/latest-mac-arm64.yml',
},
{
'url': 'https://api.github.com/repos/laurent22/joplin/releases/assets/186555028',
'id': 186555028,

View File

@@ -125,6 +125,11 @@ const handleRangeRequest = async (request: Request, targetPath: string) => {
// TODO: Use Logger.create (doesn't work for now because Logger is only initialized
// in the main process.)
const handleCustomProtocols = (logger: LoggerWrapper): CustomProtocolHandler => {
logger = {
...logger,
debug: () => {},
};
const readableDirectories: string[] = [];
const readableFiles = new Map<string, number>();

View File

@@ -79,8 +79,8 @@ android {
applicationId "net.cozic.joplin"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 2097751
versionName "3.1.3"
versionCode 2097754
versionName "3.1.6"
ndk {
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
}

View File

@@ -14,7 +14,7 @@ const showResource = async (item: ResourceEntity) => {
if (shim.mobilePlatform() === 'web') {
const url = URL.createObjectURL(await shim.fsDriver().fileAtPath(resourcePath));
const w = window.open(url, '_blank');
w.addEventListener('close', () => {
w?.addEventListener('close', () => {
URL.revokeObjectURL(url);
}, { once: true });
} else {

View File

@@ -1,4 +1,4 @@
const { BackButtonService } = require('../services/back-button.js');
import BackButtonService from '../services/BackButtonService';
const DialogBox = require('react-native-dialogbox').default;
export default class BackButtonDialogBox extends DialogBox {

View File

@@ -0,0 +1,67 @@
import * as React from 'react';
import { useState, useEffect, useCallback, useMemo } from 'react';
import { TouchableHighlight, StyleSheet, TextStyle } from 'react-native';
const Icon = require('react-native-vector-icons/Ionicons').default;
interface Props {
checked: boolean;
accessibilityLabel?: string;
onChange?: (checked: boolean)=> void;
style?: TextStyle;
iconStyle?: TextStyle;
}
const useStyles = (baseStyles: TextStyle|undefined, iconStyle: TextStyle|undefined) => {
return useMemo(() => {
return StyleSheet.create({
container: {
...(baseStyles ?? {}),
justifyContent: 'center',
alignItems: 'center',
},
icon: {
fontSize: 20,
height: 22,
color: baseStyles?.color,
...iconStyle,
},
});
}, [baseStyles, iconStyle]);
};
const Checkbox: React.FC<Props> = props => {
const [checked, setChecked] = useState(props.checked);
useEffect(() => {
setChecked(props.checked);
}, [props.checked]);
const onPress = useCallback(() => {
setChecked(checked => {
const newChecked = !checked;
props.onChange?.(newChecked);
return newChecked;
});
}, [props.onChange]);
const iconName = checked ? 'checkbox-outline' : 'square-outline';
const styles = useStyles(props.style, props.iconStyle);
const accessibilityState = useMemo(() => ({
checked,
}), [checked]);
return (
<TouchableHighlight
onPress={onPress}
style={styles.container}
accessibilityRole="checkbox"
accessibilityState={accessibilityState}
accessibilityLabel={props.accessibilityLabel ?? ''}
>
<Icon name={iconName} style={styles.icon} />
</TouchableHighlight>
);
};
export default Checkbox;

View File

@@ -78,6 +78,17 @@ class Dropdown extends Component<DropdownProps, DropdownState> {
private onCloseList = () => {
this.setState({ listVisible: false });
};
private onListLoad = (listRef: FlatList|null) => {
if (!listRef) return;
for (let i = 0; i < this.props.items.length; i++) {
const item = this.props.items[i];
if (item.value === this.props.selectedValue) {
listRef.scrollToIndex({ index: i, animated: false });
break;
}
}
};
public render() {
const items = this.props.items;
@@ -228,6 +239,7 @@ class Dropdown extends Component<DropdownProps, DropdownState> {
accessibilityRole='menu'
style={wrapperStyle}>
<FlatList
ref={this.onListLoad}
style={itemListStyle}
data={this.props.items}
renderItem={itemRenderer}

View File

@@ -15,7 +15,7 @@ const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
const dom = useMemo(() => {
// Note: Adding `runScripts: 'dangerously'` to allow running inline <script></script>s.
// Use with caution.
return new JSDOM(props.html, { runScripts: 'dangerously' });
return new JSDOM(props.html, { runScripts: 'dangerously', pretendToBeVisual: true });
}, [props.html]);
useImperativeHandle(ref, (): WebViewControl => {
@@ -46,6 +46,25 @@ const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
injectedJavaScriptRef.current = props.injectedJavaScript;
useEffect(() => {
// JSDOM polyfills
dom.window.eval(`
// Prevents the CodeMirror error "getClientRects is undefined".
// See https://github.com/jsdom/jsdom/issues/3002#issue-652790925
document.createRange = () => {
const range = new Range();
range.getBoundingClientRect = () => {};
range.getClientRects = () => {
return {
length: 0,
item: () => null,
[Symbol.iterator]: () => {},
};
};
return range;
};
`);
dom.window.eval(`
window.setWebViewApi = (api) => {
window.ReactNativeWebView = api;
@@ -74,7 +93,7 @@ const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
}, [dom]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- HACK: Allow wrapper testing logic to access the DOM.
const additionalProps: any = { document: dom?.window?.document };
const additionalProps: any = { window: dom?.window };
return (
<View style={props.style} testID={props.testID} {...additionalProps}/>
);

View File

@@ -1,5 +1,6 @@
import { useCallback } from 'react';
import shared from '@joplin/lib/components/shared/note-screen-shared';
import Logger from '@joplin/utils/Logger';
export type HandleMessageCallback = (message: string)=> void;
export type OnMarkForDownloadCallback = (resource: { resourceId: string })=> void;
@@ -12,6 +13,8 @@ interface MessageCallbacks {
onCheckboxChange: HandleMessageCallback;
}
const logger = Logger.create('useOnMessage');
export default function useOnMessage(
noteBody: string,
callbacks: MessageCallbacks,
@@ -29,10 +32,10 @@ export default function useOnMessage(
return useCallback((msg: string) => {
const isScrollMessage = msg.startsWith('onscroll:');
// Scroll messages are very frequent so we avoid logging them.
// Scroll messages are very frequent so we avoid logging them, even
// in debug mode
if (!isScrollMessage) {
// eslint-disable-next-line no-console
console.info('Got IPC message: ', msg);
logger.debug('Got IPC message: ', msg);
}
if (msg.indexOf('checkboxclick:') === 0) {

View File

@@ -132,7 +132,7 @@ const EditLinkDialog = (props: LinkDialogProps) => {
return (
<Modal
animationType="slide"
animationType="fade"
containerStyle={styles.modalContent}
transparent={true}
visible={props.visible}

View File

@@ -1,4 +1,4 @@
const React = require('react');
import * as React from 'react';
import { _ } from '@joplin/lib/locale';
import Logger from '@joplin/utils/Logger';
import Setting from '@joplin/lib/models/Setting';
@@ -18,7 +18,7 @@ import { OnMessageEvent } from '../../ExtendedWebView/types';
const logger = Logger.create('ImageEditor');
type OnSaveCallback = (svgData: string)=> void;
type OnSaveCallback = (svgData: string)=> Promise<void>;
type OnCancelCallback = ()=> void;
interface Props {
@@ -231,6 +231,15 @@ const ImageEditor = (props: Props) => {
}));
};
const saveThenClose = (drawing) => {
window.ReactNativeWebView.postMessage(
JSON.stringify({
action: 'save-and-close',
data: drawing.outerHTML,
}),
);
};
try {
if (window.editorControl === undefined) {
${shim.injectedJs('svgEditorBundle')}
@@ -239,6 +248,7 @@ const ImageEditor = (props: Props) => {
{
saveDrawing,
closeEditor,
saveThenClose,
updateEditorTemplate,
setImageHasChanges,
},
@@ -308,13 +318,16 @@ const ImageEditor = (props: Props) => {
const json = JSON.parse(data);
if (json.action === 'save') {
await clearAutosave();
props.onSave(json.data);
await props.onSave(json.data);
} else if (json.action === 'autosave') {
await writeAutosave(json.data);
} else if (json.action === 'save-toolbar') {
Setting.setValue('imageeditor.jsdrawToolbar', json.data);
} else if (json.action === 'close') {
onRequestCloseEditor(json.promptIfUnsaved);
} else if (json.action === 'save-and-close') {
await props.onSave(json.data);
onRequestCloseEditor(json.promptIfUnsaved);
} else if (json.action === 'ready-to-load-data') {
void onReadyToLoadData();
} else if (json.action === 'set-image-has-changes') {

View File

@@ -22,6 +22,7 @@ const createEditorWithCallbacks = (callbacks: Partial<ImageEditorCallbacks>) =>
const allCallbacks: ImageEditorCallbacks = {
saveDrawing: () => {},
saveThenClose: ()=> {},
closeEditor: ()=> {},
setImageHasChanges: ()=> {},
updateEditorTemplate: ()=> {},

View File

@@ -91,11 +91,15 @@ export const createJsDrawEditor = (
}
};
const saveNow = () => {
callbacks.saveDrawing(editor.toSVG({
const getEditorSVG = () => {
return editor.toSVG({
// Grow small images to this minimum size
minDimension: 50,
}), false);
});
};
const saveNow = () => {
callbacks.saveDrawing(getEditorSVG(), false);
// The image is now up-to-date with the resource
setImageHasChanges(false);
@@ -177,13 +181,7 @@ export const createJsDrawEditor = (
},
saveNow,
saveThenExit: async () => {
saveNow();
// Don't show a confirmation dialog -- it's possible that
// the code outside of the WebView still thinks changes haven't
// been saved:
const showConfirmation = false;
callbacks.closeEditor(showConfirmation);
callbacks.saveThenClose(getEditorSVG());
},
};

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