1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-24 20:19:10 +02:00

Compare commits

...

315 Commits

Author SHA1 Message Date
Laurent Cozic
d9ba532889 Desktop release v3.3.10 2025-05-02 15:27:54 +01:00
Laurent Cozic
5aca4bc72e Desktop: Fixes #11989: Notify user when they are using the Intel app on Apple Silicon 2025-05-02 15:27:24 +01:00
Laurent Cozic
ae47667644 CLI v3.3.1 2025-05-01 22:19:16 +01:00
Laurent Cozic
b191b1daf7 Lock file 2025-05-01 22:18:02 +01:00
Laurent Cozic
0e3b9fd929 Releasing sub-packages 2025-05-01 22:17:30 +01:00
Laurent Cozic
5d0fa754ae Desktop release v3.3.9 2025-05-01 20:13:16 +01:00
Laurent Cozic
70d76fa00f Merge branch 'dev' into release-3.3 2025-05-01 20:08:06 +01:00
Henry Heino
d72224a6b9 Desktop: Fixes #12197: Fix inserting note links using the mouse (#12199) 2025-05-01 20:07:33 +01:00
Henry Heino
687516e695 Chore: Fix locale test (#12201) 2025-05-01 20:06:56 +01:00
Joplin Bot
3055873406 Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-05-01 18:41:16 +00:00
Laurent Cozic
e58e419c1b iOS 13.3.7 2025-05-01 17:34:23 +01:00
Laurent Cozic
ebd55cc505 iOS 13.3.6 2025-05-01 16:50:33 +01:00
Laurent Cozic
712f4034f2 Android 3.3.8 2025-05-01 16:45:56 +01:00
Laurent Cozic
601774c611 Desktop release v3.3.8 2025-05-01 16:38:41 +01:00
Laurent Cozic
7a810380fd Merge branch 'dev' into release-3.3 2025-05-01 16:37:54 +01:00
Laurent Cozic
52d71756a7 lock file 2025-05-01 16:37:41 +01:00
Laurent Cozic
a6b430a066 Updated translations 2025-05-01 16:36:58 +01:00
pedr
5a97ce74bc Desktop: Resolves #11774: Add Sign Up call to Joplin Cloud (#12025)
Co-authored-by: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-05-01 16:22:00 +01:00
Henry Heino
e58e03c3d2 Mobile: New note menu: Force quick action shortcuts to have the same size (#12195) 2025-05-01 16:20:30 +01:00
Henry Heino
511645b1a5 Mobile: Increase space between new note/to-do buttons (#12194) 2025-05-01 16:20:22 +01:00
Joplin Bot
b9ffc7ccec Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-05-01 02:03:37 +00:00
Henry Heino
b585b8b75c Mobile: Fixes #12191: Fix new note menu size (#12193) 2025-04-30 22:49:43 +01:00
Jozef Gaal
f7d1dbde8f All: Translation: Update sk_SK.po (#12188) 2025-04-29 23:44:21 -04:00
summoner001
6009f1dd60 All: Translation: Update hu_HU.po (#12186) 2025-04-29 21:02:26 -04:00
Laurent Cozic
e50086134f Doc: Fixed new note image 2025-04-30 00:22:52 +01:00
Laurent Cozic
25c66e2d00 Doc: Added CVE-2025-27409 reference to Joplin Server changelog and credited security researcher 2025-04-29 23:45:51 +01:00
Laurent Cozic
6d89725c7a Doc: Added CVE to Joplin Server changelog and credited security researcher 2025-04-29 23:44:12 +01:00
Joplin Bot
dfdabfc0da Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-04-29 18:40:00 +00:00
Laurent Cozic
2059e96227 iOS 13.3.5 2025-04-29 14:18:50 +01:00
Laurent Cozic
40e4afbc78 Android 3.3.7 2025-04-29 14:18:20 +01:00
Laurent Cozic
76a22c31cb Desktop release v3.3.7 2025-04-29 13:58:08 +01:00
Ismaël Moret
175628b597 Translation: Update fr_FR.po (#12161)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-04-29 13:57:36 +01:00
pedr
e7584644b5 Server: Resolves #12138: Add CSS style for description list elements (#12158) 2025-04-29 13:55:23 +01:00
Henry Heino
13a572efa9 Mobile: Allow new note and new to-do buttons to wrap (#12163)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-04-29 13:54:58 +01:00
Henry Heino
d85394681d Mobile,Desktop: Fixes #12110: Editor: Allow syntax highlighting within ==highlight==s (#12167)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-04-29 13:54:19 +01:00
Mihai Vasiliu
c785388c51 All: Add plural forms for notes, users, hours, minutes, days (#12171) 2025-04-29 13:54:09 +01:00
Henry Heino
27be923fe6 Linux, Windows: Fixes #12177: Allow opening secondary app instances from the "File" menu (#12181) 2025-04-29 13:53:40 +01:00
Henry Heino
a7bfa21cd0 Desktop: Accessibility: Fix "focus viewer" doesn't move foucs to the correct line for sufficiently large documents (#12183) 2025-04-29 13:53:32 +01:00
Bernd de Kruik
e9fe944744 Desktop: Resolves #12039: "Collapse all" button in sidebar doesn't collapse trash folder (#12051) 2025-04-29 13:53:25 +01:00
Henry Heino
b93a125f95 Desktop,CLI,Mobile: Update immer to v9.0.21 (#12182) 2025-04-29 13:49:57 +01:00
Henry Heino
262c343f21 Desktop: Built-in plugins: Upgrade Backup to v1.4.3 (#12180) 2025-04-29 13:49:34 +01:00
Henry Heino
8aa37905f9 Desktop: Upgrade to Electron 35.2.1 (#12178) 2025-04-29 13:33:13 +01:00
Liffindra Angga Zaaldian
2a5b227bf0 All: Translation: Update id_ID.po (#12169)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-04-28 14:52:22 -04:00
summoner001
7223e9b87e All: Translation: Update hu_HU.po (#12165)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-04-28 14:51:54 -04:00
Joplin Bot
82d9099ec0 Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-04-28 18:43:52 +00:00
Henry Heino
c46502825d Desktop: Fixes #12168: Preserve search query when opening a note in a new window (#12175) 2025-04-28 19:36:09 +01:00
Henry Heino
58688624fc Windows: Fix firewall warning on startup (#12176) 2025-04-28 19:35:28 +01:00
Henry Heino
355356dccc Docs: Remove information from release notes about a reverted 3.3 change (#12173) 2025-04-28 15:57:53 +01:00
Laurent Cozic
d4d12c0faa Doc: Add release notes 3.3 2025-04-28 14:17:57 +01:00
Laurent Cozic
f8f8db36b6 Chore: Recorded CLA consent documents 2025-04-28 09:55:35 +01:00
Laurent Cozic
a8e30c5b54 Merge branch 'dev' into cla_signatures 2025-04-28 09:48:54 +01:00
Mihai Vasiliu
ffd775a8f8 All: Translation: Add ro_MD.po (#12155) 2025-04-24 23:39:56 -04:00
Celestial.y
8d421d907e All: Translation: Update zh_CN.po (#12156) 2025-04-24 23:07:46 -04:00
Laurent Cozic
3206b1a792 Chore: Save an archive of CLA consent records 2025-04-24 22:49:40 +01:00
Laurent Cozic
37137f3867 Chore: Created script to save CLA consent records 2025-04-24 22:49:40 +01:00
coldWater
e08fd0ea53 Server: Added endpoint configuration for StorageDriverS3 (#12032)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-04-24 20:07:04 +01:00
Joplin Bot
6265be924a Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-04-24 12:54:39 +00:00
Laurent Cozic
079c4638fe iOS 13.3.4 2025-04-24 09:54:02 +01:00
Laurent Cozic
6aba57f2f1 Android 3.3.6 2025-04-24 09:53:22 +01:00
Laurent Cozic
f083ffa73c Desktop release v3.3.6 2025-04-24 09:30:30 +01:00
Laurent Cozic
40fd0db14e Chore: Update translations 2025-04-24 09:30:14 +01:00
Laurent Cozic
6379023165 Chore: Fixed running buildTranslations on Apple Silicon 2025-04-24 09:14:45 +01:00
Henry Heino
66f6310c17 Desktop, Mobile: Fixes #10226: Make list continuation logic more predictable (#11919) 2025-04-24 09:06:15 +01:00
pedr
d6409b7826 Chore: Fixes #11972: Update Husky (#12046) 2025-04-24 09:02:35 +01:00
Wenli Looi
1f68c5dcae Web: Fix WebDAV sync error "Response with null body status cannot have body" (#12127)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-04-24 08:55:10 +01:00
Henry Heino
bfe003a6c4 Desktop: Plugin API: Fix compatibility with YesYouKan plugin (#12132) 2025-04-24 08:52:43 +01:00
Henry Heino
ddc75ecc13 Desktop: Fix infinite loop on startup after quickly moving folders (#12140) 2025-04-24 08:52:35 +01:00
Henry Heino
e953290810 Windows: Fixes #12137: Plugin API: Fix plugin renderer scripts fail to load in the Rich Text Editor (#12141) 2025-04-24 08:51:03 +01:00
Henry Heino
807bcdcf95 Web: Fix tapping outside alert/confirm dialogs doesn't dismiss them correctly (#12144) 2025-04-24 08:50:35 +01:00
Henry Heino
6bb289338e Android: Improve UI for downloading updated models (#12145) 2025-04-24 08:50:25 +01:00
Henry Heino
5f0e4bb598 Desktop: Fixes #12143: Disable WebView isolation by default, add feature flag (#12149) 2025-04-24 08:50:10 +01:00
Henry Heino
84d97e4120 Chore: Desktop: Disable dev tools while running end-to-end tests (#12151) 2025-04-24 08:49:35 +01:00
Henry Heino
a9efdad059 Chore: Desktop: Enable debug logging in end-to-end tests (#12152) 2025-04-24 08:49:13 +01:00
Henry Heino
388e5efe17 Chore: Desktop: Fix occasional end-to-end test failure related to the settings screen (#12153) 2025-04-24 08:49:05 +01:00
Henry Heino
dfa340a137 Chore: Mobile: Update mobile components for compatibility with React Native 0.79 (#12154) 2025-04-24 08:48:58 +01:00
Mihai Vasiliu
cf626bee76 All: Translation: Update ro.po (#12150) 2025-04-23 21:37:48 -04:00
Henry Heino
0fe97a1098 Mobile: Settings: Fix desktop-specific setting visible in note > advanced (#12146) 2025-04-23 09:03:33 +01:00
Henry Heino
7c6c9b3e61 Mobile: Fix error when attempting to show message boxes in some cases (#12142) 2025-04-23 09:02:31 +01:00
Henry Heino
987c273376 Chore: Desktop: Run integration tests on Windows (#12147) 2025-04-23 08:20:57 +01:00
Laurent Cozic
0da05737c6 Chore: rebuilt files 2025-04-22 17:15:32 +01:00
Mihai Vasiliu
f71ffa1644 All: Translation: Update ro.po (#12134) 2025-04-19 12:04:35 -04:00
github-actions[bot]
92a546a315 @SilverGreen93 has signed the CLA in laurent22/joplin#12134 2025-04-19 15:31:38 +00:00
github-actions[bot]
22ffa69382 @looi has signed the CLA in laurent22/joplin#12127 2025-04-18 21:55:03 +00:00
Henry Heino
e1a436f6f9 Desktop: Markdown editor: Scroll linked-to headers to the top of the editor (#12125) 2025-04-18 10:35:16 +01:00
Henry Heino
b3823025cf Mobile: Note editor: Hash links: Move cursor to header or anchor associated with link target (#12129) 2025-04-18 10:30:45 +01:00
Henry Heino
eccb8350fe Chore: Desktop: Make useFormNote test less likely to fail (#12131) 2025-04-18 10:30:24 +01:00
Henry Heino
66203bda56 Chore: Remove auto-generated js files that correspond to non-existing ts files (#12130) 2025-04-18 10:30:15 +01:00
Joplin Bot
0759fe0530 Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-04-17 18:40:09 +00:00
Laurent Cozic
6d3c3a80ab Doc: Explained how to translate the documentation 2025-04-17 15:38:51 +01:00
github-actions[bot]
a18bcac673 @tue-AqielOostenbrug has signed the CLA in laurent22/joplin#12121 2025-04-17 13:37:16 +00:00
Laurent Cozic
b3a3690b16 Desktop release v3.3.5 2025-04-17 13:03:59 +01:00
Laurent Cozic
c3fe0edeeb Desktop, Mobile: Print name of file when import fails 2025-04-17 13:03:50 +01:00
Henry Heino
56e2d3da89 Desktop: Rich Text Editor: Disallow inline event handlers (#12106) 2025-04-17 13:02:35 +01:00
Henry Heino
3ffcf065fc Desktop: Linux: Add more input-method-related start flags (#12115) 2025-04-17 13:02:21 +01:00
Henry Heino
9dd82259c6 Desktop: Fixes #12105: Link to header: Move the Markdown editor cursor to the location of the link target (#12118) 2025-04-17 13:02:13 +01:00
Henry Heino
2dbdf47239 Docs: Plugin documentation: Update links to folders containing commands (#12120) 2025-04-17 13:02:03 +01:00
Laurent Cozic
2014fbf480 Chore: Fixed Crowdin translation issues 2025-04-16 16:57:35 +01:00
Joplin Bot
e847b1b902 Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-04-16 15:11:22 +00:00
Laurent Cozic
26276efc03 Doc: Add support for German translation 2025-04-16 15:44:28 +01:00
Henry Heino
4226044527 Desktop: Remove outline from list of plugins that are broken in the new editor (#12107) 2025-04-15 09:59:05 +01:00
Henry Heino
5ba6ac57d0 Desktop: Default plugins: Update Freehand Drawing to v3.0.1 (#12112) 2025-04-15 09:58:48 +01:00
Henry Heino
0058ac5f4b Desktop: Default plugins: Update Freehand Drawing to v3.0.0 (#12103) 2025-04-14 18:52:27 +01:00
Henry Heino
81f5a8463e Desktop: Fixes #12059: Fix Rich Text Editor deletes paragraphs when pressing enter after a resized image (#12090) 2025-04-14 18:52:10 +01:00
Henry Heino
9871717de4 iOS: Accessibility: Fix to-do completion status can't be changed from the note list (#12101) 2025-04-14 17:28:35 +01:00
renovate[bot]
62ca6cb70b Update Rust crate log to v0.4.25 (#12100)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-14 15:25:50 +01:00
Dan Serbyn
bd49f3b280 Plugins: expose hash from clicked cross-note link (#12094) 2025-04-14 13:42:24 +01:00
Laurent Cozic
cb3c9b4607 Desktop: Resolves #12095: By default keep 7 days of backup 2025-04-14 09:21:20 +01:00
github-actions[bot]
4e74ca93a3 @executed has signed the CLA in laurent22/joplin#12094 2025-04-14 03:35:17 +00:00
Henry Heino
5389e59057 Desktop,Mobile: Markdown Editor: Fix numbered sublist renumbering (#12091) 2025-04-13 19:29:18 +01:00
Henry Heino
3d15e64762 Desktop: Fix returning form data from plugin dialogs (#12092) 2025-04-13 19:28:56 +01:00
Henry Heino
a8b18e9ab0 Mobile: Update to js-draw v1.29.2 (#12074) 2025-04-12 23:04:19 +01:00
Henry Heino
5876d57845 Chore: Desktop: Disable WebView tag in window config (#12086) 2025-04-12 23:04:08 +01:00
Henry Heino
90f622b3e6 Chore: Testing: Make Rich Text Editor attachment test more reliable (#12085) 2025-04-12 12:12:34 +01:00
Henry Heino
fd486e298a Desktop: Rich Text Editor: Fix editor content not updated in some cases when switching notes (#12084) 2025-04-12 12:12:26 +01:00
Henry Heino
527627b8bb Desktop: Plugins: Prevent plugin dialogs, panels, and editors from accessing the main JavaScript context (#12083) 2025-04-12 11:49:03 +01:00
Henry Heino
9638cab9ea Desktop: Rich Text Editor: Add KaTeX to supported auto-replacements (#12081) 2025-04-12 11:46:55 +01:00
Laurent Cozic
5ad891e1f3 Doc: Fix build troubleshooting file 2025-04-11 13:50:32 +01:00
Laurent Cozic
f8445a04e5 Doc: Recommend against using WSL 2025-04-11 13:47:09 +01:00
Laurent Cozic
fc92a6ea63 Doc: Update sponsors 2025-04-10 14:08:38 +01:00
Miet
418a2c17a5 All: Translation: Update nl_BE.po (#12080) 2025-04-09 20:51:17 -04:00
github-actions[bot]
c3cc412077 @tessus has signed the CLA in laurent22/joplin#12080 2025-04-09 22:06:04 +00:00
Henry Heino
600000a59a Desktop: Fixes #9588: Rich Text Editor: Fix keyboard and plugin-opened context menus sometimes not displayed or have incorrect content (#12076) 2025-04-09 14:40:11 +01:00
Henry Heino
a3be7b5222 Desktop: Resolves #12058: Fix pasting images in the Rich Text Editor (#12079) 2025-04-09 14:39:39 +01:00
Laurent Cozic
52ffd46a6a Desktop: Resolves #12006: Add a new menu item to launch the primary instance from the secondary one 2025-04-09 08:14:17 +01:00
Henry Heino
587db433a8 Desktop: Fixes #12042: Fix toggling lists in the Rich Text Editor (#12071) 2025-04-08 21:12:36 +01:00
Henry Heino
5fb9d216fc Desktop: Update Freehand Drawing to v2.16.1 (#12073) 2025-04-08 20:36:30 +01:00
Joplin Bot
bb50ad7c28 Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-04-08 02:08:13 +00:00
Laurent Cozic
7a6a4e118a Android 3.3.5 2025-04-07 20:34:36 +01:00
Laurent Cozic
b7a652fb71 iOS 13.3.3 2025-04-07 20:20:21 +01:00
Laurent Cozic
821558fe30 Desktop release v3.3.4 2025-04-07 20:13:30 +01:00
Henry Heino
5280ec12cd Desktop: Improve notification accessibility (#11752) 2025-04-07 20:12:40 +01:00
Henry Heino
a29e30e442 Mobile: Implement new note menu redesign (#11780) 2025-04-07 20:12:10 +01:00
Damian Trowski
fe88703488 Clipper: Fixed absoluteUrl and baseUrl functions (#12043) 2025-04-07 20:05:36 +01:00
Henry Heino
338dabf5da Mobile,Desktop: Resolves #11872: Explain why items could not be decrypted (#12048) 2025-04-07 20:03:55 +01:00
Henry Heino
59447f4c45 Desktop: Rich Text Editor: Fix "Remove color" button doesn't work (#12052) 2025-04-07 20:02:19 +01:00
Henry Heino
04196e4485 Mobile: Add "swap line up" and "swap line down" to toolbar extended options (#12053) 2025-04-07 20:02:06 +01:00
Henry Heino
d4fafd74d2 Mobile: Update react-native-quick-crypto (#12067) 2025-04-07 19:59:44 +01:00
Henry Heino
f185480ceb Desktop: Update Electron to v35.1.4 (#12068) 2025-04-07 19:59:35 +01:00
github-actions[bot]
4da0abfef9 @personalizedrefrigerator has signed the CLA in laurent22/joplin#12069 2025-04-07 18:42:10 +00:00
Laurent Cozic
2e73ea2d9f Plugin Generator release v3.3.1 2025-04-07 18:30:50 +01:00
Laurent Cozic
8ac19d80ea Chore: Update plugin types 2025-04-07 18:30:20 +01:00
Laurent Cozic
911689a4ac Chore: Add exported type to fix plugin type generation 2025-04-07 18:29:25 +01:00
github-actions[bot]
5c73bda416 @danobot has signed the CLA in laurent22/joplin#12065 2025-04-06 09:44:01 +00:00
github-actions[bot]
c94dbece42 @tbjgolden has signed the CLA in laurent22/joplin#12057 2025-04-05 13:49:48 +00:00
github-actions[bot]
e49e314bdb @GGpifas has signed the CLA in laurent22/joplin#12060 2025-04-05 13:12:56 +00:00
github-actions[bot]
83452dd4c3 @outrodani has signed the CLA in laurent22/joplin#12055 2025-04-04 23:22:20 +00:00
github-actions[bot]
675e3125e4 @MorbidMiyako has signed the CLA in laurent22/joplin#12051 2025-04-03 21:04:02 +00:00
Joplin Bot
da0a7faf12 Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-04-03 13:02:25 +00:00
Laurent Cozic
9be533a19e Doc: Update sponsors 2025-04-03 14:35:30 +02:00
pedr
050871bc65 Desktop: Resolves #11608: Increase the likelihood of text generation from image recognition (#12028)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-04-03 10:08:25 +02:00
Henrique Santos
8d6d7ca6d2 Desktop: Fixes #9291: A note scrolls to top if reached by following a link to a section (#12038) 2025-04-02 11:22:33 +02:00
github-actions[bot]
032f512cde @d4m14n-gh has signed the CLA in laurent22/joplin#12043 2025-04-01 21:15:20 +00:00
Joplin Bot
b8c88af82d Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-04-01 02:19:22 +00:00
github-actions[bot]
962128faa3 @Schmeilen has signed the CLA in laurent22/joplin#12038 2025-03-31 10:57:43 +00:00
Laurent Cozic
e17ef72111 Chore: Trying to fix CI (#12036) 2025-03-31 10:43:12 +02:00
Laurent Cozic
287e0da4b4 Chore: Fixing CI 2025-03-31 09:22:00 +02:00
Laurent Cozic
471f5a72fe Chore: Fixing CI 2025-03-30 23:41:49 +02:00
Laurent Cozic
1e5c41dc48 Chore: Fixed CI 2025-03-30 23:29:17 +02:00
Laurent Cozic
716e5252c1 Server v3.3.13 2025-03-30 20:29:09 +02:00
Laurent Cozic
f10fd4b2da Server: Trying to build ARM64 Docker image 2025-03-30 20:28:29 +02:00
Laurent Cozic
ff4d18dec3 Server v3.3.12 2025-03-30 19:38:58 +02:00
Laurent Cozic
020cd914af Server: Trying to build ARM64 Docker image 2025-03-30 19:38:18 +02:00
Laurent Cozic
8dcfc81cee Server v3.3.11 2025-03-30 19:14:52 +02:00
Laurent Cozic
77048caeeb Server: Trying to build ARM64 Docker image 2025-03-30 19:14:32 +02:00
Laurent Cozic
3438c58ec4 Server v3.3.10 2025-03-30 18:51:28 +02:00
Laurent Cozic
e74d5e7c23 Server: Trying to build ARM64 Docker image 2025-03-30 18:50:57 +02:00
Laurent Cozic
c921976e9d Server v3.3.9 2025-03-30 17:15:13 +02:00
Laurent Cozic
c8a2802181 Chore: Fixed dictionary 2025-03-30 17:14:51 +02:00
Laurent Cozic
0f08688ce8 Server v3.3.8 2025-03-30 16:32:47 +02:00
Laurent Cozic
f5f7b1eb60 Server: Trying to build ARM64 Docker image (#12033) 2025-03-30 16:31:45 +02:00
Joplin Bot
9d73e583b9 Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-03-30 12:56:53 +00:00
Laurent Cozic
611e8df81a Server v3.3.7 2025-03-30 12:56:47 +02:00
Laurent Cozic
b0c9c4c8ce Server: Trying to build ARM64 Docker image 2025-03-30 12:56:05 +02:00
github-actions[bot]
dad0b9448f @forsaken628 has signed the CLA in laurent22/joplin#12032 2025-03-30 10:47:29 +00:00
Laurent Cozic
c524f5a6b5 Server v3.3.6 2025-03-30 12:06:08 +02:00
Laurent Cozic
ee2b186752 Server: Trying to build ARM64 Docker image 2025-03-30 12:05:22 +02:00
Maxim Medvedev
76a3250707 Server: buildx support for Docker images (#11582)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-03-30 12:01:39 +02:00
Laurent Cozic
e8144f9ee2 Doc: Update sponsors 2025-03-30 11:15:19 +02:00
Laurent Cozic
47f6b1ce33 Chore: Remove JS files 2025-03-30 10:32:42 +02:00
Laurent Cozic
f0121e7799 Server v3.3.5 2025-03-30 10:32:01 +02:00
Laurent Cozic
0acb14d0bf Server: Trying to build Joplin Server Docker image for ARM64 (#12030) 2025-03-30 10:31:16 +02:00
Henry Heino
18ebd16428 iOS: Fixes #11711: Fix Markdown toolbar partially covered by keyboard on some iOS devices (#12027) 2025-03-29 13:46:37 +01:00
Nick
6bc1965ec0 Update Swedish translation (#12023) 2025-03-28 18:08:52 +01:00
Henry Heino
1fed875140 Mobile: Plugins: Add command to hide the plugin panel viewer (#12018) 2025-03-28 13:40:36 +01:00
Henry Heino
98fe57e87a Desktop: Resolves #11903: Accessibility: Remove redundant accessibility information from sidebar notebooks (#12020) 2025-03-28 13:40:22 +01:00
Henry Heino
5bcb2531f4 Desktop: Rich Text Editor: Add setting to allow disabling auto-format (#12022) 2025-03-28 13:40:09 +01:00
Laurent Cozic
f6d69ef702 Desktop: Fixes #12021: App without a profile directory cannot start 2025-03-27 22:14:37 +01:00
mrjo118
1f05a3212f Mobile: Resolves #10883: Remove slider component module and replace integer settings with new validated component (#11822) 2025-03-27 22:01:09 +01:00
Henry Heino
ff0321e906 Chore: Improve note list test reliability (#12019) 2025-03-27 21:58:43 +01:00
Henry Heino
6b881b226e Web: Fix crash on opening settings (#12017) 2025-03-27 21:58:11 +01:00
Henry Heino
6a26ec8105 Android: Voice typing: Disable "Download update" button while downloading an updated model (#12015) 2025-03-27 21:57:38 +01:00
Henry Heino
5966402d8b Chore: Make useFormNote.test.ts less likely to fail in CI (#12014) 2025-03-27 21:57:28 +01:00
Henry Heino
5198b598bb Android: Voice typing: Improve transcription at the end of paragraphs (#12013) 2025-03-27 21:57:05 +01:00
Henry Heino
0a4c97c631 Android: Voice typing: Performance: Disable preview generation logic (#12008) 2025-03-27 21:56:56 +01:00
Laurent Cozic
0bc62aa05e All: Fixes #11934: Restoring a note which was in a deleted notebook (#12016) 2025-03-27 21:55:36 +01:00
Joplin Bot
44d1e9e3ca Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-03-27 18:47:07 +00:00
Henry Heino
0cef6cc611 Android: Voice typing: Fix incorrectly-calculated audio length (#12012) 2025-03-27 18:57:55 +01:00
Laurent Cozic
3e5acfbc09 Doc: update sponsors 2025-03-27 18:31:54 +01:00
github-actions[bot]
fe20ab11a1 @ost-ch has signed the CLA in laurent22/joplin#12011 2025-03-27 15:35:35 +00:00
Henry Heino
675f55d152 Desktop: Resolves #11741: Accessibility: Add screen reader announcements when toggling the note list and/or sidebar (#11776) 2025-03-27 15:53:59 +01:00
Henry Heino
b1edb84b49 Android: Voice typing: Default to a larger model (#12009) 2025-03-27 15:44:19 +01:00
mrjo118
ece7a4ccf0 Mobile: Fixes #11820: Fix cursor moves to incorrect position when revising TextInput value (#11821) 2025-03-27 15:33:00 +01:00
PARAMESH T S
cfd98d2723 Desktop, Mobile: Fixes #11971: Changing the type of one list changes it for all the lists (#11986) 2025-03-27 15:25:12 +01:00
Henry Heino
2a17301a9f Chore: Testing: Attach Playwright logs to CI results (#12007) 2025-03-25 21:07:27 +01:00
Laurent Cozic
e3762dc3f8 Desktop: Resolves #11992: Multiple instances: Secure local server (#11999) 2025-03-25 19:48:11 +01:00
Henry Heino
0959a19d65 Chore: Desktop: Fix tests in IPC pull request (#12004) 2025-03-25 18:50:16 +01:00
Henry Heino
93219575b4 Chore: Desktop: Update Playwright, allow debugging Playwright tests from VSCode (#12003) 2025-03-25 12:12:38 +01:00
Laurent Cozic
baaeea1307 Server: Fixes #11910: Disable faulty dark theme to prevent published notes from being unreadable 2025-03-25 12:06:00 +01:00
Joplin Bot
de6c5d448f Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-03-24 18:45:45 +00:00
Laurent Cozic
cc2cf5f521 Doc: added sponsor 2025-03-24 15:34:58 +01:00
Laurent Cozic
ef513862a9 Plugins: Add setting.globalValues and deprecate setting.globalValue 2025-03-23 13:14:02 +01:00
Joplin Bot
d07f3b5f16 Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-03-22 02:03:52 +00:00
Laurent Cozic
a6079869bc Desktop: Fixes #11975: Regression: Restarting app is broken 2025-03-21 23:26:54 +01:00
Laurent Cozic
2fdbb22481 Android 3.3.4 2025-03-21 19:35:20 +01:00
Henry Heino
c5bb88ddf4 Android: Resolves #11955: Voice typing: Improve re-download button UI (#11979) 2025-03-21 19:00:49 +01:00
Henry Heino
5d7c78c361 Android: Voice typing: Improve processing with larger models (#11983) 2025-03-21 19:00:38 +01:00
Laurent Cozic
808eb7d49a Chore: Resolves #11993: Display a message explaining why the app did not start in dev mode 2025-03-21 12:08:09 +01:00
Laurent Cozic
2142373fff Desktop: Fixes #11989: Joplin became unusably slow on MacOS due to incorrect detection of architecture 2025-03-21 11:56:13 +01:00
Laurent Cozic
20f7f37b49 Update config.yml 2025-03-20 17:05:09 +01:00
Laurent Cozic
04fc634092 Server: Added links to social networks 2025-03-18 16:41:58 +00:00
Laurent Cozic
d40c9d3ff9 Doc: Add YouTube link to doc 2025-03-18 16:41:58 +00:00
marph91
224b4f619a Docs: update "Importing from other applications" help section (#11969)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-03-16 22:22:57 +00:00
Joplin Bot
88d1d4b7d1 Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-03-16 12:54:34 +00:00
Laurent Cozic
6a22ffbcb1 iOS 13.3.2 2025-03-16 11:47:09 +00:00
Laurent Cozic
d735cf64e0 Chore: Fixing iOS build 2025-03-16 11:46:09 +00:00
Laurent Cozic
d7d6fd5ccd Android 3.3.3 2025-03-16 10:49:01 +00:00
Laurent Cozic
23254e6ffd Desktop release v3.3.3 2025-03-16 10:25:28 +00:00
Meow
eb8bfd5aec iOS: Re-Add iOS Dark Icon (#11943)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-03-16 10:21:58 +00:00
Laurent Cozic
cb5ffd968d Desktop: Add support for multiple instances (#11963) 2025-03-16 10:18:32 +00:00
Henry Heino
7b2b3a4f80 Chore: Increase Playwright test timeouts and reduce test flakiness (#11970) 2025-03-15 23:50:53 +00:00
Henry Heino
cbfe109c41 iOS: Accessibility: Fix focus gets stuck on "Attach" in the note actions menu (#11958) 2025-03-15 13:20:23 +00:00
Henry Heino
c8b01d11d6 Mobile: Accessibility: Make default modal close button accessible (#11957) 2025-03-15 13:20:11 +00:00
Henry Heino
b042395fd1 Web: Accessibility: Fix "sort notes by" button is sometimes not keyboard focusable (#11959) 2025-03-15 13:20:01 +00:00
Henry Heino
ba5ad18093 Desktop: Accessibility: Add a menu item that moves focus to the note viewer (#11967) 2025-03-15 13:19:47 +00:00
Henry Heino
ff15232a10 Android: Resolves #11956: Voice typing: Transcribe more unprocessed audio after pressing "done" (#11960) 2025-03-15 12:29:05 +00:00
Henry Heino
5a6e72197a Desktop: Upgrade to Electron 35.0.1 (#11968) 2025-03-15 12:01:18 +00:00
summoner001
de555b6871 All: Translation: Update hu_HU.po (#11962) 2025-03-14 13:00:27 -04:00
cro
9a2548a5e3 Update webdav.md (#11951) 2025-03-14 00:15:51 +00:00
Henry Heino
107996289f Mobile: Accessibility: Fix missing label on note actions menu dismiss button (#11954) 2025-03-13 19:56:19 +00:00
Henry Heino
c3c0101555 Android: Voice typing: Fix potential output duplication when finalizing voice typing (#11953) 2025-03-13 19:14:06 +00:00
github-actions[bot]
45ee02036f @pectum83 has signed the CLA in laurent22/joplin#11951 2025-03-13 12:12:32 +00:00
Laurent Cozic
64f3dae8cc Doc: Fixed sponsor "alt" tag on website main page 2025-03-12 21:03:20 +00:00
github-actions[bot]
0909479b7f @jrb80 has signed the CLA in laurent22/joplin#11948 2025-03-10 17:21:12 +00:00
Henry Heino
a39b51cc97 Docs: Fix website build (#11947) 2025-03-10 16:40:35 +00:00
Henry Heino
10bb8ef1a9 Docs: Resolves #11860: Add guidelines for making new contributions accessible (#11863) 2025-03-08 12:12:00 +00:00
Laurent Cozic
60ba22b233 Doc: Fix "How to" documents 2025-03-08 12:09:37 +00:00
Henry Heino
1bfd997be2 Docs: Accessibility: Document how to use the app with a screen reader (#11897) 2025-03-08 11:55:36 +00:00
Henry Heino
81e4a7fb74 Desktop: Fix adding tags to a note through drag-and-drop (#11911) 2025-03-08 11:54:24 +00:00
Henry Heino
360568d325 Desktop: Fixes #11894: Fix ctrl-p doesn't open the goto anything dialog in the Rich Text Editor (#11926) 2025-03-08 11:54:12 +00:00
Henry Heino
1aa0f11670 Mobile: Accessibility: Improve focus handling in the note actions menu and modal dialogs (#11929) 2025-03-08 11:53:06 +00:00
Henry Heino
0430ccb3e7 iOS: Accessibility: Fix plugins can't be installed using VoiceOver (#11931) 2025-03-08 11:52:03 +00:00
av
c0d6c1eb0b Tools: add giflib to devbox dependencies (#11938) 2025-03-08 11:51:48 +00:00
Amine Zouaoui
215f09d73c Desktop: Resolves #11696: Add "Disable synchronisation" to Joplin Cloud prompt message (#11705) 2025-03-08 11:50:30 +00:00
pedr
1f192696de Desktop: Fixes #11939: Import audio from OneNote as file links (#11942) 2025-03-08 11:49:20 +00:00
Laurent Cozic
ab86b95fad Desktop, Mobile, Cli: Add setting migration for ocr.enabled 2025-03-07 15:47:44 +00:00
Laurent Cozic
0f07c0f53a Desktop, Mobile: Fixes #11673: Make tab size consistent between Markdown editor and viewer (and RTE) (#11940)
Co-authored-by: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com>
2025-03-07 15:42:32 +00:00
Dmitriy Q
a6d04c4781 All: Translation: Update ru_RU.po (#11937) 2025-03-06 23:44:58 -05:00
PARAMESH T S
bc27f47881 Desktop: Fixes #11923: Sharing a notebook with nobody prints "No user with ID public_key" (#11932)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-03-06 15:58:10 +00:00
github-actions[bot]
f381c91783 @DMClVG has signed the CLA in laurent22/joplin#11938 2025-03-06 12:54:17 +00:00
Henry Heino
d1d75449f5 Chore: CI: Upgrade Linux actions runner to Ubuntu 22.04 (#11927) 2025-03-06 00:19:57 +00:00
github-actions[bot]
79dc12ae0a @Paramesh-T-S has signed the CLA in laurent22/joplin#11932 2025-03-05 19:47:30 +00:00
Laurent Cozic
bbea5388ed Doc: Describe how to migrate from Joplin Cloud Basic or Pro to Team 2025-03-05 19:42:47 +00:00
Laurent Cozic
99e773855e Chore: Improve error message when website does not build 2025-03-05 18:57:02 +00:00
Laurent Cozic
55b73347e5 Doc: Fixed downloading Apple Silicon version on Download page 2025-03-05 18:56:44 +00:00
Laurent Cozic
7e8dee4906 Desktop: Added keyboard shortcut and menu item for toggleEditorPlugin command 2025-03-05 00:43:39 +00:00
Laurent Cozic
69fb1ab104 Chore: Fixed test that fails on fast enough computers 2025-03-05 00:43:39 +00:00
Helmut K. C. Tessarek
67ae0ea2d1 Desktop: improve download in install script (#11921) 2025-03-04 19:06:31 -05:00
Celestial.y
cdb61b922b All: Translation: Update zh_CN.po (#11922) 2025-03-04 18:52:12 -05:00
pedr
da80443796 Chore: Remove file created during automated test (#11915) 2025-03-04 11:58:57 +00:00
Henry Heino
1924dd31d2 Desktop: Make "toggle all folders" button also expand the folder list (#11917) 2025-03-04 11:58:31 +00:00
Henry Heino
b831d8c068 Desktop: Accessibility: Improve "toggle all notebooks" accessibility (#11918) 2025-03-04 11:57:11 +00:00
github-actions[bot]
9a697a0e34 @clsty has signed the CLA in laurent22/joplin#11922 2025-03-04 08:21:05 +00:00
Joplin Bot
4ad1b49769 Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-03-04 02:03:55 +00:00
klxiang
0d6c1067e3 All: Translation: Update zh_CN.po (#11920) 2025-03-03 21:01:55 -05:00
Eric Duarte
0bdc38a6be All: Translation: Update es_ES.po (#11913) 2025-03-03 20:43:27 -05:00
github-actions[bot]
7735fc9759 @klxiang has signed the CLA in laurent22/joplin#11920 2025-03-04 01:36:12 +00:00
Laurent Cozic
5c35569b5b Android 3.3.2 2025-03-03 22:36:37 +00:00
Laurent Cozic
5f02af9724 Server v3.3.4 2025-03-03 22:29:46 +00:00
Henry Heino
975f16d21c Server: Security: Improve request validation in default route (#11916) 2025-03-03 22:29:05 +00:00
github-actions[bot]
54428f5034 @ericdq has signed the CLA in laurent22/joplin#11913 2025-03-03 18:28:03 +00:00
github-actions[bot]
e6ef0d45c6 @sudokillnine has signed the CLA in laurent22/joplin#11889 2025-02-26 17:40:23 +00:00
github-actions[bot]
ebe9c38f9e @teardroponfire has signed the CLA in laurent22/joplin#11890 2025-02-26 17:35:44 +00:00
github-actions[bot]
ccebb9696e @itsanmol27 has signed the CLA in laurent22/joplin#11886 2025-02-26 16:02:40 +00:00
github-actions[bot]
16ba072436 @narFnarF has signed the CLA in laurent22/joplin#11867 2025-02-20 15:29:10 +00:00
github-actions[bot]
5a81e2ce84 @ttcchhmm has signed the CLA in laurent22/joplin#11865 2025-02-20 10:38:13 +00:00
github-actions[bot]
8e407dd003 @j-scheitler1 has signed the CLA in laurent22/joplin#11862 2025-02-20 00:33:03 +00:00
github-actions[bot]
6118c572ac @rabbabansh has signed the CLA in laurent22/joplin#11857 2025-02-18 20:49:29 +00:00
github-actions[bot]
fd8393b2f9 @camilajenny has signed the CLA in laurent22/joplin#11853 2025-02-17 22:46:06 +00:00
github-actions[bot]
15edc9fec7 @dandv has signed the CLA in laurent22/joplin#11834 2025-02-14 05:50:26 +00:00
github-actions[bot]
35053f2996 @rathoreSahil has signed the CLA in laurent22/joplin#11828 2025-02-12 16:07:52 +00:00
github-actions[bot]
8bd4770f33 @balaraz has signed the CLA in laurent22/joplin#11824 2025-02-11 15:38:22 +00:00
github-actions[bot]
4af459c762 @Vortrix5 has signed the CLA in laurent22/joplin#11705 2025-01-22 15:00:48 +00:00
github-actions[bot]
6ad97a01a8 @harshil048 has signed the CLA in laurent22/joplin#11695 2025-01-21 12:20:41 +00:00
github-actions[bot]
3e05b8daa0 @summoner001 has signed the CLA in laurent22/joplin#11682 2025-01-19 12:40:36 +00:00
github-actions[bot]
41cb16f5c5 @creationsofm7 has signed the CLA in laurent22/joplin#11680 2025-01-19 10:31:13 +00:00
github-actions[bot]
543ece86b0 @Sabin-Gheorghiu has signed the CLA in laurent22/joplin#11610 2025-01-13 10:19:51 +00:00
github-actions[bot]
44f98d6282 @CGHoussem has signed the CLA in laurent22/joplin#11609 2025-01-08 20:54:42 +00:00
github-actions[bot]
438bf29e5f @alan-null has signed the CLA in laurent22/joplin#11585 2025-01-06 07:34:22 +00:00
github-actions[bot]
0a1f960919 @c-n-c has signed the CLA in laurent22/joplin#11572 2025-01-03 03:24:56 +00:00
github-actions[bot]
5bc7304c63 @JasonnnW3000 has signed the CLA in laurent22/joplin#11567 2025-01-01 06:23:09 +00:00
github-actions[bot]
efec21156b @click0 has signed the CLA in laurent22/joplin#11559 2024-12-25 20:13:22 +00:00
github-actions[bot]
85747317ee @redrathnure has signed the CLA in laurent22/joplin#11554 2024-12-22 23:36:40 +00:00
github-actions[bot]
9f4a80cf89 @ARYPROGRAMMER has signed the CLA in laurent22/joplin#11551 2024-12-21 20:58:45 +00:00
github-actions[bot]
1b3ebf4fb0 @rcrisanti has signed the CLA in laurent22/joplin#11531 2024-12-17 18:57:50 +00:00
github-actions[bot]
bcd10e380b @azurelunatic has signed the CLA in laurent22/joplin#11527 2024-12-17 08:18:08 +00:00
github-actions[bot]
1bc64cfdff @RajwardhanSinghUmath has signed the CLA in laurent22/joplin#11480 2024-12-09 06:53:55 +00:00
github-actions[bot]
ab46f6decd @tnshgarg has signed the CLA in laurent22/joplin#11477 2024-12-08 07:48:55 +00:00
github-actions[bot]
49535e4a3e @ckant has signed the CLA in laurent22/joplin#11474 2024-12-06 06:14:06 +00:00
github-actions[bot]
4c35d811d6 @mshibanami has signed the CLA in laurent22/joplin#11471 2024-12-05 11:07:03 +00:00
github-actions[bot]
0dec21b2bb @awesome-pro has signed the CLA in laurent22/joplin#11435 2024-11-23 09:59:53 +00:00
github-actions[bot]
ad8c9263e8 @dodog has signed the CLA in laurent22/joplin#11433 2024-11-21 20:46:38 +00:00
github-actions[bot]
04de21a9fd @deftdawg has signed the CLA in laurent22/joplin#11406 2024-11-18 17:07:44 +00:00
github-actions[bot]
d266ec92c4 @dhakarRaghu has signed the CLA in laurent22/joplin#11388 2024-11-13 17:28:13 +00:00
github-actions[bot]
63fce9cfb0 @AdrienPoupa has signed the CLA in laurent22/joplin#11372 2024-11-11 01:15:47 +00:00
github-actions[bot]
7c50e00fb4 @laurent22 has signed the CLA in laurent22/joplin#11345 2024-11-09 22:07:06 +00:00
828 changed files with 187916 additions and 43474 deletions

View File

@@ -62,6 +62,7 @@ packages/app-mobile/locales
packages/app-mobile/node_modules
packages/app-mobile/pluginAssets/
packages/fork-*
!packages/fork-uslug
packages/default-plugins/plugin-base-repo/
packages/default-plugins/plugin-sources/
packages/htmlpack/dist/
@@ -159,7 +160,9 @@ packages/app-desktop/commands/exportNotes.js
packages/app-desktop/commands/focusElement.js
packages/app-desktop/commands/index.js
packages/app-desktop/commands/openNoteInNewWindow.js
packages/app-desktop/commands/openPrimaryAppInstance.js
packages/app-desktop/commands/openProfileDirectory.js
packages/app-desktop/commands/openSecondaryAppInstance.js
packages/app-desktop/commands/replaceMisspelling.js
packages/app-desktop/commands/restoreNoteRevision.js
packages/app-desktop/commands/startExternalEditing.js
@@ -206,6 +209,7 @@ packages/app-desktop/gui/InlineCombobox.js
packages/app-desktop/gui/ItemList.js
packages/app-desktop/gui/JoplinCloudConfigScreen.js
packages/app-desktop/gui/JoplinCloudLoginScreen.js
packages/app-desktop/gui/JoplinCloudSignUpCallToAction.js
packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.js
packages/app-desktop/gui/KeymapConfig/ShortcutRecorder.js
packages/app-desktop/gui/KeymapConfig/styles/index.js
@@ -264,6 +268,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useKeyboardRefocusHan
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useLinkTooltips.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useTabIndenter.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useTextPatternsLookup.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useWebViewApi.js
packages/app-desktop/gui/NoteEditor/NoteEditor.js
packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.js
@@ -272,6 +277,7 @@ packages/app-desktop/gui/NoteEditor/WarningBanner/BannerContent.js
packages/app-desktop/gui/NoteEditor/WarningBanner/WarningBanner.js
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteBody.js
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteTitle.js
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteViewer.js
packages/app-desktop/gui/NoteEditor/commands/focusElementToolbar.js
packages/app-desktop/gui/NoteEditor/commands/index.js
packages/app-desktop/gui/NoteEditor/commands/pasteAsText.js
@@ -353,13 +359,16 @@ packages/app-desktop/gui/NoteSearchBar.js
packages/app-desktop/gui/NoteStatusBar.js
packages/app-desktop/gui/NoteTextViewer.js
packages/app-desktop/gui/NoteToolbar/NoteToolbar.js
packages/app-desktop/gui/NotyfContext.js
packages/app-desktop/gui/OneDriveLoginScreen.js
packages/app-desktop/gui/PasswordInput/LabelledPasswordInput.js
packages/app-desktop/gui/PasswordInput/PasswordInput.js
packages/app-desktop/gui/PasswordInput/types.js
packages/app-desktop/gui/PdfViewer.js
packages/app-desktop/gui/PluginNotification/PluginNotification.js
packages/app-desktop/gui/PopupNotification/NotificationItem.js
packages/app-desktop/gui/PopupNotification/PopupNotificationList.js
packages/app-desktop/gui/PopupNotification/PopupNotificationProvider.js
packages/app-desktop/gui/PopupNotification/types.js
packages/app-desktop/gui/PromptDialog.js
packages/app-desktop/gui/ResizableLayout/LayoutItemContainer.js
packages/app-desktop/gui/ResizableLayout/MoveButtons.js
@@ -421,6 +430,7 @@ packages/app-desktop/gui/ToolbarBase.js
packages/app-desktop/gui/ToolbarButton/ToolbarButton.js
packages/app-desktop/gui/ToolbarSpace.js
packages/app-desktop/gui/TrashNotification/TrashNotification.js
packages/app-desktop/gui/TrashNotification/TrashNotificationMessage.js
packages/app-desktop/gui/UpdateNotification/UpdateNotification.js
packages/app-desktop/gui/WindowCommandsAndDialogs/AppDialogs.js
packages/app-desktop/gui/WindowCommandsAndDialogs/ModalMessageOverlay.js
@@ -525,8 +535,8 @@ packages/app-desktop/integration-tests/simpleBackup.spec.js
packages/app-desktop/integration-tests/util/activateMainMenuItem.js
packages/app-desktop/integration-tests/util/createStartupArgs.js
packages/app-desktop/integration-tests/util/extendedExpect.js
packages/app-desktop/integration-tests/util/firstNonDevToolsWindow.js
packages/app-desktop/integration-tests/util/getImageSourceSize.js
packages/app-desktop/integration-tests/util/getMainWindow.js
packages/app-desktop/integration-tests/util/setDarkMode.js
packages/app-desktop/integration-tests/util/setFilePickerResponse.js
packages/app-desktop/integration-tests/util/setMessageBoxResponse.js
@@ -547,13 +557,16 @@ packages/app-desktop/services/plugins/UserWebview.js
packages/app-desktop/services/plugins/UserWebviewDialog.js
packages/app-desktop/services/plugins/UserWebviewDialogButtonBar.js
packages/app-desktop/services/plugins/hooks/useContentSize.js
packages/app-desktop/services/plugins/hooks/useFormData.js
packages/app-desktop/services/plugins/hooks/useHtmlLoader.js
packages/app-desktop/services/plugins/hooks/useMessageHandler.js
packages/app-desktop/services/plugins/hooks/useScriptLoader.js
packages/app-desktop/services/plugins/hooks/useSubmitHandler.js
packages/app-desktop/services/plugins/hooks/useThemeCss.test.js
packages/app-desktop/services/plugins/hooks/useThemeCss.js
packages/app-desktop/services/plugins/hooks/useViewIsReady.js
packages/app-desktop/services/plugins/hooks/useWebviewToPluginMessages.js
packages/app-desktop/services/plugins/types.js
packages/app-desktop/services/restart.js
packages/app-desktop/services/sortOrder/PerFolderSortOrderService.test.js
packages/app-desktop/services/sortOrder/PerFolderSortOrderService.js
@@ -581,6 +594,7 @@ packages/app-desktop/utils/restartInSafeModeFromMain.test.js
packages/app-desktop/utils/restartInSafeModeFromMain.js
packages/app-desktop/utils/window/types.js
packages/app-mobile/PluginAssetsLoader.js
packages/app-mobile/commands/dismissPluginPanels.js
packages/app-mobile/commands/index.js
packages/app-mobile/commands/newNote.test.js
packages/app-mobile/commands/newNote.js
@@ -590,6 +604,7 @@ packages/app-mobile/commands/scrollToHash.js
packages/app-mobile/commands/util/goToNote.js
packages/app-mobile/commands/util/showResource.js
packages/app-mobile/components/BetaChip.js
packages/app-mobile/components/BottomDrawer.js
packages/app-mobile/components/CameraView/ActionButtons.js
packages/app-mobile/components/CameraView/Camera/index.jest.js
packages/app-mobile/components/CameraView/Camera/index.js
@@ -683,15 +698,22 @@ packages/app-mobile/components/SelectDateTimeDialog.js
packages/app-mobile/components/SideMenu.js
packages/app-mobile/components/SideMenuContentNote.js
packages/app-mobile/components/TextInput.js
packages/app-mobile/components/ToggleSpaceButton.js
packages/app-mobile/components/accessibility/AccessibleModalMenu.js
packages/app-mobile/components/accessibility/AccessibleView.test.js
packages/app-mobile/components/accessibility/AccessibleView.js
packages/app-mobile/components/accessibility/FocusControl/AutoFocusProvider.js
packages/app-mobile/components/accessibility/FocusControl/FocusControl.js
packages/app-mobile/components/accessibility/FocusControl/FocusControlProvider.js
packages/app-mobile/components/accessibility/FocusControl/MainAppContent.js
packages/app-mobile/components/accessibility/FocusControl/ModalWrapper.js
packages/app-mobile/components/accessibility/FocusControl/types.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/buttons/FloatingActionButton.js
packages/app-mobile/components/buttons/LabelledIconButton.js
packages/app-mobile/components/buttons/MultiTouchableOpacity.js
packages/app-mobile/components/buttons/TextButton.js
packages/app-mobile/components/buttons/index.js
packages/app-mobile/components/getResponsiveValue.test.js
@@ -738,8 +760,11 @@ packages/app-mobile/components/screens/ConfigScreen/SectionSelector/SectionTab.j
packages/app-mobile/components/screens/ConfigScreen/SectionSelector/index.js
packages/app-mobile/components/screens/ConfigScreen/SettingComponent.js
packages/app-mobile/components/screens/ConfigScreen/SettingItem.js
packages/app-mobile/components/screens/ConfigScreen/SettingTextInput.js
packages/app-mobile/components/screens/ConfigScreen/SettingsButton.js
packages/app-mobile/components/screens/ConfigScreen/SettingsToggle.js
packages/app-mobile/components/screens/ConfigScreen/ValidatedIntegerInput.test.js
packages/app-mobile/components/screens/ConfigScreen/ValidatedIntegerInput.js
packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.js
packages/app-mobile/components/screens/ConfigScreen/plugins/EnablePluginSupportPage.js
packages/app-mobile/components/screens/ConfigScreen/plugins/InstalledPluginBox.js
@@ -779,7 +804,9 @@ packages/app-mobile/components/screens/Note/commands/setTags.js
packages/app-mobile/components/screens/Note/commands/toggleVisiblePanes.js
packages/app-mobile/components/screens/Note/types.js
packages/app-mobile/components/screens/NoteTagsDialog.js
packages/app-mobile/components/screens/Notes.js
packages/app-mobile/components/screens/Notes/NewNoteButton.test.js
packages/app-mobile/components/screens/Notes/NewNoteButton.js
packages/app-mobile/components/screens/Notes/Notes.js
packages/app-mobile/components/screens/SearchScreen/SearchResults.js
packages/app-mobile/components/screens/SearchScreen/index.js
packages/app-mobile/components/screens/ShareManager/AcceptedShareItem.js
@@ -834,6 +861,7 @@ packages/app-mobile/utils/createRootStyle.js
packages/app-mobile/utils/database-driver-react-native.js
packages/app-mobile/utils/database-driver-react-native.web.js
packages/app-mobile/utils/debounce.js
packages/app-mobile/utils/focusView.js
packages/app-mobile/utils/fs-driver/constants.js
packages/app-mobile/utils/fs-driver/fs-driver-rn.js
packages/app-mobile/utils/fs-driver/fs-driver-rn.web.js
@@ -846,9 +874,10 @@ packages/app-mobile/utils/fs-driver/testUtil/createFilesFromPathRecord.js
packages/app-mobile/utils/fs-driver/testUtil/verifyDirectoryMatches.js
packages/app-mobile/utils/getPackageInfo.js
packages/app-mobile/utils/getVersionInfoText.js
packages/app-mobile/utils/hooks/useKeyboardVisible.js
packages/app-mobile/utils/hooks/useKeyboardState.js
packages/app-mobile/utils/hooks/useOnLongPressProps.js
packages/app-mobile/utils/hooks/useReduceMotionEnabled.js
packages/app-mobile/utils/hooks/useSafeAreaPadding.js
packages/app-mobile/utils/image/fileToImage.web.js
packages/app-mobile/utils/image/getImageDimensions.js
packages/app-mobile/utils/image/resizeImage.js
@@ -857,6 +886,7 @@ packages/app-mobile/utils/injectedJs.js
packages/app-mobile/utils/ipc/RNToWebViewMessenger.js
packages/app-mobile/utils/ipc/WebViewToRNMessenger.js
packages/app-mobile/utils/lockToSingleInstance.js
packages/app-mobile/utils/makeShowMessageBox.test.js
packages/app-mobile/utils/makeShowMessageBox.js
packages/app-mobile/utils/pickDocument.js
packages/app-mobile/utils/polyfills/bufferPolyfill.js
@@ -897,6 +927,8 @@ packages/editor/CodeMirror/editorCommands/duplicateLine.js
packages/editor/CodeMirror/editorCommands/editorCommands.js
packages/editor/CodeMirror/editorCommands/insertLineAfter.test.js
packages/editor/CodeMirror/editorCommands/insertLineAfter.js
packages/editor/CodeMirror/editorCommands/jumpToHash.test.js
packages/editor/CodeMirror/editorCommands/jumpToHash.js
packages/editor/CodeMirror/editorCommands/sortSelectedLines.test.js
packages/editor/CodeMirror/editorCommands/sortSelectedLines.js
packages/editor/CodeMirror/editorCommands/supportsCommand.js
@@ -912,6 +944,8 @@ packages/editor/CodeMirror/markdown/computeSelectionFormatting.test.js
packages/editor/CodeMirror/markdown/computeSelectionFormatting.js
packages/editor/CodeMirror/markdown/decoratorExtension.test.js
packages/editor/CodeMirror/markdown/decoratorExtension.js
packages/editor/CodeMirror/markdown/insertNewlineContinueMarkup.test.js
packages/editor/CodeMirror/markdown/insertNewlineContinueMarkup.js
packages/editor/CodeMirror/markdown/markdownCommands.bulletedVsChecklist.test.js
packages/editor/CodeMirror/markdown/markdownCommands.test.js
packages/editor/CodeMirror/markdown/markdownCommands.toggleList.test.js
@@ -975,6 +1009,8 @@ packages/fork-htmlparser2/src/__tests__/events.js
packages/fork-htmlparser2/src/__tests__/stream.js
packages/fork-htmlparser2/src/index.spec.js
packages/fork-htmlparser2/src/index.js
packages/fork-uslug/lib/uslug.test.js
packages/fork-uslug/lib/uslug.js
packages/generator-joplin/generators/app/templates/api/index.js
packages/generator-joplin/generators/app/templates/api/noteListType.js
packages/generator-joplin/generators/app/templates/api/types.js
@@ -1026,6 +1062,7 @@ packages/lib/commands/renderMarkup.test.js
packages/lib/commands/renderMarkup.js
packages/lib/commands/showEditorPlugin.js
packages/lib/commands/synchronize.js
packages/lib/commands/toggleAllFolders.test.js
packages/lib/commands/toggleAllFolders.js
packages/lib/commands/toggleEditorPlugin.js
packages/lib/components/EncryptionConfigScreen/utils.test.js
@@ -1063,6 +1100,8 @@ packages/lib/fs-driver-base.js
packages/lib/fs-driver-node.js
packages/lib/fsDriver.test.js
packages/lib/geolocation-node.js
packages/lib/getAppName.test.js
packages/lib/getAppName.js
packages/lib/hooks/useAsyncEffect.js
packages/lib/hooks/useElementSize.js
packages/lib/hooks/useEventListener.js
@@ -1123,6 +1162,7 @@ packages/lib/models/settings/settingValidations.js
packages/lib/models/settings/types.js
packages/lib/models/utils/areAllFoldersCollapsed.test.js
packages/lib/models/utils/areAllFoldersCollapsed.js
packages/lib/models/utils/getCanBeCollapsedFolderIds.test.js
packages/lib/models/utils/getCanBeCollapsedFolderIds.js
packages/lib/models/utils/getCollator.js
packages/lib/models/utils/getConflictFolderId.js
@@ -1595,6 +1635,7 @@ packages/tools/release-electron.js
packages/tools/release-ios.js
packages/tools/release-plugin-repo-cli.js
packages/tools/release-server.js
packages/tools/saveClaConsentRecords.js
packages/tools/setupNewRelease.js
packages/tools/spellcheck.js
packages/tools/tagServerLatest.js

View File

@@ -57,6 +57,8 @@ module.exports = {
'tinymce': 'readonly',
'JSX': 'readonly',
'NodeJS': 'readonly',
},
'parserOptions': {
'ecmaVersion': 2018,
@@ -309,7 +311,7 @@ module.exports = {
selector: 'interface',
format: null,
'filter': {
'regex': '^(RSA|RSAKeyPair)$',
'regex': '^(RSA|RSAKeyPair|iOS.*)$',
'match': true,
},
},

View File

@@ -1,8 +1,8 @@
blank_issues_enabled: false
blank_issues_enabled: true
contact_links:
- 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
about: Please ask for help here

View File

@@ -0,0 +1,34 @@
#!/bin/bash
VERSION=$(echo "$GIT_TAG_NAME" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
echo "GIT_TAG_NAME=$GIT_TAG_NAME"
echo "VERSION=$VERSION"
echo "SERVER_TAG_PREFIX=$SERVER_TAG_PREFIX"
echo "SERVER_REPOSITORY=$SERVER_REPOSITORY"
# Check if it's a server release, otherwise exit
if [[ $GIT_TAG_NAME != $SERVER_TAG_PREFIX-* ]]; then
exit 0
fi
docker manifest inspect $SERVER_REPOSITORY:arm64-$VERSION > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "Image $SERVER_REPOSITORY:arm64-$VERSION does not exist on the remote registry."
exit 0
fi
docker manifest inspect $SERVER_REPOSITORY:amd64-$VERSION > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "Image $SERVER_REPOSITORY:amd64-$VERSION does not exist on the remote registry."
exit 0
fi
docker manifest create $SERVER_REPOSITORY:$VERSION \
$SERVER_REPOSITORY:arm64-$VERSION \
$SERVER_REPOSITORY:amd64-$VERSION
docker manifest annotate $SERVER_REPOSITORY:$VERSION $SERVER_REPOSITORY:arm64-$VERSION --arch arm64
docker manifest annotate $SERVER_REPOSITORY:$VERSION $SERVER_REPOSITORY:amd64-$VERSION --arch amd64
docker manifest push $SERVER_REPOSITORY:$VERSION

View File

@@ -35,6 +35,8 @@ else
IS_MACOS=1
fi
DOCKER_IMAGE_PLATFORM="linux/amd64"
# Tests can randomly fail in some cases, so only run them when not publishing
# a release
RUN_TESTS=0
@@ -43,10 +45,33 @@ if [ "$IS_SERVER_RELEASE" = 0 ] && [ "$IS_DESKTOP_RELEASE" = 0 ]; then
RUN_TESTS=1
fi
if [ "$RUNNER_ARCH" == "ARM64" ] && [ "$IS_SERVER_RELEASE" == "0" ]; then
# We exit now because nothing works properly with the ARM64 architecture.
# We only proceed if building the server image.
echo "Running on ARM64 and not trying to build server image - early exit"
exit 0
fi
if [ "$RUNNER_ARCH" == "ARM64" ]; then
# Canvas is only needed for tests and it doesn't build in ARM64 so remove it
RUN_TESTS=0
cd "$ROOT_DIR/packages/lib"
yarn remove canvas
cd "$ROOT_DIR"
DOCKER_IMAGE_PLATFORM="linux/arm64"
# Delete certain directories because `yarn install` will fail on ARM64.
rm -rf app-desktop
rm -rf app-mobile
fi
# =============================================================================
# Print environment
# =============================================================================
echo "RUNNER_OS=$RUNNER_OS"
echo "RUNNER_ARCH=$RUNNER_ARCH"
echo "GITHUB_WORKFLOW=$GITHUB_WORKFLOW"
echo "GITHUB_EVENT_NAME=$GITHUB_EVENT_NAME"
echo "GITHUB_REF=$GITHUB_REF"
@@ -55,6 +80,7 @@ echo "GIT_TAG_NAME=$GIT_TAG_NAME"
echo "BUILD_SEQUENCIAL=$BUILD_SEQUENCIAL"
echo "SERVER_REPOSITORY=$SERVER_REPOSITORY"
echo "SERVER_TAG_PREFIX=$SERVER_TAG_PREFIX"
echo "DOCKER_IMAGE_PLATFORM=$DOCKER_IMAGE_PLATFORM"
echo "IS_CONTINUOUS_INTEGRATION=$IS_CONTINUOUS_INTEGRATION"
echo "IS_PULL_REQUEST=$IS_PULL_REQUEST"
@@ -277,7 +303,7 @@ if [ "$IS_DESKTOP_RELEASE" == "1" ]; then
elif [[ $IS_LINUX = 1 ]] && [ "$IS_SERVER_RELEASE" == "1" ]; then
echo "Step: Building Docker Image..."
cd "$ROOT_DIR"
yarn buildServerDocker --tag-name $GIT_TAG_NAME --push-images --repository $SERVER_REPOSITORY
yarn buildServerDocker --platform $DOCKER_IMAGE_PLATFORM --tag-name $GIT_TAG_NAME --push-images --repository $SERVER_REPOSITORY
else
echo "Step: Building but *not* publishing desktop application..."

View File

@@ -19,7 +19,7 @@ jobs:
# the below token should have repo scope and must be manually added by you in the repository's secret
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
with:
path-to-signatures: 'readme/cla_signatures.json'
path-to-signatures: 'readme/cla/signatures.json'
path-to-document: 'https://github.com/laurent22/joplin/blob/dev/readme/cla.md' # e.g. a CLA or a DCO document
# branch should not be protected
branch: 'cla_signatures'

View File

@@ -9,47 +9,12 @@ jobs:
matrix:
# Do not use unbuntu-latest because it causes `The operation was canceled` failures:
# https://github.com/actions/runner-images/issues/6709
os: [macos-13, ubuntu-20.04, windows-2019]
os: [macos-13, ubuntu-22.04, windows-2019, ubuntu-22.04-arm]
steps:
- uses: actions/checkout@v4
# Trying to fix random networking issues on Windows
# https://github.com/actions/runner-images/issues/1187#issuecomment-686735760
- name: Disable TCP/UDP offload on Windows
if: runner.os == 'Windows'
run: Disable-NetAdapterChecksumOffload -Name * -TcpIPv4 -UdpIPv4 -TcpIPv6 -UdpIPv6
- name: Disable TCP/UDP offload on Linux
if: runner.os == 'Linux'
run: sudo ethtool -K eth0 tx off rx off
- name: Disable TCP/UDP offload on macOS
if: runner.os == 'macOS'
run: |
sudo sysctl -w net.link.generic.system.hwcksum_tx=0
sudo sysctl -w net.link.generic.system.hwcksum_rx=0
# Silence apt-get update errors (for example when a module doesn't
# exist) since otherwise it will make the whole build fails, even though
# it might work without update. libsecret-1-dev is required for keytar -
# https://github.com/atom/node-keytar
- name: Install Linux dependencies
if: runner.os == 'Linux'
run: |
sudo apt-get update || true
sudo apt-get install -y gettext
sudo apt-get install -y libsecret-1-dev
sudo apt-get install -y translate-toolkit
sudo apt-get install -y rsync
# Provides a virtual display on Linux. Used for Playwright integration
# testing.
sudo apt-get install -y xvfb
- name: Install macOs dependencies
if: runner.os == 'macOS'
run: |
# Required for building the canvas package
brew install pango
- name: Setup build environment
uses: ./.github/workflows/shared/setup-build-environment
- name: Install Docker Engine
# if: runner.os == 'Linux' && startsWith(github.ref, 'refs/tags/server-v')
@@ -62,26 +27,11 @@ jobs:
sudo apt-get install -y lsb-release
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo \
"deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update || true
sudo apt-get install -y docker-ce docker-ce-cli containerd.io
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin
- uses: actions/checkout@v4
- uses: olegtarasov/get-tag@v2.1.3
- uses: dtolnay/rust-toolchain@stable
- uses: actions/setup-node@v4
with:
# We need to pin the version to 18.15, because 18.16+ fails with this error:
# https://github.com/facebook/react-native/issues/36440
node-version: '18.15.0'
cache: 'yarn'
- name: Install Yarn
run: |
# https://yarnpkg.com/getting-started/install
corepack enable
# Login to Docker only if we're on a server release tag. If we run this on
# a pull request it will fail because the PR doesn't have access to
# secrets
@@ -91,15 +41,6 @@ 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@v5
with:
python-version: '3.11'
- name: Run tests, build and publish Linux and macOS apps
if: runner.os == 'Linux' || runner.os == 'macOs'
env:
@@ -143,6 +84,15 @@ jobs:
run: |
yarn install && cd packages/app-desktop && yarn dist --publish=never
- name: Publish Docker manifest
if: runner.os == 'Linux'
env:
SERVER_REPOSITORY: joplin/server
SERVER_TAG_PREFIX: server
run: |
chmod 700 "${GITHUB_WORKSPACE}/.github/scripts/publish_docker_manifest.sh"
"${GITHUB_WORKSPACE}/.github/scripts/publish_docker_manifest.sh"
ServerDockerImage:
if: github.repository == 'laurent22/joplin'
runs-on: ${{ matrix.os }}
@@ -150,7 +100,7 @@ jobs:
matrix:
# Do not use unbuntu-latest because it causes `The operation was canceled` failures:
# https://github.com/actions/runner-images/issues/6709
os: [ubuntu-20.04]
os: [ubuntu-22.04, ubuntu-22.04-arm]
steps:
- name: Install Docker Engine
@@ -162,10 +112,10 @@ jobs:
sudo apt-get install -y lsb-release
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo \
"deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update || true
sudo apt-get install -y docker-ce docker-ce-cli containerd.io
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin
- uses: actions/checkout@v4
@@ -183,17 +133,30 @@ jobs:
env:
BUILD_SEQUENCIAL: 1
run: |
if [ "$RUNNER_ARCH" == "ARM64" ]; then
DOCKER_IMAGE_PLATFORM="linux/arm64"
fi
echo "RUNNER_OS=$RUNNER_OS"
echo "RUNNER_ARCH=$RUNNER_ARCH"
echo "DOCKER_IMAGE_PLATFORM=$DOCKER_IMAGE_PLATFORM"
# Canvas is only needed for tests and it doesn't build in ARM64 so remove it
cd packages/lib
yarn remove canvas
cd ../..
yarn install
yarn buildServerDocker --tag-name server-v0.0.0 --repository joplin/server
yarn buildServerDocker --platform $DOCKER_IMAGE_PLATFORM --tag-name server-v0.0.0 --repository joplin/server
# Basic test to ensure that the created build is valid. It should exit with
# code 0 if it works.
docker run joplin/server:0.0.0-beta node dist/app.js migrate list
# code 0 if it works.
docker run joplin/server:$(dpkg --print-architecture)-0.0.0 node dist/app.js migrate list
- name: Check HTTP request
run: |
# Need to pass environment variables:
docker run -p 22300:22300 joplin/server:0.0.0-beta node dist/app.js --env dev &
docker run -p 22300:22300 joplin/server:$(dpkg --print-architecture)-0.0.0 node dist/app.js --env dev &
# Wait for server to start
sleep 30

View File

@@ -0,0 +1,72 @@
name: 'Setup build environment'
description: 'Install Joplin build dependencies'
runs:
using: 'composite'
steps:
# Trying to fix random networking issues on Windows
# https://github.com/actions/runner-images/issues/1187#issuecomment-686735760
- name: Disable TCP/UDP offload on Windows
if: runner.os == 'Windows'
shell: pwsh
run: Disable-NetAdapterChecksumOffload -Name * -TcpIPv4 -UdpIPv4 -TcpIPv6 -UdpIPv6
- name: Disable TCP/UDP offload on Linux
if: runner.os == 'Linux'
shell: bash
run: sudo ethtool -K eth0 tx off rx off
- name: Disable TCP/UDP offload on macOS
if: runner.os == 'macOS'
shell: bash
run: |
sudo sysctl -w net.link.generic.system.hwcksum_tx=0
sudo sysctl -w net.link.generic.system.hwcksum_rx=0
# Silence apt-get update errors (for example when a module doesn't
# exist) since otherwise it will make the whole build fails, even though
# it might work without update. libsecret-1-dev is required for keytar -
# https://github.com/atom/node-keytar
- name: Install Linux dependencies
if: runner.os == 'Linux'
shell: bash
run: |
sudo apt-get update || true
sudo apt-get install -y gettext
sudo apt-get install -y libsecret-1-dev
sudo apt-get install -y translate-toolkit
sudo apt-get install -y rsync
# Provides a virtual display on Linux. Used for Playwright integration
# testing.
sudo apt-get install -y xvfb
- name: Install macOs dependencies
if: runner.os == 'macOS'
shell: bash
run: |
# Required for building the canvas package
brew install pango
- uses: olegtarasov/get-tag@v2.1.3
- uses: dtolnay/rust-toolchain@stable
- uses: actions/setup-node@v4
with:
# We need to pin the version to 18.15, because 18.16+ fails with this error:
# https://github.com/facebook/react-native/issues/36440
node-version: '18.15.0'
cache: 'yarn'
- name: Install Yarn
shell: bash
run: |
# https://yarnpkg.com/getting-started/install
corepack enable
# 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@v5
with:
python-version: '3.11'

30
.github/workflows/ui-tests.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: Joplin UI tests
on: [push, pull_request]
permissions:
contents: read
jobs:
Main:
# Don't run on forks
if: github.repository == 'laurent22/joplin'
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-13, ubuntu-22.04, windows-2025]
steps:
- uses: actions/checkout@v4
- name: Setup build environment
uses: ./.github/workflows/shared/setup-build-environment
- name: Build
run: yarn install
- name: Run UI tests
shell: bash
run: |
cd ${GITHUB_WORKSPACE}/packages/app-desktop/
bash ./integration-tests/run-ci.sh
# See https://playwright.dev/docs/ci-intro#setting-up-github-actions
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report-${{ matrix.os }}
path: packages/app-desktop/playwright-report/
retention-days: 7

52
.gitignore vendored
View File

@@ -134,7 +134,9 @@ packages/app-desktop/commands/exportNotes.js
packages/app-desktop/commands/focusElement.js
packages/app-desktop/commands/index.js
packages/app-desktop/commands/openNoteInNewWindow.js
packages/app-desktop/commands/openPrimaryAppInstance.js
packages/app-desktop/commands/openProfileDirectory.js
packages/app-desktop/commands/openSecondaryAppInstance.js
packages/app-desktop/commands/replaceMisspelling.js
packages/app-desktop/commands/restoreNoteRevision.js
packages/app-desktop/commands/startExternalEditing.js
@@ -181,6 +183,7 @@ packages/app-desktop/gui/InlineCombobox.js
packages/app-desktop/gui/ItemList.js
packages/app-desktop/gui/JoplinCloudConfigScreen.js
packages/app-desktop/gui/JoplinCloudLoginScreen.js
packages/app-desktop/gui/JoplinCloudSignUpCallToAction.js
packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.js
packages/app-desktop/gui/KeymapConfig/ShortcutRecorder.js
packages/app-desktop/gui/KeymapConfig/styles/index.js
@@ -239,6 +242,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useKeyboardRefocusHan
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useLinkTooltips.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useTabIndenter.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useTextPatternsLookup.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useWebViewApi.js
packages/app-desktop/gui/NoteEditor/NoteEditor.js
packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.js
@@ -247,6 +251,7 @@ packages/app-desktop/gui/NoteEditor/WarningBanner/BannerContent.js
packages/app-desktop/gui/NoteEditor/WarningBanner/WarningBanner.js
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteBody.js
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteTitle.js
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteViewer.js
packages/app-desktop/gui/NoteEditor/commands/focusElementToolbar.js
packages/app-desktop/gui/NoteEditor/commands/index.js
packages/app-desktop/gui/NoteEditor/commands/pasteAsText.js
@@ -328,13 +333,16 @@ packages/app-desktop/gui/NoteSearchBar.js
packages/app-desktop/gui/NoteStatusBar.js
packages/app-desktop/gui/NoteTextViewer.js
packages/app-desktop/gui/NoteToolbar/NoteToolbar.js
packages/app-desktop/gui/NotyfContext.js
packages/app-desktop/gui/OneDriveLoginScreen.js
packages/app-desktop/gui/PasswordInput/LabelledPasswordInput.js
packages/app-desktop/gui/PasswordInput/PasswordInput.js
packages/app-desktop/gui/PasswordInput/types.js
packages/app-desktop/gui/PdfViewer.js
packages/app-desktop/gui/PluginNotification/PluginNotification.js
packages/app-desktop/gui/PopupNotification/NotificationItem.js
packages/app-desktop/gui/PopupNotification/PopupNotificationList.js
packages/app-desktop/gui/PopupNotification/PopupNotificationProvider.js
packages/app-desktop/gui/PopupNotification/types.js
packages/app-desktop/gui/PromptDialog.js
packages/app-desktop/gui/ResizableLayout/LayoutItemContainer.js
packages/app-desktop/gui/ResizableLayout/MoveButtons.js
@@ -396,6 +404,7 @@ packages/app-desktop/gui/ToolbarBase.js
packages/app-desktop/gui/ToolbarButton/ToolbarButton.js
packages/app-desktop/gui/ToolbarSpace.js
packages/app-desktop/gui/TrashNotification/TrashNotification.js
packages/app-desktop/gui/TrashNotification/TrashNotificationMessage.js
packages/app-desktop/gui/UpdateNotification/UpdateNotification.js
packages/app-desktop/gui/WindowCommandsAndDialogs/AppDialogs.js
packages/app-desktop/gui/WindowCommandsAndDialogs/ModalMessageOverlay.js
@@ -500,8 +509,8 @@ packages/app-desktop/integration-tests/simpleBackup.spec.js
packages/app-desktop/integration-tests/util/activateMainMenuItem.js
packages/app-desktop/integration-tests/util/createStartupArgs.js
packages/app-desktop/integration-tests/util/extendedExpect.js
packages/app-desktop/integration-tests/util/firstNonDevToolsWindow.js
packages/app-desktop/integration-tests/util/getImageSourceSize.js
packages/app-desktop/integration-tests/util/getMainWindow.js
packages/app-desktop/integration-tests/util/setDarkMode.js
packages/app-desktop/integration-tests/util/setFilePickerResponse.js
packages/app-desktop/integration-tests/util/setMessageBoxResponse.js
@@ -522,13 +531,16 @@ packages/app-desktop/services/plugins/UserWebview.js
packages/app-desktop/services/plugins/UserWebviewDialog.js
packages/app-desktop/services/plugins/UserWebviewDialogButtonBar.js
packages/app-desktop/services/plugins/hooks/useContentSize.js
packages/app-desktop/services/plugins/hooks/useFormData.js
packages/app-desktop/services/plugins/hooks/useHtmlLoader.js
packages/app-desktop/services/plugins/hooks/useMessageHandler.js
packages/app-desktop/services/plugins/hooks/useScriptLoader.js
packages/app-desktop/services/plugins/hooks/useSubmitHandler.js
packages/app-desktop/services/plugins/hooks/useThemeCss.test.js
packages/app-desktop/services/plugins/hooks/useThemeCss.js
packages/app-desktop/services/plugins/hooks/useViewIsReady.js
packages/app-desktop/services/plugins/hooks/useWebviewToPluginMessages.js
packages/app-desktop/services/plugins/types.js
packages/app-desktop/services/restart.js
packages/app-desktop/services/sortOrder/PerFolderSortOrderService.test.js
packages/app-desktop/services/sortOrder/PerFolderSortOrderService.js
@@ -556,6 +568,7 @@ packages/app-desktop/utils/restartInSafeModeFromMain.test.js
packages/app-desktop/utils/restartInSafeModeFromMain.js
packages/app-desktop/utils/window/types.js
packages/app-mobile/PluginAssetsLoader.js
packages/app-mobile/commands/dismissPluginPanels.js
packages/app-mobile/commands/index.js
packages/app-mobile/commands/newNote.test.js
packages/app-mobile/commands/newNote.js
@@ -565,6 +578,7 @@ packages/app-mobile/commands/scrollToHash.js
packages/app-mobile/commands/util/goToNote.js
packages/app-mobile/commands/util/showResource.js
packages/app-mobile/components/BetaChip.js
packages/app-mobile/components/BottomDrawer.js
packages/app-mobile/components/CameraView/ActionButtons.js
packages/app-mobile/components/CameraView/Camera/index.jest.js
packages/app-mobile/components/CameraView/Camera/index.js
@@ -658,15 +672,22 @@ packages/app-mobile/components/SelectDateTimeDialog.js
packages/app-mobile/components/SideMenu.js
packages/app-mobile/components/SideMenuContentNote.js
packages/app-mobile/components/TextInput.js
packages/app-mobile/components/ToggleSpaceButton.js
packages/app-mobile/components/accessibility/AccessibleModalMenu.js
packages/app-mobile/components/accessibility/AccessibleView.test.js
packages/app-mobile/components/accessibility/AccessibleView.js
packages/app-mobile/components/accessibility/FocusControl/AutoFocusProvider.js
packages/app-mobile/components/accessibility/FocusControl/FocusControl.js
packages/app-mobile/components/accessibility/FocusControl/FocusControlProvider.js
packages/app-mobile/components/accessibility/FocusControl/MainAppContent.js
packages/app-mobile/components/accessibility/FocusControl/ModalWrapper.js
packages/app-mobile/components/accessibility/FocusControl/types.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/buttons/FloatingActionButton.js
packages/app-mobile/components/buttons/LabelledIconButton.js
packages/app-mobile/components/buttons/MultiTouchableOpacity.js
packages/app-mobile/components/buttons/TextButton.js
packages/app-mobile/components/buttons/index.js
packages/app-mobile/components/getResponsiveValue.test.js
@@ -713,8 +734,11 @@ packages/app-mobile/components/screens/ConfigScreen/SectionSelector/SectionTab.j
packages/app-mobile/components/screens/ConfigScreen/SectionSelector/index.js
packages/app-mobile/components/screens/ConfigScreen/SettingComponent.js
packages/app-mobile/components/screens/ConfigScreen/SettingItem.js
packages/app-mobile/components/screens/ConfigScreen/SettingTextInput.js
packages/app-mobile/components/screens/ConfigScreen/SettingsButton.js
packages/app-mobile/components/screens/ConfigScreen/SettingsToggle.js
packages/app-mobile/components/screens/ConfigScreen/ValidatedIntegerInput.test.js
packages/app-mobile/components/screens/ConfigScreen/ValidatedIntegerInput.js
packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.js
packages/app-mobile/components/screens/ConfigScreen/plugins/EnablePluginSupportPage.js
packages/app-mobile/components/screens/ConfigScreen/plugins/InstalledPluginBox.js
@@ -754,7 +778,9 @@ packages/app-mobile/components/screens/Note/commands/setTags.js
packages/app-mobile/components/screens/Note/commands/toggleVisiblePanes.js
packages/app-mobile/components/screens/Note/types.js
packages/app-mobile/components/screens/NoteTagsDialog.js
packages/app-mobile/components/screens/Notes.js
packages/app-mobile/components/screens/Notes/NewNoteButton.test.js
packages/app-mobile/components/screens/Notes/NewNoteButton.js
packages/app-mobile/components/screens/Notes/Notes.js
packages/app-mobile/components/screens/SearchScreen/SearchResults.js
packages/app-mobile/components/screens/SearchScreen/index.js
packages/app-mobile/components/screens/ShareManager/AcceptedShareItem.js
@@ -809,6 +835,7 @@ packages/app-mobile/utils/createRootStyle.js
packages/app-mobile/utils/database-driver-react-native.js
packages/app-mobile/utils/database-driver-react-native.web.js
packages/app-mobile/utils/debounce.js
packages/app-mobile/utils/focusView.js
packages/app-mobile/utils/fs-driver/constants.js
packages/app-mobile/utils/fs-driver/fs-driver-rn.js
packages/app-mobile/utils/fs-driver/fs-driver-rn.web.js
@@ -821,9 +848,10 @@ packages/app-mobile/utils/fs-driver/testUtil/createFilesFromPathRecord.js
packages/app-mobile/utils/fs-driver/testUtil/verifyDirectoryMatches.js
packages/app-mobile/utils/getPackageInfo.js
packages/app-mobile/utils/getVersionInfoText.js
packages/app-mobile/utils/hooks/useKeyboardVisible.js
packages/app-mobile/utils/hooks/useKeyboardState.js
packages/app-mobile/utils/hooks/useOnLongPressProps.js
packages/app-mobile/utils/hooks/useReduceMotionEnabled.js
packages/app-mobile/utils/hooks/useSafeAreaPadding.js
packages/app-mobile/utils/image/fileToImage.web.js
packages/app-mobile/utils/image/getImageDimensions.js
packages/app-mobile/utils/image/resizeImage.js
@@ -832,6 +860,7 @@ packages/app-mobile/utils/injectedJs.js
packages/app-mobile/utils/ipc/RNToWebViewMessenger.js
packages/app-mobile/utils/ipc/WebViewToRNMessenger.js
packages/app-mobile/utils/lockToSingleInstance.js
packages/app-mobile/utils/makeShowMessageBox.test.js
packages/app-mobile/utils/makeShowMessageBox.js
packages/app-mobile/utils/pickDocument.js
packages/app-mobile/utils/polyfills/bufferPolyfill.js
@@ -872,6 +901,8 @@ packages/editor/CodeMirror/editorCommands/duplicateLine.js
packages/editor/CodeMirror/editorCommands/editorCommands.js
packages/editor/CodeMirror/editorCommands/insertLineAfter.test.js
packages/editor/CodeMirror/editorCommands/insertLineAfter.js
packages/editor/CodeMirror/editorCommands/jumpToHash.test.js
packages/editor/CodeMirror/editorCommands/jumpToHash.js
packages/editor/CodeMirror/editorCommands/sortSelectedLines.test.js
packages/editor/CodeMirror/editorCommands/sortSelectedLines.js
packages/editor/CodeMirror/editorCommands/supportsCommand.js
@@ -887,6 +918,8 @@ packages/editor/CodeMirror/markdown/computeSelectionFormatting.test.js
packages/editor/CodeMirror/markdown/computeSelectionFormatting.js
packages/editor/CodeMirror/markdown/decoratorExtension.test.js
packages/editor/CodeMirror/markdown/decoratorExtension.js
packages/editor/CodeMirror/markdown/insertNewlineContinueMarkup.test.js
packages/editor/CodeMirror/markdown/insertNewlineContinueMarkup.js
packages/editor/CodeMirror/markdown/markdownCommands.bulletedVsChecklist.test.js
packages/editor/CodeMirror/markdown/markdownCommands.test.js
packages/editor/CodeMirror/markdown/markdownCommands.toggleList.test.js
@@ -950,6 +983,8 @@ packages/fork-htmlparser2/src/__tests__/events.js
packages/fork-htmlparser2/src/__tests__/stream.js
packages/fork-htmlparser2/src/index.spec.js
packages/fork-htmlparser2/src/index.js
packages/fork-uslug/lib/uslug.test.js
packages/fork-uslug/lib/uslug.js
packages/generator-joplin/generators/app/templates/api/index.js
packages/generator-joplin/generators/app/templates/api/noteListType.js
packages/generator-joplin/generators/app/templates/api/types.js
@@ -1001,6 +1036,7 @@ packages/lib/commands/renderMarkup.test.js
packages/lib/commands/renderMarkup.js
packages/lib/commands/showEditorPlugin.js
packages/lib/commands/synchronize.js
packages/lib/commands/toggleAllFolders.test.js
packages/lib/commands/toggleAllFolders.js
packages/lib/commands/toggleEditorPlugin.js
packages/lib/components/EncryptionConfigScreen/utils.test.js
@@ -1038,6 +1074,8 @@ packages/lib/fs-driver-base.js
packages/lib/fs-driver-node.js
packages/lib/fsDriver.test.js
packages/lib/geolocation-node.js
packages/lib/getAppName.test.js
packages/lib/getAppName.js
packages/lib/hooks/useAsyncEffect.js
packages/lib/hooks/useElementSize.js
packages/lib/hooks/useEventListener.js
@@ -1098,6 +1136,7 @@ packages/lib/models/settings/settingValidations.js
packages/lib/models/settings/types.js
packages/lib/models/utils/areAllFoldersCollapsed.test.js
packages/lib/models/utils/areAllFoldersCollapsed.js
packages/lib/models/utils/getCanBeCollapsedFolderIds.test.js
packages/lib/models/utils/getCanBeCollapsedFolderIds.js
packages/lib/models/utils/getCollator.js
packages/lib/models/utils/getConflictFolderId.js
@@ -1570,6 +1609,7 @@ packages/tools/release-electron.js
packages/tools/release-ios.js
packages/tools/release-plugin-repo-cli.js
packages/tools/release-server.js
packages/tools/saveClaConsentRecords.js
packages/tools/setupNewRelease.js
packages/tools/spellcheck.js
packages/tools/tagServerLatest.js

1
.husky/pre-commit Normal file
View File

@@ -0,0 +1 @@
corepack yarn lint-staged

View File

@@ -1,62 +0,0 @@
diff --git a/android/src/newarch/java/com/reactnativecommunity/slider/ReactSliderManager.java b/android/src/newarch/java/com/reactnativecommunity/slider/ReactSliderManager.java
index a5bb95eec3337b93a2338a2869a2bda176c91cae..87817688eb280c2f702c26dc35558c6a0a4db1ea 100644
--- a/android/src/newarch/java/com/reactnativecommunity/slider/ReactSliderManager.java
+++ b/android/src/newarch/java/com/reactnativecommunity/slider/ReactSliderManager.java
@@ -42,12 +42,20 @@ public class ReactSliderManager extends SimpleViewManager<ReactSlider> implement
public void onProgressChanged(SeekBar seekbar, int progress, boolean fromUser) {
ReactSlider slider = (ReactSlider)seekbar;
- if(progress < slider.getLowerLimit()) {
- progress = slider.getLowerLimit();
- seekbar.setProgress(progress);
- } else if (progress > slider.getUpperLimit()) {
- progress = slider.getUpperLimit();
- seekbar.setProgress(progress);
+ // During initialization, lowerLimit can be greater than upperLimit.
+ //
+ // If a change event is received during this, we need a check to prevent
+ // infinite recursion.
+ //
+ // Issue: https://github.com/callstack/react-native-slider/issues/571
+ if (slider.getLowerLimit() <= slider.getUpperLimit()) {
+ if(progress < slider.getLowerLimit()) {
+ progress = slider.getLowerLimit();
+ seekbar.setProgress(progress);
+ } else if (progress > slider.getUpperLimit()) {
+ progress = slider.getUpperLimit();
+ seekbar.setProgress(progress);
+ }
}
ReactContext reactContext = (ReactContext) seekbar.getContext();
diff --git a/android/src/oldarch/java/com/reactnativecommunity/slider/ReactSliderManager.java b/android/src/oldarch/java/com/reactnativecommunity/slider/ReactSliderManager.java
index 3ff5930f85a3cd92c2549925f41058abb188a57e..ab3681fdfe0b736c97020e1434e450c8183e6f18 100644
--- a/android/src/oldarch/java/com/reactnativecommunity/slider/ReactSliderManager.java
+++ b/android/src/oldarch/java/com/reactnativecommunity/slider/ReactSliderManager.java
@@ -30,12 +30,20 @@ public class ReactSliderManager extends SimpleViewManager<ReactSlider> {
public void onProgressChanged(SeekBar seekbar, int progress, boolean fromUser) {
ReactSlider slider = (ReactSlider)seekbar;
- if(progress < slider.getLowerLimit()) {
- progress = slider.getLowerLimit();
- seekbar.setProgress(progress);
- } else if(progress > slider.getUpperLimit()) {
- progress = slider.getUpperLimit();
- seekbar.setProgress(progress);
+ // During initialization, lowerLimit can be greater than upperLimit.
+ //
+ // If a change event is received during this, we need a check to prevent
+ // infinite recursion.
+ //
+ // Issue: https://github.com/callstack/react-native-slider/issues/571
+ if (slider.getLowerLimit() <= slider.getUpperLimit()) {
+ if(progress < slider.getLowerLimit()) {
+ progress = slider.getLowerLimit();
+ seekbar.setProgress(progress);
+ } else if (progress > slider.getUpperLimit()) {
+ progress = slider.getUpperLimit();
+ seekbar.setProgress(progress);
+ }
}
ReactContext reactContext = (ReactContext) seekbar.getContext();

View File

@@ -1,33 +0,0 @@
diff --git a/lib/runner/index.js b/lib/runner/index.js
index 87e3b3957619728e3ed1ca61e2d83df1c49f928f..6d5ab905415da0577341c8f5b67d4806adcf7549 100644
--- a/lib/runner/index.js
+++ b/lib/runner/index.js
@@ -68,15 +68,19 @@ function run([, scriptPath, hookName = '', HUSKY_GIT_PARAMS], getStdinFn = get_s
return 0;
}
catch (err) {
- const noVerifyMessage = [
- 'commit-msg',
- 'pre-commit',
- 'pre-rebase',
- 'pre-push'
- ].includes(hookName)
- ? '(add --no-verify to bypass)'
- : '(cannot be bypassed with --no-verify due to Git specs)';
- console.log(`husky > ${hookName} hook failed ${noVerifyMessage}`);
+ // We do not want to print this "add --no-verify to bypass" message because that's
+ // literally what some developers do instead of trying to fix the errors.
+
+ // const noVerifyMessage = [
+ // 'commit-msg',
+ // 'pre-commit',
+ // 'pre-rebase',
+ // 'pre-push'
+ // ].includes(hookName)
+ // ? '(add --no-verify to bypass)'
+ // : '(cannot be bypassed with --no-verify due to Git specs)';
+
+ console.log(`husky > ${hookName} hook failed (Please fix the errors listed above and try again)`);
return err.code;
}
});

View File

@@ -0,0 +1,55 @@
# This patch improves the note actions menu (the kebab menu)'s accessibility
# by labelling its dismiss button.
diff --git a/build/rnpm.js b/build/rnpm.js
index 1111c2de99b3d4c5651ca4eee3ba59c0ce8e13e1..d410ee12b38d02c399b0a40973217da0082d73c0 100644
--- a/build/rnpm.js
+++ b/build/rnpm.js
@@ -1573,7 +1573,9 @@
onPress = _this$props.onPress,
style = _this$props.style;
return /*#__PURE__*/React__default.createElement(reactNative.TouchableWithoutFeedback, {
- onPress: onPress
+ onPress: onPress,
+ accessibilityLabel: _this$props.accessibilityLabel,
+ accessibilityRole: 'button',
}, /*#__PURE__*/React__default.createElement(reactNative.Animated.View, {
style: [styles.fullscreen, {
opacity: this.fadeAnim
@@ -1588,7 +1590,8 @@
}(React.Component);
Backdrop.propTypes = {
- onPress: propTypes.func.isRequired
+ onPress: propTypes.func.isRequired,
+ accessibilityLabel: propTypes.string,
};
var styles = reactNative.StyleSheet.create({
fullscreen: {
@@ -1658,6 +1661,7 @@
style: styles$1.placeholder
}, /*#__PURE__*/React__default.createElement(Backdrop, {
onPress: ctx._onBackdropPress,
+ accessibilityLabel: this.props.closeButtonLabel,
style: backdropStyles,
ref: ctx.onBackdropRef
}), ctx._makeOptions());
@@ -2090,6 +2094,7 @@
}), /*#__PURE__*/React__default.createElement(MenuPlaceholder, {
ctx: this,
backdropStyles: customStyles.backdrop,
+ closeButtonLabel: this.props.closeButtonLabel,
ref: this._onPlaceholderRef
}))));
}
diff --git a/src/index.d.ts b/src/index.d.ts
index 1db1e643a915e4bfb715e33354678ec1be219f50..007157e366d1935368bdd8eff5e7a0773e183d0f 100644
--- a/src/index.d.ts
+++ b/src/index.d.ts
@@ -18,6 +18,7 @@ declare module "react-native-popup-menu" {
menuProviderWrapper?: StyleProp<ViewStyle>;
backdrop?: StyleProp<ViewStyle>;
};
+ closeButtonLabel: string;
backHandler?: boolean | Function;
skipInstanceCheck?: boolean;
children: React.ReactNode;

View File

@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 682.66669 682.66669"
height="682.66669"
width="682.66669"
xml:space="preserve"
id="svg2"
version="1.1"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
sodipodi:docname="JoplinLetterBlue.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview13"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
showgrid="false"
inkscape:zoom="0.77490232"
inkscape:cx="366.49781"
inkscape:cy="360.69062"
inkscape:window-width="1366"
inkscape:window-height="708"
inkscape:window-x="0"
inkscape:window-y="30"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<defs
id="defs6">
<linearGradient
id="linearGradient26"
spreadMethod="pad"
gradientTransform="matrix(-4387.91,4387.91,4387.91,4387.91,4753.95,366.05)"
gradientUnits="userSpaceOnUse"
y2="0"
x2="1"
y1="0"
x1="0">
<stop
id="stop22"
offset="0"
style="stop-opacity:1;stop-color:#004caf" />
<stop
id="stop24"
offset="1"
style="stop-opacity:1;stop-color:#1f95f8" />
</linearGradient>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath829"><path
id="path831"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.999997"
d="M 3961.59,4435.23 H 2570.18 c -13.15,0 -23.78,-10.64 -23.78,-23.77 v -441.84 c 0,-14.87 12.04,-26.92 26.92,-26.92 h 190.77 c 77.16,0 139.73,-59.35 146.43,-134.77 V 3505 3336.23 1728.75 1717.36 h -0.052 c 0.48,-16.84 -0.1898,-33.4 -1.83,-49.71 -0.18,-2.38 -0.5003,-4.73 -0.7902,-7.09 -1.0998,-9.53 -2.3199,-19.01 -4.17,-28.29 -1.0098,-5.29 -2.4399,-10.44 -3.7098,-15.65 -1.71,-6.93 -3.09,-13.97 -5.22,-20.75 -12.5802,-40.27 -32.4702,-77.62 -59.9802,-110.5 -1.0098,-1.17 -2.2599,-2.25 -3.2598,-3.41 -8.3901,-9.72 -17.2002,-19.19 -26.9502,-28.06 -9.84,-8.95 -20.2599,-17.27 -31.2099,-25 -77.8401,-55.14 -182.61,-79.4 -299.67,-68.2 -149.2599,14.03 -297.3399,81.72 -417.03,190.62 -119.6701,108.89 -194.08,243.62 -209.4799,379.41 -13.8501,121.48 22.5498,228.38 102.42,301.05 0.21,0.1598 0.3997,0.3098 0.5602,0.48 3.09,2.77 6.4901,5.2 9.6701,7.87 57.16,47.89 131.6701,76.91 216.7,84.91 0.96,0.09 1.8801,0.24 2.79,0.3203 8.9499,0.79 18.0699,1.15 27.27,1.49 4.8099,0.1598 9.5601,0.5003 14.4399,0.54 1.62,0.023 3.1602,0.1898 4.7802,0.1898 2.8998,0 5.91,-0.3803 8.8098,-0.42 13.4001,-0.21 26.9001,-0.7601 40.6701,-1.9401 1.74,-0.1402 3.3999,-0.08 5.19,-0.24 1.2699,-0.1297 2.5299,-0.4102 3.8001,-0.54 78,-7.82 155.2299,-31.11 228.5199,-66.3999 1.53,-0.068 3.3,-0.54 5.5099,-1.7601 22.34,-12.3399 26.6201,0.9 27.2801,9.6501 v 382.2399 282.8201 c 0,19.05 -13.2501,35.8999 -31.83,39.99 -394.7601,86.88 -782.08,-3.5501 -1055.38,-252.3401 -238.7499,-217.1799 -354.24,-530.5799 -316.8201,-859.7899 33.39,-293.23 183.9102,-574.94 423.88,-793.33 233.8901,-212.79003 531.69,-345.86006 838.8801,-374.80106 42.33,-3.918 84.8601,-5.93797 126.36,-5.93797 293.3799,0 565.6099,100.59802 766.54,283.37903 190.3401,173.3 304.35,411.27 321.0799,670.16 l 1.55,1697.91 h 0.1703 v 453.97 h 0.06 v 7.92 c 1.72,80.1199 67.05,144.58 147.61,144.58 h 190.77 c 14.8599,0 26.9199,12.05 26.9199,26.9199 v 441.84 c 0,13.13 -10.6299,23.77 -23.7799,23.77" /></clipPath></defs>
<g
id="g14"
transform="matrix(0.13333333,0,0,-0.13333333,0,682.66667)"
mask="none"
clip-path="url(#clipPath829)">
<g
clip-path="url(#clipPath20)"
id="g16">
<path
id="path28"
style="fill:url(#linearGradient26);fill-opacity:1;fill-rule:nonzero;stroke:none"
d="M 3873.89,0 H 1246.11 C 560.754,0 0,560.75 0,1246.11 V 3873.88 C 0,4559.25 560.754,5120 1246.11,5120 H 3873.89 C 4559.25,5120 5120,4559.25 5120,3873.88 V 1246.11 C 5120,560.75 4559.25,0 3873.89,0" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -48,7 +48,7 @@ const updateListWithDetails = function (dom, el, detail) {
};
const removeStyles = (dom, element: HTMLElement, styles: string[]) => {
Tools.each(styles, (style) => dom.setStyle(element, { [style]: '' }));
Tools.each(styles, (style) => dom.setStyle(element, style, ''));
};
const getEndPointNode = function (editor, rng, start, root) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View File

@@ -80,7 +80,7 @@ async 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('arm64.DMG') > 0) downloadLinks['macOsM1'] = href;
if (href.indexOf('.AppImage') > 0) downloadLinks['linux'] = href;
});
@@ -98,6 +98,8 @@ async function setupDownloadPage() {
} else {
const os = await getOs();
console.info('Found OS: ' + os);
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>');

View File

@@ -1,4 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Joplin]]></title><description><![CDATA[Joplin, the open source note-taking application]]></description><link>https://joplinapp.org</link><generator>RSS for Node</generator><lastBuildDate>Tue, 14 Jan 2025 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Tue, 14 Jan 2025 00:00:00 GMT</pubDate><item><title><![CDATA[What's new in Joplin 3.2]]></title><description><![CDATA[<h2>Import OneNote Archives<a name="import-onenote-archives" href="#import-onenote-archives" class="heading-anchor">🔗</a></h2>
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Joplin]]></title><description><![CDATA[Joplin, the open source note-taking application]]></description><link>https://joplinapp.org</link><generator>RSS for Node</generator><lastBuildDate>Mon, 28 Apr 2025 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Mon, 28 Apr 2025 00:00:00 GMT</pubDate><item><title><![CDATA[What's new in Joplin 3.3]]></title><description><![CDATA[<h2>Desktop application<a name="desktop-application" href="#desktop-application" class="heading-anchor">🔗</a></h2>
<h3>Accessibility improvements<a name="accessibility-improvements" href="#accessibility-improvements" class="heading-anchor">🔗</a></h3>
<p>The Joplin 3.3 release introduces significant accessibility enhancements designed to make the application more inclusive and user-friendly. Users can now benefit from improved keyboard navigation, thanks to newly added shortcuts and clearer labels that streamline interaction across the interface. We've also added a &quot;go to viewer&quot; menu item that moves focus from the note editor to the note viewer. Focus is moved to the location in the viewer corresponding to the location of the cursor in the editor.</p>
<p>Screen reader support has been bolstered, ensuring elements like the note list and sidebar are easier to toggle and interact with. These updates make the application more usable for individuals relying on assistive technologies.</p>
<p>Additionally, visual improvements, including increased contrast for UI components such as URLs, sidebars, and scrollbars, enhance readability for users with visual impairments. This focus on clarity ensures a more comfortable user experience.</p>
<p>The Rich Text Editor has also received accessibility-focused updates, allowing for more seamless interaction with code blocks using either a keyboard or touchscreen. These refinements highlight Joplin's dedication to creating an accessible and equitable experience for all users.</p>
<h3>Button to collapse and expand all notebooks<a name="button-to-collapse-and-expand-all-notebooks" href="#button-to-collapse-and-expand-all-notebooks" class="heading-anchor">🔗</a></h3>
<p>Joplin 3.3 introduces a convenient &quot;Collapse/Expand All&quot; button for notebooks, allowing you to quickly adjust the visibility of your notebook hierarchy. This feature simplifies navigation by letting you expand all notebooks to locate specific notes or collapse them for a tidier interface!</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20250428-collapse-all.png" alt=""></p>
<h3>New dialog to select a note and link to it<a name="new-dialog-to-select-a-note-and-link-to-it" href="#new-dialog-to-select-a-note-and-link-to-it" class="heading-anchor">🔗</a></h3>
<p>A new dialog has been added to make linking to notes easier. By pressing Shift+Option+L on macOS or Shift+Alt+L on Windows and Linux, you can quickly bring up a search box. Simply type the name of the note you want to link to, and the link will be added to your current note!</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20250428-link-notes.png" alt=""></p>
<h3>Support for multiple instances of Joplin<a name="support-for-multiple-instances-of-joplin" href="#support-for-multiple-instances-of-joplin" class="heading-anchor">🔗</a></h3>
<p>Joplin Desktop now <a href="https://joplinapp.org/help/apps/multiple_instances">lets you run multiple instances at once</a>! This means you can easily separate your work and personal notes, or use Joplin across different virtual desktops. Each instance runs independently with its own settings, plugins, and notes, so nothing overlaps. You can open a second instance through in menu <strong>File =&gt; Open secondary app instance...</strong>, and customise it however you like!</p>
<h3>Improved Rich Text editor<a name="improved-rich-text-editor" href="#improved-rich-text-editor" class="heading-anchor">🔗</a></h3>
<p>This version includes multiple improvements and bug fixes to the Rich Text Editor. In particular it has now been upgraded to TinyMCE v6, and it adds support for Markdown auto-replacement. For example, typing <code>==highlight==</code> creates highlighted text. Auto-replacement can be disabled in settings.</p>
<h2>Mobile application<a name="mobile-application" href="#mobile-application" class="heading-anchor">🔗</a></h2>
<h3>Accessibility improvements<a name="accessibility-improvements-1" href="#accessibility-improvements-1" class="heading-anchor">🔗</a></h3>
<p>Like the desktop app, the mobile app includes several accessibility improvements designed to enhance the user experience for those relying on assistive technologies. Focus handling has been improved in the note actions menu and modal dialogs, ensuring smoother navigation for screen reader users. Additionally, the default modal close button is now accessible, and issues with invisible buttons being focusable have been resolved.</p>
<p>Other updates include better contrast for faded URLs in the Markdown editor, making them more readable for users with visual impairments. The encryption configuration screen has been improved for better accessibility, and screen reader support has been added for creating and editing profiles. These changes collectively improve navigation, readability, and usability for all users.</p>
<h3>Support attaching audio recordings<a name="support-attaching-audio-recordings" href="#support-attaching-audio-recordings" class="heading-anchor">🔗</a></h3>
<p>You can now easily capture and include audio recordings directly within your notes. To use this feature, open the kebab menu and select &quot;Record audio.&quot; You can then to record your audio input. When finished, the app automatically attaches the recorded audio file to the note. This functionality is perfect for capturing ideas, reminders, or supplementary audio information in a quick and intuitive way.</p>
<h3>Improved voice typing feature<a name="improved-voice-typing-feature" href="#improved-voice-typing-feature" class="heading-anchor">🔗</a></h3>
<p>The voice typing feature in Android has undergone a significant improvement, making it more accurate. Previously introduced in version 2.11, the feature allowed you to transcribe speech into text but lacked punctuation and struggled with accuracy in certain scenarios. The revamped version now leverages Whisper, an advanced AI model, to deliver improved recognition, including automatic punctuation and paragraph formatting.</p>
<p>Despite its advancements, the feature is currently in beta due to the demanding nature of the required AI models. While it defaults to a smaller, less accurate model to accommodate older devices, you can <a href="https://github.com/joplin/voice-typing-models/releases">manually download and switch to more accurate models</a> for better performance.</p>
<p>As previously, the feature remains entirely offline, ensuring that private recordings are never sent to third-party servers, a distinct privacy advantage over similar solutions from Google or Apple. Additionally it means you can use the feature even when you don't have an internet connection. We plan to refine this feature further, eventually defaulting to the more accurate model as stability improves.</p>
<h3>Improved new note menu<a name="improved-new-note-menu" href="#improved-new-note-menu" class="heading-anchor">🔗</a></h3>
<p>The redesigned &quot;New Note&quot; menu takes note creation to a whole new level of flexibility and convenience. Previously, this menu only offered options to create a new note or a new to-do. With the latest update, you now have quick access to a variety of shortcuts, enabling you to attach files, record audio, capture photo notes, or even create drawings directly from the menu. This intuitive redesign makes it easier to choose the format that best suits your needs.</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20250428-new-note-menu.png" alt=""></p>
<h2>Security and bug fixes<a name="security-and-bug-fixes" href="#security-and-bug-fixes" class="heading-anchor">🔗</a></h2>
<p>As always, we continue to reinforce the security of Joplin. This version implements a robust content security policy for the Rich Text Editor, safeguarding the application against malicious content.</p>
<p>In total, this version delivers 39 bug fixes on desktop and 17 on mobile, enhancing both the security and stability of the application.</p>
<h2>Full changelogs<a name="full-changelogs" href="#full-changelogs" class="heading-anchor">🔗</a></h2>
<p>This is just an overview of the main features. The full changelogs are available there:</p>
<ul>
<li>Desktop: <a href="https://joplinapp.org/help/about/changelog/desktop">https://joplinapp.org/help/about/changelog/desktop</a></li>
<li>Android: <a href="https://joplinapp.org/help/about/changelog/android/">https://joplinapp.org/help/about/changelog/android/</a></li>
<li>iOS: <a href="https://joplinapp.org/help/about/changelog/ios/">https://joplinapp.org/help/about/changelog/ios/</a></li>
</ul>
]]></description><link>https://joplinapp.org/news/20250428-release-3-3</link><guid isPermaLink="false">20250428-release-3-3</guid><pubDate>Mon, 28 Apr 2025 00:00:00 GMT</pubDate><twitter-text>What&apos;s new in Joplin 3.3</twitter-text></item><item><title><![CDATA[What's new in Joplin 3.2]]></title><description><![CDATA[<h2>Import OneNote Archives<a name="import-onenote-archives" href="#import-onenote-archives" class="heading-anchor">🔗</a></h2>
<p>Joplin now supports importing OneNote archives, a significant step for users transitioning from OneNote. Microsoft has long made it challenging to leave OneNote, offering limited export options and complex formats that make it difficult for app developers to support it. Despite these hurdles, @pedr tackled these issues head-on, developing an import tool that simplifies the process. This addition makes Joplin a practical choice for those looking to move away from OneNote's ecosystem.</p>
<p>To use this feature, select <strong>File</strong> =&gt; <strong>Import</strong> =&gt; <strong>ZIP - OneNote Notebook</strong></p>
<h2>Multi-window support<a name="multi-window-support" href="#multi-window-support" class="heading-anchor">🔗</a></h2>
@@ -413,10 +452,4 @@ sys 0m38.013s</p>
<p>You will be part of a small team, so you will have an opportunity for a high-impact role, targeting hundreds of thousands of users.</p>
<p>If you're interested please contact us at job-AT-joplin.cloud</p>
<p>No agencies please.</p>
]]></description><link>https://joplinapp.org/news/20221209-job</link><guid isPermaLink="false">20221209-job</guid><pubDate>Fri, 09 Dec 2022 00:00:00 GMT</pubDate><twitter-text>Joplin is hiring!</twitter-text></item><item><title><![CDATA[Modernising and securing Joplin, one package at a time]]></title><description><![CDATA[<p>If you watch the <a href="https://github.com/laurent22/joplin">Joplin source code repository</a>, you may have noticed a lot of Renovate pull requests lately. This <a href="https://www.mend.io/free-developer-tools/renovate/">Renovate tool</a> is a way to manage dependencies - it automatically finds what needs to be updated, then upgrade it to the latest version, and create a pull request. If all tests pass, we can then merge this pull request. So far we have merged 267 of these pull requests.</p>
<p>Updating Joplin packages was long due. It is necessary so that we don't fall behind and end up using unsupported or deprecated packages. We also benefit from bug fixes and performance improvements. It is also important in terms of security, since recent package versions usually include various security fixes.</p>
<p>We used to rely on a tool called &quot;npm audit&quot; to do this, however it no longer works on the Joplin codebase, and it was always risky to use it since it would update multiple packages in one command - so if something went wrong it was difficult to find the culprit.</p>
<p>Renovate on the other hand upgrades packages one at a time, and run our test units to ensure everything is still working as expected. It also upgrades multiple instances of the same package across the monorepo, which is convenient to keep our code consistent. It also has a number of options to make our life easier, such as the ability to automatically merge a pull request for patch releases since this is usually safe (when a package is, for example upgraded from 1.0.1 to 1.0.3).</p>
<p>Although Renovate automates the package upgrades it doesn't mean all upgrades are straightforward - our tests won't catch all issues, so the apps might end up being broken or cannot be compiled anymore. So there's manual work involved to get everything working after certain upgrades - for the most part this has been done and the apps appear to be stable so far.</p>
<p>This will however be an important part of pre-release 2.10 (or should it be 3.0?) - we hope that everything works but we may need your support to try this version and report any glitch you may have found. As always pre-release regressions have the highest priority so we aim to fix them as quickly as possible.</p>
]]></description><link>https://joplinapp.org/news/20221115-renovate</link><guid isPermaLink="false">20221115-renovate</guid><pubDate>Tue, 15 Nov 2022 00:00:00 GMT</pubDate><twitter-text>Modernising and securing Joplin, one package at a time</twitter-text></item></channel></rss>
]]></description><link>https://joplinapp.org/news/20221209-job</link><guid isPermaLink="false">20221209-job</guid><pubDate>Fri, 09 Dec 2022 00:00:00 GMT</pubDate><twitter-text>Joplin is hiring!</twitter-text></item></channel></rss>

View File

@@ -398,7 +398,7 @@
<div class="text-center sponsors-org">
{{#sponsors.orgs}}
<a class="sponsor-org-item" href="{{url}}"><img title="{{title}}" src="{{imageBaseUrl}}/sponsors/{{imageName}}"></a>
<a class="sponsor-org-item" href="{{url}}"><img alt="{{alt}}" title="{{title}}" src="{{imageBaseUrl}}/sponsors/{{imageName}}"></a>
{{/sponsors.orgs}}
</div>

View File

@@ -7,6 +7,9 @@ FROM node:18 AS builder
RUN apt-get update \
&& apt-get install -y \
python3 tini \
# needed for node-canvas for ARM32 platform.
# See also https://github.com/Automattic/node-canvas/wiki/Installation:-Ubuntu-and-other-Debian-based-systems
libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev \
&& rm -rf /var/lib/apt/lists/*
# Enables Yarn
@@ -47,9 +50,9 @@ RUN sed --in-place '/onenote-converter/d' ./packages/lib/package.json
# Note that `yarn install` ignores `NODE_ENV=production` and will install dev
# dependencies too, but this is fine because we need them to build the app.
RUN BUILD_SEQUENCIAL=1 yarn install --inline-builds \
&& yarn cache clean \
&& rm -rf .yarn/berry
RUN --mount=type=cache,target=/build/.yarn/cache --mount=type=cache,target=/build/.yarn/berry/cache\
BUILD_SEQUENCIAL=1 yarn config set cacheFolder /build/.yarn/cache \
&& yarn install --inline-builds
# =============================================================================
# Final stage - we copy only the relevant files from the build stage and start
@@ -81,10 +84,11 @@ CMD ["yarn", "start-prod"]
ARG BUILD_DATE
ARG REVISION
ARG VERSION
ARG SOURCE
LABEL org.opencontainers.image.created="$BUILD_DATE" \
org.opencontainers.image.title="Joplin Server" \
org.opencontainers.image.description="Docker image for Joplin Server" \
org.opencontainers.image.url="https://joplinapp.org/" \
org.opencontainers.image.revision="$REVISION" \
org.opencontainers.image.source="https://github.com/laurent22/joplin.git" \
org.opencontainers.image.version="${VERSION}"
org.opencontainers.image.source="$SOURCE" \
org.opencontainers.image.version="$VERSION"

View File

@@ -67,10 +67,23 @@ showHelp() {
fi
}
#-----------------------------------------------------
# Setup Download Helper: DL
#-----------------------------------------------------
if [[ `command -v wget2` ]]; then
DL='wget2 -qO'
elif [[ `command -v wget` ]]; then
DL='wget -qO'
elif [[ `command -v curl` ]]; then
DL='curl -sLo'
else
print "${COLOR_RED}Error: wget2, wget, and curl not found. Please install one of these tools.${COLOR_RESET}"
exit 1
fi
#-----------------------------------------------------
# PARSE ARGUMENTS
#-----------------------------------------------------
optspec=":h-:"
while getopts "${optspec}" OPT; do
[ "${OPT}" = " " ] && continue
@@ -140,9 +153,9 @@ fi
# Get the latest version to download
if [[ "$INCLUDE_PRE_RELEASE" == true ]]; then
RELEASE_VERSION=$(wget -qO - "https://api.github.com/repos/laurent22/joplin/releases" | grep -Po '"tag_name": ?"v\K.*?(?=")' | sort -rV | head -1)
RELEASE_VERSION=$($DL - "https://api.github.com/repos/laurent22/joplin/releases" | grep -Po '"tag_name": ?"v\K.*?(?=")' | sort -rV | head -1)
else
RELEASE_VERSION=$(wget -qO - "https://api.github.com/repos/laurent22/joplin/releases/latest" | grep -Po '"tag_name": ?"v\K.*?(?=")')
RELEASE_VERSION=$($DL - "https://api.github.com/repos/laurent22/joplin/releases/latest" | grep -Po '"tag_name": ?"v\K.*?(?=")')
fi
# Check if it's in the latest version
@@ -163,8 +176,8 @@ fi
#-----------------------------------------------------
print 'Downloading Joplin...'
TEMP_DIR=$(mktemp -d)
wget -O "${TEMP_DIR}/Joplin.AppImage" "https://objects.joplinusercontent.com/v${RELEASE_VERSION}/Joplin-${RELEASE_VERSION}.AppImage?source=LinuxInstallScript&type=$DOWNLOAD_TYPE"
wget -O "${TEMP_DIR}/joplin.png" https://joplinapp.org/images/Icon512.png
$DL "${TEMP_DIR}/Joplin.AppImage" "https://objects.joplinusercontent.com/v${RELEASE_VERSION}/Joplin-${RELEASE_VERSION}.AppImage?source=LinuxInstallScript&type=$DOWNLOAD_TYPE"
$DL "${TEMP_DIR}/joplin.png" https://joplinapp.org/images/Icon512.png
#-----------------------------------------------------
print 'Installing Joplin...'
@@ -287,7 +300,7 @@ echo "$RELEASE_VERSION" > "${INSTALL_DIR}/VERSION"
#-----------------------------------------------------
if [[ "$SHOW_CHANGELOG" == true ]]; then
NOTES=$(wget -qO - https://api.github.com/repos/laurent22/joplin/releases/latest | grep -Po '"body": "\K.*(?=")')
NOTES=$($DL - https://api.github.com/repos/laurent22/joplin/releases/latest | grep -Po '"body": "\K.*(?=")')
print "${COLOR_BLUE}Changelog:${COLOR_RESET}\n${NOTES}"
fi

View File

@@ -31,7 +31,7 @@ Please see the [donation page](https://github.com/laurent22/joplin/blob/dev/read
# Sponsors
<!-- SPONSORS-ORG -->
<a href="https://seirei.ne.jp"><img title="Serei Network" width="256" src="https://joplinapp.org/images/sponsors/SeireiNetwork.png"/></a> <a href="https://www.hosting.de/nextcloud/?mtm_campaign=managed-nextcloud&amp;mtm_kwd=joplinapp&amp;mtm_source=joplinapp-webseite&amp;mtm_medium=banner"><img title="Hosting.de" width="256" src="https://joplinapp.org/images/sponsors/HostingDe.png"/></a> <a href="https://citricsheep.com"><img title="Citric Sheep" width="256" src="https://joplinapp.org/images/sponsors/CitricSheep.png"/></a> <a href="https://sorted.travel/?utm_source=joplinapp"><img title="Sorted Travel" width="256" src="https://joplinapp.org/images/sponsors/SortedTravel.png"/></a> <a href="https://celebian.com"><img title="Celebian" width="256" src="https://joplinapp.org/images/sponsors/Celebian.png"/></a> <a href="https://bestkru.com"><img title="BestKru" width="256" src="https://joplinapp.org/images/sponsors/BestKru.png"/></a> <a href="https://www.socialfollowers.uk/buy-tiktok-followers/"><img title="Social Followers" width="256" src="https://joplinapp.org/images/sponsors/SocialFollowers.png"/></a> <a href="https://stormlikes.com/"><img title="Stormlikes" width="256" src="https://joplinapp.org/images/sponsors/Stormlikes.png"/></a> <a href="https://route4me.com"><img title="Route4Me" width="256" src="https://joplinapp.org/images/sponsors/Route4Me.png"/></a> <a href="https://casinoreviews.net"><img title="Casino Reviews" width="256" src="https://joplinapp.org/images/sponsors/CasinoReviews.png"/></a> <a href="https://useviral.com.br/"><img title="Comprar seguidores Instagram" width="256" src="https://joplinapp.org/images/sponsors/Useviral.png"/></a> <a href="https://ca.edubirdie.com/"><img title="Achieve academic success with Edubirdie — your trusted partner for expert writing assistance and resources!" width="256" src="https://joplinapp.org/images/sponsors/Edubirdie.png" alt="EduBirdie"/></a> <a href="https://topagency.webflow.io"><img title="WebDesignAgency" width="256" src="https://joplinapp.org/images/sponsors/WebDesignAgency.png" alt="web design agency"/></a> <a href="https://realgambling.ca/"><img title="RealGambling.ca" width="256" src="https://joplinapp.org/images/sponsors/RealGambling.png" alt="RealGambling.ca"/></a> <a href="https://essaypro.com/"><img title="write an essay online with EssayPro" width="256" src="https://joplinapp.org/images/sponsors/EssayPro.png" alt="write an essay online with EssayPro"/></a> <a href="https://www.slotozilla.com/nz/no-deposit-bonus"><img title="casino without making any upfront cost" width="256" src="https://joplinapp.org/images/sponsors/Slotozilla.png" alt="casino without making any upfront cost"/></a>
<a href="https://seirei.ne.jp"><img title="Serei Network" width="256" src="https://joplinapp.org/images/sponsors/SeireiNetwork.png"/></a> <a href="https://www.hosting.de/nextcloud/?mtm_campaign=managed-nextcloud&amp;mtm_kwd=joplinapp&amp;mtm_source=joplinapp-webseite&amp;mtm_medium=banner"><img title="Hosting.de" width="256" src="https://joplinapp.org/images/sponsors/HostingDe.png"/></a> <a href="https://citricsheep.com"><img title="Citric Sheep" width="256" src="https://joplinapp.org/images/sponsors/CitricSheep.png"/></a> <a href="https://sorted.travel/?utm_source=joplinapp"><img title="Sorted Travel" width="256" src="https://joplinapp.org/images/sponsors/SortedTravel.png"/></a> <a href="https://celebian.com"><img title="Celebian" width="256" src="https://joplinapp.org/images/sponsors/Celebian.png"/></a> <a href="https://bestkru.com"><img title="BestKru" width="256" src="https://joplinapp.org/images/sponsors/BestKru.png"/></a> <a href="https://www.socialfollowers.uk/buy-tiktok-followers/"><img title="Social Followers" width="256" src="https://joplinapp.org/images/sponsors/SocialFollowers.png"/></a> <a href="https://stormlikes.com/"><img title="Stormlikes" width="256" src="https://joplinapp.org/images/sponsors/Stormlikes.png"/></a> <a href="https://route4me.com"><img title="Route4Me" width="256" src="https://joplinapp.org/images/sponsors/Route4Me.png"/></a> <a href="https://casinoreviews.net"><img title="Casino Reviews" width="256" src="https://joplinapp.org/images/sponsors/CasinoReviews.png"/></a> <a href="https://topagency.webflow.io"><img title="WebDesignAgency" width="256" src="https://joplinapp.org/images/sponsors/WebDesignAgency.png" alt="topagency"/></a> <a href="https://realgambling.ca/"><img title="RealGambling.ca" width="256" src="https://joplinapp.org/images/sponsors/RealGambling.png" alt="RealGambling.ca"/></a> <a href="https://essaypro.com/"><img title="write an essay online with EssayPro" width="256" src="https://joplinapp.org/images/sponsors/EssayPro.png" alt="write an essay online with EssayPro"/></a> <a href="https://www.slotozilla.com/nz/no-deposit-bonus"><img title="casino without making any upfront cost" width="256" src="https://joplinapp.org/images/sponsors/Slotozilla.png" alt="casino without making any upfront cost"/></a> <a href="https://www.reddit.com/r/tiktokRise/"><img title="Tiktok Rise" width="256" src="https://joplinapp.org/images/sponsors/TiktokRise.jpg" alt="Tiktok Rise"/></a> <a href="https://essaywriter.pro"><img title="write my essay services by EssayWriter" width="256" src="https://joplinapp.org/images/sponsors/EssayWriterPro.png" alt="write my essay services by EssayWriter"/></a>
<!-- SPONSORS-ORG -->
* * *
@@ -42,7 +42,7 @@ Please see the [donation page](https://github.com/laurent22/joplin/blob/dev/read
| <img width="50" src="https://avatars2.githubusercontent.com/u/97193607?s=96&v=4"/></br>[Akhil-CM](https://github.com/Akhil-CM) | <img width="50" src="https://avatars2.githubusercontent.com/u/552452?s=96&v=4"/></br>[andypiper](https://github.com/andypiper) | <img width="50" src="https://avatars2.githubusercontent.com/u/215668?s=96&v=4"/></br>[avanderberg](https://github.com/avanderberg) | <img width="50" src="https://avatars2.githubusercontent.com/u/67130?s=96&v=4"/></br>[chr15m](https://github.com/chr15m) |
| <img width="50" src="https://avatars2.githubusercontent.com/u/1177810?s=96&v=4"/></br>[felixstorm](https://github.com/felixstorm) | <img width="50" src="https://avatars2.githubusercontent.com/u/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/11947658?s=96&v=4"/></br>[KentBrockman](https://github.com/KentBrockman) |
| <img width="50" src="https://avatars2.githubusercontent.com/u/42319182?s=96&v=4"/></br>[marcdw1289](https://github.com/marcdw1289) | <img width="50" src="https://avatars2.githubusercontent.com/u/1788010?s=96&v=4"/></br>[maxtruxa](https://github.com/maxtruxa) | <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/668977?s=96&v=4"/></br>[ugoertz](https://github.com/ugoertz) | | | |
<!-- SPONSORS-GITHUB -->
# Community
@@ -50,9 +50,10 @@ Please see the [donation page](https://github.com/laurent22/joplin/blob/dev/read
Name | Description
--- | ---
[Support Forum](https://discourse.joplinapp.org/) | This is the main place for general discussion about Joplin, user support, software development questions, and to discuss new features. Also where the latest beta versions are released and discussed.
[Patreon page](https://www.patreon.com/joplin) |The latest news are often posted there
[Bluesky feed](https://bsky.app/profile/joplinapp.bsky.social) | Follow us on Bluesky
[Mastodon feed](https://mastodon.social/@joplinapp) | Follow us on Mastodon
[Patreon page](https://www.patreon.com/joplin) |The latest news are often posted there
[YouTube](https://www.youtube.com/@joplinapp) | Discover information and tutorials on how to use the apps
[Discord server](https://discord.gg/VSj7AFHvpq) | Our chat server
[LinkedIn](https://www.linkedin.com/company/joplin) | Our LinkedIn page
[Lemmy Community](https://sopuli.xyz/c/joplinapp) | Also a good place to get help

View File

@@ -6,18 +6,19 @@ files:
- source: /readme/**/*
translation: /readme/i18n/%two_letters_code%/docusaurus-plugin-content-docs/current/**/%original_file_name%
ignore:
- /**/*.jpg
- /**/*.json
- /**/*.png
- /**/*.yml
- /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/dev
- /readme/i18n
- /readme/licenses.md
- /readme/news
- /readme/privacy.md
- /**/*.yml
- /**/*.json
- /**/*.png
- /**/*.jpg

View File

@@ -25,7 +25,8 @@
"version": "latest",
"excluded_platforms": ["aarch64-darwin", "x86_64-darwin"],
},
"git": "latest",
"git": "latest",
"giflib": "latest",
},
"shell": {
"init_hook": [

0
node Normal file
View File

View File

@@ -38,7 +38,7 @@
"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",
"packageJsonLint": "node ./packages/tools/packageJsonLint.js",
"postinstall": "gulp build",
"postinstall": "husky && gulp build",
"postPreReleasesToForum": "node ./packages/tools/postPreReleasesToForum",
"publishAll": "git pull && yarn buildParallel && lerna version --yes --no-private --no-git-tag-version && gulp completePublishAll",
"releaseAndroid": "PATH=\"/usr/local/opt/openjdk@17/bin:$PATH\" node packages/tools/release-android.js",
@@ -64,11 +64,6 @@
"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\""
},
"husky": {
"hooks": {
"pre-commit": "corepack yarn lint-staged"
}
},
"devDependencies": {
"@crowdin/cli": "3",
"@joplin/utils": "~2.12",
@@ -86,7 +81,7 @@
"fs-extra": "11.2.0",
"glob": "10.4.5",
"gulp": "4.0.2",
"husky": "3.1.0",
"husky": "9.1.7",
"lerna": "3.22.1",
"lint-staged": "15.2.8",
"madge": "7.0.0",
@@ -108,14 +103,13 @@
"app-builder-lib@24.4.0": "patch:app-builder-lib@npm%3A24.4.0#./.yarn/patches/app-builder-lib-npm-24.4.0-05322ff057.patch",
"nanoid": "patch:nanoid@npm%3A3.3.7#./.yarn/patches/nanoid-npm-3.3.7-98824ba130.patch",
"pdfjs-dist": "patch:pdfjs-dist@npm%3A3.11.174#./.yarn/patches/pdfjs-dist-npm-3.11.174-67f2fee6d6.patch",
"@react-native-community/slider": "patch:@react-native-community/slider@npm%3A4.4.4#./.yarn/patches/@react-native-community-slider-npm-4.4.4-d78e472f48.patch",
"husky": "patch:husky@npm%3A3.1.0#./.yarn/patches/husky-npm-3.1.0-5cc13e4e34.patch",
"chokidar@^2.0.0": "3.5.3",
"react-native@0.74.1": "patch:react-native@npm%3A0.74.1#./.yarn/patches/react-native-npm-0.74.1-754c02ae9e.patch",
"rn-fetch-blob@0.12.0": "patch:rn-fetch-blob@npm%3A0.12.0#./.yarn/patches/rn-fetch-blob-npm-0.12.0-cf02e3c544.patch",
"app-builder-lib@26.0.0-alpha.7": "patch:app-builder-lib@npm%3A26.0.0-alpha.7#./.yarn/patches/app-builder-lib-npm-26.0.0-alpha.7-e1b3dca119.patch",
"app-builder-lib@24.13.3": "patch:app-builder-lib@npm%3A24.13.3#./.yarn/patches/app-builder-lib-npm-24.13.3-86a66c0bf3.patch",
"react-native-sqlite-storage@6.0.1": "patch:react-native-sqlite-storage@npm%3A6.0.1#./.yarn/patches/react-native-sqlite-storage-npm-6.0.1-8369d747bd.patch",
"react-native-paper@5.13.1": "patch:react-native-paper@npm%3A5.13.1#./.yarn/patches/react-native-paper-npm-5.13.1-f153e542e2.patch"
"react-native-paper@5.13.1": "patch:react-native-paper@npm%3A5.13.1#./.yarn/patches/react-native-paper-npm-5.13.1-f153e542e2.patch",
"react-native-popup-menu@0.16.1": "patch:react-native-popup-menu@npm%3A0.16.1#./.yarn/patches/react-native-popup-menu-npm-0.16.1-28fd66ecb5.patch"
}
}

View File

@@ -1,6 +1,6 @@
import BaseCommand from './base-command';
import app from './app';
import { _ } from '@joplin/lib/locale';
import { _, _n } from '@joplin/lib/locale';
import Note from '@joplin/lib/models/Note';
import BaseModel, { DeleteOptions } from '@joplin/lib/BaseModel';
import { NoteEntity } from '@joplin/lib/services/database/types';
@@ -31,13 +31,13 @@ class Command extends BaseCommand {
let ok = true;
if (!force && notes.length > 1) {
ok = await this.prompt(_('%d notes match this pattern. Delete them?', notes.length), { booleanAnswerDefault: 'n' });
ok = await this.prompt(_n('%d note matches this pattern. Delete it?', '%d notes match this pattern. Delete them?', notes.length, notes.length), { booleanAnswerDefault: 'n' });
}
const permanent = (args.options?.permanent === true) || notes.every(n => !!n.deleted_time);
if (!force && permanent) {
const message = (
notes.length === 1 ? _('This will permanently delete the note "%s". Continue?', notes[0].title) : _('%d notes will be permanently deleted. Continue?', notes.length)
_n('%d note will be permanently deleted. Continue?', '%d notes will be permanently deleted. Continue?', notes.length, notes.length)
);
ok = await this.prompt(message, { booleanAnswerDefault: 'n' });
}

View File

@@ -35,7 +35,7 @@
],
"owner": "Laurent Cozic"
},
"version": "3.3.0",
"version": "3.3.1",
"bin": "./main.js",
"engines": {
"node": ">=10.0.0"

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 KiB

View File

@@ -13,13 +13,6 @@ export default function(context) {
const token = tokens[idx];
if (token.info !== 'justtesting') return defaultRender(tokens, idx, options, env, self);
const postMessageWithResponseTest = `
webviewApi.postMessage('${contentScriptId}', 'justtesting').then(function(response) {
console.info('Got response in content script: ' + response);
});
return false;
`;
// Rich text editor support:
// The joplin-editable and joplin-source CSS classes mark the generated div
// as a region that needs special processing when converting back to markdown.
@@ -38,14 +31,23 @@ export default function(context) {
${richTextEditorMetadata}
<p>JUST TESTING: <pre>${markdownIt.utils.escapeHtml(leftPad(token.content.trim(), 10, 'x'))}</pre></p>
<p><a href="#" onclick="${postMessageWithResponseTest.replace(/\n/g, ' ')}">Click to post a message "justtesting" to plugin and check the response in the console</a></p>
<p>
<a
href="#"
data-content-script-id="${markdownIt.utils.escapeHtml(contentScriptId)}"
class="post-message-link"
>
Click to post a message "justtesting" to plugin and check the response in the console
</a>
</p>
</div>
`;
};
},
assets: function() {
return [
{ name: 'markdownItTestPlugin.css' }
{ name: 'markdownItTestPlugin.css' },
{ name: 'markdownItTestPluginRuntime.js' },
];
},
}

View File

@@ -0,0 +1,14 @@
const addClickHandlers = () => {
const postMessageLinks = document.querySelectorAll('.post-message-link');
for (const link of postMessageLinks) {
const contentScriptId = link.getAttribute('data-content-script-id');
link.onclick = async () => {
const response = await webviewApi.postMessage(contentScriptId, 'justtesting');
link.textContent = 'Got response in content script: ' + response;
};
}
};
document.addEventListener('joplin-noteDidUpdate', () => {
addClickHandlers();
});

View File

@@ -1,11 +1,12 @@
import Logger, { LoggerWrapper } from '@joplin/utils/Logger';
import Logger, { LoggerWrapper, TargetType } from '@joplin/utils/Logger';
import { PluginMessage } from './services/plugins/PluginRunner';
import AutoUpdaterService, { defaultUpdateInterval, initialUpdateStartup } from './services/autoUpdater/AutoUpdaterService';
import type ShimType from '@joplin/lib/shim';
const shim: typeof ShimType = require('@joplin/lib/shim').default;
import { isCallbackUrl } from '@joplin/lib/callbackUrlUtils';
import { BrowserWindow, Tray, WebContents, screen } from 'electron';
import { FileLocker } from '@joplin/utils/fs';
import { IpcMessageHandler, IpcServer, Message, newHttpError, sendMessage, SendMessageOptions, startServer, stopServer } from '@joplin/utils/ipc';
import { BrowserWindow, Tray, WebContents, screen, App } from 'electron';
import bridge from './bridge';
const url = require('url');
const path = require('path');
@@ -19,6 +20,9 @@ import handleCustomProtocols, { CustomProtocolHandler } from './utils/customProt
import { clearTimeout, setTimeout } from 'timers';
import { resolve } from 'path';
import { defaultWindowId } from '@joplin/lib/reducer';
import { msleep, Second } from '@joplin/utils/time';
import determineBaseAppDirs from '@joplin/lib/determineBaseAppDirs';
import getAppName from '@joplin/lib/getAppName';
interface RendererProcessQuitReply {
canClose: boolean;
@@ -34,13 +38,21 @@ interface SecondaryWindowData {
electronId: number;
}
export interface Options {
env: string;
profilePath: string|null;
isDebugMode: boolean;
isEndToEndTesting: boolean;
initialCallbackUrl: string;
}
export default class ElectronAppWrapper {
private logger_: Logger = null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
private electronApp_: any;
private electronApp_: App;
private env_: string;
private isDebugMode_: boolean;
private profilePath_: string;
private isEndToEndTesting_: boolean;
private win_: BrowserWindow = null;
private mainWindowHidden_ = true;
@@ -58,13 +70,29 @@ export default class ElectronAppWrapper {
private customProtocolHandler_: CustomProtocolHandler = null;
private updatePollInterval_: ReturnType<typeof setTimeout>|null = null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
public constructor(electronApp: any, env: string, profilePath: string|null, isDebugMode: boolean, initialCallbackUrl: string) {
private profileLocker_: FileLocker|null = null;
private ipcServer_: IpcServer|null = null;
private ipcStartPort_ = 2658;
private ipcLogger_: Logger;
public constructor(electronApp: App, { env, profilePath, isDebugMode, initialCallbackUrl, isEndToEndTesting }: Options) {
this.electronApp_ = electronApp;
this.env_ = env;
this.isDebugMode_ = isDebugMode;
this.profilePath_ = profilePath;
this.initialCallbackUrl_ = initialCallbackUrl;
this.isEndToEndTesting_ = isEndToEndTesting;
this.profileLocker_ = new FileLocker(`${this.profilePath_}/lock`);
// Note: in certain contexts `this.logger_` doesn't seem to be available, especially for IPC
// calls, either because it hasn't been set or other issue. So we set one here specifically
// for this.
this.ipcLogger_ = new Logger();
this.ipcLogger_.addTarget(TargetType.File, {
path: `${profilePath}/log-cross-app-ipc.txt`,
});
}
public electronApp() {
@@ -184,7 +212,6 @@ export default class ElectronAppWrapper {
spellcheck: true,
enableRemoteModule: true,
},
webviewTag: true,
// We start with a hidden window, which is then made visible depending on the showTrayIcon setting
// https://github.com/laurent22/joplin/issues/2031
//
@@ -261,7 +288,9 @@ export default class ElectronAppWrapper {
// Waiting for one of the ready events might work but they might not be triggered if there's an error, so
// the easiest is to use a timeout. Keep in mind that if you get a white window on Windows it might be due
// to this line though.
if (debugEarlyBugs) {
//
// Don't show the dev tools while end-to-end testing to simplify the logic that finds the main window.
if (debugEarlyBugs && !this.isEndToEndTesting_) {
// Since a recent release of Electron (v34?), calling openDevTools() here does nothing
// if a plugin devtool window is already opened. Maybe because they do a check on
// `isDevToolsOpened` which indeed returns `true` (but shouldn't since it's for a
@@ -410,7 +439,7 @@ export default class ElectronAppWrapper {
if (message.target === 'plugin') {
const win = this.pluginWindows_[message.pluginId];
if (!win) {
this.logger().error(`Trying to send IPC message to non-existing plugin window: ${message.pluginId}`);
this.ipcLogger_.error(`Trying to send IPC message to non-existing plugin window: ${message.pluginId}`);
return;
}
@@ -465,12 +494,24 @@ export default class ElectronAppWrapper {
});
}
public quit() {
private onExit() {
this.stopPeriodicUpdateCheck();
this.profileLocker_.unlockSync();
// Probably doesn't matter if the server is not closed cleanly? Thus the lack of `await`
// eslint-disable-next-line promise/prefer-await-to-then -- Needed here because onExit() is not async
void stopServer(this.ipcServer_).catch(_error => {
// Ignore it since we're stopping, and to prevent unnecessary messages.
});
}
public quit() {
this.onExit();
this.electronApp_.quit();
}
public exit(errorCode = 0) {
this.onExit();
this.electronApp_.exit(errorCode);
}
@@ -536,20 +577,32 @@ export default class ElectronAppWrapper {
this.tray_ = null;
}
public ensureSingleInstance() {
if (this.env_ === 'dev') return false;
public async sendCrossAppIpcMessage(message: Message, port: number|null = null, options: SendMessageOptions = null) {
this.ipcLogger_.info('Sending message:', message);
const gotTheLock = this.electronApp_.requestSingleInstanceLock();
if (port === null) port = this.ipcStartPort_;
if (!gotTheLock) {
// Another instance is already running - exit
this.quit();
return true;
return await sendMessage(port, {
...message,
sourcePort: this.ipcServer_.port,
secretKey: this.ipcServer_.secretKey,
}, {
logger: this.ipcLogger_,
...options,
});
}
public async ensureSingleInstance() {
// When end-to-end testing, multiple instances of Joplin are intentionally created at the same time,
// or very close to one another. The single instance handling logic can interfere with this, so disable it.
if (this.isEndToEndTesting_) return false;
interface OnSecondInstanceMessageData {
profilePath: string;
argv: string[];
}
// Someone tried to open a second instance - focus our window instead
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
this.electronApp_.on('second-instance', (_e: any, argv: string[]) => {
const activateWindow = (argv: string[]) => {
const win = this.mainWindow();
if (!win) return;
if (win.isMinimized()) win.restore();
@@ -562,13 +615,96 @@ export default class ElectronAppWrapper {
void this.openCallbackUrl(url);
}
}
};
const messageHandlers: Record<string, IpcMessageHandler> = {
'onSecondInstance': async (message) => {
const data = message.data as OnSecondInstanceMessageData;
if (data.profilePath === this.profilePath_) activateWindow(data.argv);
},
'restartAltInstance': async (message) => {
if (bridge().altInstanceId()) return false;
// We do this in a timeout after a short interval because we need this call to
// return the response immediately, so that the caller can call `quit()`
setTimeout(async () => {
const maxWait = 10000;
const interval = 300;
const loopCount = Math.ceil(maxWait / interval);
let callingAppGone = false;
for (let i = 0; i < loopCount; i++) {
const response = await this.sendCrossAppIpcMessage({
action: 'ping',
data: null,
secretKey: this.ipcServer_.secretKey,
}, message.sourcePort, {
sendToSpecificPortOnly: true,
});
if (!response.length) {
callingAppGone = true;
break;
}
await msleep(interval);
}
if (callingAppGone) {
// Wait a bit more because even if the app is not responding, the process
// might still be there for a short while.
await msleep(1000);
this.ipcLogger_.warn('restartAltInstance: App is gone - restarting it');
void bridge().launchAltAppInstance(this.env());
} else {
this.ipcLogger_.warn('restartAltInstance: Could not restart calling app because it was still open');
}
}, 100);
return true;
},
'ping': async (_message) => {
return true;
},
};
const defaultProfileDir = determineBaseAppDirs('', getAppName(true, this.env() === 'dev'), '').rootProfileDir;
const secretKeyFilePath = `${defaultProfileDir}/ipc_secret_key.txt`;
this.ipcLogger_.info('Starting server using secret key:', secretKeyFilePath);
this.ipcServer_ = await startServer(this.ipcStartPort_, secretKeyFilePath, async (message) => {
if (messageHandlers[message.action]) {
this.ipcLogger_.info('Got message:', message);
return messageHandlers[message.action](message);
}
throw newHttpError(404);
}, {
logger: this.ipcLogger_,
});
return false;
}
// First check that no other app is running from that profile folder
const gotAppLock = await this.profileLocker_.lock();
if (gotAppLock) return false;
public initializeCustomProtocolHandler(logger: LoggerWrapper) {
this.customProtocolHandler_ ??= handleCustomProtocols(logger);
const message: Message = {
action: 'onSecondInstance',
data: {
senderPort: this.ipcServer_.port,
profilePath: this.profilePath_,
argv: process.argv,
},
secretKey: this.ipcServer_.secretKey,
};
await this.sendCrossAppIpcMessage(message);
this.quit();
if (this.env() === 'dev') console.warn(`Closing the application because another instance is already running, or the previous instance was force-quit within the last ${Math.round(this.profileLocker_.options.interval / Second)} seconds.`);
return true;
}
// Electron's autoUpdater has to be init from the main process
@@ -606,9 +742,10 @@ export default class ElectronAppWrapper {
// the "ready" event. So we use the function below to make sure that the app is ready.
await this.waitForElectronAppReady();
const alreadyRunning = this.ensureSingleInstance();
const alreadyRunning = await this.ensureSingleInstance();
if (alreadyRunning) return;
this.customProtocolHandler_ = handleCustomProtocols();
this.createWindow();
this.electronApp_.on('before-quit', () => {

View File

@@ -1,4 +1,4 @@
import produce from 'immer';
import { produce } from 'immer';
import Setting from '@joplin/lib/models/Setting';
import { defaultState, defaultWindowState, State, WindowState } from '@joplin/lib/reducer';
import iterateItems from './gui/ResizableLayout/utils/iterateItems';

View File

@@ -92,7 +92,7 @@ class Application extends BaseApplication {
public reducer(state: AppState = appDefaultState, action: any) {
let newState = appReducer(state, action);
newState = resourceEditWatcherReducer(newState, action);
newState = super.reducer(newState, action);
newState = super.reducer(newState, action) as AppState;
return newState;
}
@@ -456,9 +456,6 @@ class Application extends BaseApplication {
bridge().openDevTools();
}
bridge().electronApp().initializeCustomProtocolHandler(
Logger.create('handleCustomProtocols'),
);
this.protocolHandler_ = bridge().electronApp().getCustomProtocolHandler();
this.protocolHandler_.allowReadAccessToDirectory(__dirname); // App bundle directory
this.protocolHandler_.allowReadAccessToDirectory(Setting.value('cacheDir'));
@@ -617,10 +614,11 @@ class Application extends BaseApplication {
clipperLogger.addTarget(TargetType.Console);
ClipperServer.instance().initialize(actionApi);
ClipperServer.instance().setEnabled(!Setting.value('altInstanceId'));
ClipperServer.instance().setLogger(clipperLogger);
ClipperServer.instance().setDispatch(this.store().dispatch);
if (Setting.value('clipperServer.autoStart')) {
if (ClipperServer.instance().enabled() && Setting.value('clipperServer.autoStart')) {
void ClipperServer.instance().start();
}

View File

@@ -6,7 +6,6 @@ import { dirname, toSystemSlashes } from '@joplin/lib/path-utils';
import { fileUriToPath } from '@joplin/utils/url';
import { urlDecode } from '@joplin/lib/string-utils';
import * as Sentry from '@sentry/electron/main';
import { ErrorEvent } from '@sentry/types/types';
import { homedir } from 'os';
import { msleep } from '@joplin/utils/time';
import { pathExists, pathExistsSync, writeFileSync } from 'fs-extra';
@@ -15,6 +14,7 @@ import isSafeToOpen from './utils/isSafeToOpen';
import { closeSync, openSync, readSync, statSync } from 'fs';
import { KB } from '@joplin/utils/bytes';
import { defaultWindowId } from '@joplin/lib/reducer';
import { execCommand } from '@joplin/utils';
interface LastSelectedPath {
file: string;
@@ -43,16 +43,18 @@ export class Bridge {
private appName_: string;
private appId_: string;
private logFilePath_ = '';
private altInstanceId_ = '';
private extraAllowedExtensions_: string[] = [];
private onAllowedExtensionsChangeListener_: OnAllowedExtensionsChange = ()=>{};
public constructor(electronWrapper: ElectronAppWrapper, appId: string, appName: string, rootProfileDir: string, autoUploadCrashDumps: boolean) {
public constructor(electronWrapper: ElectronAppWrapper, appId: string, appName: string, rootProfileDir: string, autoUploadCrashDumps: boolean, altInstanceId: string) {
this.electronWrapper_ = electronWrapper;
this.appId_ = appId;
this.appName_ = appName;
this.rootProfileDir_ = rootProfileDir;
this.autoUploadCrashDumps_ = autoUploadCrashDumps;
this.altInstanceId_ = altInstanceId;
this.lastSelectedPaths_ = {
file: null,
directory: null,
@@ -98,9 +100,9 @@ export class Bridge {
if (logAttachment) hint.attachments = [logAttachment];
const date = (new Date()).toISOString().replace(/[:-]/g, '').split('.')[0];
interface ErrorEventWithLog extends ErrorEvent {
type ErrorEventWithLog = (typeof event) & {
log: string[];
}
};
const errorEventWithLog: ErrorEventWithLog = {
...event,
@@ -120,6 +122,10 @@ export class Bridge {
},
integrations: [Sentry.electronMinidumpIntegration()],
// Using the default ipcMode value causes <iframe>s that use custom protocols to
// have isSecureOrigin: false, limiting which browser APIs are available.
ipcMode: Sentry.IPCMode.Classic,
};
if (this.autoUploadCrashDumps_) options.dsn = 'https://cceec550871b1e8a10fee4c7a28d5cf2@o4506576757522432.ingest.sentry.io/4506594281783296';
@@ -218,6 +224,10 @@ export class Bridge {
return this.electronApp().electronApp().getLocale();
};
public altInstanceId() {
return this.altInstanceId_;
}
// Applies to electron-context-menu@3:
//
// For now we have to disable spell checking in non-editor text
@@ -491,7 +501,44 @@ export class Bridge {
}
}
public restart(linuxSafeRestart = true) {
public appLaunchCommand(env: string, altInstanceId = '') {
const altInstanceArgs = altInstanceId ? ['--alt-instance-id', altInstanceId] : [];
if (env === 'dev') {
// This is convenient to quickly test on dev, but the path needs to be adjusted
// depending on how things are setup.
return {
execPath: `${homedir()}/.npm-global/bin/electron`,
args: [
`${homedir()}/src/joplin/packages/app-desktop`,
'--env', 'dev',
'--log-level', 'debug',
'--open-dev-tools',
'--no-welcome',
].concat(altInstanceArgs),
};
} else {
return {
execPath: bridge().electronApp().electronApp().getPath('exe'),
args: [].concat(altInstanceArgs),
};
}
}
public async launchAltAppInstance(env: string) {
const cmd = this.appLaunchCommand(env, 'alt1');
await execCommand([cmd.execPath].concat(cmd.args), { detached: true });
}
public async launchMainAppInstance(env: string) {
const cmd = this.appLaunchCommand(env, '');
await execCommand([cmd.execPath].concat(cmd.args), { detached: true });
}
public async restart() {
// Note that in this case we are not sending the "appClose" event
// to notify services and component that the app is about to close
// but for the current use-case it's not really needed.
@@ -502,13 +549,39 @@ export class Bridge {
execPath: process.env.PORTABLE_EXECUTABLE_FILE,
};
app.relaunch(options);
} else if (shim.isLinux() && linuxSafeRestart) {
this.showInfoMessageBox(_('The app is now going to close. Please relaunch it to complete the process.'));
} else if (this.altInstanceId_) {
// Couldn't get it to work using relaunch() - it would just "close" the app, but it
// would still be open in the tray except unusable. Or maybe it reopens it quickly but
// in a broken state. It might be due to the way it is launched from the main instance.
// So here we ask the main instance to relaunch this app after a short delay.
const responses = await this.electronApp().sendCrossAppIpcMessage({
action: 'restartAltInstance',
data: null,
});
// However is the main instance is not running, we're stuck, so the user needs to
// manually restart. `relaunch()` doesn't appear to work even when the main instance is
// not running.
const r = responses.find(r => !!r.response);
if (!r || !r.response) {
this.showInfoMessageBox(_('The app is now going to close. Please relaunch it to complete the process.'));
// Note: this should work, but doesn't:
// const cmd = this.appLaunchCommand(this.env(), this.altInstanceId_);
// app.relaunch({
// execPath: cmd.execPath,
// args: cmd.args,
// });
}
} else {
app.relaunch();
}
app.exit();
this.electronApp().exit();
}
public createImageFromPath(path: string) {
@@ -534,9 +607,9 @@ export class Bridge {
let bridge_: Bridge = null;
export function initBridge(wrapper: ElectronAppWrapper, appId: string, appName: string, rootProfileDir: string, autoUploadCrashDumps: boolean) {
export function initBridge(wrapper: ElectronAppWrapper, appId: string, appName: string, rootProfileDir: string, autoUploadCrashDumps: boolean, altInstanceId: string) {
if (bridge_) throw new Error('Bridge already initialized');
bridge_ = new Bridge(wrapper, appId, appName, rootProfileDir, autoUploadCrashDumps);
bridge_ = new Bridge(wrapper, appId, appName, rootProfileDir, autoUploadCrashDumps, altInstanceId);
return bridge_;
}

View File

@@ -7,7 +7,9 @@ import * as exportFolders from './exportFolders';
import * as exportNotes from './exportNotes';
import * as focusElement from './focusElement';
import * as openNoteInNewWindow from './openNoteInNewWindow';
import * as openPrimaryAppInstance from './openPrimaryAppInstance';
import * as openProfileDirectory from './openProfileDirectory';
import * as openSecondaryAppInstance from './openSecondaryAppInstance';
import * as replaceMisspelling from './replaceMisspelling';
import * as restoreNoteRevision from './restoreNoteRevision';
import * as startExternalEditing from './startExternalEditing';
@@ -29,7 +31,9 @@ const index: any[] = [
exportNotes,
focusElement,
openNoteInNewWindow,
openPrimaryAppInstance,
openProfileDirectory,
openSecondaryAppInstance,
replaceMisspelling,
restoreNoteRevision,
startExternalEditing,

View File

@@ -0,0 +1,19 @@
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
import { _ } from '@joplin/lib/locale';
import bridge from '../services/bridge';
import Setting from '@joplin/lib/models/Setting';
export const declaration: CommandDeclaration = {
name: 'openPrimaryAppInstance',
label: () => _('Open primary app instance...'),
};
export const runtime = (): CommandRuntime => {
return {
execute: async (_context: CommandContext) => {
await bridge().launchMainAppInstance(Setting.value('env'));
},
enabledCondition: 'isAltInstance',
};
};

View File

@@ -0,0 +1,19 @@
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
import { _ } from '@joplin/lib/locale';
import bridge from '../services/bridge';
import Setting from '@joplin/lib/models/Setting';
export const declaration: CommandDeclaration = {
name: 'openSecondaryAppInstance',
label: () => _('Open secondary app instance...'),
};
export const runtime = (): CommandRuntime => {
return {
execute: async (_context: CommandContext) => {
await bridge().launchAltAppInstance(Setting.value('env'));
},
enabledCondition: '!isAltInstance',
};
};

View File

@@ -24,6 +24,7 @@ class ClipperConfigScreenComponent extends React.Component {
}
private enableClipperServer_click() {
if (!ClipperServer.instance().enabled()) return;
Setting.setValue('clipperServer.autoStart', true);
void ClipperServer.instance().start();
}
@@ -70,6 +71,8 @@ class ClipperConfigScreenComponent extends React.Component {
const webClipperStatusComps = [];
const clipperEnabled = ClipperServer.instance().enabled();
if (this.props.clipperServerAutoStart) {
webClipperStatusComps.push(
<p key="text_1" style={theme.textStyle}>
@@ -95,13 +98,22 @@ class ClipperConfigScreenComponent extends React.Component {
</button>,
);
} else {
if (!clipperEnabled) {
webClipperStatusComps.push(
<p key="text_4" style={theme.textStyle}>
{_('The web clipper service cannot be enabled in this instance of Joplin.')}
</p>,
);
} else {
webClipperStatusComps.push(
<p key="text_4" style={theme.textStyle}>
{_('The web clipper service is not enabled.')}
</p>,
);
}
webClipperStatusComps.push(
<p key="text_4" style={theme.textStyle}>
{_('The web clipper service is not enabled.')}
</p>,
);
webClipperStatusComps.push(
<button key="enable_button" style={buttonStyle} onClick={this.enableClipperServer_click}>
<button key="enable_button" style={buttonStyle} onClick={this.enableClipperServer_click} disabled={!clipperEnabled}>
{_('Enable Web Clipper Service')}
</button>,
);

View File

@@ -40,7 +40,7 @@ export default function ButtonBar(props: Props) {
}
return (
<StyledRoot>
<StyledRoot className='button-bar'>
<Button
onClick={props.onCancelClick}
level={ButtonLevel.Secondary}

View File

@@ -7,7 +7,7 @@ import SearchPlugins from './SearchPlugins';
import PluginBox, { UpdateState } from './PluginBox';
import Button, { ButtonLevel, ButtonSize } from '../../../Button/Button';
import bridge from '../../../../services/bridge';
import produce from 'immer';
import { produce } from 'immer';
import { OnChangeEvent } from '../../../lib/SearchInput/SearchInput';
import { PluginItem, ItemEvent, OnPluginSettingChangeEvent } from '@joplin/lib/components/shared/config/plugins/types';
import RepositoryApi, { InstallMode } from '@joplin/lib/services/plugins/RepositoryApi';

View File

@@ -10,6 +10,7 @@ import { reducer, defaultState, generateApplicationConfirmUrl, checkIfLoginWasSu
import { AppState } from '../app.reducer';
import Logger from '@joplin/utils/Logger';
import { reg } from '@joplin/lib/registry';
import JoplinCloudSignUpCallToAction from './JoplinCloudSignUpCallToAction';
const logger = Logger.create('JoplinCloudLoginScreen');
const { connect } = require('react-redux');
@@ -105,6 +106,7 @@ const JoplinCloudScreenComponent = (props: Props) => {
) : null}
</p>
{state.active === 'LINK_USED' ? <div id="loading-animation" /> : null}
<JoplinCloudSignUpCallToAction />
</div>
<ButtonBar onCancelClick={() => props.dispatch({ type: 'NAV_BACK' })} />
</div>

View File

@@ -0,0 +1,20 @@
import { _ } from '@joplin/lib/locale';
import * as React from 'react';
import bridge from '../services/bridge';
const JoplinCloudSignUpCallToAction = () => {
const onJoplinCloudSignUpClick = async () => {
await bridge().openExternal('https://joplinapp.org/plans/');
};
return <div className="joplin-cloud-sign-up">
<a
href="#"
onClick={onJoplinCloudSignUpClick}
>{_('Sign up to Joplin Cloud')}</a>
</div>;
};
export default JoplinCloudSignUpCallToAction;

View File

@@ -17,7 +17,7 @@ import { AppState } from '../app.reducer';
import { saveLayout, loadLayout } from './ResizableLayout/utils/persist';
import Setting from '@joplin/lib/models/Setting';
import shouldShowMissingPasswordWarning from '@joplin/lib/components/shared/config/shouldShowMissingPasswordWarning';
import produce from 'immer';
import { produce } from 'immer';
import shim from '@joplin/lib/shim';
import bridge from '../services/bridge';
import styled from 'styled-components';
@@ -83,6 +83,7 @@ interface Props {
notesColumns: NoteListColumns;
showInvalidJoplinCloudCredential: boolean;
toast: Toast;
shouldSwitchToAppleSiliconVersion: boolean;
}
interface ShareFolderDialogOptions {
@@ -478,6 +479,10 @@ class MainScreenComponent extends React.Component<Props, State> {
});
};
const onDisableSync = () => {
Setting.setValue('sync.target', null);
};
const onViewSyncSettingsScreen = () => {
this.props.dispatch({
type: 'NAV_GO',
@@ -488,6 +493,11 @@ class MainScreenComponent extends React.Component<Props, State> {
});
};
const onDownloadAppleSiliconVersion = () => {
// The website should redirect to the correct version
shim.openUrl('https://joplinapp.org/download/');
};
const onRestartAndUpgrade = async () => {
Setting.setValue('sync.upgradeState', Setting.SYNC_UPGRADE_STATE_MUST_DO);
await Setting.saveAll();
@@ -570,11 +580,19 @@ class MainScreenComponent extends React.Component<Props, State> {
);
} else if (this.props.mustUpgradeAppMessage) {
msg = this.renderNotificationMessage(this.props.mustUpgradeAppMessage);
} else if (this.props.shouldSwitchToAppleSiliconVersion) {
msg = this.renderNotificationMessage(
_('You are running the Intel version of Joplin on an Apple Silicon processor. Download the Apple Silicon one for better performance.'),
_('Download it now'),
onDownloadAppleSiliconVersion,
);
} else if (this.props.showInvalidJoplinCloudCredential) {
msg = this.renderNotificationMessage(
_('Your Joplin Cloud credentials are invalid, please login.'),
_('Login to Joplin Cloud.'),
onViewJoplinCloudLoginScreen,
_('Disable synchronisation'),
onDisableSync,
);
}
@@ -605,7 +623,8 @@ class MainScreenComponent extends React.Component<Props, State> {
this.showShareInvitationNotification(props) ||
this.props.needApiAuth ||
!!this.props.mustUpgradeAppMessage ||
props.showInvalidJoplinCloudCredential;
props.showInvalidJoplinCloudCredential ||
props.shouldSwitchToAppleSiliconVersion;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
@@ -785,7 +804,7 @@ class MainScreenComponent extends React.Component<Props, State> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
dispatch={this.props.dispatch as any}
/>
<UpdateNotification themeId={this.props.themeId} />
<UpdateNotification />
<PluginNotification
themeId={this.props.themeId}
toast={this.props.toast}
@@ -833,6 +852,7 @@ const mapStateToProps = (state: AppState) => {
notesColumns: validateColumns(state.settings['notes.columns']),
showInvalidJoplinCloudCredential: state.settings['sync.target'] === 10 && state.mustAuthenticate,
toast: state.toast,
shouldSwitchToAppleSiliconVersion: shim.isAppleSilicon() && process.arch !== 'arm64',
};
};

View File

@@ -172,6 +172,7 @@ interface Props {
pluginMenus: any[];
['spellChecker.enabled']: boolean;
['spellChecker.languages']: string[];
markdownEditorVisible: boolean;
plugins: PluginStates;
customCss: string;
locale: string;
@@ -278,6 +279,7 @@ function useMenuStates(menu: any, props: Props) {
props['notes.sortOrder.reverse'],
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
props['folders.sortOrder.reverse'],
props.markdownEditorVisible,
props.tabMovesFocus,
props.noteListRendererId,
props.showNoteCounts,
@@ -479,6 +481,7 @@ function useMenu(props: Props) {
menuItemDic.focusElementNoteList,
menuItemDic.focusElementNoteTitle,
menuItemDic.focusElementNoteBody,
menuItemDic.focusElementNoteViewer,
menuItemDic.focusElementToolbar,
];
@@ -552,11 +555,19 @@ function useMenu(props: Props) {
const newFolderItem = menuItemDic.newFolder;
const newSubFolderItem = menuItemDic.newSubFolder;
const printItem = menuItemDic.print;
const openSecondaryAppInstance = menuItemDic.openSecondaryAppInstance;
const openPrimaryAppInstance = menuItemDic.openPrimaryAppInstance;
const switchProfileItem = {
label: _('Switch profile'),
submenu: switchProfileMenuItems,
};
const profilesAndAppInstancesItems = [
openSecondaryAppInstance,
openPrimaryAppInstance,
switchProfileItem,
];
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
let toolsItems: any[] = [];
@@ -668,7 +679,7 @@ function useMenu(props: Props) {
platforms: ['darwin'],
},
shim.isMac() ? noItem : switchProfileItem,
...(shim.isMac() ? [] : profilesAndAppInstancesItems),
shim.isMac() ? {
label: _('Hide %s', 'Joplin'),
@@ -715,8 +726,10 @@ function useMenu(props: Props) {
}, {
type: 'separator',
},
printItem,
switchProfileItem,
printItem, {
type: 'separator',
},
...profilesAndAppInstancesItems,
],
};
@@ -789,6 +802,7 @@ function useMenu(props: Props) {
shim.isMac() ? noItem : menuItemDic.toggleMenuBar,
menuItemDic.toggleNoteList,
menuItemDic.toggleVisiblePanes,
menuItemDic.toggleEditorPlugin,
{
label: _('Layout button sequence'),
submenu: layoutButtonSequenceMenuItems,
@@ -1139,7 +1153,7 @@ function MenuBar(props: Props): any {
const mapStateToProps = (state: AppState): Partial<Props> => {
const whenClauseContext = stateToWhenClauseContext(state);
const whenClauseContext = stateToWhenClauseContext(state, { windowId: state.windowId });
const secondaryWindowFocused = state.windowId !== defaultWindowId;
@@ -1165,6 +1179,7 @@ const mapStateToProps = (state: AppState): Partial<Props> => {
pluginMenus: stateUtils.selectArrayShallow({ array: pluginUtils.viewsByType(state.pluginService.plugins, 'menu') }, 'menuBar.pluginMenus'),
['spellChecker.languages']: state.settings['spellChecker.languages'],
['spellChecker.enabled']: state.settings['spellChecker.enabled'],
markdownEditorVisible: whenClauseContext.markdownEditorVisible,
plugins: state.pluginService.plugins,
customCss: state.customViewerCss,
profileConfig: state.profileConfig,

View File

@@ -167,7 +167,9 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
scrollTo: (options: ScrollOptions) => {
if (options.type === ScrollOptionTypes.Hash) {
if (!webviewRef.current) return;
webviewRef.current.send('scrollToHash', options.value as string);
const hash: string = options.value;
webviewRef.current.send('scrollToHash', hash);
editorRef.current.jumpToHash(hash);
} else if (options.type === ScrollOptionTypes.Percent) {
const percent = options.value as number;
setEditorPercentScroll(percent);

View File

@@ -129,6 +129,14 @@ const useEditorCommands = (props: Props) => {
props.webviewRef.current.send('focus');
}
},
'viewer.focus': () => {
if (props.visiblePanes.includes('viewer')) {
const editorCursorLine = editorRef.current.getCursor().line;
props.webviewRef.current.focusLine(editorCursorLine);
} else {
logger.info('Viewer not focused (not visible).');
}
},
search: () => {
return editorRef.current.execCommand(EditorCommandType.ShowSearch);
},

View File

@@ -43,6 +43,10 @@ import useKeyboardRefocusHandler from './utils/useKeyboardRefocusHandler';
import useDocument from '../../../hooks/useDocument';
import useEditDialog from './utils/useEditDialog';
import useEditDialogEventListeners from './utils/useEditDialogEventListeners';
import Setting from '@joplin/lib/models/Setting';
import useTextPatternsLookup from './utils/useTextPatternsLookup';
import { toFileProtocolPath } from '@joplin/utils/path';
import { RenderResultPluginAsset } from '@joplin/renderer/types';
const logger = Logger.create('TinyMCE');
@@ -56,7 +60,7 @@ const logger = Logger.create('TinyMCE');
//
// The problem is that the list plugin was, unknown to me, relying on this <br/>
// being present. Without it, trying to add a bullet point or checkbox on an
// empty document, does nothing. The exact reason for this is unclear
// empty document, adds an empty paragraph. The exact reason for this is unclear
// so as a workaround we manually add this <br> for empty documents,
// which fixes the issue.
//
@@ -69,8 +73,8 @@ const logger = Logger.create('TinyMCE');
//
// Perhaps upgrading the list plugin (which is a fork of TinyMCE own list plugin)
// would help?
function awfulInitHack(html: string): string {
return html === '<div id="rendered-md"></div>' ? '<div id="rendered-md"><p></p></div>' : html;
function preprocessHtml(html: string): string {
return html === '' ? '<p></p>' : html;
}
@@ -654,6 +658,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
// Create and setup the editor
// -----------------------------------------------------------------------------------------
const textPatternsLookupRef = useTextPatternsLookup({ enabled: props.enableTextPatterns, enableMath: props.mathEnabled });
useEffect(() => {
if (!scriptLoaded) return;
if (!editorContainer) return;
@@ -726,6 +731,25 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
language_url: ['en_US', 'en_GB'].includes(language) ? undefined : `${bridge().vendorDir()}/lib/tinymce/langs/${language}`,
toolbar: toolbar.join(' '),
localization_function: _,
// See https://www.tiny.cloud/docs/tinymce/latest/tinymce-and-csp/#content_security_policy
content_security_policy: Setting.value('featureFlag.richText.useStrictContentSecurityPolicy') ? [
// Media: *: Allow users to include images and videos from the internet (e.g. ![](http://example.com/image.png)).
// Media: blob: Allow loading images/videos/audio from blob URLs. The Rich Text Editor
// replaces certain base64 URLs with blob URLs.
// Media: data: Allow loading images and other media from data: URLs
'default-src \'self\'',
'img-src \'self\' blob: data: *', // Images
'media-src \'self\' blob: data: *', // Audio and video players
// Disallow certain unused features
'child-src \'none\'', // Should not contain sub-frames
'object-src \'none\'', // Objects can be used for script injection
'form-action \'none\'', // No submitting forms
// Styles: unsafe-inline: TinyMCE uses inline style="" styles.
// Styles: *: Allow users to include styles from the internet (e.g. <style src="https://example.com/style.css">)
'style-src \'self\' \'unsafe-inline\' * data:',
].join(' ; ') : undefined,
contextmenu: false,
browser_spellcheck: true,
@@ -736,9 +760,42 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
joplinSub: { inline: 'sub', remove: 'all' },
joplinSup: { inline: 'sup', remove: 'all' },
code: { inline: 'code', remove: 'all', attributes: { spellcheck: 'false' } },
forecolor: { inline: 'span', styles: { color: '%value' } },
// Foreground color: The remove_similar: true is necessary here for the "remove formatting"
// button to work. See https://github.com/tinymce/tinymce/issues/5026.
forecolor: { inline: 'span', styles: { color: '%value' }, remove_similar: true },
},
text_patterns: [],
text_patterns_lookup: () => textPatternsLookupRef.current(),
setup: (editor: Editor) => {
editor.addCommand('joplinMath', async () => {
const katex = editor.selection.getContent();
const md = `$${katex}$`;
// Save and clear the selection -- when this command is activated by a text pattern,
// TinyMCE:
// 1. Adjusts the selection just before calling the command to include the to-be-formatted text.
// 2. Calls the command.
// 3. Removes the "$" characters and restores the selection.
//
// As a result, the selection needs to be saved and restored.
const mathSelection = editor.selection.getBookmark();
const result = await markupToHtml.current(MarkupLanguage.Markdown, md, { bodyOnly: true });
// Replace the math...
const finalSelection = editor.selection.getBookmark();
editor.selection.moveToBookmark(mathSelection);
editor.selection.setContent(result.html);
editor.selection.moveToBookmark(finalSelection); // ...then move the selection back.
// Fire update events
editor.fire(TinyMceEditorEvents.JoplinChange);
dispatchDidUpdate(editor);
// The last replacement seems to need to be manually added to the undo history
editor.undoManager.add();
});
editor.addCommand('joplinAttach', () => {
insertResourcesIntoContentRef.current();
});
@@ -891,6 +948,16 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
return docHead_;
}
const assetToUrl = (asset: RenderResultPluginAsset) => {
if (asset.pathIsAbsolute) {
// This is important on Windows, where the C:/ at the start of the path
// is interpreted as a relative subfolder without the file:// prefix.
return toFileProtocolPath(asset.path);
} else {
return asset.path;
}
};
const allCssFiles = [
`${bridge().vendorDir()}/lib/@fortawesome/fontawesome-free/css/all.min.css`,
`gui/note-viewer/pluginAssets/highlight.js/${theme.codeThemeCss}`,
@@ -898,16 +965,14 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
pluginAssets
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
.filter((a: any) => a.mime === 'text/css')
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
.map((a: any) => a.path),
.map(assetToUrl),
);
const allJsFiles = [].concat(
pluginAssets
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
.filter((a: any) => a.mime === 'application/javascript')
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
.map((a: any) => a.path),
.map(assetToUrl),
);
const filePathToElementId = (path: string) => {
@@ -982,6 +1047,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
return true;
}
const lastNoteIdRef = useRef(props.noteId);
useEffect(() => {
if (!editor) return () => {};
@@ -995,7 +1061,10 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
const loadContent = async () => {
const resourcesEqual = resourceInfosEqual(lastOnChangeEventInfo.current.resourceInfos, props.resourceInfos);
if (lastOnChangeEventInfo.current.content !== props.content || !resourcesEqual) {
// Use nextOnChangeEventInfo's noteId -- lastOnChangeEventInfo can be slightly out-of-date.
const differentNoteId = lastNoteIdRef.current !== props.noteId;
const differentContent = lastOnChangeEventInfo.current.content !== props.content;
if (differentNoteId || differentContent || !resourcesEqual) {
const result = await props.markupToHtml(
props.contentMarkupLanguage,
props.content,
@@ -1006,6 +1075,11 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
// This prevents HTML-style resource URLs (e.g. <a href="file://path/to/resource/.../"></a>)
// from being discarded.
allowedFilePrefixes: [props.resourceDirectory],
// Remove the wrapping <div id="rendered-md">...</div>, which can cause
// TinyMCE to crash in some cases.
// See https://github.com/tinymce/tinymce/issues/10276
bodyOnly: true,
}),
);
if (cancelled) return;
@@ -1017,7 +1091,12 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
// when the note content is updated externally.
const offsetBookmarkId = 2;
const bookmark = editor.selection.getBookmark(offsetBookmarkId);
editor.setContent(awfulInitHack(result.html));
const htmlAndCss = [
`<style>${result.cssStrings?.join('\n')}</style>`,
preprocessHtml(result.html),
].join('\n');
editor.setContent(htmlAndCss);
lastNoteIdRef.current = props.noteId;
if (lastOnChangeEventInfo.current.contentKey !== props.contentKey) {
// Need to clear UndoManager to avoid this problem:
@@ -1049,6 +1128,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
const allAssetsOptions: NoteStyleOptions = {
contentMaxWidthTarget: '.mce-content-body',
contentWrapperSelector: '.mce-content-body',
scrollbarSize: props.scrollbarSize,
themeId: props.contentMarkupLanguage === MarkupLanguage.Html ? 1 : null,
whiteBackgroundNoteRendering: props.whiteBackgroundNoteRendering,
@@ -1066,7 +1146,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
cancelled = true;
};
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [editor, props.themeId, props.scrollbarSize, props.markupToHtml, props.allAssets, props.content, props.resourceInfos, props.contentKey, props.contentMarkupLanguage, props.whiteBackgroundNoteRendering]);
}, [editor, props.noteId, props.themeId, props.scrollbarSize, props.markupToHtml, props.allAssets, props.content, props.resourceInfos, props.contentKey, props.contentMarkupLanguage, props.whiteBackgroundNoteRendering]);
useEffect(() => {
if (!editor) return () => {};
@@ -1105,6 +1185,13 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
};
}, [editor]);
useEffect(() => {
if (!editor) return;
// Meta+P is bound by default to print by TinyMCE. It can be unbound, but it seems necessary
// to do so after the editor loads. Meta+P should be able to trigger Joplin built-in shortcuts.
editor.shortcuts.remove('Meta+P');
}, [editor]);
// -----------------------------------------------------------------------------------------
// Handle onChange event
// -----------------------------------------------------------------------------------------

View File

@@ -1576,7 +1576,7 @@
var removeStyles = function (dom, element, styles) {
Tools.each(styles, function (style) {
var _a;
return dom.setStyle(element, (_a = {}, _a[style] = '', _a));
return dom.setStyle(element, style, '');
});
};
var getEndPointNode = function (editor, rng, start, root) {

View File

@@ -7,8 +7,7 @@ import { ContextMenuOptions, ContextMenuItemType } from '../../../utils/contextM
import { menuItems } from '../../../utils/contextMenu';
import MenuUtils from '@joplin/lib/services/commands/MenuUtils';
import CommandService from '@joplin/lib/services/CommandService';
import Setting from '@joplin/lib/models/Setting';
import type { Event as ElectronEvent, MenuItemConstructorOptions } from 'electron';
import type { ContextMenuParams, Event as ElectronEvent, MenuItemConstructorOptions } from 'electron';
import Resource from '@joplin/lib/models/Resource';
import { TinyMceEditorEvents } from './types';
@@ -23,33 +22,6 @@ const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
const menuUtils = new MenuUtils(CommandService.instance());
// x and y are the absolute coordinates, as returned by the context-menu event
// handler on the webContent. This function will return null if the point is
// not within the TinyMCE editor.
function contextMenuElement(editor: Editor, x: number, y: number) {
if (!editor || !editor.getDoc()) return null;
const containerDoc = editor.getContainer().ownerDocument;
const iframes = containerDoc.getElementsByClassName('tox-edit-area__iframe');
if (!iframes.length) return null;
const zoom = Setting.value('windowContentZoomFactor') / 100;
const xScreen = x / zoom;
const yScreen = y / zoom;
// We use .elementFromPoint to handle the case where a dialog is covering
// part of the editor.
const targetElement = containerDoc.elementFromPoint(xScreen, yScreen);
if (targetElement !== iframes[0]) {
return null;
}
const iframeRect = iframes[0].getBoundingClientRect();
const relativeX = xScreen - iframeRect.left;
const relativeY = yScreen - iframeRect.top;
return editor.getDoc().elementFromPoint(relativeX, relativeY);
}
interface ContextMenuActionOptions {
current: ContextMenuOptions;
}
@@ -60,7 +32,7 @@ export default function(editor: Editor, plugins: PluginStates, dispatch: Dispatc
useEffect(() => {
if (!editor) return () => {};
const contextMenuItems = menuItems(dispatch, htmlToMd, mdToHtml);
const contextMenuItems = menuItems(dispatch);
const targetWindow = bridge().activeWindow();
const makeMainMenuItems = (element: Element) => {
@@ -130,13 +102,7 @@ export default function(editor: Editor, plugins: PluginStates, dispatch: Dispatc
return [];
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
function onContextMenu(event: ElectronEvent, params: any) {
const element = contextMenuElement(editor, params.x, params.y);
if (!element) return;
event.preventDefault();
const showContextMenu = (element: HTMLElement, misspelledWord: string|null, dictionarySuggestions: string[]) => {
const menu = new Menu();
const menuItems: MenuItemType[] = [];
const toMenuItems = (specs: MenuItemConstructorOptions[]) => {
@@ -145,7 +111,7 @@ export default function(editor: Editor, plugins: PluginStates, dispatch: Dispatc
menuItems.push(...makeEditableMenuItems(element));
menuItems.push(...makeMainMenuItems(element));
const spellCheckerMenuItems = SpellCheckerService.instance().contextMenuItems(params.misspelledWord, params.dictionarySuggestions);
const spellCheckerMenuItems = SpellCheckerService.instance().contextMenuItems(misspelledWord, dictionarySuggestions);
menuItems.push(
...toMenuItems(spellCheckerMenuItems),
);
@@ -157,13 +123,49 @@ export default function(editor: Editor, plugins: PluginStates, dispatch: Dispatc
menu.append(item);
}
menu.popup({ window: targetWindow });
}
};
targetWindow.webContents.prependListener('context-menu', onContextMenu);
let lastTarget: EventTarget|null = null;
const onElectronContextMenu = (event: ElectronEvent, params: ContextMenuParams) => {
if (!lastTarget) return;
const element = lastTarget as HTMLElement;
lastTarget = null;
event.preventDefault();
showContextMenu(element, params.misspelledWord, params.dictionarySuggestions);
};
const onBrowserContextMenu = (event: PointerEvent) => {
const isKeyboard = event.buttons === 0;
if (isKeyboard) {
// Context menu events from the keyboard seem to always use <body> as the
// event target. Since which context menu is displayed depends on what the
// target is, using event.target for keyboard-triggered contextmenu events
// would prevent keyboard-only users from accessing certain functionality.
// To fix this, use the selection instead.
lastTarget = editor.selection.getNode();
} else {
lastTarget = event.target;
}
// Plugins in the Rich Text Editor (e.g. the mermaid renderer) can sometimes
// create custom right-click events. These don't trigger the Electron 'context-menu'
// event. As such, the context menu must be shown manually.
const isFromPlugin = !event.isTrusted;
if (isFromPlugin) {
event.preventDefault();
showContextMenu(lastTarget as HTMLElement, null, []);
lastTarget = null;
}
};
targetWindow.webContents.prependListener('context-menu', onElectronContextMenu);
editor.on('contextmenu', onBrowserContextMenu);
return () => {
editor.off('contextmenu', onBrowserContextMenu);
if (!targetWindow.isDestroyed() && targetWindow?.webContents?.off) {
targetWindow.webContents.off('context-menu', onContextMenu);
targetWindow.webContents.off('context-menu', onElectronContextMenu);
}
};
}, [editor, plugins, dispatch, htmlToMd, mdToHtml, editDialog]);

View File

@@ -0,0 +1,40 @@
import { useRef } from 'react';
interface TextPatternOptions {
enabled: boolean;
enableMath: boolean;
}
const useTextPatternsLookup = ({ enabled, enableMath }: TextPatternOptions) => {
const getTextPatterns = () => {
if (!enabled) return [];
return [
// See https://www.tiny.cloud/docs/tinymce/latest/content-behavior-options/#text_patterns
// for the default TinyMCE text patterns
{ start: '==', end: '==', format: 'joplinHighlight' },
// Only replace math if math rendering is enabled.
enableMath && { start: '$', end: '$', cmd: 'joplinMath' },
{ start: '`', end: '`', format: 'code' },
{ start: '*', end: '*', format: 'italic' },
{ start: '**', end: '**', format: 'bold' },
{ start: '#', format: 'h1' },
{ start: '##', format: 'h2' },
{ start: '###', format: 'h3' },
{ start: '####', format: 'h4' },
{ start: '#####', format: 'h5' },
{ start: '######', format: 'h6' },
{ start: '1.', cmd: 'InsertOrderedList' },
{ start: '*', cmd: 'InsertUnorderedList' },
{ start: '-', cmd: 'InsertUnorderedList' },
].filter(pattern => !!pattern);
};
// Store the lookup callback in a ref so that the editor doesn't need to be reloaded
// to use the new patterns:
const patternLookupRef = useRef(getTextPatterns);
patternLookupRef.current = getTextPatterns;
return patternLookupRef;
};
export default useTextPatternsLookup;

View File

@@ -2,72 +2,37 @@ import PluginService from '@joplin/lib/services/plugins/PluginService';
import { useEffect } from 'react';
import { Editor } from 'tinymce';
const useWebViewApi = (editor: Editor, window: Window) => {
interface WebViewApi {
postMessage: (contentScriptId: string, message: unknown)=> Promise<unknown>;
}
interface ExtendedWindow extends Window {
webviewApi: WebViewApi;
}
const useWebViewApi = (editor: Editor, containerWindow: Window) => {
useEffect(() => {
if (!editor) return ()=>{};
if (!window) return ()=>{};
if (!containerWindow) return ()=>{};
const scriptElement = window.document.createElement('script');
const channelId = `plugin-post-message-${Math.random()}`;
scriptElement.appendChild(window.document.createTextNode(`
window.webviewApi = {
postMessage: (contentScriptId, message) => {
const channelId = ${JSON.stringify(channelId)};
const messageId = Math.random();
window.parent.postMessage({
channelId,
messageId,
contentScriptId,
message,
}, '*');
const waitForResponse = async () => {
while (true) {
const messageEvent = await new Promise(resolve => {
window.addEventListener('message', event => {
resolve(event);
}, {once: true});
});
if (messageEvent.source !== window.parent || messageEvent.data.messageId !== messageId) {
continue;
}
const data = messageEvent.data;
return data.response;
}
};
return waitForResponse();
},
};
`));
const editorWindow = editor.getWin();
editorWindow.document.head.appendChild(scriptElement);
const onMessageHandler = async (event: MessageEvent) => {
if (event.source !== editorWindow || event.data.channelId !== channelId) {
return;
}
const contentScriptId = event.data.contentScriptId;
const pluginService = PluginService.instance();
const plugin = pluginService.pluginById(
pluginService.pluginIdByContentScriptId(contentScriptId),
);
const result = await plugin.emitContentScriptMessage(contentScriptId, event.data.message);
editorWindow.postMessage({
messageId: event.data.messageId,
response: result,
}, '*');
const editorWindow = editor.getWin() as ExtendedWindow;
const webviewApi: WebViewApi = {
postMessage: async (contentScriptId: string, message: unknown) => {
const pluginService = PluginService.instance();
const plugin = pluginService.pluginById(
pluginService.pluginIdByContentScriptId(contentScriptId),
);
return await plugin.emitContentScriptMessage(contentScriptId, message);
},
};
window.addEventListener('message', onMessageHandler);
editorWindow.webviewApi = webviewApi;
return () => {
window.removeEventListener('message', onMessageHandler);
scriptElement.remove();
if (editorWindow.webviewApi === webviewApi) {
editorWindow.webviewApi = undefined;
}
};
}, [editor, window]);
}, [editor, containerWindow]);
};
export default useWebViewApi;

View File

@@ -163,8 +163,7 @@ function NoteEditorContent(props: NoteEditorProps) {
scrollbarSize: props.scrollbarSize,
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const allAssets = useCallback(async (markupLanguage: number, options: AllAssetsOptions = null): Promise<any[]> => {
const allAssets = useCallback(async (markupLanguage: number, options: AllAssetsOptions = null) => {
options = {
contentMaxWidthTarget: '',
...options,
@@ -172,7 +171,7 @@ function NoteEditorContent(props: NoteEditorProps) {
const theme = themeStyle(options.themeId ? options.themeId : props.themeId);
const markupToHtml = markupLanguageUtils.newMarkupToHtml({}, {
const markupToHtml = markupLanguageUtils.newMarkupToHtml(props.plugins, {
resourceBaseUrl: `joplin-content://note-viewer/${Setting.value('resourceDir')}/`,
customCss: props.customCss,
});
@@ -183,7 +182,7 @@ function NoteEditorContent(props: NoteEditorProps) {
scrollbarSize: props.scrollbarSize,
whiteBackgroundNoteRendering: options.whiteBackgroundNoteRendering,
});
}, [props.themeId, props.scrollbarSize, props.customCss, props.contentMaxWidth]);
}, [props.plugins, props.themeId, props.scrollbarSize, props.customCss, props.contentMaxWidth]);
const handleProvisionalFlag = useCallback(() => {
if (props.isProvisional) {
@@ -423,6 +422,7 @@ function NoteEditorContent(props: NoteEditorProps) {
const searchMarkers = useSearchMarkers(showLocalSearch, localSearchMarkerOptions, props.searches, props.selectedSearchId, props.highlightedWords);
const markupLanguage = formNote.markup_language;
const editorProps: NoteBodyEditorProps = {
ref: editorRef,
contentKey: formNote.id,
@@ -432,7 +432,7 @@ function NoteEditorContent(props: NoteEditorProps) {
onWillChange: onBodyWillChange,
onMessage: onMessage,
content: formNote.body,
contentMarkupLanguage: formNote.markup_language,
contentMarkupLanguage: markupLanguage,
contentOriginalCss: formNote.originalCss,
resourceInfos: resourceInfos,
resourceDirectory: Setting.value('resourceDir'),
@@ -451,11 +451,14 @@ function NoteEditorContent(props: NoteEditorProps) {
searchMarkers: searchMarkers,
visiblePanes: props.noteVisiblePanes || ['editor', 'viewer'],
keyboardMode: Setting.value('editor.keyboardMode'),
enableTextPatterns: Setting.value('editor.enableTextPatterns'),
tabMovesFocus: props.tabMovesFocus,
locale: Setting.value('locale'),
onDrop: onDrop,
noteToolbarButtonInfos: props.toolbarButtonInfos,
plugins: props.plugins,
// KaTeX isn't supported in HTML notes
mathEnabled: markupLanguage === MarkupLanguage.Markdown && Setting.value('markdown.plugin.katex'),
fontSize: Setting.value('style.editor.fontSize'),
contentMaxWidth: props.contentMaxWidth,
scrollbarSize: props.scrollbarSize,

View File

@@ -36,7 +36,6 @@ const incompatiblePluginIds = [
// cSpell:disable
'com.septemberhx.Joplin.Enhancement',
'ylc395.noteLinkSystem',
'outline',
'joplin.plugin.cmoptions',
'com.asdibiase.joplin-languagetool',
// cSpell:enable

View File

@@ -0,0 +1,22 @@
import { CommandRuntime, CommandDeclaration } from '@joplin/lib/services/CommandService';
import { _ } from '@joplin/lib/locale';
import { FocusElementOptions } from '../../../commands/focusElement';
import { WindowCommandDependencies } from '../utils/types';
export const declaration: CommandDeclaration = {
name: 'focusElementNoteViewer',
label: () => _('Note viewer'),
parentLabel: () => _('Focus'),
};
export const runtime = (dependencies: WindowCommandDependencies): CommandRuntime => {
return {
execute: async (_context: unknown, options?: FocusElementOptions) => {
await dependencies.editorRef.current.execCommand({
name: 'viewer.focus',
value: options,
});
},
enabledCondition: 'markdownEditorVisible',
};
};

View File

@@ -1,6 +1,7 @@
// AUTO-GENERATED using `gulp buildScriptIndexes`
import * as focusElementNoteBody from './focusElementNoteBody';
import * as focusElementNoteTitle from './focusElementNoteTitle';
import * as focusElementNoteViewer from './focusElementNoteViewer';
import * as focusElementToolbar from './focusElementToolbar';
import * as pasteAsText from './pasteAsText';
import * as showLocalSearch from './showLocalSearch';
@@ -9,6 +10,7 @@ import * as showRevisions from './showRevisions';
const index: any[] = [
focusElementNoteBody,
focusElementNoteTitle,
focusElementNoteViewer,
focusElementToolbar,
pasteAsText,
showLocalSearch,

View File

@@ -163,6 +163,9 @@ const declarations: CommandDeclaration[] = [
{
name: 'editor.execCommand',
},
{
name: 'viewer.focus',
},
];
export default declarations;

View File

@@ -8,13 +8,11 @@ const MenuItem = bridge().MenuItem;
import Resource, { resourceOcrStatusToString } from '@joplin/lib/models/Resource';
import BaseItem from '@joplin/lib/models/BaseItem';
import BaseModel, { ModelType } from '@joplin/lib/BaseModel';
import { processPastedHtml } from './resourceHandling';
import { NoteEntity, ResourceEntity, ResourceOcrStatus } from '@joplin/lib/services/database/types';
import { TinyMceEditorEvents } from '../NoteBody/TinyMCE/utils/types';
import { itemIsReadOnlySync, ItemSlice } from '@joplin/lib/models/utils/readOnly';
import Setting from '@joplin/lib/models/Setting';
import ItemChange from '@joplin/lib/models/ItemChange';
import { HtmlToMarkdownHandler, MarkupToHtmlHandler } from './types';
import shim from '@joplin/lib/shim';
import { openFileWithExternalEditor } from '@joplin/lib/services/ExternalEditWatcher/utils';
const fs = require('fs-extra');
@@ -81,7 +79,7 @@ export async function openItemById(itemId: string, dispatch: Function, hash = ''
}
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
export function menuItems(dispatch: Function, htmlToMd: HtmlToMarkdownHandler, mdToHtml: MarkupToHtmlHandler): ContextMenuItems {
export function menuItems(dispatch: Function): ContextMenuItems {
return {
open: {
label: _('Open...'),
@@ -195,17 +193,10 @@ export function menuItems(dispatch: Function, htmlToMd: HtmlToMarkdownHandler, m
},
paste: {
label: _('Paste'),
onAction: async (options: ContextMenuOptions) => {
const pastedHtml = clipboard.readHTML();
let content = pastedHtml ? pastedHtml : clipboard.readText();
if (pastedHtml) {
content = await processPastedHtml(pastedHtml, htmlToMd, mdToHtml);
}
options.insertContent(content);
onAction: async (_options: ContextMenuOptions) => {
bridge().activeWindow().webContents.paste();
},
isActive: (_itemType: ContextMenuItemType, options: ContextMenuOptions) => !options.isReadOnly && (!!clipboard.readText() || !!clipboard.readHTML()),
isActive: (_itemType: ContextMenuItemType, options: ContextMenuOptions) => !options.isReadOnly && clipboard.availableFormats().length > 0,
},
pasteAsText: {
label: _('Paste as text'),
@@ -228,7 +219,7 @@ export function menuItems(dispatch: Function, htmlToMd: HtmlToMarkdownHandler, m
export default async function contextMenu(options: ContextMenuOptions, dispatch: Function) {
const menu = new Menu();
const items = menuItems(dispatch, options.htmlToMd, options.mdToHtml);
const items = menuItems(dispatch);
if (!('readyOnly' in options)) options.isReadOnly = true;
for (const itemKey in items) {

View File

@@ -124,6 +124,7 @@ export interface NoteBodyEditorProps {
visiblePanes: string[];
keyboardMode: string;
tabMovesFocus: boolean;
enableTextPatterns: boolean;
resourceInfos: ResourceInfos;
resourceDirectory: string;
locale: string;
@@ -131,6 +132,7 @@ export interface NoteBodyEditorProps {
onDrop: DropHandler;
noteToolbarButtonInfos: ToolbarItem[];
plugins: PluginStates;
mathEnabled: boolean;
fontSize: number;
contentMaxWidth: number;
isSafeMode: boolean;

View File

@@ -97,7 +97,15 @@ const useRefreshFormNoteOnChange = (formNoteRef: RefObject<FormNote>, editorId:
await initNoteState(n, false);
if (event.cancelled) return;
setFormNoteRefreshScheduled(0);
setFormNoteRefreshScheduled(oldValue => {
// If a new refresh was scheduled between initNoteState
// and now:
if (oldValue !== formNoteRefreshScheduled) {
return oldValue;
}
// A refresh is no longer scheduled
return 0;
});
};
await loadNote();
@@ -106,8 +114,8 @@ const useRefreshFormNoteOnChange = (formNoteRef: RefObject<FormNote>, editorId:
const refreshFormNote = useCallback(() => {
// Increase the counter to cancel any ongoing refresh attempts
// and start a new one.
setFormNoteRefreshScheduled(formNoteRefreshScheduled + 1);
}, [formNoteRefreshScheduled]);
setFormNoteRefreshScheduled(count => count + 1);
}, []);
// When switching from the plugin editor to the built-in editor, we refresh the note since the
// plugin may have modified it via the data API.

View File

@@ -1,43 +0,0 @@
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
const react_1 = require('react');
const markupLanguageUtils_1 = require('@joplin/lib/utils/markupLanguageUtils');
const Setting_1 = require('@joplin/lib/models/Setting');
const shim_1 = require('@joplin/lib/shim');
const { themeStyle } = require('@joplin/lib/theme');
const Note_1 = require('@joplin/lib/models/Note');
const resourceUtils_1 = require('@joplin/lib/models/utils/resourceUtils');
function useMarkupToHtml(deps) {
const { themeId, customCss, plugins, whiteBackgroundNoteRendering } = deps;
const resourceBaseUrl = (0, react_1.useMemo)(() => {
return `joplin-content://note-viewer/${Setting_1.default.value('resourceDir')}/`;
}, []);
const markupToHtml = (0, react_1.useMemo)(() => {
return markupLanguageUtils_1.default.newMarkupToHtml(plugins, {
resourceBaseUrl,
customCss: customCss || '',
});
}, [plugins, customCss, resourceBaseUrl]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
return (0, react_1.useCallback)(async (markupLanguage, md, options = null) => {
options = { replaceResourceInternalToExternalLinks: false, resourceInfos: {}, platformName: shim_1.default.platformName(), ...options };
md = md || '';
const theme = themeStyle(themeId);
let resources = {};
if (options.replaceResourceInternalToExternalLinks) {
md = await Note_1.default.replaceResourceInternalToExternalLinks(md, { useAbsolutePaths: true });
} else {
resources = options.resourceInfos;
}
delete options.replaceResourceInternalToExternalLinks;
const result = await markupToHtml.render(markupLanguage, md, theme, { codeTheme: theme.codeThemeCss, resources: resources, postMessageSyntax: 'ipcProxySendToHost', splitted: true, externalAssetsOnly: true, codeHighlightCacheKey: 'useMarkupToHtml', settingValue: deps.settingValue, whiteBackgroundNoteRendering, itemIdToUrl: (id, urlParameters = '') => {
if (!(id in resources) || !resources[id]) {
return null;
}
return (0, resourceUtils_1.resourceFullPath)(resources[id].item, resourceBaseUrl) + urlParameters;
}, ...options });
return result;
}, [themeId, markupToHtml, whiteBackgroundNoteRendering, resourceBaseUrl, deps.settingValue]);
}
exports.default = useMarkupToHtml;
// # sourceMappingURL=useMarkupToHtml.js.map

View File

@@ -10,6 +10,7 @@ const commandsWithDependencies = [
require('../commands/showLocalSearch'),
require('../commands/focusElementNoteTitle'),
require('../commands/focusElementNoteBody'),
require('../commands/focusElementNoteViewer'),
require('../commands/focusElementToolbar'),
require('../commands/pasteAsText'),
];

View File

@@ -1,6 +1,6 @@
import { AppState } from '../../app.reducer';
import * as React from 'react';
import { useEffect, useRef, useMemo } from 'react';
import { useEffect, useRef, useMemo, useContext } from 'react';
import SearchBar from '../SearchBar/SearchBar';
import Button, { ButtonLevel, ButtonSize, buttonSizePx } from '../Button/Button';
import CommandService from '@joplin/lib/services/CommandService';
@@ -14,6 +14,7 @@ import stateToWhenClauseContext from '../../services/commands/stateToWhenClauseC
import { getTrashFolderId } from '@joplin/lib/services/trash';
import { Breakpoints } from '../NoteList/utils/types';
import { stateUtils } from '@joplin/lib/reducer';
import { WindowIdContext } from '../NewWindowOrIFrame';
interface Props {
showNewNoteButtons: boolean;
@@ -245,11 +246,12 @@ function NoteListControls(props: Props) {
);
}
const windowId = useContext(WindowIdContext);
return (
<StyledRoot ref={noteControlsRef} padding={props.padding} buttonVerticalGap={props.buttonVerticalGap}>
{renderNewNoteButtons()}
<BottomRow ref={searchAndSortRef} className="search-and-sort">
<SearchBar inputRef={searchBarRef}/>
<SearchBar inputRef={searchBarRef} windowId={windowId}/>
{showsSortOrderButtons() &&
<SortOrderButtonsContainer>
<StyledPairButtonL

View File

@@ -28,6 +28,7 @@ export interface NoteViewerControl {
domReady(): boolean;
setHtml(html: string, options: SetHtmlOptions): void;
send(channel: string, arg0?: unknown, arg1?: unknown): void;
focusLine(editorLine: number): void;
focus(): void;
hasFocus(): boolean;
}
@@ -107,6 +108,10 @@ const NoteTextViewer = forwardRef((props: Props, ref: ForwardedRef<NoteViewerCon
win.postMessage({ target: 'webview', name: 'focus', data: {} }, '*');
}
if (channel === 'focusLine') {
win.postMessage({ target: 'webview', name: 'focusLine', data: { line: arg0 } }, '*');
}
// External code should use .setHtml (rather than send('setHtml', ...))
if (channel === 'setHtml') {
win.postMessage({ target: 'webview', name: 'setHtml', data: { html: arg0, options: arg1 } }, '*');
@@ -139,6 +144,15 @@ const NoteTextViewer = forwardRef((props: Props, ref: ForwardedRef<NoteViewerCon
hasFocus: () => {
return webviewRef.current?.contains(parentDoc.activeElement);
},
focusLine: (lineNumber: number) => {
if (webviewRef.current) {
focus('NoteTextViewer::focusLine', webviewRef.current);
// A timeout seems necessary after focusing the viewer to prevent focus from jumping to the top
setTimeout(() => {
result.send('focusLine', lineNumber);
}, 100);
}
},
};
return result;
}, [parentDoc]);

View File

@@ -1,20 +0,0 @@
// Based on https://github.com/caroso1222/notyf/blob/master/recipes/react.md
import * as React from 'react';
import { Notyf } from 'notyf';
import { ToastType } from '@joplin/lib/services/plugins/api/types';
export default React.createContext(
new Notyf({
// Set your global Notyf configuration here
duration: 6000,
types: [
{
type: ToastType.Info,
icon: false,
className: 'notyf__toast--info',
background: 'blue', // Need to set a background, otherwise Notyf won't create the background element. But the color will be overriden in CSS.
},
],
}),
);

View File

@@ -1,8 +1,8 @@
import { useContext, useMemo } from 'react';
import NotyfContext from '../NotyfContext';
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
import * as React from 'react';
import { useContext, useEffect, useMemo } from 'react';
import { Toast, ToastType } from '@joplin/lib/services/plugins/api/types';
import { INotyfNotificationOptions } from 'notyf';
import { PopupNotificationContext } from '../PopupNotification/PopupNotificationProvider';
import { NotificationType } from '../PopupNotification/types';
const emptyToast = (): Toast => {
return {
@@ -19,26 +19,23 @@ interface Props {
}
export default (props: Props) => {
const notyfContext = useContext(NotyfContext);
const popupManager = useContext(PopupNotificationContext);
const toast = useMemo(() => {
const toast: Toast = props.toast ? props.toast : emptyToast();
return toast;
}, [props.toast]);
useAsyncEffect(async () => {
useEffect(() => {
if (!toast.message) return;
const options: Partial<INotyfNotificationOptions> = {
type: toast.type,
message: toast.message,
duration: toast.duration,
};
notyfContext.open(options);
popupManager.createPopup(() => toast.message, {
type: toast.type as string as NotificationType,
}).scheduleDismiss(toast.duration);
// toast.timestamp needs to be included in the dependency list to allow
// showing multiple toasts with the same message, one after another.
// See https://github.com/laurent22/joplin/issues/11783
}, [toast.message, toast.duration, toast.type, toast.timestamp, notyfContext]);
}, [toast.message, toast.duration, toast.type, toast.timestamp, popupManager]);
return <div style={{ display: 'none' }}/>;
};

View File

@@ -0,0 +1,52 @@
import * as React from 'react';
import { NotificationType } from './types';
import { _ } from '@joplin/lib/locale';
interface Props {
children: React.ReactNode;
key: string;
type: NotificationType;
dismissing: boolean;
popup: boolean;
}
const NotificationItem: React.FC<Props> = props => {
const [iconClassName, iconLabel] = (() => {
if (props.type === NotificationType.Success) {
return ['fas fa-check', _('Success')];
}
if (props.type === NotificationType.Error) {
return ['fas fa-times', _('Error')];
}
if (props.type === NotificationType.Info) {
return ['fas fa-info', _('Info')];
}
return ['', ''];
})();
const containerModifier = (() => {
if (props.type === NotificationType.Success) return '-success';
if (props.type === NotificationType.Error) return '-error';
if (props.type === NotificationType.Info) return '-info';
return '';
})();
const icon = <i
role='img'
aria-label={iconLabel}
className={`icon ${iconClassName}`}
/>;
return <li
role={props.popup ? 'alert' : undefined}
className={`popup-notification-item ${containerModifier} ${props.dismissing ? '-dismissing' : ''}`}
>
{iconClassName ? icon : null}
<div className='ripple'/>
<div className='content'>
{props.children}
</div>
</li>;
};
export default NotificationItem;

View File

@@ -0,0 +1,41 @@
import * as React from 'react';
import { VisibleNotificationsContext } from './PopupNotificationProvider';
import NotificationItem from './NotificationItem';
import { useContext } from 'react';
import { _ } from '@joplin/lib/locale';
interface Props {}
// This component displays the popups managed by PopupNotificationContext.
// This allows popups to be shown in multiple windows at the same time.
const PopupNotificationList: React.FC<Props> = () => {
const popupSpecs = useContext(VisibleNotificationsContext);
const popups = [];
for (const spec of popupSpecs) {
if (spec.dismissed) continue;
popups.push(
<NotificationItem
key={spec.key}
type={spec.type}
dismissing={!!spec.dismissAt}
popup={true}
>{spec.content()}</NotificationItem>,
);
}
popups.reverse();
if (popups.length) {
return <ul
className='popup-notification-list -overlay'
role='group'
aria-label={_('Notifications')}
>
{popups}
</ul>;
} else {
return null;
}
};
export default PopupNotificationList;

View File

@@ -0,0 +1,122 @@
import * as React from 'react';
import { createContext, useMemo, useRef, useState } from 'react';
import { NotificationType, PopupHandle, PopupControl as PopupManager } from './types';
import { Hour, msleep } from '@joplin/utils/time';
export const PopupNotificationContext = createContext<PopupManager|null>(null);
export const VisibleNotificationsContext = createContext<PopupSpec[]>([]);
interface Props {
children: React.ReactNode;
}
interface PopupSpec {
key: string;
dismissAt?: number;
dismissed: boolean;
type: NotificationType;
content: ()=> React.ReactNode;
}
const PopupNotificationProvider: React.FC<Props> = props => {
const [popupSpecs, setPopupSpecs] = useState<PopupSpec[]>([]);
const nextPopupKey = useRef(0);
const popupManager = useMemo((): PopupManager => {
const removeOldPopups = () => {
// The WCAG allows dismissing notifications older than 20 hours.
setPopupSpecs(popups => popups.filter(popup => {
if (!popup.dismissed) {
return true;
}
const dismissedRecently = popup.dismissAt > performance.now() - Hour * 20;
return dismissedRecently;
}));
};
const removePopupWithKey = (key: string) => {
setPopupSpecs(popups => popups.filter(p => p.key !== key));
};
type UpdatePopupCallback = (popup: PopupSpec)=> PopupSpec;
const updatePopupWithKey = (key: string, updateCallback: UpdatePopupCallback) => {
setPopupSpecs(popups => popups.map(p => {
if (p.key === key) {
return updateCallback(p);
} else {
return p;
}
}));
};
const dismissAnimationDelay = 600;
const dismissPopup = async (key: string) => {
// Start the dismiss animation
updatePopupWithKey(key, popup => ({
...popup,
dismissAt: performance.now() + dismissAnimationDelay,
}));
await msleep(dismissAnimationDelay);
updatePopupWithKey(key, popup => ({
...popup,
dismissed: true,
}));
removeOldPopups();
};
const dismissAndRemovePopup = async (key: string) => {
await dismissPopup(key);
removePopupWithKey(key);
};
const manager: PopupManager = {
createPopup: (content, { type } = {}): PopupHandle => {
const key = `popup-${nextPopupKey.current++}`;
const newPopup: PopupSpec = {
key,
content,
type,
dismissed: false,
};
setPopupSpecs(popups => {
const newPopups = [...popups];
// Replace the existing popup, if it exists
const insertIndex = newPopups.findIndex(p => p.key === key);
if (insertIndex === -1) {
newPopups.push(newPopup);
} else {
newPopups.splice(insertIndex, 1, newPopup);
}
return newPopups;
});
const handle: PopupHandle = {
remove() {
void dismissAndRemovePopup(key);
},
scheduleDismiss(delay = 5_500) {
setTimeout(() => {
void dismissPopup(key);
}, delay);
},
};
return handle;
},
};
return manager;
}, []);
return <PopupNotificationContext.Provider value={popupManager}>
<VisibleNotificationsContext.Provider value={popupSpecs}>
{props.children}
</VisibleNotificationsContext.Provider>
</PopupNotificationContext.Provider>;
};
export default PopupNotificationProvider;

View File

@@ -0,0 +1,22 @@
import * as React from 'react';
export type PopupHandle = {
remove(): void;
scheduleDismiss(delay?: number): void;
};
export enum NotificationType {
Info = 'info',
Success = 'success',
Error = 'error',
}
export type NotificationContentCallback = ()=> React.ReactNode;
export interface PopupOptions {
type?: NotificationType;
}
export interface PopupControl {
createPopup(content: NotificationContentCallback, props?: PopupOptions): PopupHandle;
}

View File

@@ -1,6 +1,6 @@
import iterateItems from './iterateItems';
import { LayoutItem, LayoutItemDirection, tempContainerPrefix } from './types';
import produce from 'immer';
import { produce } from 'immer';
import uuid from '@joplin/lib/uuid';
import validateLayout from './validateLayout';

View File

@@ -1,5 +1,5 @@
import { LayoutItem, Size } from './types';
import produce from 'immer';
import { produce } from 'immer';
import iterateItems from './iterateItems';
import validateLayout from './validateLayout';

View File

@@ -1,4 +1,4 @@
import produce from 'immer';
import { produce } from 'immer';
import iterateItems from './iterateItems';
import { LayoutItem } from './types';
import validateLayout from './validateLayout';

View File

@@ -1,4 +1,4 @@
import produce from 'immer';
import { produce } from 'immer';
import iterateItems from './iterateItems';
import { LayoutItem } from './types';
import validateLayout from './validateLayout';

View File

@@ -1,4 +1,4 @@
import produce from 'immer';
import { produce } from 'immer';
import { LayoutItem } from './types';
import validateLayout from './validateLayout';

View File

@@ -1,4 +1,4 @@
import produce from 'immer';
import { produce } from 'immer';
import iterateItems from './iterateItems';
import { LayoutItem, LayoutItemDirection } from './types';

View File

@@ -30,6 +30,7 @@ import WindowCommandsAndDialogs from './WindowCommandsAndDialogs/WindowCommandsA
import { defaultWindowId, stateUtils, WindowState } from '@joplin/lib/reducer';
import bridge from '../services/bridge';
import EditorWindow from './NoteEditor/EditorWindow';
import PopupNotificationProvider from './PopupNotification/PopupNotificationProvider';
const { ThemeProvider, StyleSheetManager, createGlobalStyle } = require('styled-components');
interface Props {
@@ -197,13 +198,15 @@ class RootComponent extends React.Component<Props, any> {
return (
<StyleSheetManager disableVendorPrefixes>
<ThemeProvider theme={theme}>
<StyleSheetContainer/>
<MenuBar/>
<GlobalStyle/>
<WindowCommandsAndDialogs windowId={defaultWindowId} />
<Navigator style={navigatorStyle} screens={screens} className={`profile-${this.props.profileConfigCurrentProfileId}`} />
{this.renderSecondaryWindows()}
{this.renderModalMessage(this.modalDialogProps())}
<PopupNotificationProvider>
<StyleSheetContainer/>
<MenuBar/>
<GlobalStyle/>
<WindowCommandsAndDialogs windowId={defaultWindowId} />
<Navigator style={navigatorStyle} screens={screens} className={`profile-${this.props.profileConfigCurrentProfileId}`} />
{this.renderSecondaryWindows()}
{this.renderModalMessage(this.modalDialogProps())}
</PopupNotificationProvider>
</ThemeProvider>
</StyleSheetManager>
);

View File

@@ -5,7 +5,7 @@ import Setting from '@joplin/lib/models/Setting';
import { stateUtils } from '@joplin/lib/reducer';
import BaseModel from '@joplin/lib/BaseModel';
import uuid from '@joplin/lib/uuid';
const { connect } = require('react-redux');
import { connect } from 'react-redux';
import Note from '@joplin/lib/models/Note';
import { AppState } from '../../app.reducer';
import { blur, focus } from '@joplin/lib/utils/focusHandler';
@@ -180,10 +180,15 @@ function SearchBar(props: Props) {
);
}
const mapStateToProps = (state: AppState) => {
interface OwnProps {
windowId: string;
}
const mapStateToProps = (state: AppState, ownProps: OwnProps) => {
const windowState = stateUtils.windowStateById(state, ownProps.windowId);
return {
notesParentType: state.notesParentType,
selectedNoteId: stateUtils.selectedNoteId(state),
notesParentType: windowState.notesParentType,
selectedNoteId: stateUtils.selectedNoteId(windowState),
isFocused: state.focusedField === 'globalSearch',
};
};

View File

@@ -22,13 +22,14 @@ interface CollapseExpandAllButtonProps {
}
const CollapseExpandAllButton = (props: CollapseExpandAllButtonProps) => {
// To allow it to be accessed by accessibility tools, the new folder button
// To allow it to be accessed by accessibility tools, the toggle button
// is not included in the portion of the list with role='tree'.
const icon = props.allFoldersCollapsed ? 'far fa-caret-square-right' : 'far fa-caret-square-down';
const label = props.allFoldersCollapsed ? _('Expand all notebooks') : _('Collapse all notebooks');
return <button onClick={() => onToggleAllFolders(props.allFoldersCollapsed)} className='sidebar-header-button -collapseall'>
<i
aria-label={_('Collapse / Expand all notebooks')}
aria-label={label}
role='img'
className={icon}
/>

View File

@@ -6,7 +6,9 @@ interface Props {
}
const EmptyExpandLink: React.FC<Props> = props => {
return <a className={`sidebar-expand-link ${props.className ?? ''}`}><ExpandIcon isVisible={false} isExpanded={false}/></a>;
return <a className={`sidebar-expand-link ${props.className ?? ''}`}>
<ExpandIcon isVisible={false} isExpanded={false}/>
</a>;
};
export default EmptyExpandLink;

View File

@@ -1,15 +1,10 @@
import * as React from 'react';
import { _ } from '@joplin/lib/locale';
type ExpandIconProps = {
interface ExpandIconProps {
isExpanded: boolean;
isVisible: true;
targetTitle: string;
}|{
isExpanded: boolean;
isVisible: false;
targetTitle?: string;
};
isVisible: boolean;
}
const ExpandIcon: React.FC<ExpandIconProps> = props => {
const classNames = ['sidebar-expand-icon'];
@@ -23,12 +18,17 @@ const ExpandIcon: React.FC<ExpandIconProps> = props => {
return undefined;
}
if (props.isExpanded) {
return _('Expanded, press space to collapse.');
return _('Expanded');
}
return _('Collapsed, press space to expand.');
return _('Collapsed');
};
const label = getLabel();
return <i className={classNames.join(' ')} aria-label={label} role='img'></i>;
return <i
className={classNames.join(' ')}
aria-hidden={!props.isVisible}
aria-label={label}
role='img'
></i>;
};
export default ExpandIcon;

View File

@@ -5,7 +5,6 @@ import EmptyExpandLink from './EmptyExpandLink';
interface ExpandLinkProps {
folderId: string;
folderTitle: string;
hasChildren: boolean;
isExpanded: boolean;
className: string;
@@ -15,7 +14,7 @@ interface ExpandLinkProps {
const ExpandLink: React.FC<ExpandLinkProps> = props => {
return props.hasChildren ? (
<a className={`sidebar-expand-link ${props.className}`} data-folder-id={props.folderId} onClick={props.onClick} role='button'>
<ExpandIcon isVisible={true} isExpanded={props.isExpanded} targetTitle={props.folderTitle}/>
<ExpandIcon isVisible={true} isExpanded={props.isExpanded} />
</a>
) : (
<EmptyExpandLink className={props.className}/>

View File

@@ -11,6 +11,7 @@ import { ModelType } from '@joplin/lib/BaseModel';
import { _ } from '@joplin/lib/locale';
import NoteCount from './NoteCount';
import ListItemWrapper, { ListItemRef } from './ListItemWrapper';
import { useId } from 'react';
const renderFolderIcon = (folderIcon: FolderIcon) => {
if (!folderIcon) {
@@ -65,6 +66,7 @@ function FolderItem(props: FolderItemProps) {
if (!showFolderIcon) return null;
return renderFolderIcon(folderIcon);
};
const titleId = useId();
return (
<ListItemWrapper
@@ -85,9 +87,13 @@ function FolderItem(props: FolderItemProps) {
data-folder-id={folderId}
data-id={folderId}
data-type={ModelType.Folder}
// Accessibility labels: Don't include the expand/collapse link in the description,
// since this information is already conveyed by aria-* props.
aria-labelledby={titleId}
>
<StyledListItemAnchor
className="list-item"
id={titleId}
isConflictFolder={folderId === Folder.conflictFolderId()}
selected={selected}
shareId={shareId}
@@ -106,7 +112,6 @@ function FolderItem(props: FolderItemProps) {
// title first.
className='toggle'
hasChildren={hasChildren}
folderTitle={folderTitle}
folderId={folderId}
onClick={onFolderToggleClick_}
isExpanded={isExpanded}

View File

@@ -23,7 +23,9 @@ interface Props {
draggable?: boolean;
'data-folder-id'?: string;
'data-id'?: string;
'data-tag-id'?: string;
'data-type'?: ModelType;
'aria-labelledby'?: string;
}
const ListItemWrapper: React.FC<Props> = props => {
@@ -55,7 +57,9 @@ const ListItemWrapper: React.FC<Props> = props => {
style={style}
data-folder-id={props['data-folder-id']}
data-id={props['data-id']}
data-tag-id={props['data-tag-id']}
data-type={props['data-type']}
aria-labelledby={props['aria-labelledby']}
>
{props.children}
</div>

View File

@@ -10,6 +10,7 @@ import useElementSize from '@joplin/lib/hooks/useElementSize';
import Button, { ButtonLevel } from '../Button/Button';
import bridge from '../../services/bridge';
import Setting from '@joplin/lib/models/Setting';
import JoplinCloudSignUpCallToAction from '../JoplinCloudSignUpCallToAction';
interface Props {
themeId: number;
@@ -210,6 +211,11 @@ export default function(props: Props) {
);
}
function renderSignUpArea(info: SyncTargetInfo) {
if (info.name !== 'joplinCloud') return null;
return <JoplinCloudSignUpCallToAction/>;
}
function renderSyncTarget(info: SyncTargetInfo) {
const key = `syncTarget_${info.name}`;
const height = info.name !== 'joplinCloud' ? descriptionHeight : null;
@@ -238,6 +244,7 @@ export default function(props: Props) {
{descriptionComp}
{featuresComp}
{renderSelectArea(info, headerId)}
{renderSignUpArea(info)}
{renderSlowSyncWarning()}
</SyncTargetBox>
);

View File

@@ -1,15 +1,13 @@
import { useContext, useCallback, useMemo, useRef } from 'react';
import * as React from 'react';
import { useContext, useEffect, useRef } from 'react';
import { StateLastDeletion } from '@joplin/lib/reducer';
import { _, _n } from '@joplin/lib/locale';
import NotyfContext from '../NotyfContext';
import { waitForElement } from '@joplin/lib/dom';
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
import { htmlentities } from '@joplin/utils/html';
import restoreItems from '@joplin/lib/services/trash/restoreItems';
import { ModelType } from '@joplin/lib/BaseModel';
import { themeStyle } from '@joplin/lib/theme';
import { Dispatch } from 'redux';
import { NotyfNotification } from 'notyf';
import { PopupNotificationContext } from '../PopupNotification/PopupNotificationProvider';
import { NotificationType } from '../PopupNotification/types';
import TrashNotificationMessage from './TrashNotificationMessage';
interface Props {
lastDeletion: StateLastDeletion;
@@ -18,50 +16,29 @@ interface Props {
dispatch: Dispatch;
}
const onCancelClick = async (lastDeletion: StateLastDeletion) => {
if (lastDeletion.folderIds.length) {
await restoreItems(ModelType.Folder, lastDeletion.folderIds);
}
if (lastDeletion.noteIds.length) {
await restoreItems(ModelType.Note, lastDeletion.noteIds);
}
};
export default (props: Props) => {
const notyfContext = useContext(NotyfContext);
const notificationRef = useRef<NotyfNotification | null>(null);
const popupManager = useContext(PopupNotificationContext);
const theme = useMemo(() => {
return themeStyle(props.themeId);
}, [props.themeId]);
const lastDeletionNotificationTimeRef = useRef<number>();
lastDeletionNotificationTimeRef.current = props.lastDeletionNotificationTime;
const notyf = useMemo(() => {
const output = notyfContext;
output.options.types = notyfContext.options.types.map(type => {
if (type.type === 'success') {
type.background = theme.backgroundColor5;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
(type.icon as any).color = theme.backgroundColor5;
}
return type;
});
return output;
}, [notyfContext, theme]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const onCancelClick = useCallback(async (event: any) => {
notyf.dismiss(notificationRef.current);
notificationRef.current = null;
const lastDeletion: StateLastDeletion = JSON.parse(event.currentTarget.getAttribute('data-lastDeletion'));
if (lastDeletion.folderIds.length) {
await restoreItems(ModelType.Folder, lastDeletion.folderIds);
}
if (lastDeletion.noteIds.length) {
await restoreItems(ModelType.Note, lastDeletion.noteIds);
}
}, [notyf]);
useAsyncEffect(async (event) => {
if (!props.lastDeletion || props.lastDeletion.timestamp <= props.lastDeletionNotificationTime) return;
useEffect(() => {
const lastDeletionNotificationTime = lastDeletionNotificationTimeRef.current;
if (!props.lastDeletion || props.lastDeletion.timestamp <= lastDeletionNotificationTime) return;
props.dispatch({ type: 'DELETION_NOTIFICATION_DONE' });
let msg = '';
if (props.lastDeletion.folderIds.length) {
msg = _('The notebook and its content was successfully moved to the trash.');
} else if (props.lastDeletion.noteIds.length) {
@@ -70,16 +47,15 @@ export default (props: Props) => {
return;
}
const linkId = `deletion-notification-cancel-${Math.floor(Math.random() * 1000000)}`;
const cancelLabel = _('Cancel');
const notification = notyf.success(`${msg} <a href="#" class="cancel" data-lastDeletion="${htmlentities(JSON.stringify(props.lastDeletion))}" id="${linkId}">${cancelLabel}</a>`);
notificationRef.current = notification;
const element: HTMLAnchorElement = await waitForElement(document, linkId);
if (event.cancelled) return;
element.addEventListener('click', onCancelClick);
}, [props.lastDeletion, notyf, props.dispatch]);
const handleCancelClick = () => {
notification.remove();
void onCancelClick(props.lastDeletion);
};
const notification = popupManager.createPopup(() => (
<TrashNotificationMessage message={msg} onCancel={handleCancelClick}/>
), { type: NotificationType.Success });
notification.scheduleDismiss();
}, [props.lastDeletion, props.dispatch, popupManager]);
return <div style={{ display: 'none' }}/>;
};

View File

@@ -0,0 +1,27 @@
import * as React from 'react';
import { _ } from '@joplin/lib/locale';
import { useCallback, useState } from 'react';
interface Props {
message: string;
onCancel: ()=> void;
}
const TrashNotificationMessage: React.FC<Props> = props => {
const [cancelling, setCancelling] = useState(false);
const onCancel = useCallback(() => {
setCancelling(true);
props.onCancel();
}, [props.onCancel]);
return <>
{props.message}
{' '}
<button
className="link-button"
onClick={onCancel}
>{cancelling ? _('Cancelling...') : _('Cancel')}</button>
</>;
};
export default TrashNotificationMessage;

View File

@@ -1,27 +0,0 @@
body .notyf {
color: var(--joplin-color5);
}
.notyf__toast {
> .notyf__wrapper {
> .notyf__message {
> .cancel {
color: var(--joplin-color5);
text-decoration: underline;
}
}
> .notyf__icon {
> .notyf__icon--success {
background-color: var(--joplin-color5);
}
}
}
}

View File

@@ -1,17 +1,15 @@
import * as React from 'react';
import { useCallback, useContext, useEffect, useMemo, useRef } from 'react';
import { themeStyle } from '@joplin/lib/theme';
import NotyfContext from '../NotyfContext';
import { useCallback, useContext, useEffect } from 'react';
import { UpdateInfo } from 'electron-updater';
import { ipcRenderer, IpcRendererEvent } from 'electron';
import { AutoUpdaterEvents } from '../../services/autoUpdater/AutoUpdaterService';
import { NotyfEvent, NotyfNotification } from 'notyf';
import { _ } from '@joplin/lib/locale';
import { htmlentities } from '@joplin/utils/html';
import shim from '@joplin/lib/shim';
import { PopupNotificationContext } from '../PopupNotification/PopupNotificationProvider';
import Button, { ButtonLevel } from '../Button/Button';
import { NotificationType } from '../PopupNotification/types';
interface UpdateNotificationProps {
themeId: number;
interface Props {
}
export enum UpdateNotificationEvents {
@@ -22,111 +20,61 @@ export enum UpdateNotificationEvents {
const changelogLink = 'https://github.com/laurent22/joplin/releases';
window.openChangelogLink = () => {
const openChangelogLink = () => {
shim.openUrl(changelogLink);
};
const UpdateNotification = ({ themeId }: UpdateNotificationProps) => {
const notyfContext = useContext(NotyfContext);
const notificationRef = useRef<NotyfNotification | null>(null); // Use ref to hold the current notification
const theme = useMemo(() => themeStyle(themeId), [themeId]);
const notyf = useMemo(() => {
const output = notyfContext;
output.options.types = notyfContext.options.types.map(type => {
if (type.type === 'success') {
type.background = theme.backgroundColor5;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
(type.icon as any).color = theme.backgroundColor5;
}
return type;
});
return output;
}, [notyfContext, theme]);
const handleDismissNotification = useCallback(() => {
notyf.dismiss(notificationRef.current);
notificationRef.current = null;
}, [notyf]);
const handleApplyUpdate = useCallback(() => {
ipcRenderer.send('apply-update-now');
handleDismissNotification();
}, [handleDismissNotification]);
const handleApplyUpdate = () => {
ipcRenderer.send('apply-update-now');
};
const UpdateNotification: React.FC<Props> = () => {
const popupManager = useContext(PopupNotificationContext);
const handleUpdateDownloaded = useCallback((_event: IpcRendererEvent, info: UpdateInfo) => {
if (notificationRef.current) return;
const updateAvailableHtml = htmlentities(_('A new update (%s) is available', info.version));
const seeChangelogHtml = htmlentities(_('See changelog'));
const restartNowHtml = htmlentities(_('Restart now'));
const updateLaterHtml = htmlentities(_('Update later'));
const messageHtml = `
<div class="update-notification" style="color: ${theme.color2};">
${updateAvailableHtml} <a href="#" onclick="openChangelogLink()" style="color: ${theme.color2};">${seeChangelogHtml}</a>
<div style="display: flex; gap: 10px; margin-top: 8px;">
<button onclick="document.dispatchEvent(new CustomEvent('${UpdateNotificationEvents.ApplyUpdate}'))" class="notyf__button notyf__button--confirm" style="color: ${theme.color2};">${restartNowHtml}</button>
<button onclick="document.dispatchEvent(new CustomEvent('${UpdateNotificationEvents.Dismiss}'))" class="notyf__button notyf__button--dismiss" style="color: ${theme.color2};">${updateLaterHtml}</button>
const notification = popupManager.createPopup(() => (
<div className='update-notification'>
{_('A new update (%s) is available', info.version)}
<button className='link-button' onClick={openChangelogLink}>{
_('See changelog')
}</button>
<div className='buttons'>
<Button
level={ButtonLevel.Tertiary}
onClick={() => {
notification.remove();
handleApplyUpdate();
}}
title={_('Restart now')}
/>
<Button
level={ButtonLevel.Tertiary}
onClick={() => notification.remove()}
title={_('Update later')}
/>
</div>
</div>
</div>
`;
const notification: NotyfNotification = notyf.open({
type: 'success',
message: messageHtml,
position: {
x: 'right',
y: 'bottom',
},
duration: 0,
});
notificationRef.current = notification;
}, [notyf, theme]);
));
}, [popupManager]);
const handleUpdateNotAvailable = useCallback(() => {
if (notificationRef.current) return;
const noUpdateMessageHtml = htmlentities(_('No updates available'));
const messageHtml = `
<div class="update-notification" style="color: ${theme.color2};">
${noUpdateMessageHtml}
const notification = popupManager.createPopup(() => (
<div className='update-notification'>
{_('No updates available')}
</div>
`;
const notification: NotyfNotification = notyf.open({
type: 'success',
message: messageHtml,
position: {
x: 'right',
y: 'bottom',
},
duration: 5000,
});
notification.on(NotyfEvent.Dismiss, () => {
notificationRef.current = null;
});
notificationRef.current = notification;
}, [notyf, theme]);
), { type: NotificationType.Info });
notification.scheduleDismiss();
}, [popupManager]);
useEffect(() => {
ipcRenderer.on(AutoUpdaterEvents.UpdateDownloaded, handleUpdateDownloaded);
ipcRenderer.on(AutoUpdaterEvents.UpdateNotAvailable, handleUpdateNotAvailable);
document.addEventListener(UpdateNotificationEvents.ApplyUpdate, handleApplyUpdate);
document.addEventListener(UpdateNotificationEvents.Dismiss, handleDismissNotification);
return () => {
ipcRenderer.removeListener(AutoUpdaterEvents.UpdateDownloaded, handleUpdateDownloaded);
ipcRenderer.removeListener(AutoUpdaterEvents.UpdateNotAvailable, handleUpdateNotAvailable);
document.removeEventListener(UpdateNotificationEvents.ApplyUpdate, handleApplyUpdate);
};
}, [handleApplyUpdate, handleDismissNotification, handleUpdateDownloaded, handleUpdateNotAvailable]);
}, [handleUpdateDownloaded, handleUpdateNotAvailable]);
return (

View File

@@ -1,27 +1,11 @@
.update-notification {
display: flex;
flex-direction: column;
align-items: flex-start;
display: flex;
flex-direction: column;
align-items: flex-start;
.button-container {
display: flex;
gap: 10px;
margin-top: 8px;
}
.notyf__button {
padding: 5px 10px;
border: 1px solid;
border-radius: 4px;
background-color: transparent;
cursor: pointer;
&:hover {
background-color: rgba(255, 255, 255, 0.2);
}
}
a {
text-decoration: underline;
}
> .buttons {
display: flex;
gap: 10px;
margin-top: 8px;
}
}

View File

@@ -18,6 +18,7 @@ import useWindowCommands from './utils/useWindowCommands';
import PluginDialogs from './PluginDialogs';
import useSyncDialogState from './utils/useSyncDialogState';
import AppDialogs from './AppDialogs';
import PopupNotificationList from '../PopupNotification/PopupNotificationList';
const PluginManager = require('@joplin/lib/services/PluginManager');
@@ -113,7 +114,9 @@ const WindowCommandsAndDialogs: React.FC<Props> = props => {
const dialogInfo = PluginManager.instance().pluginDialogToShow(props.pluginsLegacy);
const pluginDialog = !dialogInfo ? null : <dialogInfo.Dialog {...dialogInfo.props} />;
const { noteContentPropertiesDialogOptions, notePropertiesDialogOptions, shareNoteDialogOptions, shareFolderDialogOptions, promptOptions } = dialogState;
const {
noteContentPropertiesDialogOptions, notePropertiesDialogOptions, shareNoteDialogOptions, shareFolderDialogOptions, promptOptions,
} = dialogState;
return <>
@@ -173,6 +176,8 @@ const WindowCommandsAndDialogs: React.FC<Props> = props => {
buttons={promptOptions && 'buttons' in promptOptions ? promptOptions.buttons : null}
inputType={promptOptions && 'inputType' in promptOptions ? promptOptions.inputType : null}
/>
<PopupNotificationList/>
</>;
};

View File

@@ -3,6 +3,7 @@ import { _ } from '@joplin/lib/locale';
import setLayoutItemProps from '../../ResizableLayout/utils/setLayoutItemProps';
import layoutItemProp from '../../ResizableLayout/utils/layoutItemProp';
import { AppState } from '../../../app.reducer';
import { WindowControl } from '../utils/useWindowControl';
export const declaration: CommandDeclaration = {
name: 'toggleNoteList',
@@ -10,14 +11,16 @@ export const declaration: CommandDeclaration = {
iconName: 'fas fa-align-justify',
};
export const runtime = (): CommandRuntime => {
export const runtime = (control: WindowControl): CommandRuntime => {
return {
execute: async (context: CommandContext) => {
const layout = (context.state as AppState).mainLayout;
const visible = !layoutItemProp(layout, 'noteList', 'visible');
const newLayout = setLayoutItemProps(layout, 'noteList', {
visible: !layoutItemProp(layout, 'noteList', 'visible'),
visible,
});
control.announcePanelVisibility(_('Note list'), visible);
// Toggling the sidebar will affect the size of most other on-screen components.
// Dispatching a window resize event is a bit of a hack, but it ensures that any

View File

@@ -3,6 +3,7 @@ import { _ } from '@joplin/lib/locale';
import setLayoutItemProps from '../../ResizableLayout/utils/setLayoutItemProps';
import layoutItemProp from '../../ResizableLayout/utils/layoutItemProp';
import { AppState } from '../../../app.reducer';
import { WindowControl } from '../utils/useWindowControl';
export const declaration: CommandDeclaration = {
name: 'toggleSideBar',
@@ -10,14 +11,16 @@ export const declaration: CommandDeclaration = {
iconName: 'fas fa-bars',
};
export const runtime = (): CommandRuntime => {
export const runtime = (control: WindowControl): CommandRuntime => {
return {
execute: async (context: CommandContext) => {
const layout = (context.state as AppState).mainLayout;
const visible = !layoutItemProp(layout, 'sideBar', 'visible');
const newLayout = setLayoutItemProps(layout, 'sideBar', {
visible: !layoutItemProp(layout, 'sideBar', 'visible'),
visible,
});
control.announcePanelVisibility(_('Sidebar'), visible);
// Toggling the sidebar will affect the size of most other on-screen components.
// Dispatching a window resize event is a bit of a hack, but it ensures that any

View File

@@ -2,10 +2,13 @@ import * as React from 'react';
import { useMemo, useRef } from 'react';
import { DialogState } from '../types';
import { PrintCallback } from './usePrintToCallback';
import { _ } from '@joplin/lib/locale';
import announceForAccessibility from '../../utils/announceForAccessibility';
export interface WindowControl {
setState: (update: Partial<DialogState>)=> void;
printTo: PrintCallback;
announcePanelVisibility(panelName: string, visible: boolean): void;
}
export type OnSetDialogState = React.Dispatch<React.SetStateAction<DialogState>>;
@@ -24,6 +27,11 @@ const useWindowControl = (setDialogState: OnSetDialogState, onPrint: PrintCallba
}));
},
printTo: (target, options) => onPrintRef.current(target, options),
announcePanelVisibility: (panelName, visible) => {
announceForAccessibility(
visible ? _('Panel "%s" is visible', panelName) : _('Panel %s is hidden', panelName),
);
},
};
}, [setDialogState]);
};

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