Compare commits

..
Author SHA1 Message Date
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 LooiandLaurent Cozic 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
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
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
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
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
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
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
pedrandLaurent Cozic 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
Joplin Bot b8c88af82d Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-04-01 02:19:22 +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
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 MedvedevandLaurent Cozic 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
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
marph91andLaurent Cozic 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
MeowandLaurent Cozic 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
Laurent Cozic 64f3dae8cc Doc: Fixed sponsor "alt" tag on website main page 2025-03-12 21:03:20 +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 CozicandHenry Heino 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 SandLaurent Cozic 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
Henry Heino d1d75449f5 Chore: CI: Upgrade Linux actions runner to Ubuntu 22.04 (#11927) 2025-03-06 00:19:57 +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
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
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
524 changed files with 66625 additions and 40814 deletions

No files matched your search

+44 -6
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
@@ -264,6 +267,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 +276,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 +358,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 +429,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 +534,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 +556,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 +593,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 +603,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 +697,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 +759,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 +803,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 +860,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 +873,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 +885,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 +926,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 +943,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 +1008,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 +1061,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 +1099,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
+3 -1
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,
},
},
+2 -2
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
@@ -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
+27 -1
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..."
+35 -72
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
@@ -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
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
+43 -6
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
@@ -239,6 +241,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 +250,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 +332,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 +403,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 +508,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 +530,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 +567,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 +577,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 +671,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 +733,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 +777,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 +834,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 +847,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 +859,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 +900,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 +917,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 +982,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 +1035,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 +1073,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
+1
View File
@@ -0,0 +1 @@
corepack yarn lint-staged
@@ -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();
@@ -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;
}
});
@@ -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;
+77
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

@@ -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: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

