1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-09-05 20:56:22 +02:00

Compare commits

..

183 Commits

Author SHA1 Message Date
Laurent Cozic
39c8fc812d Android 2.13.10 2023-12-01 13:07:42 +01:00
Henry Heino
0638d711d7 Mobile: Resolves #9427: Drawing: Revert recent changes to input system (#9426) 2023-12-01 11:11:14 +01:00
Laurent Cozic
9bad668cc5 CLI v2.13.2 2023-11-30 19:12:07 +01:00
Laurent Cozic
c18c31ab7f Lock file 2023-11-30 19:10:57 +01:00
Laurent Cozic
7c24a2f4be Releasing sub-packages 2023-11-30 19:10:02 +01:00
Laurent Cozic
56438ea644 iOS 12.13.9 2023-11-30 18:56:49 +01:00
Laurent Cozic
7f9bc1e15c Android 2.13.9 2023-11-30 18:56:17 +01:00
Henry Heino
b1c8cb5632 Mobile: Fixes #9374: Fix tooltips don't disappear on some devices (upgrade to js-draw 1.13.2) (#9401) 2023-11-29 15:17:29 +01:00
Henry Heino
f0a1b41794 Mobile: Resolves #9377: Don't attach empty drawings when a user exits without saving (#9386) 2023-11-27 20:14:04 +01:00
Laurent Cozic
02982464a6 iOS 12.13.8 2023-11-26 13:55:26 +01:00
Laurent Cozic
62e317db05 lock file 2023-11-26 13:49:00 +01:00
Laurent Cozic
e0795748a9 Android 2.13.8 2023-11-26 13:40:59 +01:00
Laurent Cozic
67070ed3d5 Desktop release v2.13.7 2023-11-26 12:38:28 +01:00
Laurent Cozic
fec8c6131c Mobile: Fixes #9376: Sidebar is not dismissed when creating a note 2023-11-26 12:37:45 +01:00
pedr
24ed5bda63 Mobile: #9361: Fix to-dos options toggle don't toggle a rerender in (#9364) 2023-11-24 14:48:41 +01:00
Henry Heino
dbb354ad10 Mobile: Fixes #9328: Fix new note/to-do buttons not visible on app startup in some cases (#9329) 2023-11-19 10:43:57 +00:00
Laurent Cozic
92dccbe98d Merge branch 'release-2.13' into dev 2023-11-16 15:11:39 +00:00
Laurent Cozic
9b775d77f6 iOS 12.13.7 2023-11-16 13:37:15 +00:00
Laurent Cozic
4fd6937d05 lock file 2023-11-16 13:36:45 +00:00
Laurent Cozic
7230f0e698 iOS 12.13.6 2023-11-16 13:28:27 +00:00
Laurent Cozic
ada82538ee Android 2.13.7 2023-11-16 13:26:58 +00:00
Laurent Cozic
e7dd981db6 Desktop release v2.13.6 2023-11-16 13:02:07 +00:00
Laurent Cozic
767bf9f002 Server: Increase number of items that are returned during sync 2023-11-16 12:20:07 +00:00
Laurent Cozic
f698068587 Server: Fix severe performance issue for certain delta calls 2023-11-16 12:20:06 +00:00
Henry Heino
d0955b4ca2 Mobile: Fixes #9321: Restore scroll position when returning to the note viewer from the editor or camera (#9324) 2023-11-16 12:19:48 +00:00
Henry Heino
18e86a7ba3 Mobile: Resolves #9294: Implement settings search (#9320) 2023-11-16 12:17:03 +00:00
renovate[bot]
f9a1ab4d40 Update dependency react-native-vector-icons to v10.0.1 (#9316)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-16 01:02:28 +00:00
renovate[bot]
062d0898a0 Update dependency sass to v1.69.5 (#9317)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-15 23:23:22 +00:00
pedr
3b51b4fd72 Tools: Resolves #9265: Add no-constant-binary-expression to eslint rules (#9319) 2023-11-15 20:18:31 +00:00
Helmut K. C. Tessarek
1a78ff4398 All: Translation: Update it_IT.po (thanks Pietro Campanella) 2023-11-15 18:56:00 +01:00
Laurent Cozic
544af8d118 Server v2.13.4 2023-11-15 15:31:09 +00:00
pedr
2616c377a9 Tools: Create vscode launch option for debugging lib project (#9318) 2023-11-15 15:29:45 +00:00
Henry Heino
4a63331306 Plugin Repo: Resolves #9280: Allow marking specific NPM packages as superseded (#9302) 2023-11-15 13:44:09 +00:00
renovate[bot]
48621443ec Update dependency sass to v1.69.0 (#9310)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-15 13:38:48 +00:00
pedr
79fd66b94c All: Fixes #9151: Import of inter-linked md files has incorrect notebook structure (#9269) 2023-11-15 13:33:20 +00:00
Henry Heino
6a6c8c1d83 Mobile: Fixes #9312: Fix settings save confirmation not shown when navigating to encryption/profile/log screens (#9313) 2023-11-15 13:31:36 +00:00
Henry Heino
cf19dacbaf Mobile: Fixes #9308: Disable notebook list side menu in config screen (#9311) 2023-11-15 13:31:26 +00:00
renovate[bot]
50925abc40 Update dependency @types/react to v18.2.33 (#9306)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-15 10:54:33 +00:00
renovate[bot]
c80cbaa32f Update dependency react-native-safe-area-context to v4.7.4 (#9307)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-15 00:45:41 +00:00
Henry Heino
f7cb1aef4b iOS: Fixes #9271: Allow showing dropdowns in landscape mode (#9309) 2023-11-15 00:42:27 +00:00
Joplin Bot
96d5d1dfab Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-11-15 00:35:50 +00:00
renovate[bot]
98d608fec5 Update dependency @types/node to v18.18.7 (#9305)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-14 22:21:00 +00:00
Laurent Cozic
1af46b0246 Update translations 2023-11-14 19:00:52 +00:00
Henry Heino
1e530b74d4 Chore: Plugins: Allow absolute paths and URLs in screenshot srcs (#9254) 2023-11-14 18:49:45 +00:00
Henry Heino
e61c4acce5 Desktop: Resolves #9136: Install script: Work around unprivlidged user namespace restrictions by adding the --no-sandbox flag to the launcher (#9137) 2023-11-14 18:49:25 +00:00
Mohammad Ashouri
184499711d Full translation support for Farsi/Persian (#9244) 2023-11-14 18:48:32 +00:00
Henry Heino
2c0181d097 Desktop, Cli: Fixes #8788: Work around WebDAV sync issues over ipv6 (#9286) 2023-11-14 18:47:52 +00:00
Laurent Cozic
06ea12adb3 Doc: Update sponsors 2023-11-14 18:06:56 +00:00
Laurent Cozic
9923e5c821 Desktop: Resolves #9293: Preserve nested tables in RTE 2023-11-14 11:45:38 +00:00
renovate[bot]
9a06e59cfe Update dependency @testing-library/react-native to v12.3.1 (#9298)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-13 21:54:42 +00:00
renovate[bot]
80a2cd91f4 Update dependency @types/markdown-it to v13.0.5 (#9290)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-13 18:36:02 +00:00
Laurent Cozic
df9ed3e487 Update BUG_REPORT.yml 2023-11-13 14:01:49 +00:00
Laurent Cozic
368d0130f6 Update BUG_REPORT.yml 2023-11-13 14:00:44 +00:00
Laurent Cozic
824e1b44dd Update BUG_REPORT.yml 2023-11-13 13:56:45 +00:00
Laurent Cozic
ccf1c8ee31 Desktop: Improve toolbar button wrapping on RTE 2023-11-13 13:52:37 +00:00
Laurent Cozic
d5f6d83f6d Update BUG_REPORT.yml 2023-11-13 11:58:52 +00:00
Laurent Cozic
5d422f85c8 lock file 2023-11-12 18:12:53 +00:00
renovate[bot]
78aeb46d56 Update dependency nodemailer to v6.9.7 (#9277)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-12 17:28:00 +00:00
renovate[bot]
091bf45149 Update postgres Docker tag to v16 (#9289)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-12 16:16:04 +00:00
Laurent Cozic
8d9d24740b Doc: Suggest installing yo@4.3.1 2023-11-12 15:52:01 +00:00
Laurent Cozic
21e5f88cb2 Plugin Generator release v2.13.2 2023-11-12 15:40:37 +00:00
Laurent Cozic
5d4259d064 Lock file 2023-11-12 15:39:36 +00:00
Laurent Cozic
9ac03ec33a Releasing sub-packages 2023-11-12 15:38:06 +00:00
Laurent Cozic
e760276341 Chore: Fixes #9282: Plugin generator fails when updating plugin 2023-11-12 15:32:39 +00:00
Henry Heino
206f35ffe5 Mobile: Resolves #9258: Add more space between settings title and description (#9270) 2023-11-12 15:08:52 +00:00
Henry Heino
ddf716479d Chore: Partially resolves #9262: Mobile build: Convert bundle tasks to proper gulp tasks and increase output verbosity (#9266) 2023-11-12 15:07:30 +00:00
Henry Heino
ec7f94df25 Chore: Resolves #9274: Desktop: Fix end-to-end tests when the first window is the devtools window (#9275) 2023-11-12 15:06:32 +00:00
Henry Heino
bcbba0973f Mobile: Improve image editor load performance (#9281) 2023-11-12 15:06:16 +00:00
Henry Heino
bd1ddb8522 Mobile: Resolves #9195: Update js-draw to version 1.11.2 (#9120) 2023-11-12 15:04:55 +00:00
Laurent Cozic
fb47398554 lock files 2023-11-12 15:02:44 +00:00
Henry Heino
10356f4009 Chore: Fixes #9284: Desktop: Fix warning on opening settings screen (#9287) 2023-11-12 15:01:14 +00:00
renovate[bot]
ba83fca47a Update dependency mermaid to v10.5.1 (#9279)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-12 12:57:00 +00:00
Laurent Cozic
b01295f0fd Update BUG_REPORT.yml 2023-11-12 12:51:49 +00:00
renovate[bot]
b928e614cc Update dependency mermaid to v10.5.0 (#9278)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-11 18:22:46 +00:00
Laurent Cozic
973b9c354c Delete .github/ISSUE_TEMPLATE/bug_report.md 2023-11-11 18:20:48 +00:00
Wladimir Kirianov
c12444d6e8 Doc: Improved github issue template, added direct links to support and feature requests (#8974)
Co-authored-by: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2023-11-11 18:18:59 +00:00
Joplin Bot
1401d28f82 Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-11-11 18:13:30 +00:00
Laurent Cozic
335269f92d Tools: Fail more cleanly when docker-compose randomly fails to load Postgres 2023-11-11 17:50:15 +00:00
Laurent Cozic
6211606a22 Desktop: Fixed import error report 2023-11-11 17:41:08 +00:00
Laurent Cozic
5f7d438ac1 Doc: Update sponsors 2023-11-11 17:41:07 +00:00
Daeraxa
ee2df96cfb Docs: Fix BUILD.md in contributing instructions (#9273) 2023-11-11 17:17:46 +00:00
Henry Heino
692e925997 Mobile: Fixes #9259: Config screen: Fix section list scroll (#9267) 2023-11-11 17:09:34 +00:00
Henry Heino
39803f53a0 Mobile: Resolves #9260: Fade settings screen icons (#9268) 2023-11-11 17:08:58 +00:00
renovate[bot]
b3591808b7 Update dependency @types/zxcvbn to v4.4.3 (#9247)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-11 17:01:00 +00:00
github-actions[bot]
2427677fd5 @Daeraxa has signed the CLA in laurent22/joplin#9273 2023-11-11 16:18:11 +00:00
Laurent Cozic
9a051effcd Update bug_report.md 2023-11-11 13:24:00 +00:00
Henry Heino
e6e9f92e01 Mobile: Fixes #9123: Fix encryption when a resource doesn't have an associated file (#9222)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2023-11-10 14:22:26 +00:00
Laurent Cozic
ca6762c891 iOS 12.13.5 2023-11-10 14:13:43 +00:00
Laurent Cozic
76d07beb27 lock file 2023-11-10 14:13:42 +00:00
Henry Heino
f3daa7f0e4 Desktop: Resolves #9250: Make settings tabs focusable by keyboard (#9253) 2023-11-10 14:00:59 +00:00
Laurent Cozic
041ad22443 Update bug_report.md 2023-11-10 13:42:06 +00:00
Laurent Cozic
6cd0938ee4 iOS 12.13.5 2023-11-10 13:21:02 +00:00
Laurent Cozic
c3dc30ee5d lock file 2023-11-10 13:19:52 +00:00
renovate[bot]
37c925dcf2 Update dependency react-native-safe-area-context to v4.7.3 (#9248)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-10 12:05:54 +00:00
renovate[bot]
05bd51f85c Update dependency @types/yargs to v17.0.29 (#9246)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-10 12:05:41 +00:00
Joplin Bot
cfbc37df8d Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-11-10 00:41:46 +00:00
Laurent Cozic
36635c452c CLI v2.13.1 2023-11-09 20:09:25 +00:00
Laurent Cozic
d08a16c381 Lock file 2023-11-09 20:06:19 +00:00
Laurent Cozic
1cee10ce12 Releasing sub-packages 2023-11-09 20:04:28 +00:00
Laurent Cozic
b06974e104 Exclude files 2023-11-09 20:03:04 +00:00
Laurent Cozic
bfafe7a70c Publish packages 2023-11-09 20:01:49 +00:00
Laurent Cozic
05a29b4509 Releasing sub-packages 2023-11-09 19:52:38 +00:00
Laurent Cozic
54f7a83789 Android 2.13.6 2023-11-09 19:45:45 +00:00
Laurent Cozic
1d04ec6b64 Desktop release v2.13.5 2023-11-09 19:31:11 +00:00
Laurent Cozic
99d93f0a85 Merge branch 'dev' into release-2.13 2023-11-09 19:26:08 +00:00
Laurent Cozic
b78101ef90 iOS 12.13.4 2023-11-09 19:25:36 +00:00
Laurent Cozic
6261d30574 Upgrade to Electron 26.5.0 2023-11-09 19:24:02 +00:00
Henry Heino
672d028d29 Mobile: Settings screen: Create separate pages for each screen (#8567) 2023-11-09 19:19:08 +00:00
renovate[bot]
0340c7f65c Update dependency @types/react-redux to v7.1.28 (#9242)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-09 18:44:33 +00:00
pedr
632802e58b Clipper: Avoid errors when downloading media files without protocol (#9241) 2023-11-09 15:02:52 +00:00
Laurent Cozic
c3510bf26b Desktop: Fixes #9149: Toolbar icons in view mode are partly not grayed out and can be used 2023-11-09 14:58:38 +00:00
Laurent Cozic
e22aa4f6e9 Desktop: Fixes #8961: Fix rare crash when developing a plugin 2023-11-09 12:26:06 +00:00
renovate[bot]
76a8ae3a83 Update dependency @types/node-rsa to v1.1.3 (#9238)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-09 11:09:16 +00:00
renovate[bot]
1c104d2e83 Update dependency @types/uuid to v9.0.6 (#9245)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-08 23:34:41 +00:00
renovate[bot]
7c38c2c8f2 Update dependency @types/nodemailer to v6.4.13 (#9239)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-08 22:21:00 +00:00
renovate[bot]
188d9ac159 Update dependency @types/styled-components to v5.1.29 (#9243)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-08 20:32:10 +00:00
github-actions[bot]
91751d5fa3 @mimeyn has signed the CLA in laurent22/joplin#9244 2023-11-08 19:40:10 +00:00
renovate[bot]
5124cbfd9b Update dependency @types/node-fetch to v2.6.7 (#9237)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-08 19:08:06 +00:00
renovate[bot]
cb21a61d7c Update dependency @types/node to v18.18.6 (#9233)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-08 18:33:00 +00:00
Marco Rombach
f50019d098 Server: Added LDAP authentication (#9150) 2023-11-08 15:38:01 +00:00
renovate[bot]
ab9a1776c8 Update dependency @types/markdown-it to v13.0.4 (#9231)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-08 04:43:20 +00:00
renovate[bot]
a357665c77 Update dependency @types/fs-extra to v11.0.3 (#9236)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-08 00:36:05 +00:00
renovate[bot]
be097afd83 Update dependency @types/koa to v2.13.10 (#9230)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-07 21:42:13 +00:00
renovate[bot]
b417299616 Update dependency @types/mustache to v4.2.4 (#9232)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-07 19:17:44 +00:00
renovate[bot]
582b963570 Update dependency @types/jsdom to v21.1.4 (#9229)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-07 17:06:59 +00:00
Henry Heino
1405def25d Desktop: Fixes #7547: Fix inserting resources into TinyMCE from plugins (insertText command) (#9225) 2023-11-07 12:07:42 +00:00
Henry Heino
e9c598cf46 Chore: Mobile: Remove duplicate bundle minification (#9221) 2023-11-07 12:04:33 +00:00
Henry Heino
02361e37f0 Desktop: Fixes #9036: Fix note list scroll (#9211)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2023-11-07 12:04:03 +00:00
gitbreaker222
041414b11e Doc: Fixes broken faq links (#9220) 2023-11-07 12:01:00 +00:00
Henry Heino
b4ca00ebf5 Mobile: Fixes #9207: Fix search highlighting (#9206) 2023-11-07 12:00:36 +00:00
Henry Heino
9d96866531 Mobile, Desktop: Fixes #9201: Disable selection match highlighting (#9202) 2023-11-07 12:00:13 +00:00
Henry Heino
6593025051 Desktop: Fixes #8978: Rich text editor: Fix repeated newline characters discarded on save to markdown (#9199) 2023-11-07 11:59:35 +00:00
Henry Heino
a38fe11bbe Desktop: Fixes #9122: Fix underscores escaped within some text-only URLs (#9198) 2023-11-07 11:58:52 +00:00
Roman Orlowski
b030ca914d Desktop: Fixes #9130: Allow Electron --disable-gpu flag (#9179) 2023-11-07 11:58:39 +00:00
Henry Heino
88b44a0f74 All: Fixes #8561: Fix OneDrive sync crash on throttle (#9143) 2023-11-07 11:55:38 +00:00
renovate[bot]
7b2cf0e483 Update dependency @types/js-yaml to v4.0.8 (#9227)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-07 11:43:28 +00:00
renovate[bot]
5d3e920370 Update dependency @types/formidable to v3.4.4 (#9226)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-07 06:07:13 +00:00
github-actions[bot]
7cec62fc71 @gitbreaker222 has signed the CLA in laurent22/joplin#9220 2023-11-06 14:32:53 +00:00
renovate[bot]
698d16e970 Update dependency @types/node to v18.18.5 (#9218)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-05 21:58:35 +00:00
renovate[bot]
d88af474d2 Update dependency @types/node to v18.18.0 (#9214)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-05 19:35:06 +00:00
Laurent Cozic
7488129517 Doc: Fixes #9216: Fixed several broken links 2023-11-05 17:59:36 +00:00
Laurent Cozic
9772389fd9 Doc: Fixed favicon on translated websites 2023-11-05 17:59:35 +00:00
Laurent Cozic
c2a1ea8cba Tools: Add plugin API doc to linkchecker 2023-11-05 17:59:33 +00:00
Laurent Cozic
9afc94fb3b Tools: Pin Python version to 3.11 to fix desktop build on CI (#9212) 2023-11-05 13:33:38 +00:00
github-actions[bot]
390b28c3f5 @brechsteiner has signed the CLA in laurent22/joplin#9210 2023-11-03 22:08:52 +00:00
Laurent Cozic
8be22ed910 Plugins: Add support for getting plugin settings from a Markdown renderer 2023-11-03 19:45:21 +00:00
Laurent Cozic
b097ab29ee Desktop, Mobile: Resolves #9158: Add a "Retry all" button when multiple resources could not be downloaded 2023-11-03 16:01:51 +00:00
Laurent Cozic
d9bf0b7d82 Doc: Add image source 2023-11-03 16:01:50 +00:00
renovate[bot]
0f5533af55 Update dependency electron to v26 (#9203)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-03 12:40:32 +00:00
Laurent Cozic
1c47db70a0 Doc: Automatically detect Apple silicon on Download page 2023-11-03 12:36:43 +00:00
renovate[bot]
9723ab0ba6 Update dependency lint-staged to v14.0.1 (#9192)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-02 14:36:56 +00:00
Laurent Cozic
2b9d519f4b Tools: Do not run "pod install" by default 2023-11-02 10:23:00 +00:00
renovate[bot]
4478ce118a Update dependency lint-staged to v14 (#9189)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-02 09:54:05 +00:00
Henry Heino
7b56311729 Mobile: Fixes #9188: Image editor resets on theme change (#9190) 2023-11-02 09:53:38 +00:00
Henry Heino
09b52237f2 Mobile: Fixes #9159: Fix fast search (#9191) 2023-11-02 09:50:38 +00:00
renovate[bot]
c7c86c2b52 Update dependency deprecated-react-native-prop-types to v4.2.3 (#9186)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-01 18:24:56 +00:00
Laurent Cozic
dce8bced15 Server v2.13.3 2023-11-01 16:30:12 +00:00
Laurent Cozic
a9719307af lock files 2023-11-01 16:29:04 +00:00
Laurent Cozic
0c8b475736 Server: Automatically restarts the server when it crashes, with exponentiel back-off 2023-11-01 16:28:45 +00:00
Laurent Cozic
dbd3db873f Fix ts error 2023-11-01 16:25:46 +00:00
Laurent Cozic
073781da92 Chore: Fixed attachFile method 2023-11-01 14:58:21 +00:00
Laurent Cozic
7d87d0b394 Chore: Remove postinstall step from editor package 2023-11-01 14:54:48 +00:00
renovate[bot]
0cb2a3a385 Update eslint (#9184)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-01 10:48:09 +00:00
renovate[bot]
c1e970a703 Update dependency @react-native-community/datetimepicker to v7.6.1 (#9181)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-01 07:16:07 +00:00
renovate[bot]
17831bf87a Update electron (#9183)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-01 03:32:57 +00:00
Joplin Bot
a93c558479 Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-11-01 00:41:29 +00:00
Laurent Cozic
13b09aa9a4 Doc: Set Data API link position 2023-10-31 20:39:52 +00:00
Laurent Cozic
e7e5a316f1 iOS 12.13.4 2023-10-31 20:39:04 +00:00
Joplin Bot
703fe35121 Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-10-31 18:18:53 +00:00
Laurent Cozic
a7dddaf2c4 Desktop: Allow attaching a file from the Markdown editor for HTML notes 2023-10-31 16:53:47 +00:00
Laurent Cozic
a95a66104d Doc: Fixed desktop changelog generation 2023-10-31 15:59:58 +00:00
Laurent Cozic
dd47571dff Plugins: Add support for showOpenDialog method 2023-10-31 15:30:05 +00:00
Laurent Cozic
0a2c3b3a91 Doc: Add config screen link to E2EE doc 2023-10-31 15:30:04 +00:00
Henry Heino
694ca6480e Desktop: Resolves #8742: Prompt to restart in safe mode on renderer process hang/crash (#9153) 2023-10-31 15:05:28 +00:00
Henry Heino
86b00d0a2b Mobile: Resolves #9134: Image editor: Allow loading from save when the image editor is reloaded in the background (#9135) 2023-10-31 14:57:26 +00:00
Laurent Cozic
b8c26b2ef3 Doc: Fixed Crowdin translation build 2023-10-31 14:06:54 +00:00
renovate[bot]
3343c2b0aa Update dependency sass to v1.68.0 (#9177)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-31 12:28:51 +00:00
Joplin Bot
f9c60bd47b Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-10-31 11:47:05 +00:00
Piotr Narel
71d2256fb7 All: Translation: Update pl_PL.po (#9174) 2023-10-31 07:46:36 -04:00
Laurent Cozic
66fe2f9390 Doc: Add support for localisation using Crowdin 2023-10-31 11:32:55 +00:00
renovate[bot]
3fe473cdd3 Update dependency @types/js-yaml to v4.0.7 (#9173)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-31 06:53:03 +00:00
Joplin Bot
3255f4d63b Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-10-31 00:40:05 +00:00
Laurent Cozic
66036c2027 Desktop release v2.13.4 2023-10-30 22:50:51 +00:00
329 changed files with 31956 additions and 24615 deletions

View File

@@ -260,6 +260,7 @@ packages/app-desktop/gui/NoteEditor/commands/index.js
packages/app-desktop/gui/NoteEditor/commands/pasteAsText.js
packages/app-desktop/gui/NoteEditor/commands/showLocalSearch.js
packages/app-desktop/gui/NoteEditor/commands/showRevisions.js
packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.test.js
packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.js
packages/app-desktop/gui/NoteEditor/styles/index.js
packages/app-desktop/gui/NoteEditor/utils/clipboardUtils.test.js
@@ -382,6 +383,8 @@ packages/app-desktop/integration-tests/models/MainScreen.js
packages/app-desktop/integration-tests/models/NoteEditorScreen.js
packages/app-desktop/integration-tests/models/SettingsScreen.js
packages/app-desktop/integration-tests/util/activateMainMenuItem.js
packages/app-desktop/integration-tests/util/createStartupArgs.js
packages/app-desktop/integration-tests/util/firstNonDevToolsWindow.js
packages/app-desktop/integration-tests/util/test.js
packages/app-desktop/playwright.config.js
packages/app-desktop/plugins/GotoAnything.js
@@ -415,6 +418,8 @@ packages/app-desktop/utils/checkForUpdatesUtils.test.js
packages/app-desktop/utils/checkForUpdatesUtils.js
packages/app-desktop/utils/checkForUpdatesUtilsTestData.js
packages/app-desktop/utils/markupLanguageUtils.js
packages/app-desktop/utils/restartInSafeModeFromMain.test.js
packages/app-desktop/utils/restartInSafeModeFromMain.js
packages/app-mobile/PluginAssetsLoader.js
packages/app-mobile/components/ActionButton.js
packages/app-mobile/components/BackButtonDialogBox.js
@@ -424,6 +429,7 @@ packages/app-mobile/components/Dropdown.test.js
packages/app-mobile/components/Dropdown.js
packages/app-mobile/components/ExtendedWebView.js
packages/app-mobile/components/FolderPicker.js
packages/app-mobile/components/Icon.js
packages/app-mobile/components/Modal.js
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js
packages/app-mobile/components/NoteBodyViewer/hooks/useEditPopup.test.js
@@ -464,17 +470,29 @@ packages/app-mobile/components/SelectDateTimeDialog.js
packages/app-mobile/components/SideMenu.js
packages/app-mobile/components/TextInput.js
packages/app-mobile/components/app-nav.js
packages/app-mobile/components/base-screen.js
packages/app-mobile/components/biometrics/BiometricPopup.js
packages/app-mobile/components/biometrics/biometricAuthenticate.js
packages/app-mobile/components/biometrics/sensorInfo.js
packages/app-mobile/components/getResponsiveValue.test.js
packages/app-mobile/components/getResponsiveValue.js
packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.js
packages/app-mobile/components/screens/ConfigScreen/ConfigScreenButton.js
packages/app-mobile/components/screens/ConfigScreen/FileSystemPathSelector.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/ExportDebugReportButton.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/ExportProfileButton.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.test.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/exportAllFolders.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportAllFolders.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportDebugReport.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportProfile.js
packages/app-mobile/components/screens/ConfigScreen/SectionHeader.js
packages/app-mobile/components/screens/ConfigScreen/SectionSelector.js
packages/app-mobile/components/screens/ConfigScreen/SettingComponent.js
packages/app-mobile/components/screens/ConfigScreen/SettingItem.js
packages/app-mobile/components/screens/ConfigScreen/SettingsButton.js
packages/app-mobile/components/screens/ConfigScreen/SettingsToggle.js
packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.js
packages/app-mobile/components/screens/ConfigScreen/types.js
packages/app-mobile/components/screens/LogScreen.js
packages/app-mobile/components/screens/Note.js
packages/app-mobile/components/screens/Notes.js
@@ -492,7 +510,10 @@ packages/app-mobile/services/profiles/index.js
packages/app-mobile/services/voiceTyping/vosk.android.js
packages/app-mobile/services/voiceTyping/vosk.ios.js
packages/app-mobile/setupQuickActions.js
packages/app-mobile/tools/buildInjectedJs.js
packages/app-mobile/tools/buildInjectedJs/BundledFile.js
packages/app-mobile/tools/buildInjectedJs/constants.js
packages/app-mobile/tools/buildInjectedJs/copyJs.js
packages/app-mobile/tools/buildInjectedJs/gulpTasks.js
packages/app-mobile/utils/ShareExtension.js
packages/app-mobile/utils/ShareUtils.test.js
packages/app-mobile/utils/ShareUtils.js
@@ -597,6 +618,7 @@ packages/lib/commands/index.js
packages/lib/commands/openMasterPasswordDialog.js
packages/lib/commands/synchronize.js
packages/lib/components/EncryptionConfigScreen/utils.js
packages/lib/components/shared/config/config-shared.js
packages/lib/components/shared/config/shouldShowMissingPasswordWarning.test.js
packages/lib/components/shared/config/shouldShowMissingPasswordWarning.js
packages/lib/components/shared/note-screen-shared.js
@@ -823,6 +845,7 @@ packages/lib/services/rest/routes/events.test.js
packages/lib/services/rest/routes/events.js
packages/lib/services/rest/routes/folders.js
packages/lib/services/rest/routes/master_keys.js
packages/lib/services/rest/routes/notes.test.js
packages/lib/services/rest/routes/notes.js
packages/lib/services/rest/routes/ping.js
packages/lib/services/rest/routes/resources.js
@@ -902,6 +925,7 @@ packages/lib/themes/type.js
packages/lib/time.js
packages/lib/utils/credentialFiles.js
packages/lib/utils/joplinCloud.js
packages/lib/utils/processStartFlags.js
packages/lib/utils/userFetcher.js
packages/lib/utils/webDAVUtils.test.js
packages/lib/utils/webDAVUtils.js

View File

@@ -157,6 +157,8 @@ module.exports = {
// In user-facing text, it should be "notebook".
'id-denylist': ['error', 'err', 'notebook', 'notebooks'],
'prefer-arrow-callback': ['error'],
'no-constant-binary-expression': ['error'],
},
'plugins': [
'react',

62
.github/ISSUE_TEMPLATE/BUG_REPORT.yml vendored Normal file
View File

@@ -0,0 +1,62 @@
name: Bug Report
description: Report a reproducible bug or regression in Joplin.
labels: ['bug']
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: dropdown
id: os
attributes:
label: "Operating system"
multiple: false
options:
- "Windows"
- "macOS"
- "Linux"
- "Android"
- "iOS"
validations:
required: true
- type: input
id: version
attributes:
label: "Joplin version"
placeholder: "For example 2.3.6"
description:
validations:
required: true
- type: textarea
id: desktop-about-content
attributes:
label: "Desktop version info"
description: "If this issue is about the **desktop app**, please open the \"About\" dialog under the \"Help\" or \"Joplin\" menu and copy its content here."
placeholder: "Joplin 2.13.5 (dev, darwin)\n\nClient ID: ..."
- type: textarea
id: current
attributes:
label: Current behaviour
description: What did Joplin do? Include screenshots and video recordings for UI problems if needed. If you are reporting a clipper bug, please include an example URL that shows the issue.
placeholder: |
1. This
2. Then that
3. Then this
4. Etc.
- type: textarea
id: expected
attributes:
label: Expected behaviour
description: What did you expect Joplin to do?
- type: textarea
id: logs
attributes:
label: Logs
description: "If relevant, please provide a log file as described here: https://joplinapp.org/help/apps/debugging"

View File

@@ -1,52 +0,0 @@
---
name: "\U0001F41B Bug Report"
about: Report a reproducible bug or regression in Joplin.
title: ''
labels: bug
assignees: ''
---
<!--
Please provide a clear and concise description of what the bug is. (In the section Steps To Reproduce.)
Include screenshots for UI problems if needed.
DO NOT create screenshots of text !!! Copy and paste the text into a code block.
Please test using the latest Joplin release to make sure your issue has not already been fixed.
-->
<!--
IMPORTANT: If you are reporting a clipper bug, please include an example URL that shows the issue.
Without the URL the issue is likely to be closed.
-->
## Environment
Joplin version:
Platform:
OS specifics:
<!--
Platform can be one of: macOS, Linux, Windows, Android, iOS, terminal (or a combination)
OS specifics: e.g. OS version, Linux distribution, Android/iOS version...
-->
## Steps to reproduce
1.
2.
3.
<!--
Issues without reproduction steps are likely to stall.
-->
## Describe what you expected to happen
## Logfile
<!--
Please attach a debug log. Issues without a debug log are likely to stall.
For information on how to collect a log file: https://joplinapp.org/help/apps/debugging/
-->

View File

@@ -1,5 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: "\U0001F914 Feature requests and support"
url: https://discourse.joplinapp.org/
about: I have a question or feature request …
- name: Feature Requests
url: https://discourse.joplinapp.org/c/features/
about: Discuss ideas for new features or changes
- name: Support
url: https://discourse.joplinapp.org/c/support/
about: Please ask for help here

View File

@@ -75,6 +75,10 @@ if [ "$IS_PULL_REQUEST" == "1" ] || [ "$IS_DEV_BRANCH" = "1" ]; then
if [ "$IS_LINUX" == "1" ]; then
echo "Running Joplin Server tests using PostgreSQL..."
sudo docker-compose --file docker-compose.db-dev.yml up -d
cmdResult=$?
if [ $cmdResult -ne 0 ]; then
exit $cmdResult
fi
export JOPLIN_TESTS_SERVER_DB=pg
else
echo "Running Joplin Server tests using SQLite..."

View File

@@ -33,6 +33,11 @@ jobs:
# https://yarnpkg.com/getting-started/install
corepack enable
# See github-action-main.yml for explanation
- uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Build macOS M1 app
env:
APPLE_ASC_PROVIDER: ${{ secrets.APPLE_ASC_PROVIDER }}

View File

@@ -98,6 +98,15 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# macos-latest ships with Python 3.12 by default, but this removes a
# utility that's used by electron-builder (distutils) so we need to pin
# Python to an earlier version.
# Fixes error `ModuleNotFoundError: No module named 'distutils'`
# Ref: https://github.com/nodejs/node-gyp/issues/2869
- uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Run tests, build and publish Linux and macOS apps
if: runner.os == 'Linux' || runner.os == 'macOs'
env:

31
.gitignore vendored
View File

@@ -51,6 +51,7 @@ lerna-debug.log
.env
docs/**/*.mustache
.idea
/readme/i18n
# Yarn stuff
# https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored
@@ -241,6 +242,7 @@ packages/app-desktop/gui/NoteEditor/commands/index.js
packages/app-desktop/gui/NoteEditor/commands/pasteAsText.js
packages/app-desktop/gui/NoteEditor/commands/showLocalSearch.js
packages/app-desktop/gui/NoteEditor/commands/showRevisions.js
packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.test.js
packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.js
packages/app-desktop/gui/NoteEditor/styles/index.js
packages/app-desktop/gui/NoteEditor/utils/clipboardUtils.test.js
@@ -363,6 +365,8 @@ packages/app-desktop/integration-tests/models/MainScreen.js
packages/app-desktop/integration-tests/models/NoteEditorScreen.js
packages/app-desktop/integration-tests/models/SettingsScreen.js
packages/app-desktop/integration-tests/util/activateMainMenuItem.js
packages/app-desktop/integration-tests/util/createStartupArgs.js
packages/app-desktop/integration-tests/util/firstNonDevToolsWindow.js
packages/app-desktop/integration-tests/util/test.js
packages/app-desktop/playwright.config.js
packages/app-desktop/plugins/GotoAnything.js
@@ -396,6 +400,8 @@ packages/app-desktop/utils/checkForUpdatesUtils.test.js
packages/app-desktop/utils/checkForUpdatesUtils.js
packages/app-desktop/utils/checkForUpdatesUtilsTestData.js
packages/app-desktop/utils/markupLanguageUtils.js
packages/app-desktop/utils/restartInSafeModeFromMain.test.js
packages/app-desktop/utils/restartInSafeModeFromMain.js
packages/app-mobile/PluginAssetsLoader.js
packages/app-mobile/components/ActionButton.js
packages/app-mobile/components/BackButtonDialogBox.js
@@ -405,6 +411,7 @@ packages/app-mobile/components/Dropdown.test.js
packages/app-mobile/components/Dropdown.js
packages/app-mobile/components/ExtendedWebView.js
packages/app-mobile/components/FolderPicker.js
packages/app-mobile/components/Icon.js
packages/app-mobile/components/Modal.js
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js
packages/app-mobile/components/NoteBodyViewer/hooks/useEditPopup.test.js
@@ -445,17 +452,29 @@ packages/app-mobile/components/SelectDateTimeDialog.js
packages/app-mobile/components/SideMenu.js
packages/app-mobile/components/TextInput.js
packages/app-mobile/components/app-nav.js
packages/app-mobile/components/base-screen.js
packages/app-mobile/components/biometrics/BiometricPopup.js
packages/app-mobile/components/biometrics/biometricAuthenticate.js
packages/app-mobile/components/biometrics/sensorInfo.js
packages/app-mobile/components/getResponsiveValue.test.js
packages/app-mobile/components/getResponsiveValue.js
packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.js
packages/app-mobile/components/screens/ConfigScreen/ConfigScreenButton.js
packages/app-mobile/components/screens/ConfigScreen/FileSystemPathSelector.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/ExportDebugReportButton.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/ExportProfileButton.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.test.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/exportAllFolders.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportAllFolders.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportDebugReport.js
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportProfile.js
packages/app-mobile/components/screens/ConfigScreen/SectionHeader.js
packages/app-mobile/components/screens/ConfigScreen/SectionSelector.js
packages/app-mobile/components/screens/ConfigScreen/SettingComponent.js
packages/app-mobile/components/screens/ConfigScreen/SettingItem.js
packages/app-mobile/components/screens/ConfigScreen/SettingsButton.js
packages/app-mobile/components/screens/ConfigScreen/SettingsToggle.js
packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.js
packages/app-mobile/components/screens/ConfigScreen/types.js
packages/app-mobile/components/screens/LogScreen.js
packages/app-mobile/components/screens/Note.js
packages/app-mobile/components/screens/Notes.js
@@ -473,7 +492,10 @@ packages/app-mobile/services/profiles/index.js
packages/app-mobile/services/voiceTyping/vosk.android.js
packages/app-mobile/services/voiceTyping/vosk.ios.js
packages/app-mobile/setupQuickActions.js
packages/app-mobile/tools/buildInjectedJs.js
packages/app-mobile/tools/buildInjectedJs/BundledFile.js
packages/app-mobile/tools/buildInjectedJs/constants.js
packages/app-mobile/tools/buildInjectedJs/copyJs.js
packages/app-mobile/tools/buildInjectedJs/gulpTasks.js
packages/app-mobile/utils/ShareExtension.js
packages/app-mobile/utils/ShareUtils.test.js
packages/app-mobile/utils/ShareUtils.js
@@ -578,6 +600,7 @@ packages/lib/commands/index.js
packages/lib/commands/openMasterPasswordDialog.js
packages/lib/commands/synchronize.js
packages/lib/components/EncryptionConfigScreen/utils.js
packages/lib/components/shared/config/config-shared.js
packages/lib/components/shared/config/shouldShowMissingPasswordWarning.test.js
packages/lib/components/shared/config/shouldShowMissingPasswordWarning.js
packages/lib/components/shared/note-screen-shared.js
@@ -804,6 +827,7 @@ packages/lib/services/rest/routes/events.test.js
packages/lib/services/rest/routes/events.js
packages/lib/services/rest/routes/folders.js
packages/lib/services/rest/routes/master_keys.js
packages/lib/services/rest/routes/notes.test.js
packages/lib/services/rest/routes/notes.js
packages/lib/services/rest/routes/ping.js
packages/lib/services/rest/routes/resources.js
@@ -883,6 +907,7 @@ packages/lib/themes/type.js
packages/lib/time.js
packages/lib/utils/credentialFiles.js
packages/lib/utils/joplinCloud.js
packages/lib/utils/processStartFlags.js
packages/lib/utils/userFetcher.js
packages/lib/utils/webDAVUtils.test.js
packages/lib/utils/webDAVUtils.js

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.3.cjs
yarnPath: .yarn/releases/yarn-3.6.4.cjs
logFilters:

BIN
Assets/BadgeMacOSM1.psd Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -1,6 +1,31 @@
function getOs() {
async function getOs() {
// The macOS release is available for Intel and Apple silicon processors,
// and the only way to get that info is through this new
// `getHighEntropyValues` function (which is not available on all browsers).
// So here we either return "macOs" for Intel or "macOsM1" for Apple
// Silicon. If we don't know which it is, we return "macOsUndefined".
// https://stackoverflow.com/a/75177111/561309
if (navigator.appVersion.indexOf("Mac")!=-1) {
let platformInfo = null;
try {
platformInfo = await navigator.userAgentData.getHighEntropyValues(['architecture'])
} catch (error) {
console.warn('Failed getting Mac architecture:', error);
return 'macOsUndefined';
}
console.info('Got platform info:', platformInfo);
if (platformInfo.architecture === 'arm') {
return "macOsM1";
} else {
return "macOs";
}
}
if (navigator.appVersion.indexOf("Win")!=-1) return "windows";
if (navigator.appVersion.indexOf("Mac")!=-1) return "macOs";
if (navigator.appVersion.indexOf("X11")!=-1) return "linux";
if (navigator.appVersion.indexOf("Linux")!=-1) return "linux";
return null;
@@ -45,7 +70,7 @@ function setupMobileMenu() {
});
}
function setupDownloadPage() {
async function setupDownloadPage() {
if (!$('.page-download').length) return;
const downloadLinks = {};
@@ -55,6 +80,7 @@ function setupDownloadPage() {
if (href.indexOf('-Setup') > 0) downloadLinks['windows'] = href;
if (href.indexOf('.dmg') > 0) downloadLinks['macOs'] = href;
if (href.endsWith('arm64.DMG')) downloadLinks['macOsM1'] = href;
if (href.indexOf('.AppImage') > 0) downloadLinks['linux'] = href;
});
@@ -70,8 +96,17 @@ function setupDownloadPage() {
if (mobileOs) {
$('.page-download .intro').hide();
} else {
const os = getOs();
if (!os || !downloadLinks[os]) {
const os = await getOs();
if (os === 'macOsUndefined') {
// If we don't know which macOS version it is, we let the user choose.
$('.main-content .intro').html('<p class="macos-m1-info">The macOS release is available for Intel processors or for Apple Silicon (M1) processors. Please select your version:</p>');
const macOsLink = $('.download-link-macOs');
const macOsM1Link = $('.download-link-macOsM1');
$('.macos-m1-info').after('<p style="font-style: italic; font-size: .8em;">To find out what processor you have, click on the <b>Apple logo</b> in the macOS menu bar, choose <b>About This Mac</b> from the dropdown menu. If you have an Apple silicon it should say"Apple M1" under "Chip". Otherwise you have an Intel processor.</p>');
$('.macos-m1-info').after(macOsM1Link);
$('.macos-m1-info').after(macOsLink);
} else if (!os || !downloadLinks[os]) {
// If we don't know, display the section to manually download the app
$('.page-download .get-it-desktop').show();
} else if (os === 'linux') {
@@ -89,5 +124,5 @@ function setupDownloadPage() {
$(function () {
setupMobileMenu();
setupDownloadPage();
void setupDownloadPage();
});

View File

@@ -71,7 +71,7 @@ EXPOSE ${APP_PORT}
# https://github.com/nodejs/docker-node/blob/main/docs/BestPractices.md#handling-kernel-signals
WORKDIR /home/$user/packages/server
ENTRYPOINT ["tini", "--"]
CMD ["node", "dist/app.js"]
CMD ["yarn", "start-prod"]
# Build-time metadata
# https://github.com/opencontainers/image-spec/blob/master/annotations.md

View File

@@ -124,7 +124,7 @@ else
fi
if [[ $LIBFUSE == "" ]] ; then
print "${COLOR_RED}Error: Can't get libfuse2 on system, please install libfuse2${COLOR_RESET}"
print "See https://joplinapp.org/faq/#desktop-application-will-not-launch-on-linux and https://github.com/AppImage/AppImageKit/wiki/FUSE for more information"
print "See https://joplinapp.org/help/faq/#desktop-application-will-not-launch-on-linux and https://github.com/AppImage/AppImageKit/wiki/FUSE for more information"
exit 1
fi
@@ -205,9 +205,16 @@ if command -v lsb_release &> /dev/null; then
# Check for "The SUID sandbox helper binary was found, but is not configured correctly" problem.
# It is present in Debian 1X. A (temporary) patch will be applied at .desktop file
# Linux Mint 4 Debbie is based on Debian 10 and requires the same param handling.
if [[ $DISTVER =~ Debian1. ]] || [ "$DISTVER" = "Linuxmint4" ] && [ "$DISTCODENAME" = "debbie" ] || [ "$DISTVER" = "CentOS" ] && [[ "$DISTMAJOR" =~ 6|7 ]]
#
# This also works around Ubuntu 23.10+'s restrictions on unprivileged user namespaces. Electron
# uses these to sandbox processes. Unfortunately, it doesn't look like we can get around this
# without writing the AppImage to a non-user-writable location (without invalidating other security
# controls). See https://discourse.joplinapp.org/t/possible-future-requirement-for-no-sandbox-flag-for-ubuntu-23-10/.
if [[ $DISTVER = "Ubuntu23.10" || $DISTVER =~ Debian1. || ( "$DISTVER" = "Linuxmint4" && "$DISTCODENAME" = "debbie" ) || ( "$DISTVER" = "CentOS" && "$DISTMAJOR" =~ 6|7 ) ]]
then
SANDBOXPARAM="--no-sandbox"
print "${COLOR_YELLOW}WARNING${COLOR_RESET} Electron sandboxing disabled."
print " See https://discourse.joplinapp.org/t/32160/5 for details."
fi
fi

View File

@@ -42,8 +42,8 @@ Please see the [donation page](https://github.com/laurent22/joplin/blob/dev/read
| <img width="50" src="https://avatars2.githubusercontent.com/u/215668?s=96&v=4"/></br>[avanderberg](https://github.com/avanderberg) | <img width="50" src="https://avatars2.githubusercontent.com/u/67130?s=96&v=4"/></br>[chr15m](https://github.com/chr15m) | <img width="50" src="https://avatars2.githubusercontent.com/u/2793530?s=96&v=4"/></br>[CyberXZT](https://github.com/CyberXZT) | <img width="50" src="https://avatars2.githubusercontent.com/u/1307332?s=96&v=4"/></br>[dbrandonjohnson](https://github.com/dbrandonjohnson) |
| <img width="50" src="https://avatars2.githubusercontent.com/u/14873877?s=96&v=4"/></br>[dchecks](https://github.com/dchecks) | <img width="50" src="https://avatars2.githubusercontent.com/u/56287?s=96&v=4"/></br>[fats](https://github.com/fats) | <img width="50" src="https://avatars2.githubusercontent.com/u/8030470?s=96&v=4"/></br>[Galliver7](https://github.com/Galliver7) | <img width="50" src="https://avatars2.githubusercontent.com/u/64712218?s=96&v=4"/></br>[Hegghammer](https://github.com/Hegghammer) |
| <img width="50" src="https://avatars2.githubusercontent.com/u/1310474?s=96&v=4"/></br>[jknowles](https://github.com/jknowles) | <img width="50" src="https://avatars2.githubusercontent.com/u/11947658?s=96&v=4"/></br>[KentBrockman](https://github.com/KentBrockman) | <img width="50" src="https://avatars2.githubusercontent.com/u/24908652?s=96&v=4"/></br>[konishi-t](https://github.com/konishi-t) | <img width="50" src="https://avatars2.githubusercontent.com/u/42319182?s=96&v=4"/></br>[marcdw1289](https://github.com/marcdw1289) |
| <img width="50" src="https://avatars2.githubusercontent.com/u/126279083?s=96&v=4"/></br>[matmoly](https://github.com/matmoly) | <img width="50" src="https://avatars2.githubusercontent.com/u/1788010?s=96&v=4"/></br>[maxtruxa](https://github.com/maxtruxa) | <img width="50" src="https://avatars2.githubusercontent.com/u/31054972?s=96&v=4"/></br>[saarantras](https://github.com/saarantras) | <img width="50" src="https://avatars2.githubusercontent.com/u/327998?s=96&v=4"/></br>[sif](https://github.com/sif) |
| <img width="50" src="https://avatars2.githubusercontent.com/u/765564?s=96&v=4"/></br>[taskcruncher](https://github.com/taskcruncher) | <img width="50" src="https://avatars2.githubusercontent.com/u/333944?s=96&v=4"/></br>[tateisu](https://github.com/tateisu) | | |
| <img width="50" src="https://avatars2.githubusercontent.com/u/126279083?s=96&v=4"/></br>[matmoly](https://github.com/matmoly) | <img width="50" src="https://avatars2.githubusercontent.com/u/1788010?s=96&v=4"/></br>[maxtruxa](https://github.com/maxtruxa) | <img width="50" src="https://avatars2.githubusercontent.com/u/4560672?s=96&v=4"/></br>[mu88](https://github.com/mu88) | <img width="50" src="https://avatars2.githubusercontent.com/u/31054972?s=96&v=4"/></br>[saarantras](https://github.com/saarantras) |
| <img width="50" src="https://avatars2.githubusercontent.com/u/327998?s=96&v=4"/></br>[sif](https://github.com/sif) | <img width="50" src="https://avatars2.githubusercontent.com/u/765564?s=96&v=4"/></br>[taskcruncher](https://github.com/taskcruncher) | <img width="50" src="https://avatars2.githubusercontent.com/u/333944?s=96&v=4"/></br>[tateisu](https://github.com/tateisu) | |
<!-- SPONSORS-GITHUB -->
# Community

23
crowdin.yml Normal file
View File

@@ -0,0 +1,23 @@
project_id: '624298'
api_token_env: CROWDIN_PERSONAL_TOKEN
preserve_hierarchy: true
files:
- source: /readme/**/*
translation: /readme/i18n/%two_letters_code%/docusaurus-plugin-content-docs/current/**/%original_file_name%
ignore:
- /readme/_i18n
- /readme/i18n
- /readme/about/changelog
- /readme/about/stats.md
- /readme/api
- /readme/dev
- /readme/news
- /readme/cla.md
- /readme/connection_check.md
- /readme/privacy.md
- /**/*.yml
- /**/*.json
- /**/*.png
- /**/*.jpg

View File

@@ -6,7 +6,7 @@ version: '3'
services:
db:
image: postgres:15
image: postgres:16
command: postgres -c work_mem=100000
ports:
- "5432:5432"

View File

@@ -18,7 +18,7 @@ services:
- POSTGRES_PORT=5432
- POSTGRES_HOST=localhost
db:
image: postgres:15
image: postgres:16
ports:
- "5432:5432"
environment:

View File

@@ -19,7 +19,7 @@ version: '3'
services:
db:
image: postgres:15
image: postgres:16
volumes:
- ./data/postgres:/var/lib/postgresql/data
ports:

View File

@@ -182,6 +182,11 @@
"docs/images/flags": true,
"lerna-debug.log": true,
"node_modules/": true,
"packages/doc-builder/build": true,
"packages/doc-builder/help": true,
"packages/doc-builder/news": true,
"packages/doc-builder/i18n": true,
"readme/i18n": true,
"packages/app-cli/**/*.*~": true,
"packages/app-cli/**/*.mo": true,
"packages/app-cli/**/build/": true,
@@ -362,6 +367,12 @@
"type": "shell",
"command": "cd ${workspaceFolder}/packages/server && yarn tsc",
"group": "build",
},
{
"label": "transpile-lib",
"type": "shell",
"command": "cd ${workspaceFolder}/packages/lib && yarn tsc",
"group": "build",
}
]
},
@@ -390,6 +401,19 @@
"APP_BASE_URL": "http://joplincloud.local:22300",
"API_BASE_URL": "http://api.joplincloud.local:22300",
}
},
{
"type": "node",
"request": "launch",
"name": "lib: debug test file",
"preLaunchTask": "transpile-lib",
"program": "${workspaceFolder}/packages/lib/node_modules/.bin/jest",
"args": [
"${fileBasenameNoExtension}",
"--config",
"packages/lib/jest.config.js",
],
"console": "integratedTerminal",
}
]
}

View File

@@ -12,31 +12,34 @@
"node": ">=16"
},
"scripts": {
"buildParallel": "yarn workspaces foreach --verbose --interlaced --parallel --jobs 2 --topological run build && yarn run tsc",
"buildSequential": "yarn workspaces foreach --verbose --interlaced --topological run build && yarn run tsc",
"buildApiDoc": "yarn workspace joplin start apidoc ../../readme/api/references/rest_api.md",
"buildCommandIndex": "node packages/tools/gulp/tasks/buildCommandIndexRun.js",
"buildParallel": "yarn workspaces foreach --verbose --interlaced --parallel --jobs 2 --topological run build && yarn run tsc",
"buildPluginDoc": "cd packages/generate-plugin-doc && yarn run buildPluginDoc_",
"updateMarkdownDoc": "node ./packages/tools/updateMarkdownDoc",
"updateNews": "node ./packages/tools/website/updateNews",
"postPreReleasesToForum": "node ./packages/tools/postPreReleasesToForum",
"buildSequential": "yarn workspaces foreach --verbose --interlaced --topological run build && yarn run tsc",
"buildServerDocker": "node packages/tools/buildServerDocker.js",
"buildSettingJsonSchema": "yarn workspace joplin start settingschema ../../../joplin-website/docs/schema/settings.json",
"buildTranslations": "node packages/tools/build-translation.js",
"buildWebsiteTranslations": "node packages/tools/website/buildTranslations.js",
"buildWebsite": "node ./packages/tools/website/processDocs.js --env prod && node ./packages/tools/website/build.js && yarn run buildPluginDoc && yarn run buildSettingJsonSchema",
"checkLibPaths": "node ./packages/tools/checkLibPaths.js",
"buildWebsiteTranslations": "node packages/tools/website/buildTranslations.js",
"checkIgnoredFiles": "node ./packages/tools/checkIgnoredFiles.js",
"checkLibPaths": "node ./packages/tools/checkLibPaths.js",
"circularDependencyCheck": "madge --warning --circular --extensions js ./",
"clean": "npm run clean --workspaces --if-present && node packages/tools/clean && yarn cache clean",
"crowdin": "crowdin",
"crowdinDownload": "crowdin download",
"crowdinUpload": "crowdin upload",
"cspell": "cspell",
"dependencyTree": "madge",
"generateDatabaseTypes": "node packages/tools/generate-database-types",
"linkChecker": "linkchecker https://joplinapp.org",
"linkChecker": "linkchecker https://joplinapp.org/ && linkchecker --check-extern https://joplinapp.org/api/references/plugin_api/classes/joplin.html",
"linter-ci": "eslint --resolve-plugins-relative-to . --quiet --ext .js --ext .jsx --ext .ts --ext .tsx",
"linter-interactive": "eslint-interactive --resolve-plugins-relative-to . --fix --quiet --ext .js --ext .jsx --ext .ts --ext .tsx",
"linter-precommit": "eslint --resolve-plugins-relative-to . --fix --ext .js --ext .jsx --ext .ts --ext .tsx",
"linter": "eslint --resolve-plugins-relative-to . --fix --quiet --ext .js --ext .jsx --ext .ts --ext .tsx",
"linter-interactive": "eslint-interactive --resolve-plugins-relative-to . --fix --quiet --ext .js --ext .jsx --ext .ts --ext .tsx",
"packageJsonLint": "node ./packages/tools/packageJsonLint.js",
"postinstall": "gulp build",
"postPreReleasesToForum": "node ./packages/tools/postPreReleasesToForum",
"publishAll": "git pull && yarn run buildParallel && lerna version --yes --no-private --no-git-tag-version && gulp completePublishAll",
"releaseAndroid": "PATH=\"/usr/local/opt/openjdk@11/bin:$PATH\" node packages/tools/release-android.js",
"releaseAndroidClean": "node packages/tools/release-android.js",
@@ -47,15 +50,15 @@
"releasePluginGenerator": "node packages/tools/release-plugin-generator.js",
"releasePluginRepoCli": "node packages/tools/release-plugin-repo-cli.js",
"releaseServer": "node packages/tools/release-server.js",
"cspell": "cspell",
"setupNewRelease": "node ./packages/tools/setupNewRelease",
"spellcheck": "node packages/tools/spellcheck.js",
"tagServerLatest": "node packages/tools/tagServerLatest.js",
"buildServerDocker": "node packages/tools/buildServerDocker.js",
"setupNewRelease": "node ./packages/tools/setupNewRelease",
"test-ci": "yarn workspaces foreach --parallel --verbose --interlaced --jobs 2 run test-ci",
"test": "yarn workspaces foreach --parallel --verbose --interlaced --jobs 2 run test",
"tsc": "yarn workspaces foreach --parallel --verbose --interlaced run tsc",
"updateIgnored": "node packages/tools/gulp/tasks/updateIgnoredTypeScriptBuildRun.js",
"updateMarkdownDoc": "node ./packages/tools/updateMarkdownDoc",
"updateNews": "node ./packages/tools/website/updateNews",
"updatePluginTypes": "./packages/generator-joplin/updateTypes.sh",
"watch": "yarn workspaces foreach --parallel --verbose --interlaced --jobs 999 run watch",
"watchWebsite": "nodemon --delay 1 --watch Assets/WebsiteAssets --watch packages/tools/website --watch packages/tools/website/utils --watch packages/doc-builder/build --ext md,ts,js,mustache,css,tsx,gif,png,svg --exec \"node packages/tools/website/build.js && http-server --port 8077 ../joplin-website/docs -a localhost\""
@@ -66,15 +69,16 @@
}
},
"devDependencies": {
"@crowdin/cli": "3",
"@joplin/utils": "~2.12",
"@seiyab/eslint-plugin-react-hooks": "4.5.1-beta.0",
"@typescript-eslint/eslint-plugin": "6.0.0",
"@typescript-eslint/parser": "6.0.0",
"@typescript-eslint/eslint-plugin": "6.7.2",
"@typescript-eslint/parser": "6.7.2",
"cspell": "5.21.2",
"eslint": "8.47.0",
"eslint": "8.49.0",
"eslint-interactive": "10.8.0",
"eslint-plugin-import": "2.28.1",
"eslint-plugin-jest": "27.2.3",
"eslint-plugin-jest": "27.4.0",
"eslint-plugin-promise": "6.1.1",
"eslint-plugin-react": "7.33.2",
"execa": "5.1.1",
@@ -83,23 +87,23 @@
"gulp": "4.0.2",
"husky": "3.1.0",
"lerna": "3.22.1",
"lint-staged": "13.3.0",
"lint-staged": "14.0.1",
"madge": "6.1.0",
"npm-package-json-lint": "7.0.0",
"typescript": "5.1.6"
"typescript": "5.2.2"
},
"dependencies": {
"@types/fs-extra": "11.0.2",
"eslint-plugin-github": "4.9.2",
"@types/fs-extra": "11.0.3",
"eslint-plugin-github": "4.10.0",
"http-server": "14.1.1",
"node-gyp": "9.4.0",
"nodemon": "3.0.1"
},
"packageManager": "yarn@3.6.3",
"packageManager": "yarn@3.6.4",
"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.47.0#./.yarn/patches/eslint-npm-8.39.0-d92bace04d.patch",
"eslint": "patch:eslint@8.49.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",
"react-native@0.71.10": "patch:react-native@npm%3A0.71.10#./.yarn/patches/react-native-animation-fix/react-native-npm-0.71.10-f9c32562d8.patch"
}

View File

@@ -57,6 +57,10 @@ class Command extends BaseCommand {
const lines = [];
lines.push('---');
lines.push('sidebar_position: 2');
lines.push('---');
lines.push('');
lines.push('# Joplin Data API');
lines.push('');
lines.push('This API is available when the clipper server is running. It provides access to the notes, notebooks, tags and other Joplin object via a REST API. Plugins can also access this API even when the clipper server is not running.');

View File

@@ -52,7 +52,7 @@ export default class PluginRunner extends BasePluginRunner {
this.activeSandboxCalls_[callId] = true;
const promise = executeSandboxCall(pluginId, sandbox, `joplin.${path}`, mapEventHandlersToIds(args, this.eventHandlers_), this.eventHandler);
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
promise.finally(() => {
void promise.finally(() => {
delete this.activeSandboxCalls_[callId];
});
return promise;

View File

@@ -35,7 +35,7 @@
],
"owner": "Laurent Cozic"
},
"version": "2.13.0",
"version": "2.13.2",
"bin": "./main.js",
"engines": {
"node": ">=10.0.0"
@@ -71,13 +71,13 @@
},
"devDependencies": {
"@joplin/tools": "~2.13",
"@types/fs-extra": "11.0.2",
"@types/jest": "29.5.4",
"@types/node": "18.17.19",
"@types/fs-extra": "11.0.3",
"@types/jest": "29.5.5",
"@types/node": "18.18.7",
"@types/proper-lockfile": "^4.1.2",
"gulp": "4.0.2",
"jest": "29.6.4",
"jest": "29.7.0",
"temp": "0.9.4",
"typescript": "5.1.6"
"typescript": "5.2.2"
}
}

View File

@@ -36,6 +36,10 @@ describe('HtmlToMd', () => {
htmlToMdOptions.preserveImageTagsWithSize = true;
}
if (htmlFilename.indexOf('preserve_nested_tables') === 0) {
htmlToMdOptions.preserveNestedTables = true;
}
const html = await readFile(htmlPath, 'utf8');
let expectedMd = await readFile(mdPath, 'utf8');
@@ -83,8 +87,8 @@ describe('HtmlToMd', () => {
it('should allow disabling escape', async () => {
const htmlToMd = new HtmlToMd();
expect(htmlToMd.parse('https://test.com/1_2_3.pdf', { disableEscapeContent: true })).toBe('https://test.com/1_2_3.pdf');
expect(htmlToMd.parse('https://test.com/1_2_3.pdf', { disableEscapeContent: false })).toBe('https://test.com/1\\_2\\_3.pdf');
expect(htmlToMd.parse('> 1 _2_ 3.pdf', { disableEscapeContent: true })).toBe('> 1 _2_ 3.pdf');
expect(htmlToMd.parse('> 1 _2_ 3.pdf', { disableEscapeContent: false })).toBe('\\> 1 \\_2_ 3.pdf');
});
});

View File

@@ -66,8 +66,10 @@ describe('MdToHtml', () => {
actualHtml,
'--------------------------------- Raw:',
actualHtml.split('\n'),
'--------------------------------- Expected:',
'--------------------------------- Expected (Lines)',
expectedHtml.split('\n'),
'--------------------------------- Expected (Text)',
expectedHtml,
'--------------------------------------------',
'',
];

View File

@@ -15,7 +15,4 @@ however.
Because it isn't
necessary.
...
<br/><br/><br/>...

View File

@@ -0,0 +1,19 @@
<body>
<table border="5px" bordercolor="#8707B0">
<tr>
<td>Left side of the main table</td>
<td>
<table border="5px" bordercolor="#F35557">
<h4 align="center">Nested Table</h4>
<tr>
<td>nested table C1</td>
<td>nested table C2</td>
</tr>
<tr>
<td>nested table</td>
<td>nested table</td>
</tr>
</table>
</td>
</tr>
</table>

View File

@@ -0,0 +1 @@
<table border="5px" bordercolor="#8707B0"><tbody><tr><td>Left side of the main table</td><td><h4 align="center">Nested Table</h4><table border="5px" bordercolor="#F35557"><tbody><tr><td>nested table C1</td><td>nested table C2</td></tr><tr><td>nested table</td><td>nested table</td></tr></tbody></table></td></tr></tbody></table>

View File

@@ -0,0 +1,4 @@
A<br/><br/><br/>test.<br/>
A single &lt;br/&gt;<br/>can use two spaces at the end of the line,
but<br/><br/>the markdown renderer discards these if the line is otherwise empty.

View File

@@ -0,0 +1,5 @@
A
<br/><br/>test.
A single &lt;br/&gt;
can use two spaces at the end of the line, but
<br/>the markdown renderer discards these if the line is otherwise empty.

View File

@@ -0,0 +1,14 @@
<p>
Some URLs in the Rich_Text_Editor contain <code>_</code> characters, but haven't been converted
to links yet. For example, https://www.example.com/a_test_of_links.
</p>
<p>We should preserve the underscores _without escaping them_ to prevent the links from breaking.</p>
<p>
This should also correctly handle unicode characters. For example, punctuation❯_requires escapes_,
but 𝔏𝔈𝔗𝔗𝔈𝕽_𝔠𝔥𝔞𝔯𝔞𝔠𝔱𝔢𝔯𝔰_and_897_numbers_𝒟on_'t.
</p>
<p>
_Note_ that what [_causes_] a `_` to create italics_ seems to depend only on the character before
and an escape at the _beginning_ seems to be sufficient.
</p>
<p>_s also don't need escapes if _ followed _ by a _space.</p>

View File

@@ -0,0 +1,9 @@
Some URLs in the Rich_Text_Editor contain `_` characters, but haven't been converted to links yet. For example, https://www.example.com/a_test_of_links.
We should preserve the underscores \_without escaping them_ to prevent the links from breaking.
This should also correctly handle unicode characters. For example, punctuation❯\_requires escapes_, but 𝔏𝔈𝔗𝔗𝔈𝕽_𝔠𝔥𝔞𝔯𝔞𝔠𝔱𝔢𝔯𝔰_and_897_numbers_𝒟on_'t.
\_Note_ that what \[\_causes_\] a \`\_\` to create italics_ seems to depend only on the character before and an escape at the \_beginning_ seems to be sufficient.
\_s also don't need escapes if _ followed _ by a \_space.

View File

@@ -0,0 +1,7 @@
<p><a href=":/62d16d1c1e28418da6624fa8742a7ed0" class="jop-noMdConv">Resource link</a></p>
<p><a href="https://example.com/ok" class="jop-noMdConv">ok</a></p>
<p><a href="http://example.com/ok" class="jop-noMdConv">ok</a></p>
<p><a href="mailto:name@email.com" class="jop-noMdConv">ok</a></p>
<p><a href="joplin://62d16d1c1e28418da6624fa8742a7ed0" class="jop-noMdConv">ok</a></p>
<p><a href="#" class="jop-noMdConv">not ok</a></p>
<p><a href="#" class="jop-noMdConv">not ok</a></p>

View File

@@ -0,0 +1,13 @@
<a href=":/62d16d1c1e28418da6624fa8742a7ed0">Resource link</a>
<a href="https://example.com/ok">ok</a>
<a href="http://example.com/ok">ok</a>
<a href="mailto:name@email.com">ok</a>
<a href="joplin://62d16d1c1e28418da6624fa8742a7ed0">ok</a>
<a href="file:///etc/passwd">not ok</a>
<a href="data://blabla">not ok</a>

View File

@@ -206,7 +206,7 @@ describe('services_PluginService', () => {
const mdToHtml = new MdToHtml();
const module = require(contentScript.path).default;
mdToHtml.loadExtraRendererRule(contentScript.id, tempDir, module({}));
mdToHtml.loadExtraRendererRule(contentScript.id, tempDir, module({}), '');
const result = await mdToHtml.render([
'```justtesting',

View File

@@ -74,7 +74,7 @@ To get such an external script file to compile, you need to add it to the `extra
## More information
- [Joplin Plugin API](https://joplinapp.org/api/references/plugin_api/classes/joplin.html)
- [Joplin Data API](https://joplinapp.org/api/references/rest_api/)
- [Joplin Data API](https://joplinapp.org/help/api/references/rest_api)
- [Joplin Plugin Manifest](https://joplinapp.org/api/references/plugin_manifest/)
- Ask for help on the [forum](https://discourse.joplinapp.org/) or our [Discord channel](https://discord.gg/VSj7AFHvpq)

View File

@@ -9,7 +9,10 @@ const url = require('url');
const path = require('path');
const { dirname } = require('@joplin/lib/path-utils');
const fs = require('fs-extra');
const { ipcMain } = require('electron');
import { dialog, ipcMain } from 'electron';
import { _ } from '@joplin/lib/locale';
import restartInSafeModeFromMain from './utils/restartInSafeModeFromMain';
interface RendererProcessQuitReply {
canClose: boolean;
@@ -34,7 +37,7 @@ export default class ElectronAppWrapper {
private pluginWindows_: PluginWindows = {};
private initialCallbackUrl_: string = null;
public constructor(electronApp: any, env: string, profilePath: string, isDebugMode: boolean, initialCallbackUrl: string) {
public constructor(electronApp: any, env: string, profilePath: string|null, isDebugMode: boolean, initialCallbackUrl: string) {
this.electronApp_ = electronApp;
this.env_ = env;
this.isDebugMode_ = isDebugMode;
@@ -66,6 +69,42 @@ export default class ElectronAppWrapper {
return this.initialCallbackUrl_;
}
// Call when the app fails in a significant way.
//
// Assumes that the renderer process may be in an invalid state and so cannot
// be accessed.
public async handleAppFailure(errorMessage: string, canIgnore: boolean, isTesting?: boolean) {
const buttons = [];
buttons.push(_('Quit'));
const exitIndex = 0;
if (canIgnore) {
buttons.push(_('Ignore'));
}
const restartIndex = buttons.length;
buttons.push(_('Restart in safe mode'));
const { response } = await dialog.showMessageBox({
message: _('An error occurred: %s', errorMessage),
buttons,
});
if (response === restartIndex) {
await restartInSafeModeFromMain();
// A hung renderer seems to prevent the process from exiting completely.
// In this case, crashing the renderer allows the window to close.
//
// Also only run this if not testing (crashing the renderer breaks automated
// tests).
if (this.win_ && !this.win_.webContents.isCrashed() && !isTesting) {
this.win_.webContents.forcefullyCrashRenderer();
}
} else if (response === exitIndex) {
process.exit(1);
}
}
public createWindow() {
// Set to true to view errors if the application does not start
const debugEarlyBugs = this.env_ === 'dev' || this.isDebugMode_;
@@ -121,6 +160,20 @@ export default class ElectronAppWrapper {
this.win_.setPosition(primaryDisplayWidth / 2 - windowWidth, primaryDisplayHeight / 2 - windowHeight);
}
this.win_.webContents.on('unresponsive', async () => {
await this.handleAppFailure(_('Window unresponsive.'), true);
});
this.win_.webContents.on('render-process-gone', async _event => {
await this.handleAppFailure('Renderer process gone.', false);
});
this.win_.webContents.on('did-fail-load', async event => {
if ((event as any).isMainFrame) {
await this.handleAppFailure('Renderer process failed to load', false);
}
});
void this.win_.loadURL(url.format({
pathname: path.join(__dirname, 'index.html'),
protocol: 'file:',

View File

@@ -26,7 +26,7 @@ const { FoldersScreenUtils } = require('@joplin/lib/folders-screen-utils.js');
import Folder from '@joplin/lib/models/Folder';
import Tag from '@joplin/lib/models/Tag';
import { reg } from '@joplin/lib/registry';
const packageInfo = require('./packageInfo.js');
const packageInfo: PackageInfo = require('./packageInfo.js');
import DecryptionWorker from '@joplin/lib/services/DecryptionWorker';
import ClipperServer from '@joplin/lib/ClipperServer';
const { webFrame } = require('electron');
@@ -68,6 +68,7 @@ import path = require('path');
import { checkPreInstalledDefaultPlugins, installDefaultPlugins, setSettingsForDefaultPlugins } from '@joplin/lib/services/plugins/defaultPlugins/defaultPluginsUtils';
import userFetcher, { initializeUserFetcher } from '@joplin/lib/utils/userFetcher';
import { parseNotesParent } from '@joplin/lib/reducer';
import { PackageInfo } from '@joplin/lib/versionInfo';
const pluginClasses = [
require('./plugins/GotoAnything').default,

View File

@@ -5,8 +5,9 @@ import bridge from './services/bridge';
import KvStore from '@joplin/lib/services/KvStore';
import * as ArrayUtils from '@joplin/lib/ArrayUtils';
import { CheckForUpdateOptions, extractVersionInfo, GitHubRelease } from './utils/checkForUpdatesUtils';
const packageInfo = require('./packageInfo.js');
import { PackageInfo } from '@joplin/lib/versionInfo';
import { compareVersions } from 'compare-versions';
const packageInfo: PackageInfo = require('./packageInfo.js');
const logger = Logger.create('checkForUpdates');

View File

@@ -12,7 +12,7 @@ const { connect } = require('react-redux');
const { themeStyle } = require('@joplin/lib/theme');
const pathUtils = require('@joplin/lib/path-utils');
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
const shared = require('@joplin/lib/components/shared/config/config-shared.js');
import * as shared from '@joplin/lib/components/shared/config/config-shared.js';
import ClipperConfigScreen from '../ClipperConfigScreen';
import restart from '../../services/restart';
import PluginService from '@joplin/lib/services/plugins/PluginService';
@@ -35,9 +35,10 @@ class ConfigScreenComponent extends React.Component<any, any> {
public constructor(props: any) {
super(props);
shared.init(this, reg);
shared.init(reg);
this.state = {
...shared.defaultScreenState,
selectedSectionName: 'general',
screenName: '',
changedSettingKeys: [],
@@ -98,7 +99,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
}
public sectionByName(name: string) {
const sections = shared.settingsSections({ device: 'desktop', settings: this.state.settings });
const sections = shared.settingsSections({ device: AppType.Desktop, settings: this.state.settings });
for (const section of sections) {
if (section.name === name) return section;
}
@@ -699,7 +700,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
const hasChanges = this.hasChanges();
const settingComps = shared.settingsToComponents2(this, 'desktop', settings, this.state.selectedSectionName);
const settingComps = shared.settingsToComponents2(this, AppType.Desktop, settings, this.state.selectedSectionName);
// screenComp is a custom config screen, such as the encryption config screen or keymap config screen.
// These screens handle their own loading/saving of settings and have bespoke rendering.
@@ -708,7 +709,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
if (screenComp) containerStyle.display = 'none';
const sections = shared.settingsSections({ device: 'desktop', settings });
const sections = shared.settingsSections({ device: AppType.Desktop, settings });
const needRestartComp: any = this.state.needRestart ? (
<div style={{ ...theme.textStyle, padding: 10, paddingLeft: 24, backgroundColor: theme.warningBackgroundColor, color: theme.color }}>

View File

@@ -1,4 +1,4 @@
import { SettingSectionSource } from '@joplin/lib/models/Setting';
import { AppType, SettingSectionSource } from '@joplin/lib/models/Setting';
import * as React from 'react';
import { useMemo } from 'react';
import Setting from '@joplin/lib/models/Setting';
@@ -92,8 +92,18 @@ export default function Sidebar(props: Props) {
function renderButton(section: any) {
const selected = props.selection === section.name;
return (
<StyledListItem key={section.name} isSubSection={Setting.isSubSection(section.name)} selected={selected} onClick={() => { props.onSelectionChange({ section: section }); }}>
<StyledListItemIcon className={Setting.sectionNameToIcon(section.name)} />
<StyledListItem
key={section.name}
href='#'
role='tab'
aria-selected={selected}
isSubSection={Setting.isSubSection(section.name)}
selected={selected}
onClick={() => { props.onSelectionChange({ section: section }); }}
>
<StyledListItemIcon
className={Setting.sectionNameToIcon(section.name, AppType.Desktop)}
/>
<StyledListItemLabel>
{Setting.sectionNameToLabel(section.name)}
</StyledListItemLabel>
@@ -121,7 +131,7 @@ export default function Sidebar(props: Props) {
}
return (
<StyledRoot>
<StyledRoot role='tablist'>
{buttons}
</StyledRoot>
);

View File

@@ -1,9 +1,9 @@
import * as React from 'react';
import versionInfo from '@joplin/lib/versionInfo';
import versionInfo, { PackageInfo } from '@joplin/lib/versionInfo';
import PluginService, { Plugins } from '@joplin/lib/services/plugins/PluginService';
import Setting from '@joplin/lib/models/Setting';
import restart from '../services/restart';
const packageInfo = require('../packageInfo.js');
const packageInfo: PackageInfo = require('../packageInfo.js');
const ipcRenderer = require('electron').ipcRenderer;
interface ErrorInfo {

View File

@@ -8,7 +8,7 @@ import KeymapService from '@joplin/lib/services/KeymapService';
import { PluginStates, utils as pluginUtils } from '@joplin/lib/services/plugins/reducer';
import shim from '@joplin/lib/shim';
import Setting from '@joplin/lib/models/Setting';
import versionInfo from '@joplin/lib/versionInfo';
import versionInfo, { PackageInfo } from '@joplin/lib/versionInfo';
import makeDiscourseDebugUrl from '@joplin/lib/makeDiscourseDebugUrl';
import { ImportModule } from '@joplin/lib/services/interop/Module';
import InteropServiceHelper from '../InteropServiceHelper';
@@ -25,7 +25,7 @@ import { ProfileConfig } from '@joplin/lib/services/profileConfig/types';
import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/PluginService';
import { getListRendererById, getListRendererIds } from '@joplin/lib/services/noteList/renderers';
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
const packageInfo = require('../packageInfo.js');
const packageInfo: PackageInfo = require('../packageInfo.js');
const { clipboard } = require('electron');
const Menu = bridge().Menu;

View File

@@ -136,7 +136,11 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
editorRef.current.insertAtCursor(cmd.value.markdownTags.join('\n'));
} else if (cmd.value.type === 'files') {
const pos = cursorPositionToTextOffset(editorRef.current.getCursor(), props.content);
const newBody = await commandAttachFileToBody(props.content, cmd.value.paths, { createFileURL: !!cmd.value.createFileURL, position: pos });
const newBody = await commandAttachFileToBody(props.content, cmd.value.paths, {
createFileURL: !!cmd.value.createFileURL,
position: pos,
markupLanguage: props.contentMarkupLanguage,
});
editorRef.current.updateBody(newBody);
} else {
reg.logger().warn('CodeMirror: unsupported drop item: ', cmd);
@@ -214,7 +218,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
const cursor = editorRef.current.getCursor();
const pos = cursorPositionToTextOffset(cursor, props.content);
const newBody = await commandAttachFileToBody(props.content, null, { position: pos });
const newBody = await commandAttachFileToBody(props.content, null, { position: pos, markupLanguage: props.contentMarkupLanguage });
if (newBody) editorRef.current.updateBody(newBody);
},
textNumberedList: () => {
@@ -255,7 +259,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
},
};
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [props.content, props.visiblePanes, addListItem, wrapSelectionWithStrings, setEditorPercentScroll, setViewerPercentScroll, resetScroll]);
}, [props.content, props.visiblePanes, props.contentMarkupLanguage, addListItem, wrapSelectionWithStrings, setEditorPercentScroll, setViewerPercentScroll, resetScroll]);
const onEditorPaste = useCallback(async (event: any = null) => {
const resourceMds = await getResourcesFromPasteEvent(event);

View File

@@ -151,6 +151,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
editorCopyText, editorCutText, editorPaste,
editorContent: props.content,
visiblePanes: props.visiblePanes,
contentMarkupLanguage: props.contentMarkupLanguage,
});
useImperativeHandle(ref, () => {

View File

@@ -7,6 +7,7 @@ import dialogs from '../../../../dialogs';
import { EditorCommandType } from '@joplin/editor/types';
import Logger from '@joplin/utils/Logger';
import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
import { MarkupLanguage } from '@joplin/renderer';
const logger = Logger.create('CodeMirror 6 commands');
@@ -40,6 +41,7 @@ interface Props {
selectionRange: { from: number; to: number };
visiblePanes: string[];
contentMarkupLanguage: MarkupLanguage;
}
const useEditorCommands = (props: Props) => {
@@ -57,7 +59,7 @@ const useEditorCommands = (props: Props) => {
editorRef.current.insertText(cmd.markdownTags.join('\n'));
} else if (cmd.type === 'files') {
const pos = props.selectionRange.from;
const newBody = await commandAttachFileToBody(props.editorContent, cmd.paths, { createFileURL: !!cmd.createFileURL, position: pos });
const newBody = await commandAttachFileToBody(props.editorContent, cmd.paths, { createFileURL: !!cmd.createFileURL, position: pos, markupLanguage: props.contentMarkupLanguage });
editorRef.current.updateBody(newBody);
} else {
logger.warn('CodeMirror: unsupported drop item: ', cmd);
@@ -92,7 +94,7 @@ const useEditorCommands = (props: Props) => {
insertText: (value: any) => editorRef.current.insertText(value),
attachFile: async () => {
const newBody = await commandAttachFileToBody(
props.editorContent, null, { position: props.selectionRange.from },
props.editorContent, null, { position: props.selectionRange.from, markupLanguage: props.contentMarkupLanguage },
);
if (newBody) {
editorRef.current.updateBody(newBody);
@@ -129,7 +131,7 @@ const useEditorCommands = (props: Props) => {
}, [
props.visiblePanes, props.editorContent, props.editorCopyText, props.editorCutText, props.editorPaste,
props.selectionRange,
props.contentMarkupLanguage,
props.webviewRef, editorRef,
]);
};

View File

@@ -203,7 +203,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
let commandProcessed = true;
if (cmd.name === 'insertText') {
const result = await markupToHtml.current(MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN, cmd.value, { bodyOnly: true });
const result = await markupToHtml.current(MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN, cmd.value, markupRenderOptions({ bodyOnly: true }));
editor.insertContent(result.html);
} else if (cmd.name === 'editor.focus') {
editor.focus();
@@ -559,11 +559,21 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
const toolbarPluginButtons = pluginCommandNames.length ? ` | ${pluginCommandNames.join(' ')}` : '';
// The toolbar is going to wrap based on groups of buttons
// (delimited by |). It means that if we leave large groups of
// buttons towards the end of the toolbar it's going to needlessly
// hide many buttons even when there is space. So this is why below,
// we create small groups of just one button towards the end.
const toolbar = [
'bold', 'italic', 'joplinHighlight', 'joplinStrikethrough', 'formattingExtras', '|',
'link', 'joplinInlineCode', 'joplinCodeBlock', 'joplinAttach', '|',
'bullist', 'numlist', 'joplinChecklist', '|',
'h1', 'h2', 'h3', 'hr', 'blockquote', 'table', `joplinInsertDateTime${toolbarPluginButtons}`,
'h1', 'h2', 'h3', '|',
'hr', '|',
'blockquote', '|',
'table', '|',
`joplinInsertDateTime${toolbarPluginButtons}`,
];
const editors = await (window as any).tinymce.init({

View File

@@ -48,6 +48,7 @@ import ItemChange from '@joplin/lib/models/ItemChange';
import PlainEditor from './NoteBody/PlainEditor/PlainEditor';
import CodeMirror6 from './NoteBody/CodeMirror/v6/CodeMirror';
import CodeMirror5 from './NoteBody/CodeMirror/v5/CodeMirror';
import { namespacedKey } from '@joplin/lib/services/plugins/api/JoplinSettings';
const commands = [
require('./commands/showRevisions'),
@@ -159,10 +160,15 @@ function NoteEditor(props: NoteEditorProps) {
return formNote.saveActionQueue.waitForAllDone();
}
const settingValue = useCallback((pluginId: string, key: string) => {
return Setting.value(namespacedKey(pluginId, key));
}, []);
const markupToHtml = useMarkupToHtml({
themeId: props.themeId,
customCss: props.customCss,
plugins: props.plugins,
settingValue,
});
const allAssets = useCallback(async (markupLanguage: number, options: AllAssetsOptions = null): Promise<any[]> => {

View File

@@ -0,0 +1,64 @@
import WhenClause from '@joplin/lib/services/WhenClause';
import { enabledCondition } from './editorCommandDeclarations';
const baseContext: Record<string, any> = {
modalDialogVisible: false,
gotoAnythingVisible: false,
markdownEditorPaneVisible: true,
oneNoteSelected: true,
noteIsMarkdown: true,
noteIsReadOnly: false,
richTextEditorVisible: false,
};
describe('editorCommandDeclarations', () => {
test.each([
[
{},
true,
],
[
{
markdownEditorPaneVisible: false,
},
false,
],
[
{
noteIsReadOnly: true,
},
false,
],
[
// In the Markdown editor, but only the viewer is visible
{
markdownEditorPaneVisible: false,
richTextEditorVisible: false,
},
false,
],
[
// In the Markdown editor, and the viewer is visible
{
markdownEditorPaneVisible: true,
richTextEditorVisible: false,
},
true,
],
[
// In the RT editor
{
markdownEditorPaneVisible: false,
richTextEditorVisible: true,
},
true,
],
])('should create the enabledCondition', (context: Record<string, any>, expected: boolean) => {
const condition = enabledCondition('textBold');
const wc = new WhenClause(condition);
const actual = wc.evaluate({ ...baseContext, ...context });
expect(actual).toBe(expected);
});
});

View File

@@ -2,9 +2,24 @@ import { CommandDeclaration } from '@joplin/lib/services/CommandService';
import { _ } from '@joplin/lib/locale';
import { joplinCommandToTinyMceCommands } from './NoteBody/TinyMCE/utils/joplinCommandToTinyMceCommands';
const workWithHtmlNotes = [
'attachFile',
];
export const enabledCondition = (commandName: string) => {
const markdownEditorOnly = !Object.keys(joplinCommandToTinyMceCommands).includes(commandName);
return `(!modalDialogVisible || gotoAnythingVisible) ${markdownEditorOnly ? '&& markdownEditorPaneVisible' : ''} && oneNoteSelected && noteIsMarkdown && !noteIsReadOnly`;
const noteMustBeMarkdown = !workWithHtmlNotes.includes(commandName);
const output = [
'!modalDialogVisible',
'!gotoAnythingVisible',
markdownEditorOnly ? 'markdownEditorPaneVisible' : '(markdownEditorPaneVisible || richTextEditorVisible)',
'oneNoteSelected',
noteMustBeMarkdown ? 'noteIsMarkdown' : '',
'!noteIsReadOnly',
];
return output.filter(c => !!c).join(' && ');
};
const declarations: CommandDeclaration[] = [

View File

@@ -9,7 +9,10 @@ export async function htmlToMarkdown(markupLanguage: number, html: string, origi
if (markupLanguage === MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN) {
const htmlToMd = new HtmlToMd();
newBody = htmlToMd.parse(html, { preserveImageTagsWithSize: true });
newBody = htmlToMd.parse(html, {
preserveImageTagsWithSize: true,
preserveNestedTables: true,
});
newBody = await Note.replaceResourceExternalToInternalLinks(newBody, { useAbsolutePaths: true });
} else {
newBody = await Note.replaceResourceExternalToInternalLinks(html, { useAbsolutePaths: true });

View File

@@ -9,6 +9,7 @@ import htmlUtils from '@joplin/lib/htmlUtils';
import rendererHtmlUtils, { extractHtmlBody } from '@joplin/renderer/htmlUtils';
import Logger from '@joplin/utils/Logger';
import { fileUriToPath } from '@joplin/utils/url';
import { MarkupLanguage } from '@joplin/renderer';
const joplinRendererUtils = require('@joplin/renderer').utils;
const { clipboard } = require('electron');
const mimeUtils = require('@joplin/lib/mime-utils.js').mime;
@@ -62,6 +63,7 @@ export async function commandAttachFileToBody(body: string, filePaths: string[]
options = {
createFileURL: false,
position: 0,
markupLanguage: MarkupLanguage.Markdown,
...options,
};
@@ -79,6 +81,7 @@ export async function commandAttachFileToBody(body: string, filePaths: string[]
const newBody = await shim.attachFileToNoteBody(body, filePath, options.position, {
createFileURL: options.createFileURL,
resizeLargeImages: Setting.value('imageResizing'),
markupLanguage: options.markupLanguage,
});
if (!newBody) {

View File

@@ -12,6 +12,7 @@ interface HookDependencies {
themeId: number;
customCss: string;
plugins: PluginStates;
settingValue: (pluginId: string, key: string)=> any;
}
export interface MarkupToHtmlOptions {
@@ -59,12 +60,16 @@ export default function useMarkupToHtml(deps: HookDependencies) {
delete options.replaceResourceInternalToExternalLinks;
const result = await markupToHtml.render(markupLanguage, md, theme, { codeTheme: theme.codeThemeCss,
const result = await markupToHtml.render(markupLanguage, md, theme, {
codeTheme: theme.codeThemeCss,
resources: resources,
postMessageSyntax: 'ipcProxySendToHost',
splitted: true,
externalAssetsOnly: true,
codeHighlightCacheKey: 'useMarkupToHtml', ...options });
codeHighlightCacheKey: 'useMarkupToHtml',
settingValue: deps.settingValue,
...options,
});
return result;
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied

View File

@@ -145,6 +145,7 @@ const NoteListItem = (props: NoteItemProps, ref: LegacyRef<HTMLDivElement>) => {
tabIndex={0}
className={className}
data-id={noteId}
style={{ height: props.itemSize.height }}
onContextMenu={props.onContextMenu}
onDragStart={props.onDragStart}
onDragOver={props.onDragOver}

View File

@@ -2,18 +2,18 @@ import * as React from 'react';
import { useState, useEffect } from 'react';
import ButtonBar from '../ConfigScreen/ButtonBar';
import { _ } from '@joplin/lib/locale';
const { connect } = require('react-redux');
import Setting from '@joplin/lib/models/Setting';
const { themeStyle } = require('@joplin/lib/theme');
import ReportService from '@joplin/lib/services/ReportService';
import { themeStyle } from '@joplin/lib/theme';
import ReportService, { ReportItem, ReportSection, RetryAllHandler } from '@joplin/lib/services/ReportService';
import Button, { ButtonLevel } from '../Button/Button';
import bridge from '../../services/bridge';
const fs = require('fs-extra');
import styled from 'styled-components';
import { AppState } from '../../app.reducer';
import { writeFileSync } from 'fs';
interface Props {
themeId: string;
themeId: number;
style: any;
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
dispatch: Function;
@@ -34,12 +34,12 @@ async function exportDebugReportClick() {
if (!filePath) return;
const service = new ReportService();
const csv = await service.basicItemList({ format: 'csv' });
await fs.writeFileSync(filePath, csv);
const csv = (await service.basicItemList({ format: 'csv' })) as string;
await writeFileSync(filePath, csv);
}
function StatusScreen(props: Props) {
const [report, setReport] = useState<any[]>([]);
const [report, setReport] = useState<ReportSection[]>([]);
async function resfreshScreen() {
const service = new ReportService();
@@ -65,7 +65,7 @@ function StatusScreen(props: Props) {
const containerStyle = { ...theme.containerStyle, padding: containerPadding,
flex: 1 };
function renderSectionTitleHtml(key: string, title: string) {
function renderSectionTitle(key: string, title: string) {
return (
<h2 key={`section_${key}`} style={theme.h2Style}>
{title}
@@ -73,7 +73,7 @@ function StatusScreen(props: Props) {
);
}
function renderSectionRetryAllHtml(key: string, retryAllHandler: any) {
function renderSectionRetryAll(key: string, retryAllHandler: RetryAllHandler) {
return (
<a key={`retry_all_${key}`} href="#" onClick={retryAllHandler} style={retryAllStyle}>
{_('Retry All')}
@@ -81,13 +81,26 @@ function StatusScreen(props: Props) {
);
}
const renderSectionHtml = (key: string, section: any) => {
const itemsHtml = [];
const renderRetryAll = (section: ReportSection) => {
const items: React.JSX.Element[] = [];
if (section.canRetryAll) {
items.push(renderSectionRetryAll(section.title, async () => {
await section.retryAllHandler();
void resfreshScreen();
}));
}
return items;
};
itemsHtml.push(renderSectionTitleHtml(section.title, section.title));
const renderSection = (key: string, section: ReportSection) => {
let items = [];
items.push(renderSectionTitle(section.title, section.title));
items = items.concat(renderRetryAll(section));
let currentListKey = '';
let listItems: any[] = [];
let listItems: React.JSX.Element[] = [];
for (const n in section.body) {
if (!section.body.hasOwnProperty(n)) continue;
const item = section.body[n];
@@ -115,12 +128,12 @@ function StatusScreen(props: Props) {
}
if (itemType === 'openList') {
currentListKey = item.key;
currentListKey = (item as ReportItem).key;
continue;
}
if (itemType === 'closeList') {
itemsHtml.push(<ul key={currentListKey}>{listItems}</ul>);
items.push(<ul key={currentListKey}>{listItems}</ul>);
currentListKey = '';
listItems = [];
continue;
@@ -136,7 +149,7 @@ function StatusScreen(props: Props) {
</li>,
);
} else {
itemsHtml.push(
items.push(
<div style={theme.textStyle} key={`item_${n}`}>
<span>{text}</span>
{retryLink}
@@ -145,26 +158,21 @@ function StatusScreen(props: Props) {
}
}
if (section.canRetryAll) {
itemsHtml.push(renderSectionRetryAllHtml(section.title, async () => {
await section.retryAllHandler();
void resfreshScreen();
}));
}
items = items.concat(renderRetryAll(section));
return <div key={key}>{itemsHtml}</div>;
return <div key={key}>{items}</div>;
};
function renderBodyHtml(report: any) {
const sectionsHtml = [];
function renderBody(report: ReportSection[]) {
const sections = [];
for (let i = 0; i < report.length; i++) {
const section = report[i];
if (!section.body.length) continue;
sectionsHtml.push(renderSectionHtml(`${i}`, section));
sections.push(renderSection(`${i}`, section));
}
return <div>{sectionsHtml}</div>;
return <div>{sections}</div>;
}
function renderTools() {
@@ -180,7 +188,7 @@ function StatusScreen(props: Props) {
);
}
const body = renderBodyHtml(report);
const body = renderBody(report);
return (
<div style={style}>
@@ -195,7 +203,7 @@ function StatusScreen(props: Props) {
);
}
const mapStateToProps = (state: any) => {
const mapStateToProps = (state: AppState) => {
return {
themeId: state.settings.theme,
settings: state.settings,

View File

@@ -2,6 +2,11 @@ import { test, expect } from './util/test';
import MainScreen from './models/MainScreen';
import activateMainMenuItem from './util/activateMainMenuItem';
import SettingsScreen from './models/SettingsScreen';
import { _electron as electron } from '@playwright/test';
import { writeFile } from 'fs-extra';
import { join } from 'path';
import createStartupArgs from './util/createStartupArgs';
import firstNonDevToolsWindow from './util/firstNonDevToolsWindow';
test.describe('main', () => {
@@ -121,4 +126,21 @@ test.describe('main', () => {
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');
// We need to write to the force-safe-mode file before opening the Electron app.
// Open the app ourselves:
const startupArgs = createStartupArgs(profileDirectory);
const electronApp = await electron.launch({ args: startupArgs });
const mainWindow = await firstNonDevToolsWindow(electronApp);
const safeModeDisableLink = mainWindow.getByText('Disable safe mode and restart');
await safeModeDisableLink.waitFor();
await expect(safeModeDisableLink).toBeInViewport();
await electronApp.close();
});
});

View File

@@ -0,0 +1,9 @@
const createStartupArgs = (profileDirectory: string) => {
// We need to run with --env dev to disable the single instance check.
return [
'main.js', '--env', 'dev', '--profile', profileDirectory,
];
};
export default createStartupArgs;

View File

@@ -0,0 +1,43 @@
import { ElectronApplication, Page } from '@playwright/test';
const isDevTools = async (page: Page) => {
// It seems that the developer tools window can have titles in different
// formats (e.g. DevTools, Developer Tools).
return (await page.title()).match(/Dev(eloper)?\s*Tools/i);
};
const firstNonDevToolsWindow = async (electronApp: ElectronApplication) => {
// Wait for the window event as soon as possible -- it's possible that
// the window we want will be shown while doing other async checks.
const nextNonDevToolsPage = electronApp.waitForEvent('window', {
predicate: async page => {
return !(await isDevTools(page));
},
});
// First use firstWindow -- it's possible that the first window
// has already been shown.
let mainWindow = await electronApp.firstWindow();
if (await isDevTools(mainWindow)) {
for (const window of electronApp.windows()) {
if (!(await isDevTools(window))) {
mainWindow = window;
break;
}
}
if (await isDevTools(mainWindow)) {
mainWindow = await nextNonDevToolsPage;
}
}
// waitForEvent will throw if no additional windows are created.
// Ignore.
// eslint-disable-next-line promise/prefer-await-to-then
nextNonDevToolsPage.catch(_error => {});
return mainWindow;
};
export default firstNonDevToolsWindow;

View File

@@ -2,10 +2,13 @@ import { resolve, join, dirname } from 'path';
import { remove, mkdirp } from 'fs-extra';
import { _electron as electron, Page, ElectronApplication, test as base } from '@playwright/test';
import uuid from '@joplin/lib/uuid';
import createStartupArgs from './createStartupArgs';
import firstNonDevToolsWindow from './firstNonDevToolsWindow';
type JoplinFixtures = {
profileDirectory: string;
electronApp: ElectronApplication;
mainWindow: Page;
};
@@ -20,24 +23,29 @@ export const test = base.extend<JoplinFixtures>({
// See https://github.com/microsoft/playwright/issues/8798
//
// eslint-disable-next-line no-empty-pattern
electronApp: async ({ }, use) => {
profileDirectory: async ({ }, use) => {
const profilePath = resolve(join(dirname(__dirname), 'test-profile'));
const profileSubdir = join(profilePath, uuid.createNano());
await mkdirp(profileSubdir);
const startupArgs = ['main.js', '--env', 'dev', '--profile', profileSubdir];
await use(profileSubdir);
await remove(profileSubdir);
},
electronApp: async ({ profileDirectory }, use) => {
const startupArgs = createStartupArgs(profileDirectory);
const electronApp = await electron.launch({ args: startupArgs });
await use(electronApp);
await electronApp.firstWindow();
await electronApp.close();
await remove(profileSubdir);
},
mainWindow: async ({ electronApp }, use) => {
const window = await electronApp.firstWindow();
await use(window);
const mainWindow = await firstNonDevToolsWindow(electronApp);
await use(mainWindow);
},
});

View File

@@ -31,117 +31,122 @@ const React = require('react');
const nodeSqlite = require('sqlite3');
const initLib = require('@joplin/lib/initLib').default;
if (bridge().env() === 'dev') {
const newConsole = function(oldConsole) {
const output = {};
const fnNames = ['assert', 'clear', 'context', 'count', 'countReset', 'debug', 'dir', 'dirxml', 'error', 'group', 'groupCollapsed', 'groupEnd', 'info', 'log', 'memory', 'profile', 'profileEnd', 'table', 'time', 'timeEnd', 'timeLog', 'timeStamp', 'trace', 'warn'];
for (const fnName of fnNames) {
if (fnName === 'warn') {
output.warn = function(...text) {
const s = [...text].join('');
// React spams the console with walls of warnings even outside of strict mode, and even after having renamed
// unsafe methods to UNSAFE_xxxx, so we need to hack the console to remove them...
if (s.indexOf('Warning: componentWillReceiveProps has been renamed, and is not recommended for use') === 0) return;
if (s.indexOf('Warning: componentWillUpdate has been renamed, and is not recommended for use.') === 0) return;
oldConsole.warn(...text);
};
} else {
output[fnName] = function(...text) {
return oldConsole[fnName](...text);
};
const main = async () => {
if (bridge().env() === 'dev') {
const newConsole = function(oldConsole) {
const output = {};
const fnNames = ['assert', 'clear', 'context', 'count', 'countReset', 'debug', 'dir', 'dirxml', 'error', 'group', 'groupCollapsed', 'groupEnd', 'info', 'log', 'memory', 'profile', 'profileEnd', 'table', 'time', 'timeEnd', 'timeLog', 'timeStamp', 'trace', 'warn'];
for (const fnName of fnNames) {
if (fnName === 'warn') {
output.warn = function(...text) {
const s = [...text].join('');
// React spams the console with walls of warnings even outside of strict mode, and even after having renamed
// unsafe methods to UNSAFE_xxxx, so we need to hack the console to remove them...
if (s.indexOf('Warning: componentWillReceiveProps has been renamed, and is not recommended for use') === 0) return;
if (s.indexOf('Warning: componentWillUpdate has been renamed, and is not recommended for use.') === 0) return;
oldConsole.warn(...text);
};
} else {
output[fnName] = function(...text) {
return oldConsole[fnName](...text);
};
}
}
}
return output;
}(window.console);
return output;
}(window.console);
window.console = newConsole;
}
window.console = newConsole;
}
// eslint-disable-next-line no-console
console.info(`Environment: ${bridge().env()}`);
// eslint-disable-next-line no-console
console.info(`Environment: ${bridge().env()}`);
const fsDriver = new FsDriverNode();
Logger.fsDriver_ = fsDriver;
Resource.fsDriver_ = fsDriver;
EncryptionService.fsDriver_ = fsDriver;
FileApiDriverLocal.fsDriver_ = fsDriver;
const fsDriver = new FsDriverNode();
Logger.fsDriver_ = fsDriver;
Resource.fsDriver_ = fsDriver;
EncryptionService.fsDriver_ = fsDriver;
FileApiDriverLocal.fsDriver_ = fsDriver;
// That's not good, but it's to avoid circular dependency issues
// in the BaseItem class.
BaseItem.loadClass('Note', Note);
BaseItem.loadClass('Folder', Folder);
BaseItem.loadClass('Resource', Resource);
BaseItem.loadClass('Tag', Tag);
BaseItem.loadClass('NoteTag', NoteTag);
BaseItem.loadClass('MasterKey', MasterKey);
BaseItem.loadClass('Revision', Revision);
// That's not good, but it's to avoid circular dependency issues
// in the BaseItem class.
BaseItem.loadClass('Note', Note);
BaseItem.loadClass('Folder', Folder);
BaseItem.loadClass('Resource', Resource);
BaseItem.loadClass('Tag', Tag);
BaseItem.loadClass('NoteTag', NoteTag);
BaseItem.loadClass('MasterKey', MasterKey);
BaseItem.loadClass('Revision', Revision);
Setting.setConstant('appId', `net.cozic.joplin${bridge().env() === 'dev' ? 'dev' : ''}-desktop`);
Setting.setConstant('appType', 'desktop');
Setting.setConstant('appId', `net.cozic.joplin${bridge().env() === 'dev' ? 'dev' : ''}-desktop`);
Setting.setConstant('appType', 'desktop');
// eslint-disable-next-line no-console
console.info(`appId: ${Setting.value('appId')}`);
// eslint-disable-next-line no-console
console.info(`appType: ${Setting.value('appType')}`);
// eslint-disable-next-line no-console
console.info(`appId: ${Setting.value('appId')}`);
// eslint-disable-next-line no-console
console.info(`appType: ${Setting.value('appType')}`);
let keytar;
try {
keytar = shim.platformSupportsKeyChain() ? require('keytar') : null;
} catch (error) {
console.error('Cannot load keytar - keychain support will be disabled', error);
keytar = null;
}
let keytar;
try {
keytar = shim.platformSupportsKeyChain() ? require('keytar') : null;
} catch (error) {
console.error('Cannot load keytar - keychain support will be disabled', error);
keytar = null;
}
function appVersion() {
const p = require('./packageInfo.js');
return p.version;
}
function appVersion() {
const p = require('./packageInfo.js');
return p.version;
}
shimInit({
keytar,
React,
appVersion,
electronBridge: bridge(),
nodeSqlite,
});
shimInit({
keytar,
React,
appVersion,
electronBridge: bridge(),
nodeSqlite,
});
// Disable drag and drop of links inside application (which would
// open it as if the whole app was a browser)
document.addEventListener('dragover', event => event.preventDefault());
document.addEventListener('drop', event => event.preventDefault());
// Disable drag and drop of links inside application (which would
// open it as if the whole app was a browser)
document.addEventListener('dragover', event => event.preventDefault());
document.addEventListener('drop', event => event.preventDefault());
// Disable middle-click (which would open a new browser window, but we don't want this)
document.addEventListener('auxclick', event => event.preventDefault());
// Disable middle-click (which would open a new browser window, but we don't want this)
document.addEventListener('auxclick', event => event.preventDefault());
// Each link (rendered as a button or list item) has its own custom click event
// so disable the default. In particular this will disable Ctrl+Clicking a link
// which would open a new browser window.
document.addEventListener('click', (event) => {
// We don't apply this to labels and inputs because it would break
// checkboxes. Such a global event handler is probably not a good idea
// anyway but keeping it for now, as it doesn't seem to break anything else.
// https://github.com/facebook/react/issues/13477#issuecomment-489274045
if (['LABEL', 'INPUT'].includes(event.target.nodeName)) return;
// Each link (rendered as a button or list item) has its own custom click event
// so disable the default. In particular this will disable Ctrl+Clicking a link
// which would open a new browser window.
document.addEventListener('click', (event) => {
// We don't apply this to labels and inputs because it would break
// checkboxes. Such a global event handler is probably not a good idea
// anyway but keeping it for now, as it doesn't seem to break anything else.
// https://github.com/facebook/react/issues/13477#issuecomment-489274045
if (['LABEL', 'INPUT'].includes(event.target.nodeName)) return;
event.preventDefault();
});
event.preventDefault();
});
const logger = new Logger();
Logger.initializeGlobalLogger(logger);
initLib(logger);
const logger = new Logger();
Logger.initializeGlobalLogger(logger);
initLib(logger);
app().start(bridge().processArgv()).then((result) => {
if (!result || !result.action) {
const startResult = await app().start(bridge().processArgv());
if (!startResult || !startResult.action) {
require('./gui/Root');
} else if (result.action === 'upgradeSyncTarget') {
} else if (startResult.action === 'upgradeSyncTarget') {
require('./gui/Root_UpgradeSyncTarget');
}
}).catch((error) => {
const env = bridge().env();
};
main().catch((error) => {
const env = bridge().env();
console.error(error);
let errorMessage;
if (error.code === 'flagError') {
bridge().showErrorMessageBox(error.message);
errorMessage = error.message;
} else {
// If something goes wrong at this stage we don't have a console or a log file
// so display the error in a message box.
@@ -150,13 +155,12 @@ app().start(bridge().processArgv()).then((result) => {
if (error.lineNumber) msg.push(error.lineNumber);
if (error.stack) msg.push(error.stack);
if (env === 'dev') {
console.error(error);
} else {
bridge().showErrorMessageBox(msg.join('\n\n'));
}
errorMessage = msg.join('\n\n');
}
// In dev, we leave the app open as debug statements in the console can be useful
if (env !== 'dev') bridge().electronApp().exit(1);
// In dev, we give the option to leave the app open as debug statements in the
// console can be useful
const canIgnore = env === 'dev';
bridge().electronApp().handleAppFailure(errorMessage, canIgnore);
});

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
"version": "2.13.3",
"version": "2.13.7",
"description": "Joplin for Desktop",
"main": "main.js",
"private": true,
@@ -119,21 +119,21 @@
"@joplin/tools": "~2.13",
"@playwright/test": "1.38.1",
"@testing-library/react-hooks": "8.0.1",
"@types/jest": "29.5.4",
"@types/node": "18.17.19",
"@types/react": "18.2.31",
"@types/react-redux": "7.1.27",
"@types/styled-components": "5.1.28",
"electron": "25.9.0",
"electron-builder": "24.4.0",
"@types/jest": "29.5.5",
"@types/node": "18.18.7",
"@types/react": "18.2.33",
"@types/react-redux": "7.1.28",
"@types/styled-components": "5.1.29",
"electron": "26.5.0",
"electron-builder": "24.6.4",
"glob": "10.3.10",
"gulp": "4.0.2",
"jest": "29.6.4",
"jest-environment-jsdom": "29.6.4",
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"js-sha512": "0.8.0",
"nan": "2.18.0",
"react-test-renderer": "18.2.0",
"typescript": "5.1.6"
"typescript": "5.2.2"
},
"optionalDependencies": {
"7zip-bin-linux": "^1.0.1",
@@ -142,14 +142,14 @@
},
"dependencies": {
"@electron/notarize": "2.1.0",
"@electron/remote": "2.0.11",
"@electron/remote": "2.0.12",
"@fortawesome/fontawesome-free": "5.15.4",
"@joeattardi/emoji-button": "4.6.4",
"@joplin/editor": "~2.13",
"@joplin/lib": "~2.13",
"@joplin/renderer": "~2.13",
"@joplin/utils": "~2.13",
"@types/mustache": "4.2.3",
"@types/mustache": "4.2.4",
"async-mutex": "0.4.0",
"codemirror": "5.65.9",
"color": "3.2.1",

View File

@@ -57,9 +57,12 @@ export default class PlatformImplementation extends BasePlatformImplementation {
this.joplin_ = {
views: {
dialogs: {
showMessageBox: async function(message: string) {
showMessageBox: async (message: string) => {
return bridge().showMessageBox(message);
},
showOpenDialog: async (options) => {
return bridge().showOpenDialog(options);
},
},
},
};

View File

@@ -32,7 +32,7 @@ export default function(frameWindow: any, isReady: boolean, postMessage: Functio
frameWindow.addEventListener('message', onMessage);
return () => {
frameWindow.removeEventListener('message', onMessage);
if (frameWindow.removeEventListener) frameWindow.removeEventListener('message', onMessage);
};
}, [frameWindow, htmlHash]);

View File

@@ -5,8 +5,8 @@
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
TEMP_PATH=~/src/plugin-tests
NEED_COMPILING=0
PLUGIN_PATH=~/src/joplin/packages/app-cli/tests/support/plugins/simple
NEED_COMPILING=1
PLUGIN_PATH=~/src/plugin-abc
if [[ $NEED_COMPILING == 1 ]]; then
mkdir -p "$TEMP_PATH"

View File

@@ -0,0 +1,45 @@
let currentProfileDirectory: string;
jest.doMock('../bridge', () => ({
// Mock the bridge functions used by restartInSafeModeFromMain
// to remove the dependency on Electron.
default: () => ({
restart: jest.fn(),
processArgv: () => [
// The argument parser expects the first two arguments to
// be the path to NodeJS and the second to be the main filename.
process.argv[0], __filename,
// Only the following arguments are used.
'--profile', currentProfileDirectory,
],
env: () => 'dev',
}),
}));
import { mkdtemp, readFile, remove } from 'fs-extra';
import restartInSafeModeFromMain from './restartInSafeModeFromMain';
import { tmpdir } from 'os';
import { join } from 'path';
import { safeModeFlagFilename } from '@joplin/lib/BaseApplication';
describe('restartInSafeModeFromMain', () => {
beforeEach(async () => {
currentProfileDirectory = await mkdtemp(join(tmpdir(), 'safemode-restart-test'));
});
afterEach(async () => {
await remove(currentProfileDirectory);
});
test('should create a safe mode flag file', async () => {
await restartInSafeModeFromMain();
const safeModeFlagFilepath = join(
currentProfileDirectory, safeModeFlagFilename,
);
expect(await readFile(safeModeFlagFilepath, 'utf8')).toBe('true');
});
});

View File

@@ -0,0 +1,33 @@
import Setting from '@joplin/lib/models/Setting';
import bridge from '../bridge';
import processStartFlags from '@joplin/lib/utils/processStartFlags';
import BaseApplication, { safeModeFlagFilename } from '@joplin/lib/BaseApplication';
import initProfile from '@joplin/lib/services/profileConfig/initProfile';
import { writeFile } from 'fs-extra';
import { join } from 'path';
const restartInSafeModeFromMain = async () => {
// Only set constants here -- the main process doesn't have easy access (without loading
// a large amount of other code) to the database.
const appName = `joplin${bridge().env() === 'dev' ? 'dev' : ''}-desktop`;
Setting.setConstant('appId', `net.cozic.${appName}`);
Setting.setConstant('appType', 'desktop');
Setting.setConstant('appName', appName);
// Load just enough for us to write a file in the profile directory
const { shimInit } = require('@joplin/lib/shim-init-node.js');
shimInit({});
const startFlags = await processStartFlags(bridge().processArgv());
const rootProfileDir = BaseApplication.determineProfileDir(startFlags.matched);
const { profileDir } = await initProfile(rootProfileDir);
// We can't access the database, so write to a file instead.
const safeModeFlagFile = join(profileDir, safeModeFlagFilename);
await writeFile(safeModeFlagFile, 'true', 'utf8');
bridge().restart();
};
export default restartInSafeModeFromMain;

View File

@@ -110,8 +110,8 @@ android {
applicationId "net.cozic.joplin"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 2097725
versionName "2.13.5"
versionCode 2097730
versionName "2.13.10"
ndk {
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
}

View File

@@ -1,10 +1,12 @@
const React = require('react');
import { useState, useCallback, useMemo } from 'react';
const Icon = require('react-native-vector-icons/Ionicons').default;
import { FAB, Portal } from 'react-native-paper';
import { _ } from '@joplin/lib/locale';
import { Dispatch } from 'redux';
const Icon = require('react-native-vector-icons/Ionicons').default;
// eslint-disable-next-line no-undef -- Don't know why it says React is undefined when it's defined above
type FABGroupProps = React.ComponentProps<typeof FAB.Group>;
type OnButtonPress = ()=> void;
interface ButtonSpec {
@@ -19,6 +21,7 @@ interface ActionButtonProps {
// If not given, an "add" button will be used.
mainButton?: ButtonSpec;
dispatch: Dispatch;
}
const defaultOnPress = () => {};
@@ -36,10 +39,12 @@ const useIcon = (iconName: string) => {
const ActionButton = (props: ActionButtonProps) => {
const [open, setOpen] = useState(false);
const onMenuToggled = useCallback(
(state: { open: boolean }) => setOpen(state.open)
, [setOpen]);
const onMenuToggled: FABGroupProps['onStateChange'] = useCallback(state => {
props.dispatch({
type: 'SIDE_MENU_CLOSE',
});
setOpen(state.open);
}, [setOpen, props.dispatch]);
const actions = useMemo(() => (props.buttons ?? []).map(button => {
return {

View File

@@ -178,6 +178,7 @@ class Dropdown extends Component<DropdownProps, DropdownState> {
onRequestClose={() => {
closeList();
}}
supportedOrientations={['landscape', 'portrait']}
>
<TouchableWithoutFeedback
accessibilityElementsHidden={true}

View File

@@ -48,6 +48,8 @@ interface Props {
// See react-native-webview's prop with the same name.
mixedContentMode?: 'never' | 'always';
allowFileAccessFromJs?: boolean;
// Initial javascript. Must evaluate to true.
injectedJavaScript: string;
@@ -143,6 +145,7 @@ const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
originWhitelist={['file://*', './*', 'http://*', 'https://*']}
mixedContentMode={props.mixedContentMode}
allowFileAccess={true}
allowFileAccessFromFileURLs={props.allowFileAccessFromJs}
injectedJavaScript={props.injectedJavaScript}
onMessage={props.onMessage}
onError={props.onError}

View File

@@ -0,0 +1,44 @@
import * as React from 'react';
import { TextStyle } from 'react-native';
const FontAwesomeIcon = require('react-native-vector-icons/FontAwesome5').default;
interface Props {
name: string;
style: TextStyle;
// If `null` is given, the content must be labeled elsewhere.
accessibilityLabel: string|null;
}
const Icon: React.FC<Props> = props => {
// Matches:
// 1. A prefix of word characters (\w+)
// 2. A suffix of non-spaces (\S+)
// An "fa-" at the beginning of the suffix is ignored.
const nameMatch = props.name.match(/^(\w+)\s+(?:fa-)?(\S+)$/);
const namePrefix = nameMatch ? nameMatch[1] : '';
const nameSuffix = nameMatch ? nameMatch[2] : props.name;
// If there's no label, make sure that the screen reader doesn't try
// to read the characters from the icon font (they don't make sense
// without the icon font applied).
const accessibilityHidden = props.accessibilityLabel === null;
return (
<FontAwesomeIcon
brand={namePrefix.startsWith('fab')}
solid={namePrefix.startsWith('fas')}
accessibilityLabel={props.accessibilityLabel}
aria-hidden={accessibilityHidden}
importantForAccessibility={
accessibilityHidden ? 'no-hide-descendants' : 'yes'
}
name={nameSuffix}
style={props.style}
/>
);
};
export default Icon;

View File

@@ -1,14 +1,14 @@
import { useRef, useCallback } from 'react';
import useSource from './hooks/useSource';
import useOnMessage, { HandleMessageCallback, OnMarkForDownloadCallback } from './hooks/useOnMessage';
import useOnMessage, { HandleMessageCallback, HandleScrollCallback, OnMarkForDownloadCallback } from './hooks/useOnMessage';
import useOnResourceLongPress from './hooks/useOnResourceLongPress';
const React = require('react');
import { View } from 'react-native';
import BackButtonDialogBox from '../BackButtonDialogBox';
import { reg } from '@joplin/lib/registry';
import ExtendedWebView from '../ExtendedWebView';
import ExtendedWebView, { WebViewControl } from '../ExtendedWebView';
interface Props {
themeId: number;
@@ -18,11 +18,13 @@ interface Props {
highlightedKeywords: string[];
noteResources: any;
paddingBottom: number;
initialScroll: number|null;
noteHash: string;
onJoplinLinkClick: HandleMessageCallback;
onCheckboxChange?: HandleMessageCallback;
onRequestEditResource?: HandleMessageCallback;
onMarkForDownload?: OnMarkForDownloadCallback;
onScroll: HandleScrollCallback;
onLoadEnd?: ()=> void;
}
@@ -32,6 +34,7 @@ const webViewStyle = {
export default function NoteBodyViewer(props: Props) {
const dialogBoxRef = useRef(null);
const webviewRef = useRef<WebViewControl>(null);
const { html, injectedJs } = useSource(
props.noteBody,
@@ -41,6 +44,7 @@ export default function NoteBodyViewer(props: Props) {
props.noteResources,
props.paddingBottom,
props.noteHash,
props.initialScroll,
);
const onResourceLongPress = useOnResourceLongPress(
@@ -59,6 +63,7 @@ export default function NoteBodyViewer(props: Props) {
onJoplinLinkClick: props.onJoplinLinkClick,
onRequestEditResource: props.onRequestEditResource,
onResourceLongPress,
onMainContainerScroll: props.onScroll,
},
);
@@ -96,6 +101,7 @@ export default function NoteBodyViewer(props: Props) {
return (
<View style={props.style}>
<ExtendedWebView
ref={webviewRef}
webviewInstanceId='NoteBodyViewer'
themeId={props.themeId}
style={webViewStyle}

View File

@@ -3,6 +3,7 @@ import shared from '@joplin/lib/components/shared/note-screen-shared';
export type HandleMessageCallback = (message: string)=> void;
export type OnMarkForDownloadCallback = (resource: { resourceId: string })=> void;
export type HandleScrollCallback = (scrollTop: number)=> void;
interface MessageCallbacks {
onMarkForDownload?: OnMarkForDownloadCallback;
@@ -10,6 +11,7 @@ interface MessageCallbacks {
onResourceLongPress: HandleMessageCallback;
onRequestEditResource?: HandleMessageCallback;
onCheckboxChange: HandleMessageCallback;
onMainContainerScroll: HandleScrollCallback;
}
export default function useOnMessage(
@@ -24,6 +26,7 @@ export default function useOnMessage(
// Thus, useCallback should depend on each callback individually.
const {
onMarkForDownload, onResourceLongPress, onCheckboxChange, onRequestEditResource, onJoplinLinkClick,
onMainContainerScroll,
} = callbacks;
return useCallback((event: any) => {
@@ -35,10 +38,23 @@ export default function useOnMessage(
// https://github.com/laurent22/joplin/issues/4494
const msg = event.nativeEvent.data;
// eslint-disable-next-line no-console
console.info('Got IPC message: ', msg);
const isScrollMessage = msg.startsWith('onscroll:');
if (msg.indexOf('checkboxclick:') === 0) {
// Scroll messages are very frequent so we avoid logging them.
if (!isScrollMessage) {
// eslint-disable-next-line no-console
console.info('Got IPC message: ', msg);
}
if (isScrollMessage) {
const eventData = JSON.parse(msg.substring(msg.indexOf(':') + 1));
if (typeof eventData.scrollTop !== 'number') {
throw new Error(`Invalid scroll message, ${msg}`);
}
onMainContainerScroll?.(eventData.scrollTop);
} else if (msg.indexOf('checkboxclick:') === 0) {
const newBody = shared.toggleCheckbox(msg, noteBody);
onCheckboxChange?.(newBody);
} else if (msg.indexOf('markForDownload:') === 0) {
@@ -63,5 +79,6 @@ export default function useOnMessage(
onJoplinLinkClick,
onResourceLongPress,
onRequestEditResource,
onMainContainerScroll,
]);
}

View File

@@ -40,7 +40,16 @@ const onlyCheckboxHasChangedHack = (previousBody: string, newBody: string) => {
return true;
};
export default function useSource(noteBody: string, noteMarkupLanguage: number, themeId: number, highlightedKeywords: string[], noteResources: any, paddingBottom: number, noteHash: string): UseSourceResult {
export default function useSource(
noteBody: string,
noteMarkupLanguage: number,
themeId: number,
highlightedKeywords: string[],
noteResources: any,
paddingBottom: number,
noteHash: string,
initialScroll: number|null,
): UseSourceResult {
const [html, setHtml] = useState<string>('');
const [injectedJs, setInjectedJs] = useState<string[]>([]);
const [resourceLoadedTime, setResourceLoadedTime] = useState(0);
@@ -142,6 +151,12 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
const resourceDownloadMode = Setting.value('sync.resourceDownloadMode');
// On iOS, the root container has slow inertial scroll, which feels very different from
// the native scroll in other apps. This is not the case, however, when a child (e.g. a div)
// scrolls the content instead.
// Use a div to scroll on iOS instead of the main container:
const scrollRenderedMdContainer = shim.mobilePlatform() === 'ios';
const js = [];
js.push('try {');
js.push(shim.injectedJs('webviewLib'));
@@ -149,15 +164,46 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
// the ReactNativeWebView actually supports only one, so the second arg is ignored (and currently not needed for the mobile app).
js.push('window.joplinPostMessage_ = (msg, args) => { return window.ReactNativeWebView.postMessage(msg); };');
js.push('webviewLib.initialize({ postMessage: msg => { return window.ReactNativeWebView.postMessage(msg); } });');
js.push(`
const scrollingElement =
${scrollRenderedMdContainer ? 'document.querySelector("#rendered-md")' : 'document.scrollingElement'};
let lastScrollTop;
const onMainContentScroll = () => {
const newScrollTop = scrollingElement.scrollTop;
if (lastScrollTop !== newScrollTop) {
const eventData = { scrollTop: newScrollTop };
window.ReactNativeWebView.postMessage('onscroll:' + JSON.stringify(eventData));
}
};
// Listen for events on both scrollingElement and window
// - On Android, scrollingElement.addEventListener('scroll', callback) doesn't call callback on
// scroll. However, window.addEventListener('scroll', callback) does.
// - iOS needs a listener to be added to scrollingElement -- events aren't received when
// the listener is added to window with window.addEventListener('scroll', ...).
scrollingElement.addEventListener('scroll', onMainContentScroll);
window.addEventListener('scroll', onMainContentScroll);
const scrollContentToPosition = (position) => {
scrollingElement.scrollTop = position;
};
`);
js.push(`
const readyStateCheckInterval = setInterval(function() {
if (document.readyState === "complete") {
clearInterval(readyStateCheckInterval);
if ("${resourceDownloadMode}" === "manual") webviewLib.setupResourceManualDownload();
const hash = "${noteHash}";
// Gives it a bit of time before scrolling to the anchor
// so that images are loaded.
if (hash) {
const initialScroll = ${JSON.stringify(initialScroll)};
// Don't scroll to a hash if we're given initial scroll (initial scroll
// overrides scrolling to a hash).
if ((initialScroll ?? null) !== null) {
scrollContentToPosition(initialScroll);
} else if (hash) {
// Gives it a bit of time before scrolling to the anchor
// so that images are loaded.
setTimeout(() => {
const e = document.getElementById(hash);
if (!e) {
@@ -171,6 +217,7 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
}, 10);
`);
js.push('} catch (e) {');
js.push(' console.error(e);');
js.push(' window.ReactNativeWebView.postMessage("error:" + e.message + ": " + JSON.stringify(e))');
js.push(' true;');
js.push('}');
@@ -186,10 +233,11 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
}
}
/*
iOS seems to increase inertial scrolling friction when the WebView body/root elements
scroll. Scroll the main container instead.
*/
:root > body {
padding: 0;
}
`;
const scrollRenderedMdContainerCss = `
body > #rendered-md {
width: 100vw;
overflow: auto;
@@ -197,10 +245,6 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
padding-bottom: ${paddingBottom}px;
padding-top: ${paddingTop};
}
:root > body {
padding: 0;
}
`;
const defaultCss = `
code {
@@ -219,6 +263,7 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
<style>
${defaultCss}
${shim.mobilePlatform() === 'ios' ? iOSSpecificCss : ''}
${scrollRenderedMdContainer ? scrollRenderedMdContainerCss : ''}
${editPopupCss}
</style>
${assetsToHeaders(result.pluginAssets, { asHtml: true })}

View File

@@ -11,16 +11,17 @@ import { WebViewMessageEvent } from 'react-native-webview';
import ExtendedWebView, { WebViewControl } from '../../ExtendedWebView';
import { clearAutosave, writeAutosave } from './autosave';
import { LocalizedStrings } from './js-draw/types';
import VersionInfo from 'react-native-version-info';
const logger = Logger.create('ImageEditor');
type OnSaveCallback = (svgData: string)=> void;
type OnCancelCallback = ()=> void;
type LoadInitialSVGCallback = ()=> Promise<string>;
interface Props {
themeId: number;
loadInitialSVGData: LoadInitialSVGCallback|null;
resourceFilename: string|null;
onSave: OnSaveCallback;
onExit: OnCancelCallback;
}
@@ -128,20 +129,31 @@ const ImageEditor = (props: Props) => {
}, [onRequestCloseEditor]);
const css = useCss(editorTheme);
const html = useMemo(() => `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"/>
const [html, setHtml] = useState('');
<style>
${css}
</style>
</head>
<body></body>
</html>
`, [css]);
useEffect(() => {
setHtml(`
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"/>
<style id='main-style'>
${css}
</style>
</head>
<body></body>
</html>
`);
// Only set HTML initially (and don't reset). Changing the HTML reloads
// the page.
//
// We need the HTML to initially have the correct CSS to prevent color
// changes on load.
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps
}, []);
// A set of localization overrides (Joplin is better localized than js-draw).
// All localizable strings (some unused?) can be found at
@@ -153,10 +165,23 @@ const ImageEditor = (props: Props) => {
redo: _('Redo'),
}), []);
const appInfo = useMemo(() => {
return {
name: 'Joplin',
description: `v${VersionInfo.appVersion}`,
};
}, []);
const injectedJavaScript = useMemo(() => `
window.onerror = (message, source, lineno) => {
window.ReactNativeWebView.postMessage(
"error: " + message + " in file://" + source + ", line " + lineno
"error: " + message + " in file://" + source + ", line " + lineno,
);
};
window.onunhandledrejection = (error) => {
window.ReactNativeWebView.postMessage(
"error: " + error.reason,
);
};
@@ -216,6 +241,7 @@ const ImageEditor = (props: Props) => {
${JSON.stringify(Setting.value('imageeditor.jsdrawToolbar'))},
${JSON.stringify(Setting.value('locale'))},
${JSON.stringify(localizedStrings)},
${JSON.stringify({ appInfo })},
);
// Start loading the SVG file (if present) after loading the editor.
@@ -229,30 +255,30 @@ const ImageEditor = (props: Props) => {
);
}
true;
`, [localizedStrings]);
`, [localizedStrings, appInfo]);
useEffect(() => {
webviewRef.current?.injectJS(`
document.querySelector('#main-style').innerText = ${JSON.stringify(css)};
if (window.editorControl) {
window.editorControl.onThemeUpdate();
}
`);
}, [editorTheme]);
}, [css]);
const onReadyToLoadData = useCallback(async () => {
const initialSVGData = await props.loadInitialSVGData?.() ?? '';
// It can take some time for initialSVGData to be transferred to the WebView.
// Thus, do so after the main content has been loaded.
webviewRef.current.injectJS(`(async () => {
if (window.editorControl) {
const initialSVGData = ${JSON.stringify(initialSVGData)};
const initialSVGPath = ${JSON.stringify(props.resourceFilename)};
const initialTemplateData = ${JSON.stringify(Setting.value('imageeditor.imageTemplate'))};
editorControl.loadImageOrTemplate(initialSVGData, initialTemplateData);
editorControl.loadImageOrTemplate(initialSVGPath, initialTemplateData);
}
})();`);
}, [webviewRef, props.loadInitialSVGData]);
}, [webviewRef, props.resourceFilename]);
const onMessage = useCallback(async (event: WebViewMessageEvent) => {
const data = event.nativeEvent.data;
@@ -291,6 +317,7 @@ const ImageEditor = (props: Props) => {
themeId={props.themeId}
html={html}
injectedJavaScript={injectedJavaScript}
allowFileAccessFromJs={true}
onMessage={onMessage}
onError={onError}
ref={webviewRef}

View File

@@ -57,7 +57,7 @@ describe('createJsDrawEditor', () => {
});
// Load no image and an empty template so that autosave can start
await editorControl.loadImageOrTemplate(undefined, '{}');
await editorControl.loadImageOrTemplate('', '{}');
expect(calledAutosaveCount).toBe(0);

View File

@@ -120,20 +120,44 @@ export const createJsDrawEditor = (
editor.showLoadingWarning(0);
editor.setReadOnly(true);
const fetchInitialSvgData = (resourceUrl: string) => {
return new Promise<string>((resolve, reject) => {
if (!resourceUrl) {
resolve('');
}
// fetch seems to be unable to request file:// URLs.
// https://github.com/react-native-webview/react-native-webview/issues/1560#issuecomment-1783611805
const request = new XMLHttpRequest();
const onError = () => {
reject(`Failed to load initial SVG data: ${request.status}, ${request.statusText}, ${request.responseText}`);
};
request.addEventListener('load', _ => {
resolve(request.responseText);
});
request.addEventListener('error', onError);
request.addEventListener('abort', onError);
request.open('GET', resourceUrl);
request.send();
});
};
const editorControl = {
editor,
loadImageOrTemplate: async (svgData: string|undefined, templateData: string) => {
loadImageOrTemplate: async (resourceUrl: string, templateData: string) => {
// loadFromSVG shows its own loading message. Hide the original.
editor.hideLoadingWarning();
if (svgData && svgData.length > 0) {
await editor.loadFromSVG(svgData);
} else {
await applyTemplateToEditor(editor, templateData);
const svgData = await fetchInitialSvgData(resourceUrl);
// The editor expects to be saved initially (without
// unsaved changes). Save now.
saveNow();
// Load from a template if no initial data
if (svgData === '') {
await applyTemplateToEditor(editor, templateData);
} else {
await editor.loadFromSVG(svgData);
}
// We can now edit and save safely (without data loss).

View File

@@ -6,8 +6,8 @@ import { defaultSearchState, SearchPanel } from './SearchPanel';
import ExtendedWebView from '../ExtendedWebView';
import * as React from 'react';
import { forwardRef, RefObject, useImperativeHandle } from 'react';
import { useEffect, useMemo, useState, useCallback, useRef } from 'react';
import { forwardRef, useImperativeHandle } from 'react';
import { useMemo, useState, useCallback, useRef } from 'react';
import { LayoutChangeEvent, View, ViewStyle } from 'react-native';
const { editorFont } = require('../global-style');
@@ -126,7 +126,6 @@ type OnSearchStateChangeCallback = (state: SearchState)=> void;
const useEditorControl = (
injectJS: OnInjectJSCallback, setLinkDialogVisible: OnSetVisibleCallback,
setSearchState: OnSearchStateChangeCallback,
searchStateRef: RefObject<SearchState>,
): EditorControl => {
return useMemo(() => {
const execCommand = (command: EditorCommandType) => {
@@ -252,16 +251,10 @@ const useEditorControl = (
},
showSearch() {
setSearchState({
...searchStateRef.current,
dialogVisible: true,
});
execCommand(EditorCommandType.ShowSearch);
},
hideSearch() {
setSearchState({
...searchStateRef.current,
dialogVisible: false,
});
execCommand(EditorCommandType.HideSearch);
},
setSearchState: setSearchStateCallback,
@@ -269,7 +262,7 @@ const useEditorControl = (
};
return control;
}, [injectJS, searchStateRef, setLinkDialogVisible, setSearchState]);
}, [injectJS, setLinkDialogVisible, setSearchState]);
};
function NoteEditor(props: Props, ref: any) {
@@ -356,22 +349,13 @@ function NoteEditor(props: Props, ref: any) {
const [linkDialogVisible, setLinkDialogVisible] = useState(false);
const [searchState, setSearchState] = useState(defaultSearchState);
// Having a [searchStateRef] allows [editorControl] to not be re-created
// whenever [searchState] changes.
const searchStateRef = useRef(defaultSearchState);
// Keep the reference and the [searchState] in sync
useEffect(() => {
searchStateRef.current = searchState;
}, [searchState]);
// Runs [js] in the context of the CodeMirror frame.
const injectJS = (js: string) => {
webviewRef.current.injectJS(js);
};
const editorControl = useEditorControl(
injectJS, setLinkDialogVisible, setSearchState, searchStateRef,
injectJS, setLinkDialogVisible, setSearchState,
);
useImperativeHandle(ref, () => {

View File

@@ -40,9 +40,7 @@ interface ActionButtonProps {
onPress: Callback;
}
const ActionButton = (
props: ActionButtonProps,
) => {
const ActionButton = (props: ActionButtonProps) => {
return (
<CustomButton
themeId={props.themeId}

View File

@@ -70,6 +70,7 @@ interface ScreenHeaderProps {
onRedoButtonPress: OnPressCallback;
onSaveButtonPress: OnPressCallback;
sortButton_press?: OnPressCallback;
onSearchButtonPress?: OnPressCallback;
showSideMenuButton?: boolean;
showSearchButton?: boolean;
@@ -242,7 +243,11 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
}
private searchButton_press() {
void NavService.go('Search');
if (this.props.onSearchButtonPress) {
this.props.onSearchButtonPress();
} else {
void NavService.go('Search');
}
}
private async duplicateButton_press() {

View File

@@ -1,12 +1,12 @@
const React = require('react');
const { StyleSheet } = require('react-native');
import * as React from 'react';
import { StyleSheet } from 'react-native';
const { themeStyle } = require('./global-style.js');
const rootStyles_ = {};
const rootStyles_: Record<number, any> = {};
class BaseScreenComponent extends React.Component {
class BaseScreenComponent<Props, State> extends React.Component<Props, State> {
rootStyle(themeId) {
protected rootStyle(themeId: number) {
const theme = themeStyle(themeId);
if (rootStyles_[themeId]) return rootStyles_[themeId];
rootStyles_[themeId] = StyleSheet.create({
@@ -19,4 +19,5 @@ class BaseScreenComponent extends React.Component {
}
}
module.exports = { BaseScreenComponent };
export { BaseScreenComponent };
export default BaseScreenComponent;

View File

@@ -0,0 +1,65 @@
import * as React from 'react';
import shim from '@joplin/lib/shim';
import { FunctionComponent, useCallback, useEffect, useState } from 'react';
import { ConfigScreenStyles } from './configScreenStyles';
import { TouchableNativeFeedback, View, Text } from 'react-native';
import Setting, { SettingItem } from '@joplin/lib/models/Setting';
import { openDocumentTree } from '@joplin/react-native-saf-x';
import { UpdateSettingValueCallback } from './types';
import { reg } from '@joplin/lib/registry';
interface Props {
styles: ConfigScreenStyles;
settingMetadata: SettingItem;
updateSettingValue: UpdateSettingValueCallback;
}
const FileSystemPathSelector: FunctionComponent<Props> = props => {
const [fileSystemPath, setFileSystemPath] = useState<string>('');
const settingId = props.settingMetadata.key;
useEffect(() => {
setFileSystemPath(Setting.value(settingId));
}, [settingId]);
const selectDirectoryButtonPress = useCallback(async () => {
try {
const doc = await openDocumentTree(true);
if (doc?.uri) {
setFileSystemPath(doc.uri);
await props.updateSettingValue(settingId, doc.uri);
} else {
throw new Error('User cancelled operation');
}
} catch (e) {
reg.logger().info('Didn\'t pick sync dir: ', e);
}
}, [props.updateSettingValue, settingId]);
// Unsupported on non-Android platforms.
if (!shim.fsDriver().isUsingAndroidSAF()) {
return null;
}
const styleSheet = props.styles.styleSheet;
return (
<TouchableNativeFeedback
onPress={selectDirectoryButtonPress}
style={styleSheet.settingContainer}
>
<View style={styleSheet.settingContainer}>
<Text key="label" style={styleSheet.settingText}>
{props.settingMetadata.label()}
</Text>
<Text style={styleSheet.settingControl}>
{fileSystemPath}
</Text>
</View>
</TouchableNativeFeedback>
);
};
export default FileSystemPathSelector;

View File

@@ -0,0 +1,44 @@
import * as React from 'react';
import { useCallback, useState } from 'react';
import { _ } from '@joplin/lib/locale';
import exportDebugReport from './utils/exportDebugReport';
import shim from '@joplin/lib/shim';
import SettingsButton from '../SettingsButton';
import { ConfigScreenStyles } from '../configScreenStyles';
interface Props {
styles: ConfigScreenStyles;
}
export const exportDebugReportTitle = () => _('Export Debug Report');
const ExportDebugReportButton = (props: Props) => {
const [creatingReport, setCreatingReport] = useState(false);
const exportDebugButtonPress = useCallback(async () => {
setCreatingReport(true);
await exportDebugReport();
setCreatingReport(false);
}, [setCreatingReport]);
const exportDebugReportButton = (
<SettingsButton
title={creatingReport ? _('Creating report...') : exportDebugReportTitle()}
clickHandler={exportDebugButtonPress}
styles={props.styles}
disabled={creatingReport}
/>
);
// The debug functionality is only supported on Android.
if (shim.mobilePlatform() !== 'android') {
return null;
}
return exportDebugReportButton;
};
export default ExportDebugReportButton;

View File

@@ -0,0 +1,81 @@
import * as React from 'react';
import { useCallback, useState } from 'react';
import { View, Button } from 'react-native';
import { TextInput } from 'react-native-paper';
import { _ } from '@joplin/lib/locale';
import shim from '@joplin/lib/shim';
import exportProfile from './utils/exportProfile';
import { ConfigScreenStyles } from '../configScreenStyles';
import SettingsButton from '../SettingsButton';
interface Props {
styles: ConfigScreenStyles;
}
export const exportProfileButtonTitle = () => _('Export profile');
const ExportProfileButton = (props: Props) => {
const [profileExportStatus, setProfileExportStatus] = useState<'idle'|'prompt'|'exporting'>('idle');
const [profileExportPath, setProfileExportPath] = useState<string>('');
const exportProfileButtonPress = useCallback(async () => {
const externalDir = await shim.fsDriver().getExternalDirectoryPath();
if (!externalDir) {
return;
}
const p = profileExportPath ? profileExportPath : `${externalDir}/JoplinProfileExport`;
setProfileExportStatus('prompt');
setProfileExportPath(p);
}, [profileExportPath]);
const exportProfileButton = (
<SettingsButton
styles={props.styles}
title={profileExportStatus === 'exporting' ? _('Exporting profile...') : exportProfileButtonTitle()}
clickHandler={exportProfileButtonPress}
description={_('For debugging purpose only: export your profile to an external SD card.')}
disabled={profileExportStatus === 'exporting'}
/>
);
const exportProfileButtonPress2 = useCallback(async () => {
setProfileExportStatus('exporting');
await exportProfile(profileExportPath);
setProfileExportStatus('idle');
}, [profileExportPath]);
const profileExportPrompt = (
<View>
<TextInput
label={_('Path:')}
onChangeText={text => setProfileExportPath(text)}
value={profileExportPath}
placeholder="/path/to/sdcard"
keyboardAppearance={props.styles.keyboardAppearance} />
<Button
onPress={exportProfileButtonPress2}
title={_('OK')}
/>
</View>
);
const mainContent = (
<>
{exportProfileButton}
{profileExportStatus === 'prompt' ? profileExportPrompt : null}
</>
);
// The debug functionality is only supported on Android.
if (shim.mobilePlatform() !== 'android') {
return null;
}
return mainContent;
};
export default ExportProfileButton;

View File

@@ -7,10 +7,10 @@ import { FunctionComponent, useCallback, useState } from 'react';
import shim from '@joplin/lib/shim';
import { join } from 'path';
import Share from 'react-native-share';
import exportAllFolders, { makeExportCacheDirectory } from './exportAllFolders';
import exportAllFolders, { makeExportCacheDirectory } from './utils/exportAllFolders';
import { ExportProgressState } from '@joplin/lib/services/interop/types';
import { ConfigScreenStyles } from '../configScreenStyles';
import ConfigScreenButton from '../ConfigScreenButton';
import SettingsButton from '../SettingsButton';
const logger = Logger.create('NoteExportButton');
@@ -24,6 +24,9 @@ enum ExportStatus {
Exported,
}
export const exportButtonTitle = () => _('Export all notes as JEX');
export const exportButtonDescription = () => _('Share a copy of all notes in a file format that can be imported by Joplin on a computer.');
const NoteExportButton: FunctionComponent<Props> = props => {
const [exportStatus, setExportStatus] = useState<ExportStatus>(ExportStatus.NotStarted);
const [exportProgress, setExportProgress] = useState<number|undefined>(0);
@@ -80,13 +83,12 @@ const NoteExportButton: FunctionComponent<Props> = props => {
indeterminate={exportProgress === undefined}
progress={exportProgress}/>
);
const descriptionText = _('Share a copy of all notes in a file format that can be imported by Joplin on a computer.');
const startOrCancelExportButton = (
<ConfigScreenButton
title={exportStatus === ExportStatus.Exporting ? _('Exporting...') : _('Export all notes as JEX')}
<SettingsButton
title={exportStatus === ExportStatus.Exporting ? _('Exporting...') : exportButtonTitle()}
disabled={exportStatus === ExportStatus.Exporting}
description={exportStatus === ExportStatus.NotStarted ? descriptionText : null}
description={exportStatus === ExportStatus.NotStarted ? exportButtonDescription() : null}
statusComponent={progressComponent}
clickHandler={startExport}
styles={props.styles}
@@ -96,14 +98,14 @@ const NoteExportButton: FunctionComponent<Props> = props => {
return startOrCancelExportButton;
} else {
const warningComponent = (
<Text style={props.styles.warningText}>
<Text style={props.styles.styleSheet.warningText}>
{_('Warnings:\n%s', warnings)}
</Text>
);
const exportSummary = (
<View style={props.styles.settingContainer}>
<Text style={props.styles.descriptionText}>{_('Exported successfully!')}</Text>
<View style={props.styles.styleSheet.settingContainer}>
<Text style={props.styles.styleSheet.descriptionText}>{_('Exported successfully!')}</Text>
{warnings.length > 0 ? warningComponent : null}
</View>
);

View File

@@ -0,0 +1,32 @@
import { reg } from '@joplin/lib/registry';
import ReportService from '@joplin/lib/services/ReportService';
import shim from '@joplin/lib/shim';
import time from '@joplin/lib/time';
const exportDebugReport = async () => {
const service = new ReportService();
const logItems = await reg.logger().lastEntries(null);
const logItemRows = [['Date', 'Level', 'Message']];
for (let i = 0; i < logItems.length; i++) {
const item = logItems[i];
logItemRows.push([time.formatMsToLocal(item.timestamp, 'MM-DDTHH:mm:ss'), item.level, item.message]);
}
const logItemCsv = service.csvCreate(logItemRows);
const itemListCsv = await service.basicItemList({ format: 'csv' });
const externalDir = await shim.fsDriver().getExternalDirectoryPath();
if (!externalDir) {
return;
}
const filePath = `${externalDir}/syncReport-${new Date().getTime()}.txt`;
const finalText = [logItemCsv, itemListCsv].join('\n================================================================================\n');
await shim.fsDriver().writeFile(filePath, finalText, 'utf8');
alert(`Debug report exported to ${filePath}`);
};
export default exportDebugReport;

View File

@@ -0,0 +1,35 @@
import shim from '@joplin/lib/shim';
import { reg } from '@joplin/lib/registry';
import Setting from '@joplin/lib/models/Setting';
const exportProfile = async (profileExportPath: string) => {
const dbPath = '/data/data/net.cozic.joplin/databases';
const exportPath = profileExportPath;
const resourcePath = `${exportPath}/resources`;
try {
const copyFiles = async (source: string, dest: string) => {
await shim.fsDriver().mkdir(dest);
const files = await shim.fsDriver().readDirStats(source);
for (const file of files) {
const source_ = `${source}/${file.path}`;
const dest_ = `${dest}/${file.path}`;
if (!file.isDirectory()) {
reg.logger().info(`Copying profile: ${source_} => ${dest_}`);
await shim.fsDriver().copy(source_, dest_);
} else {
await copyFiles(source_, dest_);
}
}
};
await copyFiles(dbPath, exportPath);
await copyFiles(Setting.value('resourceDir'), resourcePath);
alert('Profile has been exported!');
} catch (error) {
alert(`Could not export files: ${error.message}`);
}
};
export default exportProfile;

View File

@@ -0,0 +1,25 @@
import * as React from 'react';
import { ConfigScreenStyleSheet } from './configScreenStyles';
import { View, Text, LayoutChangeEvent } from 'react-native';
interface Props {
styles: ConfigScreenStyleSheet;
title: string;
onLayout?: (event: LayoutChangeEvent)=> void;
}
const SectionHeader: React.FunctionComponent<Props> = props => {
return (
<View
style={props.styles.headerWrapperStyle}
onLayout={props.onLayout}
>
<Text style={props.styles.headerTextStyle}>
{props.title}
</Text>
</View>
);
};
export default SectionHeader;

View File

@@ -0,0 +1,108 @@
import * as React from 'react';
import Setting, { AppType, SettingMetadataSection } from '@joplin/lib/models/Setting';
import { FunctionComponent, useEffect, useMemo, useState } from 'react';
import { ConfigScreenStyles } from './configScreenStyles';
import { FlatList, Text, Pressable, View, ViewStyle } from 'react-native';
import { settingsSections } from '@joplin/lib/components/shared/config/config-shared';
import Icon from '../../Icon';
interface Props {
styles: ConfigScreenStyles;
width: number|undefined;
settings: any;
selectedSectionName: string|null;
openSection: (sectionName: string)=> void;
}
const SectionSelector: FunctionComponent<Props> = props => {
const sections = useMemo(() => {
return settingsSections({ device: AppType.Mobile, settings: props.settings });
}, [props.settings]);
const styles = props.styles.styleSheet;
const itemHeight = styles.sidebarButton.height;
const onRenderButton = ({ item }: { item: SettingMetadataSection }) => {
const section = item;
const selected = props.selectedSectionName === section.name;
const icon = Setting.sectionNameToIcon(section.name, AppType.Mobile);
const label = Setting.sectionNameToLabel(section.name);
const shortDescription = Setting.sectionMetadataToSummary(section);
return (
<Pressable
key={section.name}
role='tab'
aria-selected={selected}
onPress={() => props.openSection(section.name)}
style={selected ? styles.selectedSidebarButton : styles.sidebarButton}
>
<Icon
name={icon}
accessibilityLabel={null}
style={styles.sidebarIcon}
/>
<View style={{ display: 'flex', flexDirection: 'column', flex: 1 }}>
<Text
style={selected ? styles.sidebarSelectedButtonText : styles.sidebarButtonMainText}
>
{label}
</Text>
<Text
style={styles.sidebarButtonDescriptionText}
numberOfLines={1}
ellipsizeMode='tail'
>
{shortDescription ?? ''}
</Text>
</View>
</Pressable>
);
};
const [flatListRef, setFlatListRef] = useState<FlatList|null>(null);
useEffect(() => {
if (flatListRef && props.selectedSectionName) {
let selectedIndex = 0;
for (const section of sections) {
if (section.name === props.selectedSectionName) {
break;
}
selectedIndex ++;
}
flatListRef.scrollToIndex({
index: selectedIndex,
viewPosition: 0.5,
});
}
}, [props.selectedSectionName, flatListRef, sections]);
const containerStyle: ViewStyle = useMemo(() => ({
width: props.width,
maxWidth: props.width,
minWidth: props.width,
flex: 1,
}), [props.width]);
return (
<View style={containerStyle}>
<FlatList
role='tablist'
ref={setFlatListRef}
data={sections}
renderItem={onRenderButton}
keyExtractor={item => item.name}
getItemLayout={(_data, index) => ({
length: itemHeight, offset: itemHeight * index, index,
})}
/>
</View>
);
};
export default SectionSelector;

View File

@@ -0,0 +1,156 @@
import * as React from 'react';
import { UpdateSettingValueCallback } from './types';
import { View, Text, TextInput } from 'react-native';
import Setting from '@joplin/lib/models/Setting';
import Dropdown from '../../Dropdown';
import { ConfigScreenStyles } from './configScreenStyles';
import Slider from '@react-native-community/slider';
import SettingsToggle from './SettingsToggle';
import FileSystemPathSelector from './FileSystemPathSelector';
import shim from '@joplin/lib/shim';
const { themeStyle } = require('../../global-style.js');
interface Props {
settingId: string;
// The value associated with the given settings key
value: any;
styles: ConfigScreenStyles;
themeId: number;
updateSettingValue: UpdateSettingValueCallback;
}
const SettingComponent: React.FunctionComponent<Props> = props => {
const themeId = props.themeId;
const theme = themeStyle(themeId);
const output: any = null;
const md = Setting.settingMetadata(props.settingId);
const settingDescription = md.description ? md.description() : '';
const styleSheet = props.styles.styleSheet;
const descriptionComp = !settingDescription ? null : <Text style={styleSheet.settingDescriptionText}>{settingDescription}</Text>;
const containerStyle = props.styles.getContainerStyle(!!settingDescription);
if (md.isEnum) {
const value = props.value.toString();
const items = Setting.enumOptionsToValueLabels(md.options(), md.optionsOrder ? md.optionsOrder() : []);
return (
<View key={props.settingId} style={{ flexDirection: 'column', borderBottomWidth: 1, borderBottomColor: theme.dividerColor }}>
<View style={containerStyle}>
<Text key="label" style={styleSheet.settingText}>
{md.label()}
</Text>
<Dropdown
key="control"
items={items as any}
selectedValue={value}
itemListStyle={{
backgroundColor: theme.backgroundColor,
}}
headerStyle={{
color: theme.color,
fontSize: theme.fontSize,
}}
itemStyle={{
color: theme.color,
fontSize: theme.fontSize,
}}
onValueChange={(itemValue: string) => {
void props.updateSettingValue(props.settingId, itemValue);
}}
/>
</View>
{descriptionComp}
</View>
);
} else if (md.type === Setting.TYPE_BOOL) {
return (
<SettingsToggle
settingId={props.settingId}
value={props.value}
themeId={props.themeId}
styles={props.styles}
label={md.label()}
updateSettingValue={props.updateSettingValue}
description={descriptionComp}
/>
);
} else if (md.type === Setting.TYPE_INT) {
const unitLabel = md.unitLabel ? md.unitLabel(props.value) : props.value;
const minimum = 'minimum' in md ? md.minimum : 0;
const maximum = 'maximum' in md ? md.maximum : 10;
// Note: Do NOT add the minimumTrackTintColor and maximumTrackTintColor props
// on the Slider as they are buggy and can crash the app on certain devices.
// https://github.com/laurent22/joplin/issues/2733
// https://github.com/react-native-community/react-native-slider/issues/161
return (
<View key={props.settingId} style={styleSheet.settingContainer}>
<Text key="label" style={styleSheet.settingText}>
{md.label()}
</Text>
<View style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', flex: 1 }}>
<Text style={styleSheet.sliderUnits}>{unitLabel}</Text>
<Slider
key="control"
style={{ flex: 1 }}
step={md.step}
minimumValue={minimum}
maximumValue={maximum}
value={props.value}
onValueChange={newValue => void props.updateSettingValue(props.settingId, newValue)}
/>
</View>
</View>
);
} else if (md.type === Setting.TYPE_STRING) {
if (md.key === 'sync.2.path' && shim.fsDriver().isUsingAndroidSAF()) {
return (
<FileSystemPathSelector
styles={props.styles}
settingMetadata={md}
updateSettingValue={props.updateSettingValue}
/>
);
}
return (
<View key={props.settingId} style={{ flexDirection: 'column', borderBottomWidth: 1, borderBottomColor: theme.dividerColor }}>
<View key={props.settingId} style={containerStyle}>
<Text key="label" style={styleSheet.settingText}>
{md.label()}
</Text>
<TextInput
autoCorrect={false}
autoComplete="off"
selectionColor={theme.textSelectionColor}
keyboardAppearance={theme.settingKeyboardAppearance}
autoCapitalize="none"
key="control"
style={styleSheet.settingControl}
value={props.value}
onChangeText={(newValue: string) => void props.updateSettingValue(props.settingId, newValue)}
secureTextEntry={!!md.secure}
/>
</View>
{descriptionComp}
</View>
);
} else if (md.type === Setting.TYPE_BUTTON) {
// TODO: Not yet supported
} else if (Setting.value('env') === 'dev') {
throw new Error(`Unsupported setting type: ${md.type}`);
}
return output;
};
export default SettingComponent;

View File

@@ -6,25 +6,27 @@ import { ConfigScreenStyles } from './configScreenStyles';
interface Props {
title: string;
description: string;
description?: string;
clickHandler: ()=> void;
styles: ConfigScreenStyles;
disabled?: boolean;
statusComponent?: ReactNode;
}
const ConfigScreenButton: FunctionComponent<Props> = props => {
const SettingsButton: FunctionComponent<Props> = props => {
const styles = props.styles.styleSheet;
let descriptionComp = null;
if (props.description) {
descriptionComp = (
<View style={{ flex: 1, marginTop: 10 }}>
<Text style={props.styles.descriptionText}>{props.description}</Text>
<Text style={styles.descriptionText}>{props.description}</Text>
</View>
);
}
return (
<View style={props.styles.settingContainer}>
<View style={styles.settingContainer}>
<View style={{ flex: 1, flexDirection: 'column' }}>
<View style={{ flex: 1 }}>
<Button title={props.title} onPress={props.clickHandler} disabled={!!props.disabled} />
@@ -35,4 +37,4 @@ const ConfigScreenButton: FunctionComponent<Props> = props => {
</View>
);
};
export default ConfigScreenButton;
export default SettingsButton;

View File

@@ -0,0 +1,45 @@
import * as React from 'react';
import { FunctionComponent, ReactNode } from 'react';
import { View, Text, Switch } from 'react-native';
import { UpdateSettingValueCallback } from './types';
import { themeStyle } from '@joplin/lib/theme';
import { ConfigScreenStyles } from './configScreenStyles';
interface Props {
settingId: string;
value: any;
themeId: number;
styles: ConfigScreenStyles;
label: string;
updateSettingValue: UpdateSettingValueCallback;
description?: ReactNode;
}
const SettingsToggle: FunctionComponent<Props> = props => {
const theme = themeStyle(props.themeId);
const styleSheet = props.styles.styleSheet;
return (
<View>
<View style={props.styles.getContainerStyle(false)}>
<Text key="label" style={styleSheet.switchSettingText}>
{props.label}
</Text>
<Switch
key="control"
style={styleSheet.switchSettingControl}
trackColor={{ false: theme.dividerColor }}
value={props.value}
onValueChange={(value: boolean) => void props.updateSettingValue(props.settingId, value)}
/>
</View>
{props.description}
</View>
);
};
export default SettingsToggle;

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