Compare commits
315 Commits
sqlcipher_
...
v3.3.10
Author | SHA1 | Date | |
---|---|---|---|
|
d9ba532889 | ||
|
5aca4bc72e | ||
|
ae47667644 | ||
|
b191b1daf7 | ||
|
0e3b9fd929 | ||
|
5d0fa754ae | ||
|
70d76fa00f | ||
|
d72224a6b9 | ||
|
687516e695 | ||
|
3055873406 | ||
|
e58e419c1b | ||
|
ebd55cc505 | ||
|
712f4034f2 | ||
|
601774c611 | ||
|
7a810380fd | ||
|
52d71756a7 | ||
|
a6b430a066 | ||
|
5a97ce74bc | ||
|
e58e03c3d2 | ||
|
511645b1a5 | ||
|
b9ffc7ccec | ||
|
b585b8b75c | ||
|
f7d1dbde8f | ||
|
6009f1dd60 | ||
|
e50086134f | ||
|
25c66e2d00 | ||
|
6d89725c7a | ||
|
dfdabfc0da | ||
|
2059e96227 | ||
|
40e4afbc78 | ||
|
76a22c31cb | ||
|
175628b597 | ||
|
e7584644b5 | ||
|
13a572efa9 | ||
|
d85394681d | ||
|
c785388c51 | ||
|
27be923fe6 | ||
|
a7bfa21cd0 | ||
|
e9fe944744 | ||
|
b93a125f95 | ||
|
262c343f21 | ||
|
8aa37905f9 | ||
|
2a5b227bf0 | ||
|
7223e9b87e | ||
|
82d9099ec0 | ||
|
c46502825d | ||
|
58688624fc | ||
|
355356dccc | ||
|
d4d12c0faa | ||
|
f8f8db36b6 | ||
|
a8e30c5b54 | ||
|
ffd775a8f8 | ||
|
8d421d907e | ||
|
3206b1a792 | ||
|
37137f3867 | ||
|
e08fd0ea53 | ||
|
6265be924a | ||
|
079c4638fe | ||
|
6aba57f2f1 | ||
|
f083ffa73c | ||
|
40fd0db14e | ||
|
6379023165 | ||
|
66f6310c17 | ||
|
d6409b7826 | ||
|
1f68c5dcae | ||
|
bfe003a6c4 | ||
|
ddc75ecc13 | ||
|
e953290810 | ||
|
807bcdcf95 | ||
|
6bb289338e | ||
|
5f0e4bb598 | ||
|
84d97e4120 | ||
|
a9efdad059 | ||
|
388e5efe17 | ||
|
dfa340a137 | ||
|
cf626bee76 | ||
|
0fe97a1098 | ||
|
7c6c9b3e61 | ||
|
987c273376 | ||
|
0da05737c6 | ||
|
f71ffa1644 | ||
|
92a546a315 | ||
|
22ffa69382 | ||
|
e1a436f6f9 | ||
|
b3823025cf | ||
|
eccb8350fe | ||
|
66203bda56 | ||
|
0759fe0530 | ||
|
6d3c3a80ab | ||
|
a18bcac673 | ||
|
b3a3690b16 | ||
|
c3fe0edeeb | ||
|
56e2d3da89 | ||
|
3ffcf065fc | ||
|
9dd82259c6 | ||
|
2dbdf47239 | ||
|
2014fbf480 | ||
|
e847b1b902 | ||
|
26276efc03 | ||
|
4226044527 | ||
|
5ba6ac57d0 | ||
|
0058ac5f4b | ||
|
81f5a8463e | ||
|
9871717de4 | ||
|
62ca6cb70b | ||
|
bd49f3b280 | ||
|
cb3c9b4607 | ||
|
4e74ca93a3 | ||
|
5389e59057 | ||
|
3d15e64762 | ||
|
a8b18e9ab0 | ||
|
5876d57845 | ||
|
90f622b3e6 | ||
|
fd486e298a | ||
|
527627b8bb | ||
|
9638cab9ea | ||
|
5ad891e1f3 | ||
|
f8445a04e5 | ||
|
fc92a6ea63 | ||
|
418a2c17a5 | ||
|
c3cc412077 | ||
|
600000a59a | ||
|
a3be7b5222 | ||
|
52ffd46a6a | ||
|
587db433a8 | ||
|
5fb9d216fc | ||
|
bb50ad7c28 | ||
|
7a6a4e118a | ||
|
b7a652fb71 | ||
|
821558fe30 | ||
|
5280ec12cd | ||
|
a29e30e442 | ||
|
fe88703488 | ||
|
338dabf5da | ||
|
59447f4c45 | ||
|
04196e4485 | ||
|
d4fafd74d2 | ||
|
f185480ceb | ||
|
4da0abfef9 | ||
|
2e73ea2d9f | ||
|
8ac19d80ea | ||
|
911689a4ac | ||
|
5c73bda416 | ||
|
c94dbece42 | ||
|
e49e314bdb | ||
|
83452dd4c3 | ||
|
675e3125e4 | ||
|
da0a7faf12 | ||
|
9be533a19e | ||
|
050871bc65 | ||
|
8d6d7ca6d2 | ||
|
032f512cde | ||
|
b8c88af82d | ||
|
962128faa3 | ||
|
e17ef72111 | ||
|
287e0da4b4 | ||
|
471f5a72fe | ||
|
1e5c41dc48 | ||
|
716e5252c1 | ||
|
f10fd4b2da | ||
|
ff4d18dec3 | ||
|
020cd914af | ||
|
8dcfc81cee | ||
|
77048caeeb | ||
|
3438c58ec4 | ||
|
e74d5e7c23 | ||
|
c921976e9d | ||
|
c8a2802181 | ||
|
0f08688ce8 | ||
|
f5f7b1eb60 | ||
|
9d73e583b9 | ||
|
611e8df81a | ||
|
b0c9c4c8ce | ||
|
dad0b9448f | ||
|
c524f5a6b5 | ||
|
ee2b186752 | ||
|
76a3250707 | ||
|
e8144f9ee2 | ||
|
47f6b1ce33 | ||
|
f0121e7799 | ||
|
0acb14d0bf | ||
|
18ebd16428 | ||
|
6bc1965ec0 | ||
|
1fed875140 | ||
|
98fe57e87a | ||
|
5bcb2531f4 | ||
|
f6d69ef702 | ||
|
1f05a3212f | ||
|
ff0321e906 | ||
|
6b881b226e | ||
|
6a26ec8105 | ||
|
5966402d8b | ||
|
5198b598bb | ||
|
0a4c97c631 | ||
|
0bc62aa05e | ||
|
44d1e9e3ca | ||
|
0cef6cc611 | ||
|
3e5acfbc09 | ||
|
fe20ab11a1 | ||
|
675f55d152 | ||
|
b1edb84b49 | ||
|
ece7a4ccf0 | ||
|
cfd98d2723 | ||
|
2a17301a9f | ||
|
e3762dc3f8 | ||
|
0959a19d65 | ||
|
93219575b4 | ||
|
baaeea1307 | ||
|
de6c5d448f | ||
|
cc2cf5f521 | ||
|
ef513862a9 | ||
|
d07f3b5f16 | ||
|
a6079869bc | ||
|
2fdbb22481 | ||
|
c5bb88ddf4 | ||
|
5d7c78c361 | ||
|
808eb7d49a | ||
|
2142373fff | ||
|
20f7f37b49 | ||
|
04fc634092 | ||
|
d40c9d3ff9 | ||
|
224b4f619a | ||
|
88d1d4b7d1 | ||
|
6a22ffbcb1 | ||
|
d735cf64e0 | ||
|
d7d6fd5ccd | ||
|
23254e6ffd | ||
|
eb8bfd5aec | ||
|
cb5ffd968d | ||
|
7b2b3a4f80 | ||
|
cbfe109c41 | ||
|
c8b01d11d6 | ||
|
b042395fd1 | ||
|
ba5ad18093 | ||
|
ff15232a10 | ||
|
5a6e72197a | ||
|
de555b6871 | ||
|
9a2548a5e3 | ||
|
107996289f | ||
|
c3c0101555 | ||
|
45ee02036f | ||
|
64f3dae8cc | ||
|
0909479b7f | ||
|
a39b51cc97 | ||
|
10bb8ef1a9 | ||
|
60ba22b233 | ||
|
1bfd997be2 | ||
|
81e4a7fb74 | ||
|
360568d325 | ||
|
1aa0f11670 | ||
|
0430ccb3e7 | ||
|
c0d6c1eb0b | ||
|
215f09d73c | ||
|
1f192696de | ||
|
ab86b95fad | ||
|
0f07c0f53a | ||
|
a6d04c4781 | ||
|
bc27f47881 | ||
|
f381c91783 | ||
|
d1d75449f5 | ||
|
79dc12ae0a | ||
|
bbea5388ed | ||
|
99e773855e | ||
|
55b73347e5 | ||
|
7e8dee4906 | ||
|
69fb1ab104 | ||
|
67ae0ea2d1 | ||
|
cdb61b922b | ||
|
da80443796 | ||
|
1924dd31d2 | ||
|
b831d8c068 | ||
|
9a697a0e34 | ||
|
4ad1b49769 | ||
|
0d6c1067e3 | ||
|
0bdc38a6be | ||
|
7735fc9759 | ||
|
5c35569b5b | ||
|
5f02af9724 | ||
|
975f16d21c | ||
|
54428f5034 | ||
|
e6ef0d45c6 | ||
|
ebe9c38f9e | ||
|
ccebb9696e | ||
|
16ba072436 | ||
|
5a81e2ce84 | ||
|
8e407dd003 | ||
|
6118c572ac | ||
|
fd8393b2f9 | ||
|
15edc9fec7 | ||
|
35053f2996 | ||
|
8bd4770f33 | ||
|
4af459c762 | ||
|
6ad97a01a8 | ||
|
3e05b8daa0 | ||
|
41cb16f5c5 | ||
|
543ece86b0 | ||
|
44f98d6282 | ||
|
438bf29e5f | ||
|
0a1f960919 | ||
|
5bc7304c63 | ||
|
efec21156b | ||
|
85747317ee | ||
|
9f4a80cf89 | ||
|
1b3ebf4fb0 | ||
|
bcd10e380b | ||
|
1bc64cfdff | ||
|
ab46f6decd | ||
|
49535e4a3e | ||
|
4c35d811d6 | ||
|
0dec21b2bb | ||
|
ad8c9263e8 | ||
|
04de21a9fd | ||
|
d266ec92c4 | ||
|
63fce9cfb0 | ||
|
7c50e00fb4 |
@@ -62,6 +62,7 @@ packages/app-mobile/locales
|
||||
packages/app-mobile/node_modules
|
||||
packages/app-mobile/pluginAssets/
|
||||
packages/fork-*
|
||||
!packages/fork-uslug
|
||||
packages/default-plugins/plugin-base-repo/
|
||||
packages/default-plugins/plugin-sources/
|
||||
packages/htmlpack/dist/
|
||||
@@ -159,7 +160,9 @@ packages/app-desktop/commands/exportNotes.js
|
||||
packages/app-desktop/commands/focusElement.js
|
||||
packages/app-desktop/commands/index.js
|
||||
packages/app-desktop/commands/openNoteInNewWindow.js
|
||||
packages/app-desktop/commands/openPrimaryAppInstance.js
|
||||
packages/app-desktop/commands/openProfileDirectory.js
|
||||
packages/app-desktop/commands/openSecondaryAppInstance.js
|
||||
packages/app-desktop/commands/replaceMisspelling.js
|
||||
packages/app-desktop/commands/restoreNoteRevision.js
|
||||
packages/app-desktop/commands/startExternalEditing.js
|
||||
@@ -206,6 +209,7 @@ packages/app-desktop/gui/InlineCombobox.js
|
||||
packages/app-desktop/gui/ItemList.js
|
||||
packages/app-desktop/gui/JoplinCloudConfigScreen.js
|
||||
packages/app-desktop/gui/JoplinCloudLoginScreen.js
|
||||
packages/app-desktop/gui/JoplinCloudSignUpCallToAction.js
|
||||
packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.js
|
||||
packages/app-desktop/gui/KeymapConfig/ShortcutRecorder.js
|
||||
packages/app-desktop/gui/KeymapConfig/styles/index.js
|
||||
@@ -264,6 +268,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useKeyboardRefocusHan
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useLinkTooltips.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useTabIndenter.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useTextPatternsLookup.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useWebViewApi.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteEditor.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.js
|
||||
@@ -272,6 +277,7 @@ packages/app-desktop/gui/NoteEditor/WarningBanner/BannerContent.js
|
||||
packages/app-desktop/gui/NoteEditor/WarningBanner/WarningBanner.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteBody.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteTitle.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteViewer.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/focusElementToolbar.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/index.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/pasteAsText.js
|
||||
@@ -353,13 +359,16 @@ packages/app-desktop/gui/NoteSearchBar.js
|
||||
packages/app-desktop/gui/NoteStatusBar.js
|
||||
packages/app-desktop/gui/NoteTextViewer.js
|
||||
packages/app-desktop/gui/NoteToolbar/NoteToolbar.js
|
||||
packages/app-desktop/gui/NotyfContext.js
|
||||
packages/app-desktop/gui/OneDriveLoginScreen.js
|
||||
packages/app-desktop/gui/PasswordInput/LabelledPasswordInput.js
|
||||
packages/app-desktop/gui/PasswordInput/PasswordInput.js
|
||||
packages/app-desktop/gui/PasswordInput/types.js
|
||||
packages/app-desktop/gui/PdfViewer.js
|
||||
packages/app-desktop/gui/PluginNotification/PluginNotification.js
|
||||
packages/app-desktop/gui/PopupNotification/NotificationItem.js
|
||||
packages/app-desktop/gui/PopupNotification/PopupNotificationList.js
|
||||
packages/app-desktop/gui/PopupNotification/PopupNotificationProvider.js
|
||||
packages/app-desktop/gui/PopupNotification/types.js
|
||||
packages/app-desktop/gui/PromptDialog.js
|
||||
packages/app-desktop/gui/ResizableLayout/LayoutItemContainer.js
|
||||
packages/app-desktop/gui/ResizableLayout/MoveButtons.js
|
||||
@@ -421,6 +430,7 @@ packages/app-desktop/gui/ToolbarBase.js
|
||||
packages/app-desktop/gui/ToolbarButton/ToolbarButton.js
|
||||
packages/app-desktop/gui/ToolbarSpace.js
|
||||
packages/app-desktop/gui/TrashNotification/TrashNotification.js
|
||||
packages/app-desktop/gui/TrashNotification/TrashNotificationMessage.js
|
||||
packages/app-desktop/gui/UpdateNotification/UpdateNotification.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/AppDialogs.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/ModalMessageOverlay.js
|
||||
@@ -525,8 +535,8 @@ packages/app-desktop/integration-tests/simpleBackup.spec.js
|
||||
packages/app-desktop/integration-tests/util/activateMainMenuItem.js
|
||||
packages/app-desktop/integration-tests/util/createStartupArgs.js
|
||||
packages/app-desktop/integration-tests/util/extendedExpect.js
|
||||
packages/app-desktop/integration-tests/util/firstNonDevToolsWindow.js
|
||||
packages/app-desktop/integration-tests/util/getImageSourceSize.js
|
||||
packages/app-desktop/integration-tests/util/getMainWindow.js
|
||||
packages/app-desktop/integration-tests/util/setDarkMode.js
|
||||
packages/app-desktop/integration-tests/util/setFilePickerResponse.js
|
||||
packages/app-desktop/integration-tests/util/setMessageBoxResponse.js
|
||||
@@ -547,13 +557,16 @@ packages/app-desktop/services/plugins/UserWebview.js
|
||||
packages/app-desktop/services/plugins/UserWebviewDialog.js
|
||||
packages/app-desktop/services/plugins/UserWebviewDialogButtonBar.js
|
||||
packages/app-desktop/services/plugins/hooks/useContentSize.js
|
||||
packages/app-desktop/services/plugins/hooks/useFormData.js
|
||||
packages/app-desktop/services/plugins/hooks/useHtmlLoader.js
|
||||
packages/app-desktop/services/plugins/hooks/useMessageHandler.js
|
||||
packages/app-desktop/services/plugins/hooks/useScriptLoader.js
|
||||
packages/app-desktop/services/plugins/hooks/useSubmitHandler.js
|
||||
packages/app-desktop/services/plugins/hooks/useThemeCss.test.js
|
||||
packages/app-desktop/services/plugins/hooks/useThemeCss.js
|
||||
packages/app-desktop/services/plugins/hooks/useViewIsReady.js
|
||||
packages/app-desktop/services/plugins/hooks/useWebviewToPluginMessages.js
|
||||
packages/app-desktop/services/plugins/types.js
|
||||
packages/app-desktop/services/restart.js
|
||||
packages/app-desktop/services/sortOrder/PerFolderSortOrderService.test.js
|
||||
packages/app-desktop/services/sortOrder/PerFolderSortOrderService.js
|
||||
@@ -581,6 +594,7 @@ packages/app-desktop/utils/restartInSafeModeFromMain.test.js
|
||||
packages/app-desktop/utils/restartInSafeModeFromMain.js
|
||||
packages/app-desktop/utils/window/types.js
|
||||
packages/app-mobile/PluginAssetsLoader.js
|
||||
packages/app-mobile/commands/dismissPluginPanels.js
|
||||
packages/app-mobile/commands/index.js
|
||||
packages/app-mobile/commands/newNote.test.js
|
||||
packages/app-mobile/commands/newNote.js
|
||||
@@ -590,6 +604,7 @@ packages/app-mobile/commands/scrollToHash.js
|
||||
packages/app-mobile/commands/util/goToNote.js
|
||||
packages/app-mobile/commands/util/showResource.js
|
||||
packages/app-mobile/components/BetaChip.js
|
||||
packages/app-mobile/components/BottomDrawer.js
|
||||
packages/app-mobile/components/CameraView/ActionButtons.js
|
||||
packages/app-mobile/components/CameraView/Camera/index.jest.js
|
||||
packages/app-mobile/components/CameraView/Camera/index.js
|
||||
@@ -683,15 +698,22 @@ packages/app-mobile/components/SelectDateTimeDialog.js
|
||||
packages/app-mobile/components/SideMenu.js
|
||||
packages/app-mobile/components/SideMenuContentNote.js
|
||||
packages/app-mobile/components/TextInput.js
|
||||
packages/app-mobile/components/ToggleSpaceButton.js
|
||||
packages/app-mobile/components/accessibility/AccessibleModalMenu.js
|
||||
packages/app-mobile/components/accessibility/AccessibleView.test.js
|
||||
packages/app-mobile/components/accessibility/AccessibleView.js
|
||||
packages/app-mobile/components/accessibility/FocusControl/AutoFocusProvider.js
|
||||
packages/app-mobile/components/accessibility/FocusControl/FocusControl.js
|
||||
packages/app-mobile/components/accessibility/FocusControl/FocusControlProvider.js
|
||||
packages/app-mobile/components/accessibility/FocusControl/MainAppContent.js
|
||||
packages/app-mobile/components/accessibility/FocusControl/ModalWrapper.js
|
||||
packages/app-mobile/components/accessibility/FocusControl/types.js
|
||||
packages/app-mobile/components/app-nav.js
|
||||
packages/app-mobile/components/base-screen.js
|
||||
packages/app-mobile/components/biometrics/BiometricPopup.js
|
||||
packages/app-mobile/components/biometrics/biometricAuthenticate.js
|
||||
packages/app-mobile/components/biometrics/sensorInfo.js
|
||||
packages/app-mobile/components/buttons/FloatingActionButton.js
|
||||
packages/app-mobile/components/buttons/LabelledIconButton.js
|
||||
packages/app-mobile/components/buttons/MultiTouchableOpacity.js
|
||||
packages/app-mobile/components/buttons/TextButton.js
|
||||
packages/app-mobile/components/buttons/index.js
|
||||
packages/app-mobile/components/getResponsiveValue.test.js
|
||||
@@ -738,8 +760,11 @@ packages/app-mobile/components/screens/ConfigScreen/SectionSelector/SectionTab.j
|
||||
packages/app-mobile/components/screens/ConfigScreen/SectionSelector/index.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingComponent.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingItem.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingTextInput.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingsButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingsToggle.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/ValidatedIntegerInput.test.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/ValidatedIntegerInput.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/EnablePluginSupportPage.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/InstalledPluginBox.js
|
||||
@@ -779,7 +804,9 @@ packages/app-mobile/components/screens/Note/commands/setTags.js
|
||||
packages/app-mobile/components/screens/Note/commands/toggleVisiblePanes.js
|
||||
packages/app-mobile/components/screens/Note/types.js
|
||||
packages/app-mobile/components/screens/NoteTagsDialog.js
|
||||
packages/app-mobile/components/screens/Notes.js
|
||||
packages/app-mobile/components/screens/Notes/NewNoteButton.test.js
|
||||
packages/app-mobile/components/screens/Notes/NewNoteButton.js
|
||||
packages/app-mobile/components/screens/Notes/Notes.js
|
||||
packages/app-mobile/components/screens/SearchScreen/SearchResults.js
|
||||
packages/app-mobile/components/screens/SearchScreen/index.js
|
||||
packages/app-mobile/components/screens/ShareManager/AcceptedShareItem.js
|
||||
@@ -834,6 +861,7 @@ packages/app-mobile/utils/createRootStyle.js
|
||||
packages/app-mobile/utils/database-driver-react-native.js
|
||||
packages/app-mobile/utils/database-driver-react-native.web.js
|
||||
packages/app-mobile/utils/debounce.js
|
||||
packages/app-mobile/utils/focusView.js
|
||||
packages/app-mobile/utils/fs-driver/constants.js
|
||||
packages/app-mobile/utils/fs-driver/fs-driver-rn.js
|
||||
packages/app-mobile/utils/fs-driver/fs-driver-rn.web.js
|
||||
@@ -846,9 +874,10 @@ packages/app-mobile/utils/fs-driver/testUtil/createFilesFromPathRecord.js
|
||||
packages/app-mobile/utils/fs-driver/testUtil/verifyDirectoryMatches.js
|
||||
packages/app-mobile/utils/getPackageInfo.js
|
||||
packages/app-mobile/utils/getVersionInfoText.js
|
||||
packages/app-mobile/utils/hooks/useKeyboardVisible.js
|
||||
packages/app-mobile/utils/hooks/useKeyboardState.js
|
||||
packages/app-mobile/utils/hooks/useOnLongPressProps.js
|
||||
packages/app-mobile/utils/hooks/useReduceMotionEnabled.js
|
||||
packages/app-mobile/utils/hooks/useSafeAreaPadding.js
|
||||
packages/app-mobile/utils/image/fileToImage.web.js
|
||||
packages/app-mobile/utils/image/getImageDimensions.js
|
||||
packages/app-mobile/utils/image/resizeImage.js
|
||||
@@ -857,6 +886,7 @@ packages/app-mobile/utils/injectedJs.js
|
||||
packages/app-mobile/utils/ipc/RNToWebViewMessenger.js
|
||||
packages/app-mobile/utils/ipc/WebViewToRNMessenger.js
|
||||
packages/app-mobile/utils/lockToSingleInstance.js
|
||||
packages/app-mobile/utils/makeShowMessageBox.test.js
|
||||
packages/app-mobile/utils/makeShowMessageBox.js
|
||||
packages/app-mobile/utils/pickDocument.js
|
||||
packages/app-mobile/utils/polyfills/bufferPolyfill.js
|
||||
@@ -897,6 +927,8 @@ packages/editor/CodeMirror/editorCommands/duplicateLine.js
|
||||
packages/editor/CodeMirror/editorCommands/editorCommands.js
|
||||
packages/editor/CodeMirror/editorCommands/insertLineAfter.test.js
|
||||
packages/editor/CodeMirror/editorCommands/insertLineAfter.js
|
||||
packages/editor/CodeMirror/editorCommands/jumpToHash.test.js
|
||||
packages/editor/CodeMirror/editorCommands/jumpToHash.js
|
||||
packages/editor/CodeMirror/editorCommands/sortSelectedLines.test.js
|
||||
packages/editor/CodeMirror/editorCommands/sortSelectedLines.js
|
||||
packages/editor/CodeMirror/editorCommands/supportsCommand.js
|
||||
@@ -912,6 +944,8 @@ packages/editor/CodeMirror/markdown/computeSelectionFormatting.test.js
|
||||
packages/editor/CodeMirror/markdown/computeSelectionFormatting.js
|
||||
packages/editor/CodeMirror/markdown/decoratorExtension.test.js
|
||||
packages/editor/CodeMirror/markdown/decoratorExtension.js
|
||||
packages/editor/CodeMirror/markdown/insertNewlineContinueMarkup.test.js
|
||||
packages/editor/CodeMirror/markdown/insertNewlineContinueMarkup.js
|
||||
packages/editor/CodeMirror/markdown/markdownCommands.bulletedVsChecklist.test.js
|
||||
packages/editor/CodeMirror/markdown/markdownCommands.test.js
|
||||
packages/editor/CodeMirror/markdown/markdownCommands.toggleList.test.js
|
||||
@@ -975,6 +1009,8 @@ packages/fork-htmlparser2/src/__tests__/events.js
|
||||
packages/fork-htmlparser2/src/__tests__/stream.js
|
||||
packages/fork-htmlparser2/src/index.spec.js
|
||||
packages/fork-htmlparser2/src/index.js
|
||||
packages/fork-uslug/lib/uslug.test.js
|
||||
packages/fork-uslug/lib/uslug.js
|
||||
packages/generator-joplin/generators/app/templates/api/index.js
|
||||
packages/generator-joplin/generators/app/templates/api/noteListType.js
|
||||
packages/generator-joplin/generators/app/templates/api/types.js
|
||||
@@ -1026,6 +1062,7 @@ packages/lib/commands/renderMarkup.test.js
|
||||
packages/lib/commands/renderMarkup.js
|
||||
packages/lib/commands/showEditorPlugin.js
|
||||
packages/lib/commands/synchronize.js
|
||||
packages/lib/commands/toggleAllFolders.test.js
|
||||
packages/lib/commands/toggleAllFolders.js
|
||||
packages/lib/commands/toggleEditorPlugin.js
|
||||
packages/lib/components/EncryptionConfigScreen/utils.test.js
|
||||
@@ -1063,6 +1100,8 @@ packages/lib/fs-driver-base.js
|
||||
packages/lib/fs-driver-node.js
|
||||
packages/lib/fsDriver.test.js
|
||||
packages/lib/geolocation-node.js
|
||||
packages/lib/getAppName.test.js
|
||||
packages/lib/getAppName.js
|
||||
packages/lib/hooks/useAsyncEffect.js
|
||||
packages/lib/hooks/useElementSize.js
|
||||
packages/lib/hooks/useEventListener.js
|
||||
@@ -1123,6 +1162,7 @@ packages/lib/models/settings/settingValidations.js
|
||||
packages/lib/models/settings/types.js
|
||||
packages/lib/models/utils/areAllFoldersCollapsed.test.js
|
||||
packages/lib/models/utils/areAllFoldersCollapsed.js
|
||||
packages/lib/models/utils/getCanBeCollapsedFolderIds.test.js
|
||||
packages/lib/models/utils/getCanBeCollapsedFolderIds.js
|
||||
packages/lib/models/utils/getCollator.js
|
||||
packages/lib/models/utils/getConflictFolderId.js
|
||||
@@ -1595,6 +1635,7 @@ packages/tools/release-electron.js
|
||||
packages/tools/release-ios.js
|
||||
packages/tools/release-plugin-repo-cli.js
|
||||
packages/tools/release-server.js
|
||||
packages/tools/saveClaConsentRecords.js
|
||||
packages/tools/setupNewRelease.js
|
||||
packages/tools/spellcheck.js
|
||||
packages/tools/tagServerLatest.js
|
||||
|
@@ -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,
|
||||
},
|
||||
},
|
||||
|
4
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -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
|
||||
|
34
.github/scripts/publish_docker_manifest.sh
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
#!/bin/bash
|
||||
|
||||
VERSION=$(echo "$GIT_TAG_NAME" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
|
||||
|
||||
echo "GIT_TAG_NAME=$GIT_TAG_NAME"
|
||||
echo "VERSION=$VERSION"
|
||||
echo "SERVER_TAG_PREFIX=$SERVER_TAG_PREFIX"
|
||||
echo "SERVER_REPOSITORY=$SERVER_REPOSITORY"
|
||||
|
||||
# Check if it's a server release, otherwise exit
|
||||
if [[ $GIT_TAG_NAME != $SERVER_TAG_PREFIX-* ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
docker manifest inspect $SERVER_REPOSITORY:arm64-$VERSION > /dev/null 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Image $SERVER_REPOSITORY:arm64-$VERSION does not exist on the remote registry."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
docker manifest inspect $SERVER_REPOSITORY:amd64-$VERSION > /dev/null 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Image $SERVER_REPOSITORY:amd64-$VERSION does not exist on the remote registry."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
docker manifest create $SERVER_REPOSITORY:$VERSION \
|
||||
$SERVER_REPOSITORY:arm64-$VERSION \
|
||||
$SERVER_REPOSITORY:amd64-$VERSION
|
||||
|
||||
docker manifest annotate $SERVER_REPOSITORY:$VERSION $SERVER_REPOSITORY:arm64-$VERSION --arch arm64
|
||||
docker manifest annotate $SERVER_REPOSITORY:$VERSION $SERVER_REPOSITORY:amd64-$VERSION --arch amd64
|
||||
|
||||
docker manifest push $SERVER_REPOSITORY:$VERSION
|
28
.github/scripts/run_ci.sh
vendored
@@ -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..."
|
||||
|
||||
|
2
.github/workflows/cla.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
# the below token should have repo scope and must be manually added by you in the repository's secret
|
||||
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
with:
|
||||
path-to-signatures: 'readme/cla_signatures.json'
|
||||
path-to-signatures: 'readme/cla/signatures.json'
|
||||
path-to-document: 'https://github.com/laurent22/joplin/blob/dev/readme/cla.md' # e.g. a CLA or a DCO document
|
||||
# branch should not be protected
|
||||
branch: 'cla_signatures'
|
||||
|
107
.github/workflows/github-actions-main.yml
vendored
@@ -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
|
||||
|
72
.github/workflows/shared/setup-build-environment/action.yml
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
name: 'Setup build environment'
|
||||
description: 'Install Joplin build dependencies'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
# Trying to fix random networking issues on Windows
|
||||
# https://github.com/actions/runner-images/issues/1187#issuecomment-686735760
|
||||
- name: Disable TCP/UDP offload on Windows
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: Disable-NetAdapterChecksumOffload -Name * -TcpIPv4 -UdpIPv4 -TcpIPv6 -UdpIPv6
|
||||
|
||||
- name: Disable TCP/UDP offload on Linux
|
||||
if: runner.os == 'Linux'
|
||||
shell: bash
|
||||
run: sudo ethtool -K eth0 tx off rx off
|
||||
|
||||
- name: Disable TCP/UDP offload on macOS
|
||||
if: runner.os == 'macOS'
|
||||
shell: bash
|
||||
run: |
|
||||
sudo sysctl -w net.link.generic.system.hwcksum_tx=0
|
||||
sudo sysctl -w net.link.generic.system.hwcksum_rx=0
|
||||
|
||||
# Silence apt-get update errors (for example when a module doesn't
|
||||
# exist) since otherwise it will make the whole build fails, even though
|
||||
# it might work without update. libsecret-1-dev is required for keytar -
|
||||
# https://github.com/atom/node-keytar
|
||||
|
||||
- name: Install Linux dependencies
|
||||
if: runner.os == 'Linux'
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get update || true
|
||||
sudo apt-get install -y gettext
|
||||
sudo apt-get install -y libsecret-1-dev
|
||||
sudo apt-get install -y translate-toolkit
|
||||
sudo apt-get install -y rsync
|
||||
# Provides a virtual display on Linux. Used for Playwright integration
|
||||
# testing.
|
||||
sudo apt-get install -y xvfb
|
||||
|
||||
- name: Install macOs dependencies
|
||||
if: runner.os == 'macOS'
|
||||
shell: bash
|
||||
run: |
|
||||
# Required for building the canvas package
|
||||
brew install pango
|
||||
|
||||
- uses: olegtarasov/get-tag@v2.1.3
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
# We need to pin the version to 18.15, because 18.16+ fails with this error:
|
||||
# https://github.com/facebook/react-native/issues/36440
|
||||
node-version: '18.15.0'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install Yarn
|
||||
shell: bash
|
||||
run: |
|
||||
# https://yarnpkg.com/getting-started/install
|
||||
corepack enable
|
||||
|
||||
# macos-latest ships with Python 3.12 by default, but this removes a
|
||||
# utility that's used by electron-builder (distutils) so we need to pin
|
||||
# Python to an earlier version.
|
||||
# Fixes error `ModuleNotFoundError: No module named 'distutils'`
|
||||
# Ref: https://github.com/nodejs/node-gyp/issues/2869
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
30
.github/workflows/ui-tests.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: Joplin UI tests
|
||||
on: [push, pull_request]
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
Main:
|
||||
# Don't run on forks
|
||||
if: github.repository == 'laurent22/joplin'
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-13, ubuntu-22.04, windows-2025]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup build environment
|
||||
uses: ./.github/workflows/shared/setup-build-environment
|
||||
- name: Build
|
||||
run: yarn install
|
||||
- name: Run UI tests
|
||||
shell: bash
|
||||
run: |
|
||||
cd ${GITHUB_WORKSPACE}/packages/app-desktop/
|
||||
bash ./integration-tests/run-ci.sh
|
||||
# See https://playwright.dev/docs/ci-intro#setting-up-github-actions
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: playwright-report-${{ matrix.os }}
|
||||
path: packages/app-desktop/playwright-report/
|
||||
retention-days: 7
|
52
.gitignore
vendored
@@ -134,7 +134,9 @@ packages/app-desktop/commands/exportNotes.js
|
||||
packages/app-desktop/commands/focusElement.js
|
||||
packages/app-desktop/commands/index.js
|
||||
packages/app-desktop/commands/openNoteInNewWindow.js
|
||||
packages/app-desktop/commands/openPrimaryAppInstance.js
|
||||
packages/app-desktop/commands/openProfileDirectory.js
|
||||
packages/app-desktop/commands/openSecondaryAppInstance.js
|
||||
packages/app-desktop/commands/replaceMisspelling.js
|
||||
packages/app-desktop/commands/restoreNoteRevision.js
|
||||
packages/app-desktop/commands/startExternalEditing.js
|
||||
@@ -181,6 +183,7 @@ packages/app-desktop/gui/InlineCombobox.js
|
||||
packages/app-desktop/gui/ItemList.js
|
||||
packages/app-desktop/gui/JoplinCloudConfigScreen.js
|
||||
packages/app-desktop/gui/JoplinCloudLoginScreen.js
|
||||
packages/app-desktop/gui/JoplinCloudSignUpCallToAction.js
|
||||
packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.js
|
||||
packages/app-desktop/gui/KeymapConfig/ShortcutRecorder.js
|
||||
packages/app-desktop/gui/KeymapConfig/styles/index.js
|
||||
@@ -239,6 +242,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useKeyboardRefocusHan
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useLinkTooltips.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useTabIndenter.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useTextPatternsLookup.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useWebViewApi.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteEditor.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.js
|
||||
@@ -247,6 +251,7 @@ packages/app-desktop/gui/NoteEditor/WarningBanner/BannerContent.js
|
||||
packages/app-desktop/gui/NoteEditor/WarningBanner/WarningBanner.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteBody.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteTitle.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteViewer.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/focusElementToolbar.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/index.js
|
||||
packages/app-desktop/gui/NoteEditor/commands/pasteAsText.js
|
||||
@@ -328,13 +333,16 @@ packages/app-desktop/gui/NoteSearchBar.js
|
||||
packages/app-desktop/gui/NoteStatusBar.js
|
||||
packages/app-desktop/gui/NoteTextViewer.js
|
||||
packages/app-desktop/gui/NoteToolbar/NoteToolbar.js
|
||||
packages/app-desktop/gui/NotyfContext.js
|
||||
packages/app-desktop/gui/OneDriveLoginScreen.js
|
||||
packages/app-desktop/gui/PasswordInput/LabelledPasswordInput.js
|
||||
packages/app-desktop/gui/PasswordInput/PasswordInput.js
|
||||
packages/app-desktop/gui/PasswordInput/types.js
|
||||
packages/app-desktop/gui/PdfViewer.js
|
||||
packages/app-desktop/gui/PluginNotification/PluginNotification.js
|
||||
packages/app-desktop/gui/PopupNotification/NotificationItem.js
|
||||
packages/app-desktop/gui/PopupNotification/PopupNotificationList.js
|
||||
packages/app-desktop/gui/PopupNotification/PopupNotificationProvider.js
|
||||
packages/app-desktop/gui/PopupNotification/types.js
|
||||
packages/app-desktop/gui/PromptDialog.js
|
||||
packages/app-desktop/gui/ResizableLayout/LayoutItemContainer.js
|
||||
packages/app-desktop/gui/ResizableLayout/MoveButtons.js
|
||||
@@ -396,6 +404,7 @@ packages/app-desktop/gui/ToolbarBase.js
|
||||
packages/app-desktop/gui/ToolbarButton/ToolbarButton.js
|
||||
packages/app-desktop/gui/ToolbarSpace.js
|
||||
packages/app-desktop/gui/TrashNotification/TrashNotification.js
|
||||
packages/app-desktop/gui/TrashNotification/TrashNotificationMessage.js
|
||||
packages/app-desktop/gui/UpdateNotification/UpdateNotification.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/AppDialogs.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/ModalMessageOverlay.js
|
||||
@@ -500,8 +509,8 @@ packages/app-desktop/integration-tests/simpleBackup.spec.js
|
||||
packages/app-desktop/integration-tests/util/activateMainMenuItem.js
|
||||
packages/app-desktop/integration-tests/util/createStartupArgs.js
|
||||
packages/app-desktop/integration-tests/util/extendedExpect.js
|
||||
packages/app-desktop/integration-tests/util/firstNonDevToolsWindow.js
|
||||
packages/app-desktop/integration-tests/util/getImageSourceSize.js
|
||||
packages/app-desktop/integration-tests/util/getMainWindow.js
|
||||
packages/app-desktop/integration-tests/util/setDarkMode.js
|
||||
packages/app-desktop/integration-tests/util/setFilePickerResponse.js
|
||||
packages/app-desktop/integration-tests/util/setMessageBoxResponse.js
|
||||
@@ -522,13 +531,16 @@ packages/app-desktop/services/plugins/UserWebview.js
|
||||
packages/app-desktop/services/plugins/UserWebviewDialog.js
|
||||
packages/app-desktop/services/plugins/UserWebviewDialogButtonBar.js
|
||||
packages/app-desktop/services/plugins/hooks/useContentSize.js
|
||||
packages/app-desktop/services/plugins/hooks/useFormData.js
|
||||
packages/app-desktop/services/plugins/hooks/useHtmlLoader.js
|
||||
packages/app-desktop/services/plugins/hooks/useMessageHandler.js
|
||||
packages/app-desktop/services/plugins/hooks/useScriptLoader.js
|
||||
packages/app-desktop/services/plugins/hooks/useSubmitHandler.js
|
||||
packages/app-desktop/services/plugins/hooks/useThemeCss.test.js
|
||||
packages/app-desktop/services/plugins/hooks/useThemeCss.js
|
||||
packages/app-desktop/services/plugins/hooks/useViewIsReady.js
|
||||
packages/app-desktop/services/plugins/hooks/useWebviewToPluginMessages.js
|
||||
packages/app-desktop/services/plugins/types.js
|
||||
packages/app-desktop/services/restart.js
|
||||
packages/app-desktop/services/sortOrder/PerFolderSortOrderService.test.js
|
||||
packages/app-desktop/services/sortOrder/PerFolderSortOrderService.js
|
||||
@@ -556,6 +568,7 @@ packages/app-desktop/utils/restartInSafeModeFromMain.test.js
|
||||
packages/app-desktop/utils/restartInSafeModeFromMain.js
|
||||
packages/app-desktop/utils/window/types.js
|
||||
packages/app-mobile/PluginAssetsLoader.js
|
||||
packages/app-mobile/commands/dismissPluginPanels.js
|
||||
packages/app-mobile/commands/index.js
|
||||
packages/app-mobile/commands/newNote.test.js
|
||||
packages/app-mobile/commands/newNote.js
|
||||
@@ -565,6 +578,7 @@ packages/app-mobile/commands/scrollToHash.js
|
||||
packages/app-mobile/commands/util/goToNote.js
|
||||
packages/app-mobile/commands/util/showResource.js
|
||||
packages/app-mobile/components/BetaChip.js
|
||||
packages/app-mobile/components/BottomDrawer.js
|
||||
packages/app-mobile/components/CameraView/ActionButtons.js
|
||||
packages/app-mobile/components/CameraView/Camera/index.jest.js
|
||||
packages/app-mobile/components/CameraView/Camera/index.js
|
||||
@@ -658,15 +672,22 @@ packages/app-mobile/components/SelectDateTimeDialog.js
|
||||
packages/app-mobile/components/SideMenu.js
|
||||
packages/app-mobile/components/SideMenuContentNote.js
|
||||
packages/app-mobile/components/TextInput.js
|
||||
packages/app-mobile/components/ToggleSpaceButton.js
|
||||
packages/app-mobile/components/accessibility/AccessibleModalMenu.js
|
||||
packages/app-mobile/components/accessibility/AccessibleView.test.js
|
||||
packages/app-mobile/components/accessibility/AccessibleView.js
|
||||
packages/app-mobile/components/accessibility/FocusControl/AutoFocusProvider.js
|
||||
packages/app-mobile/components/accessibility/FocusControl/FocusControl.js
|
||||
packages/app-mobile/components/accessibility/FocusControl/FocusControlProvider.js
|
||||
packages/app-mobile/components/accessibility/FocusControl/MainAppContent.js
|
||||
packages/app-mobile/components/accessibility/FocusControl/ModalWrapper.js
|
||||
packages/app-mobile/components/accessibility/FocusControl/types.js
|
||||
packages/app-mobile/components/app-nav.js
|
||||
packages/app-mobile/components/base-screen.js
|
||||
packages/app-mobile/components/biometrics/BiometricPopup.js
|
||||
packages/app-mobile/components/biometrics/biometricAuthenticate.js
|
||||
packages/app-mobile/components/biometrics/sensorInfo.js
|
||||
packages/app-mobile/components/buttons/FloatingActionButton.js
|
||||
packages/app-mobile/components/buttons/LabelledIconButton.js
|
||||
packages/app-mobile/components/buttons/MultiTouchableOpacity.js
|
||||
packages/app-mobile/components/buttons/TextButton.js
|
||||
packages/app-mobile/components/buttons/index.js
|
||||
packages/app-mobile/components/getResponsiveValue.test.js
|
||||
@@ -713,8 +734,11 @@ packages/app-mobile/components/screens/ConfigScreen/SectionSelector/SectionTab.j
|
||||
packages/app-mobile/components/screens/ConfigScreen/SectionSelector/index.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingComponent.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingItem.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingTextInput.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingsButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingsToggle.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/ValidatedIntegerInput.test.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/ValidatedIntegerInput.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/EnablePluginSupportPage.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/InstalledPluginBox.js
|
||||
@@ -754,7 +778,9 @@ packages/app-mobile/components/screens/Note/commands/setTags.js
|
||||
packages/app-mobile/components/screens/Note/commands/toggleVisiblePanes.js
|
||||
packages/app-mobile/components/screens/Note/types.js
|
||||
packages/app-mobile/components/screens/NoteTagsDialog.js
|
||||
packages/app-mobile/components/screens/Notes.js
|
||||
packages/app-mobile/components/screens/Notes/NewNoteButton.test.js
|
||||
packages/app-mobile/components/screens/Notes/NewNoteButton.js
|
||||
packages/app-mobile/components/screens/Notes/Notes.js
|
||||
packages/app-mobile/components/screens/SearchScreen/SearchResults.js
|
||||
packages/app-mobile/components/screens/SearchScreen/index.js
|
||||
packages/app-mobile/components/screens/ShareManager/AcceptedShareItem.js
|
||||
@@ -809,6 +835,7 @@ packages/app-mobile/utils/createRootStyle.js
|
||||
packages/app-mobile/utils/database-driver-react-native.js
|
||||
packages/app-mobile/utils/database-driver-react-native.web.js
|
||||
packages/app-mobile/utils/debounce.js
|
||||
packages/app-mobile/utils/focusView.js
|
||||
packages/app-mobile/utils/fs-driver/constants.js
|
||||
packages/app-mobile/utils/fs-driver/fs-driver-rn.js
|
||||
packages/app-mobile/utils/fs-driver/fs-driver-rn.web.js
|
||||
@@ -821,9 +848,10 @@ packages/app-mobile/utils/fs-driver/testUtil/createFilesFromPathRecord.js
|
||||
packages/app-mobile/utils/fs-driver/testUtil/verifyDirectoryMatches.js
|
||||
packages/app-mobile/utils/getPackageInfo.js
|
||||
packages/app-mobile/utils/getVersionInfoText.js
|
||||
packages/app-mobile/utils/hooks/useKeyboardVisible.js
|
||||
packages/app-mobile/utils/hooks/useKeyboardState.js
|
||||
packages/app-mobile/utils/hooks/useOnLongPressProps.js
|
||||
packages/app-mobile/utils/hooks/useReduceMotionEnabled.js
|
||||
packages/app-mobile/utils/hooks/useSafeAreaPadding.js
|
||||
packages/app-mobile/utils/image/fileToImage.web.js
|
||||
packages/app-mobile/utils/image/getImageDimensions.js
|
||||
packages/app-mobile/utils/image/resizeImage.js
|
||||
@@ -832,6 +860,7 @@ packages/app-mobile/utils/injectedJs.js
|
||||
packages/app-mobile/utils/ipc/RNToWebViewMessenger.js
|
||||
packages/app-mobile/utils/ipc/WebViewToRNMessenger.js
|
||||
packages/app-mobile/utils/lockToSingleInstance.js
|
||||
packages/app-mobile/utils/makeShowMessageBox.test.js
|
||||
packages/app-mobile/utils/makeShowMessageBox.js
|
||||
packages/app-mobile/utils/pickDocument.js
|
||||
packages/app-mobile/utils/polyfills/bufferPolyfill.js
|
||||
@@ -872,6 +901,8 @@ packages/editor/CodeMirror/editorCommands/duplicateLine.js
|
||||
packages/editor/CodeMirror/editorCommands/editorCommands.js
|
||||
packages/editor/CodeMirror/editorCommands/insertLineAfter.test.js
|
||||
packages/editor/CodeMirror/editorCommands/insertLineAfter.js
|
||||
packages/editor/CodeMirror/editorCommands/jumpToHash.test.js
|
||||
packages/editor/CodeMirror/editorCommands/jumpToHash.js
|
||||
packages/editor/CodeMirror/editorCommands/sortSelectedLines.test.js
|
||||
packages/editor/CodeMirror/editorCommands/sortSelectedLines.js
|
||||
packages/editor/CodeMirror/editorCommands/supportsCommand.js
|
||||
@@ -887,6 +918,8 @@ packages/editor/CodeMirror/markdown/computeSelectionFormatting.test.js
|
||||
packages/editor/CodeMirror/markdown/computeSelectionFormatting.js
|
||||
packages/editor/CodeMirror/markdown/decoratorExtension.test.js
|
||||
packages/editor/CodeMirror/markdown/decoratorExtension.js
|
||||
packages/editor/CodeMirror/markdown/insertNewlineContinueMarkup.test.js
|
||||
packages/editor/CodeMirror/markdown/insertNewlineContinueMarkup.js
|
||||
packages/editor/CodeMirror/markdown/markdownCommands.bulletedVsChecklist.test.js
|
||||
packages/editor/CodeMirror/markdown/markdownCommands.test.js
|
||||
packages/editor/CodeMirror/markdown/markdownCommands.toggleList.test.js
|
||||
@@ -950,6 +983,8 @@ packages/fork-htmlparser2/src/__tests__/events.js
|
||||
packages/fork-htmlparser2/src/__tests__/stream.js
|
||||
packages/fork-htmlparser2/src/index.spec.js
|
||||
packages/fork-htmlparser2/src/index.js
|
||||
packages/fork-uslug/lib/uslug.test.js
|
||||
packages/fork-uslug/lib/uslug.js
|
||||
packages/generator-joplin/generators/app/templates/api/index.js
|
||||
packages/generator-joplin/generators/app/templates/api/noteListType.js
|
||||
packages/generator-joplin/generators/app/templates/api/types.js
|
||||
@@ -1001,6 +1036,7 @@ packages/lib/commands/renderMarkup.test.js
|
||||
packages/lib/commands/renderMarkup.js
|
||||
packages/lib/commands/showEditorPlugin.js
|
||||
packages/lib/commands/synchronize.js
|
||||
packages/lib/commands/toggleAllFolders.test.js
|
||||
packages/lib/commands/toggleAllFolders.js
|
||||
packages/lib/commands/toggleEditorPlugin.js
|
||||
packages/lib/components/EncryptionConfigScreen/utils.test.js
|
||||
@@ -1038,6 +1074,8 @@ packages/lib/fs-driver-base.js
|
||||
packages/lib/fs-driver-node.js
|
||||
packages/lib/fsDriver.test.js
|
||||
packages/lib/geolocation-node.js
|
||||
packages/lib/getAppName.test.js
|
||||
packages/lib/getAppName.js
|
||||
packages/lib/hooks/useAsyncEffect.js
|
||||
packages/lib/hooks/useElementSize.js
|
||||
packages/lib/hooks/useEventListener.js
|
||||
@@ -1098,6 +1136,7 @@ packages/lib/models/settings/settingValidations.js
|
||||
packages/lib/models/settings/types.js
|
||||
packages/lib/models/utils/areAllFoldersCollapsed.test.js
|
||||
packages/lib/models/utils/areAllFoldersCollapsed.js
|
||||
packages/lib/models/utils/getCanBeCollapsedFolderIds.test.js
|
||||
packages/lib/models/utils/getCanBeCollapsedFolderIds.js
|
||||
packages/lib/models/utils/getCollator.js
|
||||
packages/lib/models/utils/getConflictFolderId.js
|
||||
@@ -1570,6 +1609,7 @@ packages/tools/release-electron.js
|
||||
packages/tools/release-ios.js
|
||||
packages/tools/release-plugin-repo-cli.js
|
||||
packages/tools/release-server.js
|
||||
packages/tools/saveClaConsentRecords.js
|
||||
packages/tools/setupNewRelease.js
|
||||
packages/tools/spellcheck.js
|
||||
packages/tools/tagServerLatest.js
|
||||
|
1
.husky/pre-commit
Normal file
@@ -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
Assets/JoplinLetterBlue.svg
Normal 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) {
|
||||
|
BIN
Assets/WebsiteAssets/images/news/20250428-collapse-all.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
Assets/WebsiteAssets/images/news/20250428-link-notes.png
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
Assets/WebsiteAssets/images/news/20250428-new-note-menu.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
Assets/WebsiteAssets/images/sponsors/EssayWriterPro.png
Normal file
After Width: | Height: | Size: 56 KiB |
BIN
Assets/WebsiteAssets/images/sponsors/TiktokRise.jpg
Normal file
After Width: | Height: | Size: 71 KiB |
@@ -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>');
|
||||
|
@@ -1,4 +1,43 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Joplin]]></title><description><![CDATA[Joplin, the open source note-taking application]]></description><link>https://joplinapp.org</link><generator>RSS for Node</generator><lastBuildDate>Tue, 14 Jan 2025 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Tue, 14 Jan 2025 00:00:00 GMT</pubDate><item><title><![CDATA[What's new in Joplin 3.2]]></title><description><![CDATA[<h2>Import OneNote Archives<a name="import-onenote-archives" href="#import-onenote-archives" class="heading-anchor">🔗</a></h2>
|
||||
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Joplin]]></title><description><![CDATA[Joplin, the open source note-taking application]]></description><link>https://joplinapp.org</link><generator>RSS for Node</generator><lastBuildDate>Mon, 28 Apr 2025 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Mon, 28 Apr 2025 00:00:00 GMT</pubDate><item><title><![CDATA[What's new in Joplin 3.3]]></title><description><![CDATA[<h2>Desktop application<a name="desktop-application" href="#desktop-application" class="heading-anchor">🔗</a></h2>
|
||||
<h3>Accessibility improvements<a name="accessibility-improvements" href="#accessibility-improvements" class="heading-anchor">🔗</a></h3>
|
||||
<p>The Joplin 3.3 release introduces significant accessibility enhancements designed to make the application more inclusive and user-friendly. Users can now benefit from improved keyboard navigation, thanks to newly added shortcuts and clearer labels that streamline interaction across the interface. We've also added a "go to viewer" menu item that moves focus from the note editor to the note viewer. Focus is moved to the location in the viewer corresponding to the location of the cursor in the editor.</p>
|
||||
<p>Screen reader support has been bolstered, ensuring elements like the note list and sidebar are easier to toggle and interact with. These updates make the application more usable for individuals relying on assistive technologies.</p>
|
||||
<p>Additionally, visual improvements, including increased contrast for UI components such as URLs, sidebars, and scrollbars, enhance readability for users with visual impairments. This focus on clarity ensures a more comfortable user experience.</p>
|
||||
<p>The Rich Text Editor has also received accessibility-focused updates, allowing for more seamless interaction with code blocks using either a keyboard or touchscreen. These refinements highlight Joplin's dedication to creating an accessible and equitable experience for all users.</p>
|
||||
<h3>Button to collapse and expand all notebooks<a name="button-to-collapse-and-expand-all-notebooks" href="#button-to-collapse-and-expand-all-notebooks" class="heading-anchor">🔗</a></h3>
|
||||
<p>Joplin 3.3 introduces a convenient "Collapse/Expand All" button for notebooks, allowing you to quickly adjust the visibility of your notebook hierarchy. This feature simplifies navigation by letting you expand all notebooks to locate specific notes or collapse them for a tidier interface!</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20250428-collapse-all.png" alt=""></p>
|
||||
<h3>New dialog to select a note and link to it<a name="new-dialog-to-select-a-note-and-link-to-it" href="#new-dialog-to-select-a-note-and-link-to-it" class="heading-anchor">🔗</a></h3>
|
||||
<p>A new dialog has been added to make linking to notes easier. By pressing Shift+Option+L on macOS or Shift+Alt+L on Windows and Linux, you can quickly bring up a search box. Simply type the name of the note you want to link to, and the link will be added to your current note!</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20250428-link-notes.png" alt=""></p>
|
||||
<h3>Support for multiple instances of Joplin<a name="support-for-multiple-instances-of-joplin" href="#support-for-multiple-instances-of-joplin" class="heading-anchor">🔗</a></h3>
|
||||
<p>Joplin Desktop now <a href="https://joplinapp.org/help/apps/multiple_instances">lets you run multiple instances at once</a>! This means you can easily separate your work and personal notes, or use Joplin across different virtual desktops. Each instance runs independently with its own settings, plugins, and notes, so nothing overlaps. You can open a second instance through in menu <strong>File => Open secondary app instance...</strong>, and customise it however you like!</p>
|
||||
<h3>Improved Rich Text editor<a name="improved-rich-text-editor" href="#improved-rich-text-editor" class="heading-anchor">🔗</a></h3>
|
||||
<p>This version includes multiple improvements and bug fixes to the Rich Text Editor. In particular it has now been upgraded to TinyMCE v6, and it adds support for Markdown auto-replacement. For example, typing <code>==highlight==</code> creates highlighted text. Auto-replacement can be disabled in settings.</p>
|
||||
<h2>Mobile application<a name="mobile-application" href="#mobile-application" class="heading-anchor">🔗</a></h2>
|
||||
<h3>Accessibility improvements<a name="accessibility-improvements-1" href="#accessibility-improvements-1" class="heading-anchor">🔗</a></h3>
|
||||
<p>Like the desktop app, the mobile app includes several accessibility improvements designed to enhance the user experience for those relying on assistive technologies. Focus handling has been improved in the note actions menu and modal dialogs, ensuring smoother navigation for screen reader users. Additionally, the default modal close button is now accessible, and issues with invisible buttons being focusable have been resolved.</p>
|
||||
<p>Other updates include better contrast for faded URLs in the Markdown editor, making them more readable for users with visual impairments. The encryption configuration screen has been improved for better accessibility, and screen reader support has been added for creating and editing profiles. These changes collectively improve navigation, readability, and usability for all users.</p>
|
||||
<h3>Support attaching audio recordings<a name="support-attaching-audio-recordings" href="#support-attaching-audio-recordings" class="heading-anchor">🔗</a></h3>
|
||||
<p>You can now easily capture and include audio recordings directly within your notes. To use this feature, open the kebab menu and select "Record audio." You can then to record your audio input. When finished, the app automatically attaches the recorded audio file to the note. This functionality is perfect for capturing ideas, reminders, or supplementary audio information in a quick and intuitive way.</p>
|
||||
<h3>Improved voice typing feature<a name="improved-voice-typing-feature" href="#improved-voice-typing-feature" class="heading-anchor">🔗</a></h3>
|
||||
<p>The voice typing feature in Android has undergone a significant improvement, making it more accurate. Previously introduced in version 2.11, the feature allowed you to transcribe speech into text but lacked punctuation and struggled with accuracy in certain scenarios. The revamped version now leverages Whisper, an advanced AI model, to deliver improved recognition, including automatic punctuation and paragraph formatting.</p>
|
||||
<p>Despite its advancements, the feature is currently in beta due to the demanding nature of the required AI models. While it defaults to a smaller, less accurate model to accommodate older devices, you can <a href="https://github.com/joplin/voice-typing-models/releases">manually download and switch to more accurate models</a> for better performance.</p>
|
||||
<p>As previously, the feature remains entirely offline, ensuring that private recordings are never sent to third-party servers, a distinct privacy advantage over similar solutions from Google or Apple. Additionally it means you can use the feature even when you don't have an internet connection. We plan to refine this feature further, eventually defaulting to the more accurate model as stability improves.</p>
|
||||
<h3>Improved new note menu<a name="improved-new-note-menu" href="#improved-new-note-menu" class="heading-anchor">🔗</a></h3>
|
||||
<p>The redesigned "New Note" menu takes note creation to a whole new level of flexibility and convenience. Previously, this menu only offered options to create a new note or a new to-do. With the latest update, you now have quick access to a variety of shortcuts, enabling you to attach files, record audio, capture photo notes, or even create drawings directly from the menu. This intuitive redesign makes it easier to choose the format that best suits your needs.</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20250428-new-note-menu.png" alt=""></p>
|
||||
<h2>Security and bug fixes<a name="security-and-bug-fixes" href="#security-and-bug-fixes" class="heading-anchor">🔗</a></h2>
|
||||
<p>As always, we continue to reinforce the security of Joplin. This version implements a robust content security policy for the Rich Text Editor, safeguarding the application against malicious content.</p>
|
||||
<p>In total, this version delivers 39 bug fixes on desktop and 17 on mobile, enhancing both the security and stability of the application.</p>
|
||||
<h2>Full changelogs<a name="full-changelogs" href="#full-changelogs" class="heading-anchor">🔗</a></h2>
|
||||
<p>This is just an overview of the main features. The full changelogs are available there:</p>
|
||||
<ul>
|
||||
<li>Desktop: <a href="https://joplinapp.org/help/about/changelog/desktop">https://joplinapp.org/help/about/changelog/desktop</a></li>
|
||||
<li>Android: <a href="https://joplinapp.org/help/about/changelog/android/">https://joplinapp.org/help/about/changelog/android/</a></li>
|
||||
<li>iOS: <a href="https://joplinapp.org/help/about/changelog/ios/">https://joplinapp.org/help/about/changelog/ios/</a></li>
|
||||
</ul>
|
||||
]]></description><link>https://joplinapp.org/news/20250428-release-3-3</link><guid isPermaLink="false">20250428-release-3-3</guid><pubDate>Mon, 28 Apr 2025 00:00:00 GMT</pubDate><twitter-text>What's new in Joplin 3.3</twitter-text></item><item><title><![CDATA[What's new in Joplin 3.2]]></title><description><![CDATA[<h2>Import OneNote Archives<a name="import-onenote-archives" href="#import-onenote-archives" class="heading-anchor">🔗</a></h2>
|
||||
<p>Joplin now supports importing OneNote archives, a significant step for users transitioning from OneNote. Microsoft has long made it challenging to leave OneNote, offering limited export options and complex formats that make it difficult for app developers to support it. Despite these hurdles, @pedr tackled these issues head-on, developing an import tool that simplifies the process. This addition makes Joplin a practical choice for those looking to move away from OneNote's ecosystem.</p>
|
||||
<p>To use this feature, select <strong>File</strong> => <strong>Import</strong> => <strong>ZIP - OneNote Notebook</strong></p>
|
||||
<h2>Multi-window support<a name="multi-window-support" href="#multi-window-support" class="heading-anchor">🔗</a></h2>
|
||||
@@ -413,10 +452,4 @@ sys 0m38.013s</p>
|
||||
<p>You will be part of a small team, so you will have an opportunity for a high-impact role, targeting hundreds of thousands of users.</p>
|
||||
<p>If you're interested please contact us at job-AT-joplin.cloud</p>
|
||||
<p>No agencies please.</p>
|
||||
]]></description><link>https://joplinapp.org/news/20221209-job</link><guid isPermaLink="false">20221209-job</guid><pubDate>Fri, 09 Dec 2022 00:00:00 GMT</pubDate><twitter-text>Joplin is hiring!</twitter-text></item><item><title><![CDATA[Modernising and securing Joplin, one package at a time]]></title><description><![CDATA[<p>If you watch the <a href="https://github.com/laurent22/joplin">Joplin source code repository</a>, you may have noticed a lot of Renovate pull requests lately. This <a href="https://www.mend.io/free-developer-tools/renovate/">Renovate tool</a> is a way to manage dependencies - it automatically finds what needs to be updated, then upgrade it to the latest version, and create a pull request. If all tests pass, we can then merge this pull request. So far we have merged 267 of these pull requests.</p>
|
||||
<p>Updating Joplin packages was long due. It is necessary so that we don't fall behind and end up using unsupported or deprecated packages. We also benefit from bug fixes and performance improvements. It is also important in terms of security, since recent package versions usually include various security fixes.</p>
|
||||
<p>We used to rely on a tool called "npm audit" to do this, however it no longer works on the Joplin codebase, and it was always risky to use it since it would update multiple packages in one command - so if something went wrong it was difficult to find the culprit.</p>
|
||||
<p>Renovate on the other hand upgrades packages one at a time, and run our test units to ensure everything is still working as expected. It also upgrades multiple instances of the same package across the monorepo, which is convenient to keep our code consistent. It also has a number of options to make our life easier, such as the ability to automatically merge a pull request for patch releases since this is usually safe (when a package is, for example upgraded from 1.0.1 to 1.0.3).</p>
|
||||
<p>Although Renovate automates the package upgrades it doesn't mean all upgrades are straightforward - our tests won't catch all issues, so the apps might end up being broken or cannot be compiled anymore. So there's manual work involved to get everything working after certain upgrades - for the most part this has been done and the apps appear to be stable so far.</p>
|
||||
<p>This will however be an important part of pre-release 2.10 (or should it be 3.0?) - we hope that everything works but we may need your support to try this version and report any glitch you may have found. As always pre-release regressions have the highest priority so we aim to fix them as quickly as possible.</p>
|
||||
]]></description><link>https://joplinapp.org/news/20221115-renovate</link><guid isPermaLink="false">20221115-renovate</guid><pubDate>Tue, 15 Nov 2022 00:00:00 GMT</pubDate><twitter-text>Modernising and securing Joplin, one package at a time</twitter-text></item></channel></rss>
|
||||
]]></description><link>https://joplinapp.org/news/20221209-job</link><guid isPermaLink="false">20221209-job</guid><pubDate>Fri, 09 Dec 2022 00:00:00 GMT</pubDate><twitter-text>Joplin is hiring!</twitter-text></item></channel></rss>
|
@@ -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>
|
||||
|
||||
|
@@ -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"
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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&mtm_kwd=joplinapp&mtm_source=joplinapp-webseite&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&mtm_kwd=joplinapp&mtm_source=joplinapp-webseite&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
|
||||
|
15
crowdin.yml
@@ -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
|
||||
|
@@ -25,7 +25,8 @@
|
||||
"version": "latest",
|
||||
"excluded_platforms": ["aarch64-darwin", "x86_64-darwin"],
|
||||
},
|
||||
"git": "latest",
|
||||
"git": "latest",
|
||||
"giflib": "latest",
|
||||
},
|
||||
"shell": {
|
||||
"init_hook": [
|
||||
|
14
package.json
@@ -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"
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import BaseCommand from './base-command';
|
||||
import app from './app';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { _, _n } from '@joplin/lib/locale';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import BaseModel, { DeleteOptions } from '@joplin/lib/BaseModel';
|
||||
import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||
@@ -31,13 +31,13 @@ class Command extends BaseCommand {
|
||||
|
||||
let ok = true;
|
||||
if (!force && notes.length > 1) {
|
||||
ok = await this.prompt(_('%d notes match this pattern. Delete them?', notes.length), { booleanAnswerDefault: 'n' });
|
||||
ok = await this.prompt(_n('%d note matches this pattern. Delete it?', '%d notes match this pattern. Delete them?', notes.length, notes.length), { booleanAnswerDefault: 'n' });
|
||||
}
|
||||
|
||||
const permanent = (args.options?.permanent === true) || notes.every(n => !!n.deleted_time);
|
||||
if (!force && permanent) {
|
||||
const message = (
|
||||
notes.length === 1 ? _('This will permanently delete the note "%s". Continue?', notes[0].title) : _('%d notes will be permanently deleted. Continue?', notes.length)
|
||||
_n('%d note will be permanently deleted. Continue?', '%d notes will be permanently deleted. Continue?', notes.length, notes.length)
|
||||
);
|
||||
ok = await this.prompt(message, { booleanAnswerDefault: 'n' });
|
||||
}
|
||||
|
@@ -35,7 +35,7 @@
|
||||
],
|
||||
"owner": "Laurent Cozic"
|
||||
},
|
||||
"version": "3.3.0",
|
||||
"version": "3.3.1",
|
||||
"bin": "./main.js",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
|
BIN
packages/app-cli/tests/ocr_samples/low_confidence_testing.png
Normal file
After Width: | Height: | Size: 384 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();
|
||||
});
|
@@ -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', () => {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import produce from 'immer';
|
||||
import { produce } from 'immer';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { defaultState, defaultWindowState, State, WindowState } from '@joplin/lib/reducer';
|
||||
import iterateItems from './gui/ResizableLayout/utils/iterateItems';
|
||||
|
@@ -92,7 +92,7 @@ class Application extends BaseApplication {
|
||||
public reducer(state: AppState = appDefaultState, action: any) {
|
||||
let newState = appReducer(state, action);
|
||||
newState = resourceEditWatcherReducer(newState, action);
|
||||
newState = super.reducer(newState, action);
|
||||
newState = super.reducer(newState, action) as AppState;
|
||||
return newState;
|
||||
}
|
||||
|
||||
@@ -456,9 +456,6 @@ class Application extends BaseApplication {
|
||||
bridge().openDevTools();
|
||||
}
|
||||
|
||||
bridge().electronApp().initializeCustomProtocolHandler(
|
||||
Logger.create('handleCustomProtocols'),
|
||||
);
|
||||
this.protocolHandler_ = bridge().electronApp().getCustomProtocolHandler();
|
||||
this.protocolHandler_.allowReadAccessToDirectory(__dirname); // App bundle directory
|
||||
this.protocolHandler_.allowReadAccessToDirectory(Setting.value('cacheDir'));
|
||||
@@ -617,10 +614,11 @@ class Application extends BaseApplication {
|
||||
clipperLogger.addTarget(TargetType.Console);
|
||||
|
||||
ClipperServer.instance().initialize(actionApi);
|
||||
ClipperServer.instance().setEnabled(!Setting.value('altInstanceId'));
|
||||
ClipperServer.instance().setLogger(clipperLogger);
|
||||
ClipperServer.instance().setDispatch(this.store().dispatch);
|
||||
|
||||
if (Setting.value('clipperServer.autoStart')) {
|
||||
if (ClipperServer.instance().enabled() && Setting.value('clipperServer.autoStart')) {
|
||||
void ClipperServer.instance().start();
|
||||
}
|
||||
|
||||
|
@@ -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_;
|
||||
}
|
||||
|
||||
|
@@ -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,
|
||||
|
19
packages/app-desktop/commands/openPrimaryAppInstance.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import bridge from '../services/bridge';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'openPrimaryAppInstance',
|
||||
label: () => _('Open primary app instance...'),
|
||||
};
|
||||
|
||||
export const runtime = (): CommandRuntime => {
|
||||
return {
|
||||
execute: async (_context: CommandContext) => {
|
||||
await bridge().launchMainAppInstance(Setting.value('env'));
|
||||
},
|
||||
|
||||
enabledCondition: 'isAltInstance',
|
||||
};
|
||||
};
|
19
packages/app-desktop/commands/openSecondaryAppInstance.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import bridge from '../services/bridge';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'openSecondaryAppInstance',
|
||||
label: () => _('Open secondary app instance...'),
|
||||
};
|
||||
|
||||
export const runtime = (): CommandRuntime => {
|
||||
return {
|
||||
execute: async (_context: CommandContext) => {
|
||||
await bridge().launchAltAppInstance(Setting.value('env'));
|
||||
},
|
||||
|
||||
enabledCondition: '!isAltInstance',
|
||||
};
|
||||
};
|
@@ -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}
|
||||
|
@@ -7,7 +7,7 @@ import SearchPlugins from './SearchPlugins';
|
||||
import PluginBox, { UpdateState } from './PluginBox';
|
||||
import Button, { ButtonLevel, ButtonSize } from '../../../Button/Button';
|
||||
import bridge from '../../../../services/bridge';
|
||||
import produce from 'immer';
|
||||
import { produce } from 'immer';
|
||||
import { OnChangeEvent } from '../../../lib/SearchInput/SearchInput';
|
||||
import { PluginItem, ItemEvent, OnPluginSettingChangeEvent } from '@joplin/lib/components/shared/config/plugins/types';
|
||||
import RepositoryApi, { InstallMode } from '@joplin/lib/services/plugins/RepositoryApi';
|
||||
|
@@ -10,6 +10,7 @@ import { reducer, defaultState, generateApplicationConfirmUrl, checkIfLoginWasSu
|
||||
import { AppState } from '../app.reducer';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import JoplinCloudSignUpCallToAction from './JoplinCloudSignUpCallToAction';
|
||||
|
||||
const logger = Logger.create('JoplinCloudLoginScreen');
|
||||
const { connect } = require('react-redux');
|
||||
@@ -105,6 +106,7 @@ const JoplinCloudScreenComponent = (props: Props) => {
|
||||
) : null}
|
||||
</p>
|
||||
{state.active === 'LINK_USED' ? <div id="loading-animation" /> : null}
|
||||
<JoplinCloudSignUpCallToAction />
|
||||
</div>
|
||||
<ButtonBar onCancelClick={() => props.dispatch({ type: 'NAV_BACK' })} />
|
||||
</div>
|
||||
|
20
packages/app-desktop/gui/JoplinCloudSignUpCallToAction.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import * as React from 'react';
|
||||
import bridge from '../services/bridge';
|
||||
|
||||
const JoplinCloudSignUpCallToAction = () => {
|
||||
|
||||
const onJoplinCloudSignUpClick = async () => {
|
||||
await bridge().openExternal('https://joplinapp.org/plans/');
|
||||
};
|
||||
|
||||
return <div className="joplin-cloud-sign-up">
|
||||
<a
|
||||
href="#"
|
||||
onClick={onJoplinCloudSignUpClick}
|
||||
>{_('Sign up to Joplin Cloud')}</a>
|
||||
</div>;
|
||||
|
||||
};
|
||||
|
||||
export default JoplinCloudSignUpCallToAction;
|
@@ -17,7 +17,7 @@ import { AppState } from '../app.reducer';
|
||||
import { saveLayout, loadLayout } from './ResizableLayout/utils/persist';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import shouldShowMissingPasswordWarning from '@joplin/lib/components/shared/config/shouldShowMissingPasswordWarning';
|
||||
import produce from 'immer';
|
||||
import { produce } from 'immer';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import bridge from '../services/bridge';
|
||||
import styled from 'styled-components';
|
||||
@@ -83,6 +83,7 @@ interface Props {
|
||||
notesColumns: NoteListColumns;
|
||||
showInvalidJoplinCloudCredential: boolean;
|
||||
toast: Toast;
|
||||
shouldSwitchToAppleSiliconVersion: boolean;
|
||||
}
|
||||
|
||||
interface ShareFolderDialogOptions {
|
||||
@@ -478,6 +479,10 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
});
|
||||
};
|
||||
|
||||
const onDisableSync = () => {
|
||||
Setting.setValue('sync.target', null);
|
||||
};
|
||||
|
||||
const onViewSyncSettingsScreen = () => {
|
||||
this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
@@ -488,6 +493,11 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
});
|
||||
};
|
||||
|
||||
const onDownloadAppleSiliconVersion = () => {
|
||||
// The website should redirect to the correct version
|
||||
shim.openUrl('https://joplinapp.org/download/');
|
||||
};
|
||||
|
||||
const onRestartAndUpgrade = async () => {
|
||||
Setting.setValue('sync.upgradeState', Setting.SYNC_UPGRADE_STATE_MUST_DO);
|
||||
await Setting.saveAll();
|
||||
@@ -570,11 +580,19 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
);
|
||||
} else if (this.props.mustUpgradeAppMessage) {
|
||||
msg = this.renderNotificationMessage(this.props.mustUpgradeAppMessage);
|
||||
} else if (this.props.shouldSwitchToAppleSiliconVersion) {
|
||||
msg = this.renderNotificationMessage(
|
||||
_('You are running the Intel version of Joplin on an Apple Silicon processor. Download the Apple Silicon one for better performance.'),
|
||||
_('Download it now'),
|
||||
onDownloadAppleSiliconVersion,
|
||||
);
|
||||
} else if (this.props.showInvalidJoplinCloudCredential) {
|
||||
msg = this.renderNotificationMessage(
|
||||
_('Your Joplin Cloud credentials are invalid, please login.'),
|
||||
_('Login to Joplin Cloud.'),
|
||||
onViewJoplinCloudLoginScreen,
|
||||
_('Disable synchronisation'),
|
||||
onDisableSync,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -605,7 +623,8 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
this.showShareInvitationNotification(props) ||
|
||||
this.props.needApiAuth ||
|
||||
!!this.props.mustUpgradeAppMessage ||
|
||||
props.showInvalidJoplinCloudCredential;
|
||||
props.showInvalidJoplinCloudCredential ||
|
||||
props.shouldSwitchToAppleSiliconVersion;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
@@ -785,7 +804,7 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
dispatch={this.props.dispatch as any}
|
||||
/>
|
||||
<UpdateNotification themeId={this.props.themeId} />
|
||||
<UpdateNotification />
|
||||
<PluginNotification
|
||||
themeId={this.props.themeId}
|
||||
toast={this.props.toast}
|
||||
@@ -833,6 +852,7 @@ const mapStateToProps = (state: AppState) => {
|
||||
notesColumns: validateColumns(state.settings['notes.columns']),
|
||||
showInvalidJoplinCloudCredential: state.settings['sync.target'] === 10 && state.mustAuthenticate,
|
||||
toast: state.toast,
|
||||
shouldSwitchToAppleSiliconVersion: shim.isAppleSilicon() && process.arch !== 'arm64',
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -172,6 +172,7 @@ interface Props {
|
||||
pluginMenus: any[];
|
||||
['spellChecker.enabled']: boolean;
|
||||
['spellChecker.languages']: string[];
|
||||
markdownEditorVisible: boolean;
|
||||
plugins: PluginStates;
|
||||
customCss: string;
|
||||
locale: string;
|
||||
@@ -278,6 +279,7 @@ function useMenuStates(menu: any, props: Props) {
|
||||
props['notes.sortOrder.reverse'],
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
props['folders.sortOrder.reverse'],
|
||||
props.markdownEditorVisible,
|
||||
props.tabMovesFocus,
|
||||
props.noteListRendererId,
|
||||
props.showNoteCounts,
|
||||
@@ -479,6 +481,7 @@ function useMenu(props: Props) {
|
||||
menuItemDic.focusElementNoteList,
|
||||
menuItemDic.focusElementNoteTitle,
|
||||
menuItemDic.focusElementNoteBody,
|
||||
menuItemDic.focusElementNoteViewer,
|
||||
menuItemDic.focusElementToolbar,
|
||||
];
|
||||
|
||||
@@ -552,11 +555,19 @@ function useMenu(props: Props) {
|
||||
const newFolderItem = menuItemDic.newFolder;
|
||||
const newSubFolderItem = menuItemDic.newSubFolder;
|
||||
const printItem = menuItemDic.print;
|
||||
const openSecondaryAppInstance = menuItemDic.openSecondaryAppInstance;
|
||||
const openPrimaryAppInstance = menuItemDic.openPrimaryAppInstance;
|
||||
const switchProfileItem = {
|
||||
label: _('Switch profile'),
|
||||
submenu: switchProfileMenuItems,
|
||||
};
|
||||
|
||||
const profilesAndAppInstancesItems = [
|
||||
openSecondaryAppInstance,
|
||||
openPrimaryAppInstance,
|
||||
switchProfileItem,
|
||||
];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
let toolsItems: any[] = [];
|
||||
|
||||
@@ -668,7 +679,7 @@ function useMenu(props: Props) {
|
||||
platforms: ['darwin'],
|
||||
},
|
||||
|
||||
shim.isMac() ? noItem : switchProfileItem,
|
||||
...(shim.isMac() ? [] : profilesAndAppInstancesItems),
|
||||
|
||||
shim.isMac() ? {
|
||||
label: _('Hide %s', 'Joplin'),
|
||||
@@ -715,8 +726,10 @@ function useMenu(props: Props) {
|
||||
}, {
|
||||
type: 'separator',
|
||||
},
|
||||
printItem,
|
||||
switchProfileItem,
|
||||
printItem, {
|
||||
type: 'separator',
|
||||
},
|
||||
...profilesAndAppInstancesItems,
|
||||
],
|
||||
};
|
||||
|
||||
@@ -789,6 +802,7 @@ function useMenu(props: Props) {
|
||||
shim.isMac() ? noItem : menuItemDic.toggleMenuBar,
|
||||
menuItemDic.toggleNoteList,
|
||||
menuItemDic.toggleVisiblePanes,
|
||||
menuItemDic.toggleEditorPlugin,
|
||||
{
|
||||
label: _('Layout button sequence'),
|
||||
submenu: layoutButtonSequenceMenuItems,
|
||||
@@ -1139,7 +1153,7 @@ function MenuBar(props: Props): any {
|
||||
|
||||
|
||||
const mapStateToProps = (state: AppState): Partial<Props> => {
|
||||
const whenClauseContext = stateToWhenClauseContext(state);
|
||||
const whenClauseContext = stateToWhenClauseContext(state, { windowId: state.windowId });
|
||||
|
||||
const secondaryWindowFocused = state.windowId !== defaultWindowId;
|
||||
|
||||
@@ -1165,6 +1179,7 @@ const mapStateToProps = (state: AppState): Partial<Props> => {
|
||||
pluginMenus: stateUtils.selectArrayShallow({ array: pluginUtils.viewsByType(state.pluginService.plugins, 'menu') }, 'menuBar.pluginMenus'),
|
||||
['spellChecker.languages']: state.settings['spellChecker.languages'],
|
||||
['spellChecker.enabled']: state.settings['spellChecker.enabled'],
|
||||
markdownEditorVisible: whenClauseContext.markdownEditorVisible,
|
||||
plugins: state.pluginService.plugins,
|
||||
customCss: state.customViewerCss,
|
||||
profileConfig: state.profileConfig,
|
||||
|
@@ -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. ).
|
||||
// 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'),
|
||||
];
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { AppState } from '../../app.reducer';
|
||||
import * as React from 'react';
|
||||
import { useEffect, useRef, useMemo } from 'react';
|
||||
import { useEffect, useRef, useMemo, useContext } from 'react';
|
||||
import SearchBar from '../SearchBar/SearchBar';
|
||||
import Button, { ButtonLevel, ButtonSize, buttonSizePx } from '../Button/Button';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
@@ -14,6 +14,7 @@ import stateToWhenClauseContext from '../../services/commands/stateToWhenClauseC
|
||||
import { getTrashFolderId } from '@joplin/lib/services/trash';
|
||||
import { Breakpoints } from '../NoteList/utils/types';
|
||||
import { stateUtils } from '@joplin/lib/reducer';
|
||||
import { WindowIdContext } from '../NewWindowOrIFrame';
|
||||
|
||||
interface Props {
|
||||
showNewNoteButtons: boolean;
|
||||
@@ -245,11 +246,12 @@ function NoteListControls(props: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
const windowId = useContext(WindowIdContext);
|
||||
return (
|
||||
<StyledRoot ref={noteControlsRef} padding={props.padding} buttonVerticalGap={props.buttonVerticalGap}>
|
||||
{renderNewNoteButtons()}
|
||||
<BottomRow ref={searchAndSortRef} className="search-and-sort">
|
||||
<SearchBar inputRef={searchBarRef}/>
|
||||
<SearchBar inputRef={searchBarRef} windowId={windowId}/>
|
||||
{showsSortOrderButtons() &&
|
||||
<SortOrderButtonsContainer>
|
||||
<StyledPairButtonL
|
||||
|
@@ -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]);
|
||||
|
@@ -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;
|
22
packages/app-desktop/gui/PopupNotification/types.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export type PopupHandle = {
|
||||
remove(): void;
|
||||
scheduleDismiss(delay?: number): void;
|
||||
};
|
||||
|
||||
export enum NotificationType {
|
||||
Info = 'info',
|
||||
Success = 'success',
|
||||
Error = 'error',
|
||||
}
|
||||
|
||||
export type NotificationContentCallback = ()=> React.ReactNode;
|
||||
|
||||
export interface PopupOptions {
|
||||
type?: NotificationType;
|
||||
}
|
||||
|
||||
export interface PopupControl {
|
||||
createPopup(content: NotificationContentCallback, props?: PopupOptions): PopupHandle;
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
import iterateItems from './iterateItems';
|
||||
import { LayoutItem, LayoutItemDirection, tempContainerPrefix } from './types';
|
||||
import produce from 'immer';
|
||||
import { produce } from 'immer';
|
||||
import uuid from '@joplin/lib/uuid';
|
||||
import validateLayout from './validateLayout';
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { LayoutItem, Size } from './types';
|
||||
import produce from 'immer';
|
||||
import { produce } from 'immer';
|
||||
import iterateItems from './iterateItems';
|
||||
import validateLayout from './validateLayout';
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import produce from 'immer';
|
||||
import { produce } from 'immer';
|
||||
import iterateItems from './iterateItems';
|
||||
import { LayoutItem } from './types';
|
||||
import validateLayout from './validateLayout';
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import produce from 'immer';
|
||||
import { produce } from 'immer';
|
||||
import iterateItems from './iterateItems';
|
||||
import { LayoutItem } from './types';
|
||||
import validateLayout from './validateLayout';
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import produce from 'immer';
|
||||
import { produce } from 'immer';
|
||||
import { LayoutItem } from './types';
|
||||
import validateLayout from './validateLayout';
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import produce from 'immer';
|
||||
import { produce } from 'immer';
|
||||
import iterateItems from './iterateItems';
|
||||
import { LayoutItem, LayoutItemDirection } from './types';
|
||||
|
||||
|
@@ -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>
|
||||
);
|
||||
|
@@ -5,7 +5,7 @@ import Setting from '@joplin/lib/models/Setting';
|
||||
import { stateUtils } from '@joplin/lib/reducer';
|
||||
import BaseModel from '@joplin/lib/BaseModel';
|
||||
import uuid from '@joplin/lib/uuid';
|
||||
const { connect } = require('react-redux');
|
||||
import { connect } from 'react-redux';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import { blur, focus } from '@joplin/lib/utils/focusHandler';
|
||||
@@ -180,10 +180,15 @@ function SearchBar(props: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
interface OwnProps {
|
||||
windowId: string;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState, ownProps: OwnProps) => {
|
||||
const windowState = stateUtils.windowStateById(state, ownProps.windowId);
|
||||
return {
|
||||
notesParentType: state.notesParentType,
|
||||
selectedNoteId: stateUtils.selectedNoteId(state),
|
||||
notesParentType: windowState.notesParentType,
|
||||
selectedNoteId: stateUtils.selectedNoteId(windowState),
|
||||
isFocused: state.focusedField === 'globalSearch',
|
||||
};
|
||||
};
|
||||
|
@@ -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>
|
||||
|
@@ -10,6 +10,7 @@ import useElementSize from '@joplin/lib/hooks/useElementSize';
|
||||
import Button, { ButtonLevel } from '../Button/Button';
|
||||
import bridge from '../../services/bridge';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import JoplinCloudSignUpCallToAction from '../JoplinCloudSignUpCallToAction';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
@@ -210,6 +211,11 @@ export default function(props: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
function renderSignUpArea(info: SyncTargetInfo) {
|
||||
if (info.name !== 'joplinCloud') return null;
|
||||
return <JoplinCloudSignUpCallToAction/>;
|
||||
}
|
||||
|
||||
function renderSyncTarget(info: SyncTargetInfo) {
|
||||
const key = `syncTarget_${info.name}`;
|
||||
const height = info.name !== 'joplinCloud' ? descriptionHeight : null;
|
||||
@@ -238,6 +244,7 @@ export default function(props: Props) {
|
||||
{descriptionComp}
|
||||
{featuresComp}
|
||||
{renderSelectArea(info, headerId)}
|
||||
{renderSignUpArea(info)}
|
||||
{renderSlowSyncWarning()}
|
||||
</SyncTargetBox>
|
||||
);
|
||||
|
@@ -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]);
|
||||
};
|
||||
|