+3 -1
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>');
@@ -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>
+9 -5
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"
+19 -6
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
+4 -3
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
+8 -7
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
+2 -1
View File
@@ -25,7 +25,8 @@
"version": "latest",
"excluded_platforms": ["aarch64-darwin", "x86_64-darwin"],
},
"git": "latest",
"git": "latest",
"giflib": "latest",
},
"shell": {
"init_hook": [
View File
Whitespace-only changes.
+4 -10
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"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 385 KiB

@@ -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' },
];
},
}
@@ -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();
});
+163 -26
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', () => {
+2 -4
View File
@@ -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();
}
+83 -10
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_;
}
+4
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,
@@ -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',
};
};
@@ -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',
};
};
@@ -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>,
);
@@ -40,7 +40,7 @@ export default function ButtonBar(props: Props) {
}
return (
<StyledRoot>
<StyledRoot className='button-bar'>
<Button
onClick={props.onCancelClick}
level={ButtonLevel.Secondary}
+22 -2
View File
@@ -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',
};
};
+13 -2
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,6 +555,8 @@ 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,
@@ -715,8 +720,12 @@ function useMenu(props: Props) {
}, {
type: 'separator',
},
printItem,
printItem, {
type: 'separator',
},
switchProfileItem,
openSecondaryAppInstance,
openPrimaryAppInstance,
],
};
@@ -789,6 +798,7 @@ function useMenu(props: Props) {
shim.isMac() ? noItem : menuItemDic.toggleMenuBar,
menuItemDic.toggleNoteList,
menuItemDic.toggleVisiblePanes,
menuItemDic.toggleEditorPlugin,
{
label: _('Layout button sequence'),
submenu: layoutButtonSequenceMenuItems,
@@ -1139,7 +1149,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 +1175,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,
@@ -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);
@@ -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);
},
@@ -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
// -----------------------------------------------------------------------------------------
@@ -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) {
@@ -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]);
@@ -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;
@@ -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;
@@ -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,
@@ -36,7 +36,6 @@ const incompatiblePluginIds = [
// cSpell:disable
'com.septemberhx.Joplin.Enhancement',
'ylc395.noteLinkSystem',
'outline',
'joplin.plugin.cmoptions',
'com.asdibiase.joplin-languagetool',
// cSpell:enable
@@ -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',
};
};
@@ -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,
@@ -163,6 +163,9 @@ const declarations: CommandDeclaration[] = [
{
name: 'editor.execCommand',
},
{
name: 'viewer.focus',
},
];
export default declarations;
@@ -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) {
@@ -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;
@@ -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.
@@ -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
@@ -10,6 +10,7 @@ const commandsWithDependencies = [
require('../commands/showLocalSearch'),
require('../commands/focusElementNoteTitle'),
require('../commands/focusElementNoteBody'),
require('../commands/focusElementNoteViewer'),
require('../commands/focusElementToolbar'),
require('../commands/pasteAsText'),
];
@@ -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]);
-20
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.
},
],
}),
);
@@ -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' }}/>;
};
@@ -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;
@@ -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;
@@ -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;
@@ -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;
}
+10 -7
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>
);
@@ -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}
/>
@@ -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;
@@ -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;
@@ -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}/>
@@ -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}
@@ -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>
@@ -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' }}/>;
};
@@ -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;
@@ -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);
}
}
}
}
@@ -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 (
@@ -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;
}
}
@@ -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/>
</>;
};
@@ -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
@@ -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
@@ -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]);
};
@@ -4,6 +4,7 @@ export default function() {
'copyDevCommand',
'exportPdf',
'focusElementNoteBody',
'focusElementNoteViewer',
'focusElementNoteList',
'focusElementNoteTitle',
'focusElementSideBar',
@@ -43,9 +44,12 @@ export default function() {
'togglePerFolderSortOrder',
'toggleSideBar',
'toggleVisiblePanes',
'toggleEditorPlugin',
'toggleTabMovesFocus',
'editor.deleteLine',
'editor.duplicateLine',
'openSecondaryAppInstance',
'openPrimaryAppInstance',
// We cannot put the undo/redo commands in the menu because they are
// editor-specific commands. If we put them there it will break the
// undo/redo in regular text fields.
@@ -377,6 +377,53 @@
contentElement.scrollTop = scrollTop;
}
const getLineCorrespondingTo = (editorLineNumber) => {
const lineElements = document.getElementsByClassName('maps-to-line');
let lastLineElement;
let lastLine = 0;
for (const element of lineElements) {
// Stop just before the element that corresponds to a greater position
if (Number(element.getAttribute('source-line')) > editorLineNumber) {
break;
}
lastLineElement = element;
}
return lastLineElement;
};
const makeTemporarilyFocusable = (element) => {
const dataOriginalTabIndexAttr = 'data-original-tabindex';
const originalTabIndex = (
element.getAttribute(dataOriginalTabIndexAttr) ?? element.getAttribute('tabindex')
);
element.setAttribute(dataOriginalTabIndexAttr, originalTabIndex);
element.setAttribute('tabindex', '0');
return {
reset: () => {
element.setAttribute('tabindex', originalTabIndex);
element.removeAttribute(dataOriginalTabIndexAttr);
},
};
};
ipc.focusLine = (event) => {
const targetLine = event.line;
const lineElement = getLineCorrespondingTo(targetLine);
if (lineElement) {
// To allow focusing, the element needs to briefly have tabindex=0.
const { reset } = makeTemporarilyFocusable(lineElement);
lineElement.focus({ preventScroll: true });
// Reset the tabindex after the browser has had time to focus the element.
// When a screen reader is enabled, focus stays on the lineElement.
setTimeout(() => {
reset();
}, 50);
}
}
const rewriteFileUrls = (accessKey) => {
if (!accessKey) return;
@@ -3,6 +3,7 @@
@use './user-webview-dialog.scss';
@use './prompt-dialog.scss';
@use './flat-button.scss';
@use './link-button.scss';
@use './help-text.scss';
@use './toolbar-button.scss';
@use './toolbar-icon.scss';
@@ -14,3 +15,5 @@
@use './combobox-wrapper.scss';
@use './combobox-suggestion-option.scss';
@use './change-app-layout-dialog.scss';
@use './popup-notification-list.scss';
@use './popup-notification-item.scss';
@@ -0,0 +1,13 @@
.link-button {
background: transparent;
border: none;
font-size: inherit;
font-weight: inherit;
color: inherit;
padding: 0;
margin: 0;
text-decoration: underline;
cursor: pointer;
}
@@ -0,0 +1,126 @@
@keyframes slide-in {
from {
opacity: 0;
transform: translateY(25%);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slide-out {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(25%);
}
}
@keyframes grow {
from {
transform: scale(0);
}
to {
transform: scale(1);
}
}
.popup-notification-item {
margin: 12px;
padding: 13px 15px;
border-radius: 4px;
overflow: clip;
position: relative;
display: flex;
align-items: center;
box-shadow: 0 3px 7px 0px rgba(0, 0, 0, 0.25);
--text-color: var(--joplin-color5);
--ripple-color: var(--joplin-background-color5);
background-color: color-mix(in srgb, var(--ripple-color) 20%, transparent 70%);
color: var(--text-color);
animation: slide-in 0.3s ease-in both;
> .icon {
font-size: 14px;
text-align: center;
width: 24px;
height: 24px;
// Make the line hight slightly larger than the icon size
// to vertically center the text
line-height: 26px;
margin-inline-end: 13px;
border-radius: 50%;
color: var(--ripple-color);
background-color: var(--text-color);
}
> .content {
padding: 10px 0;
max-width: min(280px, 70vw);
font-size: 1.1em;
font-weight: 500;
}
> .ripple {
--ripple-size: 500px;
position: absolute;
transform-origin: bottom right;
top: calc(var(--ripple-size) / -2);
right: -40px;
z-index: -1;
background-color: var(--ripple-color);
width: var(--ripple-size);
height: var(--ripple-size);
border-radius: calc(var(--ripple-size) / 2);
transform: scale(0);
animation: grow 0.4s ease-out forwards;
}
&.-dismissing {
// Animate the icon and content first
animation: slide-out 0.25s ease-out both;
animation-delay: 0.25s;
& > .content, & > .icon {
animation: slide-out 0.3s ease-out both;
}
}
&.-success {
--ripple-color: var(--joplin-color-correct);
}
&.-error {
--ripple-color: var(--joplin-color-error);
}
&.-info {
--text-color: var(--joplin-color5);
--ripple-color: var(--joplin-background-color5);
}
@media (prefers-reduced-motion) {
&, & > .content, & > .icon {
transform: none !important;
}
> .ripple {
transform: scale(1);
animation: none;
}
}
}
@@ -0,0 +1,22 @@
.popup-notification-list {
display: flex;
align-items: end;
flex-direction: column;
list-style-type: none;
padding-left: 0;
padding-right: 0;
&.-overlay {
// Focus should jump to the bottom item first
flex-direction: column-reverse;
position: absolute;
bottom: 0;
inset-inline-end: 0; // right: 0 in ltr, left: 0 in rtl
z-index: 10;
max-height: 100vh;
overflow-y: auto;
}
}
-2
View File
@@ -10,7 +10,6 @@
<title>Joplin</title>
<!-- Note: Add new dynamic CSS imports to style.scss to allow them to be included in secondary windows. -->
<link rel="stylesheet" href="style.min.css">
<link rel="stylesheet" href="./node_modules/notyf/notyf.min.css">
<script src="vendor/lib/smalltalk/dist/smalltalk.min.js"></script>
<script src="./node_modules/tesseract.js/dist/tesseract.min.js"></script>
@@ -19,6 +18,5 @@
<div id="react-root"></div>
<script src="./utils/window/eventHandlerOverrides.js"></script>
<script src="main-html.js"></script>
<script src="./node_modules/notyf/notyf.min.js"></script>
</body>
</html>
@@ -1,7 +1,9 @@
# Integration tests
The integration tests in this directory can be run with `yarn playwright test`.
The integration tests in this directory can be run with `yarn test-ui`.
- To run all tests from a specific file, use `yarn test-ui testFileName`. For example, `yarn test-ui wcag` to run the tests in `wcag.ts`.
- To run all tests matching a pattern, use `yarn test-ui -g "pattern here"`, where `-g` is short for "grep".
- Tests use a `test-profile` directory that should be re-created before every test.
- Only one Electron application should be instantiated per test file.
- Files in the `models/` directory follow [the page object model](https://playwright.dev/docs/pom).
@@ -15,3 +17,13 @@ with Playwright:
- [The Playwright ElectronApp docs](https://playwright.dev/docs/api/class-electronapplication)
- [Electron Playwright example repository](https://github.com/spaceagetv/electron-playwright-example)
- [Playwright best practices](https://playwright.dev/docs/best-practices)
- [Running and debugging tests from VSCode](https://playwright.dev/docs/getting-started-vscode#running-tests).
# FAQ
## How do I fix timeout-related test failures?
If Playwright tests are timing out, consider modifying `playwright.config.ts` in the `app-desktop` folder. For example, increase the `timeout` option to `120_000` (2 minutes).
Alternatively, try temporarily disabling `fullyParallel` (which disables running tests in parallel).
@@ -4,7 +4,7 @@ import { _electron as electron } from '@playwright/test';
import { writeFile } from 'fs-extra';
import { join } from 'path';
import createStartupArgs from './util/createStartupArgs';
import firstNonDevToolsWindow from './util/firstNonDevToolsWindow';
import getMainWindow from './util/getMainWindow';
import setFilePickerResponse from './util/setFilePickerResponse';
import setMessageBoxResponse from './util/setMessageBoxResponse';
import getImageSourceSize from './util/getImageSourceSize';
@@ -116,7 +116,10 @@ test.describe('main', () => {
await editor.attachFileButton.click();
const viewerFrame = editor.getNoteViewerFrameLocator();
const renderedImage = viewerFrame.getByAltText(filename);
const renderedImage = viewerFrame
.getByAltText(filename)
// Work around occasional "resolved to 2 elements" errors in CI
.last();
const fullSize = await getImageSourceSize(renderedImage);
@@ -193,7 +196,7 @@ test.describe('main', () => {
// Open the app ourselves:
const startupArgs = createStartupArgs(profileDirectory);
const electronApp = await electron.launch({ args: startupArgs });
const mainWindow = await firstNonDevToolsWindow(electronApp);
const mainWindow = await getMainWindow(electronApp);
const safeModeDisableLink = mainWindow.getByText('Disable safe mode and restart');
await safeModeDisableLink.waitFor();
@@ -230,5 +230,28 @@ test.describe('markdownEditor', () => {
// Editor should be focused
await expect(focusInMarkdownEditor).toBeAttached();
});
test('focusElementNoteViewer should move focus to the viewer', async ({ mainWindow, electronApp }) => {
const mainScreen = await new MainScreen(mainWindow).setup();
await mainScreen.waitFor();
const noteEditor = mainScreen.noteEditor;
await mainScreen.createNewNote('Note');
await noteEditor.focusCodeMirrorEditor();
await mainWindow.keyboard.type('# Test');
await mainWindow.keyboard.press('Enter');
await mainWindow.keyboard.press('Enter');
await mainWindow.keyboard.type('Test paragraph.');
// Wait for rendering
await expect(noteEditor.getNoteViewerFrameLocator().getByText('Test paragraph.')).toBeAttached();
// Move focus
await mainScreen.goToAnything.runCommand(electronApp, 'focusElementNoteViewer');
// Note viewer should be focused
await expect(noteEditor.noteViewerContainer).toBeFocused();
});
});
@@ -80,7 +80,11 @@ export default class NoteEditorPage {
// We use frameLocator(':scope') to convert the richTextEditor Locator into
// a FrameLocator. (:scope selects the locator itself).
// https://playwright.dev/docs/api/class-framelocator
return this.richTextEditor.frameLocator(':scope');
return this.richTextEditor.contentFrame();
}
public getRichTextEditorBody() {
return this.richTextEditor.contentFrame().locator('body');
}
public focusCodeMirrorEditor() {
@@ -25,6 +25,7 @@ export default class NoteList {
public async focusContent(electronApp: ElectronApplication) {
await activateMainMenuItem(electronApp, 'Note list', 'Focus');
await expect(this.container.locator(':focus')).toBeAttached();
}
// The resultant locator may fail to resolve if the item is not visible
@@ -2,20 +2,22 @@
import { Page, Locator } from '@playwright/test';
export default class SettingsScreen {
private readonly container: Locator;
public readonly okayButton: Locator;
public readonly appearanceTabButton: Locator;
public constructor(private page: Page) {
this.okayButton = page.locator('button', { hasText: 'OK' });
public constructor(page: Page) {
this.container = page.locator('.config-screen');
this.okayButton = this.container.locator('.button-bar button', { hasText: 'OK' });
this.appearanceTabButton = this.getTabLocator('Appearance');
}
public getTabLocator(tabName: string) {
return this.page.getByRole('tab', { name: tabName });
return this.container.getByRole('tab', { name: tabName });
}
public getLastTab() {
return this.page.getByRole('tablist').getByRole('tab').last();
return this.container.getByRole('tablist').getByRole('tab').last();
}
public async waitFor() {
@@ -75,6 +75,32 @@ test.describe('noteList', () => {
await expect(noteList.getNoteItemByTitle('test note 1')).toBeVisible();
});
test('deleting a note to the trash should show a notification', async ({ electronApp, mainWindow }) => {
const mainScreen = await new MainScreen(mainWindow).setup();
await mainScreen.createNewNote('test note 1');
const noteList = mainScreen.noteList;
await noteList.focusContent(electronApp);
const testNoteItem = noteList.getNoteItemByTitle('test note 1');
await expect(testNoteItem).toBeVisible();
// Should be removed after deleting
await testNoteItem.press('Delete');
await expect(testNoteItem).not.toBeVisible();
// Should show a deleted notification
const notification = mainWindow.locator('[role=alert]', {
hasText: /The note was successfully moved to the trash./i,
});
await expect(notification).toBeVisible();
// Should be possible to un-delete
const undeleteButton = notification.getByRole('button', { name: 'Cancel' });
await undeleteButton.click();
await expect(testNoteItem).toBeVisible();
});
test('arrow keys should navigate the note list', async ({ electronApp, mainWindow }) => {
const mainScreen = await new MainScreen(mainWindow).setup();
const sidebar = mainScreen.sidebar;
@@ -23,6 +23,27 @@ test.describe('pluginApi', () => {
await editor.expectToHaveText('PASS');
});
test('should return form data from the dialog API', async ({ startAppWithPlugins }) => {
const { app, mainWindow } = await startAppWithPlugins(['resources/test-plugins/dialogs.js']);
const mainScreen = await new MainScreen(mainWindow).setup();
await mainScreen.createNewNote('First note');
const editor = mainScreen.noteEditor;
await editor.expectToHaveText('');
await mainScreen.goToAnything.runCommand(app, 'showTestDialog');
// Wait for the iframe to load
const dialogContent = mainScreen.dialog.locator('iframe').contentFrame();
await dialogContent.locator('form').waitFor();
// Submitting the dialog should include form data in the output
await mainScreen.dialog.getByRole('button', { name: 'Okay' }).click();
await editor.expectToHaveText(JSON.stringify({
id: 'ok',
hasFormData: true,
}));
});
test('should be possible to create multiple toasts with the same text from a plugin', async ({ startAppWithPlugins }) => {
const { app, mainWindow } = await startAppWithPlugins(['resources/test-plugins/showToast.js']);
const mainScreen = await new MainScreen(mainWindow).setup();
@@ -0,0 +1,51 @@
// Allows referencing the Joplin global:
/* eslint-disable no-undef */
// Allows the `joplin-manifest` block comment:
/* eslint-disable multiline-comment-style */
/* joplin-manifest:
{
"id": "org.joplinapp.plugins.example.dialogs",
"manifest_version": 1,
"app_min_version": "3.1",
"name": "JS Bundle test",
"description": "JS Bundle Test plugin",
"version": "1.0.0",
"author": "",
"homepage_url": "https://joplinapp.org"
}
*/
joplin.plugins.register({
onStart: async function() {
const dialogs = joplin.views.dialogs;
const dialogHandle = await dialogs.create('test-dialog');
await dialogs.setHtml(
dialogHandle,
`
<form name="main-form">
<label>Test: <input type="checkbox" name="test" checked/></label>
</form>
`,
);
await dialogs.setButtons(dialogHandle, [
{
id: 'ok',
title: 'Okay',
},
]);
await joplin.commands.register({
name: 'showTestDialog',
label: 'showTestDialog',
iconName: 'fas fa-drum',
execute: async () => {
const result = await joplin.views.dialogs.open(dialogHandle);
await joplin.commands.execute('editor.setText', JSON.stringify({
id: result.id,
hasFormData: !!result.formData,
}));
},
});
},
});
@@ -62,6 +62,9 @@ test.describe('richTextEditor', () => {
await setFilePickerResponse(electronApp, [pathToAttach]);
await editor.attachFileButton.click();
// Wait for it to render
await expect(editor.getNoteViewerFrameLocator().getByText('test-file.txt')).toBeVisible();
// Switch to the RTE
await editor.toggleEditorsButton.click();
await editor.richTextEditor.waitFor();
@@ -82,6 +85,44 @@ test.describe('richTextEditor', () => {
expect(await openPathResult).toContain(basename(pathToAttach));
});
test('should not remove text when pressing [enter] at the end of a line with an image', async ({ mainWindow }) => {
const mainScreen = await new MainScreen(mainWindow).setup();
await mainScreen.createNewNote('Testing pressing enter!');
const editor = mainScreen.noteEditor;
// Set the initial content
await editor.codeMirrorEditor.click();
await mainWindow.keyboard.type([
'<img',
' src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAYAAABWKLW/AAAAEklEQVQIW2P8z8AARBDAiJMDAIzoBf635fcVAAAAAElFTkSuQmCC"',
' width="200"',
' height="200"',
' alt="test image"',
'/>',
].join(' '));
await mainWindow.keyboard.press('Enter');
await mainWindow.keyboard.press('Enter');
await mainWindow.keyboard.type('Test secondary paragraph.');
// Switch to the RTE
await editor.toggleEditorsButton.click();
await editor.richTextEditor.waitFor();
const richTextEditorFrame = editor.getRichTextFrameLocator();
const testParagraph = richTextEditorFrame.getByText('Test secondary paragraph.');
await expect(testParagraph).toBeAttached();
// Move the cursor just after the image, then press enter.
const testImage = richTextEditorFrame.getByRole('img', { name: 'test image' });
await testImage.click();
await mainWindow.keyboard.press('ArrowRight');
await mainWindow.keyboard.press('Enter');
// Should not have removed the image or the test paragraph.
await expect(testImage).toBeAttached();
await expect(testParagraph).toBeAttached();
});
test('pressing Tab should indent', async ({ mainWindow }) => {
const mainScreen = await new MainScreen(mainWindow).setup();
await mainScreen.createNewNote('Testing tabs!');
@@ -212,5 +253,33 @@ test.describe('richTextEditor', () => {
await expect(editor.noteTitleInput).not.toBeFocused();
await expect(editor.richTextEditor).toBeFocused();
});
test('note should have correct content even if opened quickly after last edit', async ({ mainWindow }) => {
const mainScreen = await new MainScreen(mainWindow).setup();
await mainScreen.createNewNote('Test 1');
await mainScreen.createNewNote('Test 2');
const test1Header = mainScreen.noteList.getNoteItemByTitle('Test 1');
const test2Header = mainScreen.noteList.getNoteItemByTitle('Test 2');
const editor = mainScreen.noteEditor;
await editor.toggleEditorsButton.click();
await editor.richTextEditor.waitFor();
const editorBody = editor.getRichTextEditorBody();
const setEditorText = async (targetText: string) => {
await editorBody.pressSequentially(targetText);
await expect(editorBody).toHaveText(targetText);
};
await test1Header.click();
await expect(editorBody).toHaveText('');
await setEditorText('Test 1');
await test2Header.click();
// Previously, after switching to note 2, the "Test 1" text would remain present in the
// editor.
await expect(editorBody).toHaveText('');
});
});
@@ -7,7 +7,7 @@ export CI=true
if test "$RUNNER_OS" = "Linux" ; then
# The Ubuntu Github CI doesn't have a display server.
# Start a virtual one with xvfb-run.
xvfb-run -- yarn playwright test
xvfb-run -- yarn test-ui
else
yarn playwright test
yarn test-ui
fi
@@ -1,8 +1,13 @@
import { dirname, resolve } from 'path';
const createStartupArgs = (profileDirectory: string) => {
// Input paths need to be absolute when running from VSCode
const baseDirectory = dirname(dirname(__dirname));
const mainPath = resolve(baseDirectory, 'main.js');
// We need to run with --env dev to disable the single instance check.
return [
'main.js', '--env', 'dev', '--no-welcome', '--profile', profileDirectory,
mainPath, '--env', 'dev', '--log-level', 'debug', '--no-welcome', '--running-tests', '--profile', resolve(profileDirectory),
];
};
@@ -1,43 +0,0 @@
import { ElectronApplication, Page } from '@playwright/test';
const isDevTools = async (page: Page) => {
// It seems that the developer tools window can have titles in different
// formats (e.g. DevTools, Developer Tools).
return (await page.title()).match(/Dev(eloper)?\s*Tools/i);
};
const firstNonDevToolsWindow = async (electronApp: ElectronApplication) => {
// Wait for the window event as soon as possible -- it's possible that
// the window we want will be shown while doing other async checks.
const nextNonDevToolsPage = electronApp.waitForEvent('window', {
predicate: async page => {
return !(await isDevTools(page));
},
});
// First use firstWindow -- it's possible that the first window
// has already been shown.
let mainWindow = await electronApp.firstWindow();
if (await isDevTools(mainWindow)) {
for (const window of electronApp.windows()) {
if (!(await isDevTools(window))) {
mainWindow = window;
break;
}
}
if (await isDevTools(mainWindow)) {
mainWindow = await nextNonDevToolsPage;
}
}
// waitForEvent will throw if no additional windows are created.
// Ignore.
// eslint-disable-next-line promise/prefer-await-to-then
nextNonDevToolsPage.catch(_error => {});
return mainWindow;
};
export default firstNonDevToolsWindow;
Loaded 100 of 524 files, more files were not shown because too many files have changed in this diff. Show more