Compare commits
100 Commits
android-v3
...
server-v3.
Author | SHA1 | Date | |
---|---|---|---|
|
9bf1e19d61 | ||
|
f7a970f466 | ||
|
f7fa7a195f | ||
|
e6ec27a501 | ||
|
331f7ebe5c | ||
|
afcd2d2a39 | ||
|
8129f4a89f | ||
|
72c1bb3724 | ||
|
b69a7403bc | ||
|
bdc9fa9dc3 | ||
|
9c07e57e28 | ||
|
821daeca94 | ||
|
480bf238f6 | ||
|
8ff13e5fc4 | ||
|
8e1970d08e | ||
|
86d92dd302 | ||
|
71b466507f | ||
|
11ce5f6c52 | ||
|
630b4061f0 | ||
|
912c943114 | ||
|
8e377e0306 | ||
|
1535e020a3 | ||
|
23d5d3426d | ||
|
6ab7a0836e | ||
|
278691211d | ||
|
356d4688a0 | ||
|
6b1d31387b | ||
|
71f70f4d2c | ||
|
2d984ce9a8 | ||
|
eaf160e0b1 | ||
|
624bfd9175 | ||
|
9ad1249f11 | ||
|
668849603d | ||
|
24f4c8e6ab | ||
|
46f5784edc | ||
|
fae2443481 | ||
|
37d65e000a | ||
|
6dd90eb03f | ||
|
3d8f713eb7 | ||
|
c35efe15d2 | ||
|
1596b46b86 | ||
|
4de0236194 | ||
|
2ab9702e32 | ||
|
24954bd0f0 | ||
|
2d4322be56 | ||
|
abb069bf50 | ||
|
a81d9fe17a | ||
|
6d44158050 | ||
|
a63cf3a90d | ||
|
ddb4f8c45b | ||
|
d7adab59ef | ||
|
e41374496e | ||
|
62d514463c | ||
|
332078b4ea | ||
|
c60e11646d | ||
|
c607fe9c75 | ||
|
1a4ba2c74a | ||
|
731260926d | ||
|
a43635610a | ||
|
e307459652 | ||
|
c197a83de8 | ||
|
7e4533d811 | ||
|
be117bca86 | ||
|
2b7bd902f3 | ||
|
6d7fd19167 | ||
|
c3520d9eb1 | ||
|
5fd3cecc96 | ||
|
0d8666c946 | ||
|
12db667128 | ||
|
6215de6080 | ||
|
7d2f384475 | ||
|
6ea1ac09a4 | ||
|
f2841a9a94 | ||
|
46ade2e0f8 | ||
|
d89be23069 | ||
|
337d50437b | ||
|
2479a8471e | ||
|
6c091910cd | ||
|
a074532497 | ||
|
5d2df358ac | ||
|
dfdc2fda27 | ||
|
a1f9c9c3d8 | ||
|
838da6f161 | ||
|
a86ee1d34e | ||
|
5f34a1bc92 | ||
|
f781face3a | ||
|
78ecd28d73 | ||
|
85e57a3953 | ||
|
95968f6690 | ||
|
f0b73ee916 | ||
|
a44412ae78 | ||
|
c7116b135f | ||
|
801d36c41f | ||
|
1d46adf801 | ||
|
94edaea210 | ||
|
5db88995c0 | ||
|
8eda8d3c84 | ||
|
1437dd5f27 | ||
|
9eb4944614 | ||
|
b4ef5abb88 |
@@ -520,12 +520,15 @@ packages/app-mobile/components/CameraView.js
|
||||
packages/app-mobile/components/DismissibleDialog.js
|
||||
packages/app-mobile/components/Dropdown.test.js
|
||||
packages/app-mobile/components/Dropdown.js
|
||||
packages/app-mobile/components/ExtendedWebView.js
|
||||
packages/app-mobile/components/ExtendedWebView/index.jest.js
|
||||
packages/app-mobile/components/ExtendedWebView/index.js
|
||||
packages/app-mobile/components/ExtendedWebView/types.js
|
||||
packages/app-mobile/components/FolderPicker.js
|
||||
packages/app-mobile/components/Icon.js
|
||||
packages/app-mobile/components/IconButton.js
|
||||
packages/app-mobile/components/Modal.js
|
||||
packages/app-mobile/components/ModalDialog.js
|
||||
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.test.js
|
||||
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js
|
||||
packages/app-mobile/components/NoteBodyViewer/bundledJs/Renderer.test.js
|
||||
packages/app-mobile/components/NoteBodyViewer/bundledJs/Renderer.js
|
||||
@@ -596,6 +599,27 @@ packages/app-mobile/components/buttons/index.js
|
||||
packages/app-mobile/components/getResponsiveValue.test.js
|
||||
packages/app-mobile/components/getResponsiveValue.js
|
||||
packages/app-mobile/components/global-style.js
|
||||
packages/app-mobile/components/plugins/PluginRunner.js
|
||||
packages/app-mobile/components/plugins/PluginRunnerWebView.js
|
||||
packages/app-mobile/components/plugins/backgroundPage/initializeDialogWebView.js
|
||||
packages/app-mobile/components/plugins/backgroundPage/initializePluginBackgroundIframe.js
|
||||
packages/app-mobile/components/plugins/backgroundPage/pluginRunnerBackgroundPage.js
|
||||
packages/app-mobile/components/plugins/backgroundPage/startStopPlugin.js
|
||||
packages/app-mobile/components/plugins/backgroundPage/utils/getFormData.test.js
|
||||
packages/app-mobile/components/plugins/backgroundPage/utils/getFormData.js
|
||||
packages/app-mobile/components/plugins/backgroundPage/utils/makeSandboxedIframe.js
|
||||
packages/app-mobile/components/plugins/backgroundPage/utils/reportUnhandledErrors.js
|
||||
packages/app-mobile/components/plugins/backgroundPage/utils/wrapConsoleLog.js
|
||||
packages/app-mobile/components/plugins/dialogs/PluginDialogManager.js
|
||||
packages/app-mobile/components/plugins/dialogs/PluginDialogWebView.js
|
||||
packages/app-mobile/components/plugins/dialogs/PluginPanelViewer.js
|
||||
packages/app-mobile/components/plugins/dialogs/PluginUserWebView.js
|
||||
packages/app-mobile/components/plugins/dialogs/hooks/useDialogMessenger.js
|
||||
packages/app-mobile/components/plugins/dialogs/hooks/useDialogSize.js
|
||||
packages/app-mobile/components/plugins/dialogs/hooks/useViewInfos.js
|
||||
packages/app-mobile/components/plugins/dialogs/hooks/useWebViewSetup.js
|
||||
packages/app-mobile/components/plugins/types.js
|
||||
packages/app-mobile/components/plugins/utils/createOnLogHandler.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/FileSystemPathSelector.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/JoplinCloudConfig.js
|
||||
@@ -660,36 +684,11 @@ packages/app-mobile/components/screens/status.js
|
||||
packages/app-mobile/components/side-menu-content.js
|
||||
packages/app-mobile/components/voiceTyping/VoiceTypingDialog.js
|
||||
packages/app-mobile/gulpfile.js
|
||||
packages/app-mobile/plugins/PlatformImplementation.js
|
||||
packages/app-mobile/plugins/PluginRunner/PluginRunner.js
|
||||
packages/app-mobile/plugins/PluginRunner/PluginRunnerWebView.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/initializeDialogWebView.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/initializePluginBackgroundIframe.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/pluginRunnerBackgroundPage.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/startStopPlugin.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/getFormData.test.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/getFormData.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/makeSandboxedIframe.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/reportUnhandledErrors.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/wrapConsoleLog.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/PluginDialogManager.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/PluginDialogWebView.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/PluginPanelViewer.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/PluginUserWebView.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/hooks/useDialogMessenger.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/hooks/useDialogSize.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/hooks/useViewInfos.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/hooks/useWebViewSetup.js
|
||||
packages/app-mobile/plugins/PluginRunner/types.js
|
||||
packages/app-mobile/plugins/PluginRunner/utils/createOnLogHandler.js
|
||||
packages/app-mobile/plugins/hooks/usePlugin.js
|
||||
packages/app-mobile/plugins/loadPlugins.test.js
|
||||
packages/app-mobile/plugins/loadPlugins.js
|
||||
packages/app-mobile/plugins/testing/MockPluginRunner.js
|
||||
packages/app-mobile/root.js
|
||||
packages/app-mobile/services/AlarmServiceDriver.android.js
|
||||
packages/app-mobile/services/AlarmServiceDriver.ios.js
|
||||
packages/app-mobile/services/e2ee/RSA.react-native.js
|
||||
packages/app-mobile/services/plugins/PlatformImplementation.js
|
||||
packages/app-mobile/services/profiles/index.js
|
||||
packages/app-mobile/services/voiceTyping/vosk.android.js
|
||||
packages/app-mobile/services/voiceTyping/vosk.ios.js
|
||||
@@ -717,7 +716,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/image/getImageDimensions.js
|
||||
packages/app-mobile/utils/image/resizeImage.js
|
||||
packages/app-mobile/utils/initializeCommandService.js
|
||||
packages/app-mobile/utils/injectedJs.js
|
||||
packages/app-mobile/utils/ipc/RNToWebViewMessenger.js
|
||||
packages/app-mobile/utils/ipc/WebViewToRNMessenger.js
|
||||
packages/app-mobile/utils/pickDocument.js
|
||||
@@ -725,6 +727,7 @@ packages/app-mobile/utils/polyfills/bufferPolyfill.js
|
||||
packages/app-mobile/utils/polyfills/index.js
|
||||
packages/app-mobile/utils/setupNotifications.js
|
||||
packages/app-mobile/utils/shareHandler.js
|
||||
packages/app-mobile/utils/shim-init-react.js
|
||||
packages/app-mobile/utils/showMessageBox.js
|
||||
packages/app-mobile/utils/testing/createMockReduxStore.js
|
||||
packages/app-mobile/utils/types.js
|
||||
@@ -798,6 +801,7 @@ packages/editor/CodeMirror/utils/formatting/toggleRegionFormatGlobally.js
|
||||
packages/editor/CodeMirror/utils/formatting/toggleSelectedLinesStartWith.js
|
||||
packages/editor/CodeMirror/utils/formatting/types.js
|
||||
packages/editor/CodeMirror/utils/growSelectionToNode.js
|
||||
packages/editor/CodeMirror/utils/handlePasteEvent.js
|
||||
packages/editor/CodeMirror/utils/isInSyntaxNode.js
|
||||
packages/editor/CodeMirror/utils/setupVim.js
|
||||
packages/editor/SelectionFormatting.js
|
||||
@@ -898,6 +902,7 @@ packages/lib/geolocation-node.js
|
||||
packages/lib/hooks/useAsyncEffect.js
|
||||
packages/lib/hooks/useElementSize.js
|
||||
packages/lib/hooks/useEventListener.js
|
||||
packages/lib/hooks/usePlugin.js
|
||||
packages/lib/hooks/usePrevious.js
|
||||
packages/lib/htmlUtils.test.js
|
||||
packages/lib/htmlUtils.js
|
||||
@@ -944,8 +949,10 @@ packages/lib/models/Tag.test.js
|
||||
packages/lib/models/Tag.js
|
||||
packages/lib/models/dateTimeFormats.test.js
|
||||
packages/lib/models/settings/FileHandler.js
|
||||
packages/lib/models/settings/builtInMetadata.js
|
||||
packages/lib/models/settings/settingValidations.test.js
|
||||
packages/lib/models/settings/settingValidations.js
|
||||
packages/lib/models/settings/types.js
|
||||
packages/lib/models/utils/getCollator.js
|
||||
packages/lib/models/utils/getConflictFolderId.js
|
||||
packages/lib/models/utils/isItemId.js
|
||||
@@ -1110,7 +1117,11 @@ packages/lib/services/plugins/api/noteListType.js
|
||||
packages/lib/services/plugins/api/types.js
|
||||
packages/lib/services/plugins/defaultPlugins/defaultPluginsUtils.js
|
||||
packages/lib/services/plugins/defaultPlugins/desktopDefaultPluginsInfo.js
|
||||
packages/lib/services/plugins/loadPlugins.test.js
|
||||
packages/lib/services/plugins/loadPlugins.js
|
||||
packages/lib/services/plugins/reducer.js
|
||||
packages/lib/services/plugins/testing/MockPlatformImplementation.js
|
||||
packages/lib/services/plugins/testing/MockPluginRunner.js
|
||||
packages/lib/services/plugins/utils/createViewHandle.js
|
||||
packages/lib/services/plugins/utils/executeSandboxCall.js
|
||||
packages/lib/services/plugins/utils/getPluginIssueReportUrl.test.js
|
||||
@@ -1386,6 +1397,7 @@ packages/tools/updateMarkdownDoc.js
|
||||
packages/tools/utils/discourse.js
|
||||
packages/tools/utils/loadSponsors.js
|
||||
packages/tools/utils/translation.js
|
||||
packages/tools/validateFilenames.js
|
||||
packages/tools/website/build.js
|
||||
packages/tools/website/buildTranslations.js
|
||||
packages/tools/website/processDocs.test.js
|
||||
|
17
.github/workflows/automerge.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
name: automerge
|
||||
on:
|
||||
schedule:
|
||||
- cron: '*/10 * * * *'
|
||||
jobs:
|
||||
automerge:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- id: automerge
|
||||
name: automerge
|
||||
uses: "pascalgn/automerge-action@v0.16.3"
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
MERGE_METHOD: "squash"
|
||||
LOG: "DEBUG"
|
66
.gitignore
vendored
@@ -499,12 +499,15 @@ packages/app-mobile/components/CameraView.js
|
||||
packages/app-mobile/components/DismissibleDialog.js
|
||||
packages/app-mobile/components/Dropdown.test.js
|
||||
packages/app-mobile/components/Dropdown.js
|
||||
packages/app-mobile/components/ExtendedWebView.js
|
||||
packages/app-mobile/components/ExtendedWebView/index.jest.js
|
||||
packages/app-mobile/components/ExtendedWebView/index.js
|
||||
packages/app-mobile/components/ExtendedWebView/types.js
|
||||
packages/app-mobile/components/FolderPicker.js
|
||||
packages/app-mobile/components/Icon.js
|
||||
packages/app-mobile/components/IconButton.js
|
||||
packages/app-mobile/components/Modal.js
|
||||
packages/app-mobile/components/ModalDialog.js
|
||||
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.test.js
|
||||
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js
|
||||
packages/app-mobile/components/NoteBodyViewer/bundledJs/Renderer.test.js
|
||||
packages/app-mobile/components/NoteBodyViewer/bundledJs/Renderer.js
|
||||
@@ -575,6 +578,27 @@ packages/app-mobile/components/buttons/index.js
|
||||
packages/app-mobile/components/getResponsiveValue.test.js
|
||||
packages/app-mobile/components/getResponsiveValue.js
|
||||
packages/app-mobile/components/global-style.js
|
||||
packages/app-mobile/components/plugins/PluginRunner.js
|
||||
packages/app-mobile/components/plugins/PluginRunnerWebView.js
|
||||
packages/app-mobile/components/plugins/backgroundPage/initializeDialogWebView.js
|
||||
packages/app-mobile/components/plugins/backgroundPage/initializePluginBackgroundIframe.js
|
||||
packages/app-mobile/components/plugins/backgroundPage/pluginRunnerBackgroundPage.js
|
||||
packages/app-mobile/components/plugins/backgroundPage/startStopPlugin.js
|
||||
packages/app-mobile/components/plugins/backgroundPage/utils/getFormData.test.js
|
||||
packages/app-mobile/components/plugins/backgroundPage/utils/getFormData.js
|
||||
packages/app-mobile/components/plugins/backgroundPage/utils/makeSandboxedIframe.js
|
||||
packages/app-mobile/components/plugins/backgroundPage/utils/reportUnhandledErrors.js
|
||||
packages/app-mobile/components/plugins/backgroundPage/utils/wrapConsoleLog.js
|
||||
packages/app-mobile/components/plugins/dialogs/PluginDialogManager.js
|
||||
packages/app-mobile/components/plugins/dialogs/PluginDialogWebView.js
|
||||
packages/app-mobile/components/plugins/dialogs/PluginPanelViewer.js
|
||||
packages/app-mobile/components/plugins/dialogs/PluginUserWebView.js
|
||||
packages/app-mobile/components/plugins/dialogs/hooks/useDialogMessenger.js
|
||||
packages/app-mobile/components/plugins/dialogs/hooks/useDialogSize.js
|
||||
packages/app-mobile/components/plugins/dialogs/hooks/useViewInfos.js
|
||||
packages/app-mobile/components/plugins/dialogs/hooks/useWebViewSetup.js
|
||||
packages/app-mobile/components/plugins/types.js
|
||||
packages/app-mobile/components/plugins/utils/createOnLogHandler.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/FileSystemPathSelector.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/JoplinCloudConfig.js
|
||||
@@ -639,36 +663,11 @@ packages/app-mobile/components/screens/status.js
|
||||
packages/app-mobile/components/side-menu-content.js
|
||||
packages/app-mobile/components/voiceTyping/VoiceTypingDialog.js
|
||||
packages/app-mobile/gulpfile.js
|
||||
packages/app-mobile/plugins/PlatformImplementation.js
|
||||
packages/app-mobile/plugins/PluginRunner/PluginRunner.js
|
||||
packages/app-mobile/plugins/PluginRunner/PluginRunnerWebView.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/initializeDialogWebView.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/initializePluginBackgroundIframe.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/pluginRunnerBackgroundPage.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/startStopPlugin.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/getFormData.test.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/getFormData.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/makeSandboxedIframe.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/reportUnhandledErrors.js
|
||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/wrapConsoleLog.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/PluginDialogManager.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/PluginDialogWebView.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/PluginPanelViewer.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/PluginUserWebView.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/hooks/useDialogMessenger.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/hooks/useDialogSize.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/hooks/useViewInfos.js
|
||||
packages/app-mobile/plugins/PluginRunner/dialogs/hooks/useWebViewSetup.js
|
||||
packages/app-mobile/plugins/PluginRunner/types.js
|
||||
packages/app-mobile/plugins/PluginRunner/utils/createOnLogHandler.js
|
||||
packages/app-mobile/plugins/hooks/usePlugin.js
|
||||
packages/app-mobile/plugins/loadPlugins.test.js
|
||||
packages/app-mobile/plugins/loadPlugins.js
|
||||
packages/app-mobile/plugins/testing/MockPluginRunner.js
|
||||
packages/app-mobile/root.js
|
||||
packages/app-mobile/services/AlarmServiceDriver.android.js
|
||||
packages/app-mobile/services/AlarmServiceDriver.ios.js
|
||||
packages/app-mobile/services/e2ee/RSA.react-native.js
|
||||
packages/app-mobile/services/plugins/PlatformImplementation.js
|
||||
packages/app-mobile/services/profiles/index.js
|
||||
packages/app-mobile/services/voiceTyping/vosk.android.js
|
||||
packages/app-mobile/services/voiceTyping/vosk.ios.js
|
||||
@@ -696,7 +695,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/image/getImageDimensions.js
|
||||
packages/app-mobile/utils/image/resizeImage.js
|
||||
packages/app-mobile/utils/initializeCommandService.js
|
||||
packages/app-mobile/utils/injectedJs.js
|
||||
packages/app-mobile/utils/ipc/RNToWebViewMessenger.js
|
||||
packages/app-mobile/utils/ipc/WebViewToRNMessenger.js
|
||||
packages/app-mobile/utils/pickDocument.js
|
||||
@@ -704,6 +706,7 @@ packages/app-mobile/utils/polyfills/bufferPolyfill.js
|
||||
packages/app-mobile/utils/polyfills/index.js
|
||||
packages/app-mobile/utils/setupNotifications.js
|
||||
packages/app-mobile/utils/shareHandler.js
|
||||
packages/app-mobile/utils/shim-init-react.js
|
||||
packages/app-mobile/utils/showMessageBox.js
|
||||
packages/app-mobile/utils/testing/createMockReduxStore.js
|
||||
packages/app-mobile/utils/types.js
|
||||
@@ -777,6 +780,7 @@ packages/editor/CodeMirror/utils/formatting/toggleRegionFormatGlobally.js
|
||||
packages/editor/CodeMirror/utils/formatting/toggleSelectedLinesStartWith.js
|
||||
packages/editor/CodeMirror/utils/formatting/types.js
|
||||
packages/editor/CodeMirror/utils/growSelectionToNode.js
|
||||
packages/editor/CodeMirror/utils/handlePasteEvent.js
|
||||
packages/editor/CodeMirror/utils/isInSyntaxNode.js
|
||||
packages/editor/CodeMirror/utils/setupVim.js
|
||||
packages/editor/SelectionFormatting.js
|
||||
@@ -877,6 +881,7 @@ packages/lib/geolocation-node.js
|
||||
packages/lib/hooks/useAsyncEffect.js
|
||||
packages/lib/hooks/useElementSize.js
|
||||
packages/lib/hooks/useEventListener.js
|
||||
packages/lib/hooks/usePlugin.js
|
||||
packages/lib/hooks/usePrevious.js
|
||||
packages/lib/htmlUtils.test.js
|
||||
packages/lib/htmlUtils.js
|
||||
@@ -923,8 +928,10 @@ packages/lib/models/Tag.test.js
|
||||
packages/lib/models/Tag.js
|
||||
packages/lib/models/dateTimeFormats.test.js
|
||||
packages/lib/models/settings/FileHandler.js
|
||||
packages/lib/models/settings/builtInMetadata.js
|
||||
packages/lib/models/settings/settingValidations.test.js
|
||||
packages/lib/models/settings/settingValidations.js
|
||||
packages/lib/models/settings/types.js
|
||||
packages/lib/models/utils/getCollator.js
|
||||
packages/lib/models/utils/getConflictFolderId.js
|
||||
packages/lib/models/utils/isItemId.js
|
||||
@@ -1089,7 +1096,11 @@ packages/lib/services/plugins/api/noteListType.js
|
||||
packages/lib/services/plugins/api/types.js
|
||||
packages/lib/services/plugins/defaultPlugins/defaultPluginsUtils.js
|
||||
packages/lib/services/plugins/defaultPlugins/desktopDefaultPluginsInfo.js
|
||||
packages/lib/services/plugins/loadPlugins.test.js
|
||||
packages/lib/services/plugins/loadPlugins.js
|
||||
packages/lib/services/plugins/reducer.js
|
||||
packages/lib/services/plugins/testing/MockPlatformImplementation.js
|
||||
packages/lib/services/plugins/testing/MockPluginRunner.js
|
||||
packages/lib/services/plugins/utils/createViewHandle.js
|
||||
packages/lib/services/plugins/utils/executeSandboxCall.js
|
||||
packages/lib/services/plugins/utils/getPluginIssueReportUrl.test.js
|
||||
@@ -1365,6 +1376,7 @@ packages/tools/updateMarkdownDoc.js
|
||||
packages/tools/utils/discourse.js
|
||||
packages/tools/utils/loadSponsors.js
|
||||
packages/tools/utils/translation.js
|
||||
packages/tools/validateFilenames.js
|
||||
packages/tools/website/build.js
|
||||
packages/tools/website/buildTranslations.js
|
||||
packages/tools/website/processDocs.test.js
|
||||
|
118
.yarn/patches/rn-fetch-blob-npm-0.12.0-cf02e3c544.patch
Normal file
@@ -0,0 +1,118 @@
|
||||
# Fixes sync issues caused by locale-sensitive lowercasing
|
||||
# of HTTP headers.
|
||||
# See https://github.com/laurent22/joplin/issues/10681
|
||||
diff --git a/android/src/main/java/com/RNFetchBlob/RNFetchBlobConfig.java b/android/src/main/java/com/RNFetchBlob/RNFetchBlobConfig.java
|
||||
index 8ac9e7a855162cefbf99024eb013c8a3b11de1ec..1c639cf9d84821b6ffc132960e2d1c044bedbd48 100644
|
||||
--- a/android/src/main/java/com/RNFetchBlob/RNFetchBlobConfig.java
|
||||
+++ b/android/src/main/java/com/RNFetchBlob/RNFetchBlobConfig.java
|
||||
@@ -2,6 +2,7 @@ package com.RNFetchBlob;
|
||||
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
+import java.util.Locale;
|
||||
|
||||
class RNFetchBlobConfig {
|
||||
|
||||
@@ -33,7 +34,7 @@ class RNFetchBlobConfig {
|
||||
}
|
||||
if(options.hasKey("binaryContentTypes"))
|
||||
this.binaryContentTypes = options.getArray("binaryContentTypes");
|
||||
- if(this.path != null && path.toLowerCase().contains("?append=true")) {
|
||||
+ if(this.path != null && path.toLowerCase(Locale.ROOT).contains("?append=true")) {
|
||||
this.overwrite = false;
|
||||
}
|
||||
if(options.hasKey("overwrite"))
|
||||
diff --git a/android/src/main/java/com/RNFetchBlob/RNFetchBlobFS.java b/android/src/main/java/com/RNFetchBlob/RNFetchBlobFS.java
|
||||
index a4d70153f41e6c14eec65412b5b59822f1c6750b..d98c439f7b0aeb79afc82ab9f653e9c021086426 100644
|
||||
--- a/android/src/main/java/com/RNFetchBlob/RNFetchBlobFS.java
|
||||
+++ b/android/src/main/java/com/RNFetchBlob/RNFetchBlobFS.java
|
||||
@@ -29,6 +29,7 @@ import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
+import java.util.Locale;
|
||||
|
||||
class RNFetchBlobFS {
|
||||
|
||||
@@ -210,7 +211,7 @@ class RNFetchBlobFS {
|
||||
return;
|
||||
}
|
||||
|
||||
- switch (encoding.toLowerCase()) {
|
||||
+ switch (encoding.toLowerCase(Locale.ROOT)) {
|
||||
case "base64" :
|
||||
promise.resolve(Base64.encodeToString(bytes, Base64.NO_WRAP));
|
||||
break;
|
||||
@@ -1050,7 +1051,7 @@ class RNFetchBlobFS {
|
||||
if(encoding.equalsIgnoreCase("ascii")) {
|
||||
return data.getBytes(Charset.forName("US-ASCII"));
|
||||
}
|
||||
- else if(encoding.toLowerCase().contains("base64")) {
|
||||
+ else if(encoding.toLowerCase(Locale.ROOT).contains("base64")) {
|
||||
return Base64.decode(data, Base64.NO_WRAP);
|
||||
|
||||
}
|
||||
diff --git a/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java b/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java
|
||||
index a8abd71833879201e3438b2fa51d712a311c4551..b70cc13c004229f69157de5f82ae5ec3abf4358e 100644
|
||||
--- a/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java
|
||||
+++ b/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java
|
||||
@@ -49,6 +49,7 @@ import java.security.KeyStore;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
+import java.util.Locale;
|
||||
import java.util.HashMap;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -300,14 +301,14 @@ public class RNFetchBlobReq extends BroadcastReceiver implements Runnable {
|
||||
responseFormat = ResponseFormat.UTF8;
|
||||
}
|
||||
else {
|
||||
- builder.header(key.toLowerCase(), value);
|
||||
- mheaders.put(key.toLowerCase(), value);
|
||||
+ builder.header(key.toLowerCase(Locale.ROOT), value);
|
||||
+ mheaders.put(key.toLowerCase(Locale.ROOT), value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(method.equalsIgnoreCase("post") || method.equalsIgnoreCase("put") || method.equalsIgnoreCase("patch")) {
|
||||
- String cType = getHeaderIgnoreCases(mheaders, "Content-Type").toLowerCase();
|
||||
+ String cType = getHeaderIgnoreCases(mheaders, "Content-Type").toLowerCase(Locale.ROOT);
|
||||
|
||||
if(rawRequestBodyArray != null) {
|
||||
requestType = RequestType.Form;
|
||||
@@ -323,7 +324,7 @@ public class RNFetchBlobReq extends BroadcastReceiver implements Runnable {
|
||||
|| rawRequestBody.startsWith(RNFetchBlobConst.CONTENT_PREFIX)) {
|
||||
requestType = RequestType.SingleFile;
|
||||
}
|
||||
- else if (cType.toLowerCase().contains(";base64") || cType.toLowerCase().startsWith("application/octet")) {
|
||||
+ else if (cType.toLowerCase(Locale.ROOT).contains(";base64") || cType.toLowerCase(Locale.ROOT).startsWith("application/octet")) {
|
||||
cType = cType.replace(";base64","").replace(";BASE64","");
|
||||
if(mheaders.containsKey("content-type"))
|
||||
mheaders.put("content-type", cType);
|
||||
@@ -686,7 +687,7 @@ public class RNFetchBlobReq extends BroadcastReceiver implements Runnable {
|
||||
boolean isCustomBinary = false;
|
||||
if(options.binaryContentTypes != null) {
|
||||
for(int i = 0; i< options.binaryContentTypes.size();i++) {
|
||||
- if(ctype.toLowerCase().contains(options.binaryContentTypes.getString(i).toLowerCase())) {
|
||||
+ if(ctype.toLowerCase(Locale.ROOT).contains(options.binaryContentTypes.getString(i).toLowerCase(Locale.ROOT))) {
|
||||
isCustomBinary = true;
|
||||
break;
|
||||
}
|
||||
@@ -698,13 +699,13 @@ public class RNFetchBlobReq extends BroadcastReceiver implements Runnable {
|
||||
private String getHeaderIgnoreCases(Headers headers, String field) {
|
||||
String val = headers.get(field);
|
||||
if(val != null) return val;
|
||||
- return headers.get(field.toLowerCase()) == null ? "" : headers.get(field.toLowerCase());
|
||||
+ return headers.get(field.toLowerCase(Locale.ROOT)) == null ? "" : headers.get(field.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
private String getHeaderIgnoreCases(HashMap<String,String> headers, String field) {
|
||||
String val = headers.get(field);
|
||||
if(val != null) return val;
|
||||
- String lowerCasedValue = headers.get(field.toLowerCase());
|
||||
+ String lowerCasedValue = headers.get(field.toLowerCase(Locale.ROOT));
|
||||
return lowerCasedValue == null ? "" : lowerCasedValue;
|
||||
}
|
||||
|
BIN
Assets/WebsiteAssets/images/desktop-set-alarm.png
Normal file
After Width: | Height: | Size: 41 KiB |
BIN
Assets/WebsiteAssets/images/news/20240701-mfa.png
Normal file
After Width: | Height: | Size: 244 KiB |
BIN
Assets/WebsiteAssets/images/news/20240701-mobile-plugins.png
Normal file
After Width: | Height: | Size: 337 KiB |
BIN
Assets/WebsiteAssets/images/news/20240701-note-list-multi.png
Normal file
After Width: | Height: | Size: 82 KiB |
BIN
Assets/WebsiteAssets/images/news/20240701-ocr-data.png
Normal file
After Width: | Height: | Size: 71 KiB |
BIN
Assets/WebsiteAssets/images/news/20240701-trash.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
Assets/WebsiteAssets/images/sponsors/Route4Me.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
Assets/WebsiteAssets/images/sponsors/Stormlikes.png
Normal file
After Width: | Height: | Size: 20 KiB |
@@ -1,4 +1,47 @@
|
||||
<?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>Fri, 01 Mar 2024 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Fri, 01 Mar 2024 00:00:00 GMT</pubDate><item><title><![CDATA[What's new in Joplin 2.14]]></title><description><![CDATA[<h2>OCR<a name="ocr" href="#ocr" 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, 01 Jul 2024 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Mon, 01 Jul 2024 00:00:00 GMT</pubDate><item><title><![CDATA[What's new in Joplin 3.0]]></title><description><![CDATA[<h2>Desktop application<a name="desktop-application" href="#desktop-application" class="heading-anchor">🔗</a></h2>
|
||||
<h3>Trash folder<a name="trash-folder" href="#trash-folder" class="heading-anchor">🔗</a></h3>
|
||||
<p>Joplin now support a trash folder - any deleted notes or notebooks will be moved to that folder. You can also choose to have these notes permanently deleted after a number of days.</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20240701-trash.png" alt=""></p>
|
||||
<p>Support for the trash folder has a somewhat long history in Joplin since it's an obvious and important feature to add, yet it can be particularly tricky once you start realising how many parts of the app it's going to impact.</p>
|
||||
<p>Many attempts have been made over time: my first attempt was based on the note history feature. Indeed since this feature already saves versions of notes, it seems to make sense to use it for the trash feature, and indeed the note history feature <a href="https://joplinapp.org/news/20190523-221026">was designed for this originally</a>. However that approach turned to be needlessly complicated and after modifying hundreds of files just for this, the idea was dropped.</p>
|
||||
<p>The next one was based on using a <a href="https://github.com/laurent22/joplin/issues/483">special "trash" tag</a> - deleted notes would have this tag attached to them and would appear in a special "trash" folder. This approach also had <a href="https://github.com/laurent22/joplin/issues/483">many issues</a> probably the main one being that notebooks can't be tagged, which means we would have to add support for tagged notebooks and that in itself would also be a massive change.</p>
|
||||
<p><a href="https://discourse.joplinapp.org/t/trashcan/3998/16">Various</a>, <a href="https://discourse.joplinapp.org/t/poll-trash-bin-plugin/19951">ideas,</a> were also attempted using plugins, by creating a special "trash folder", but in the end no such plugin was ever created, probably due to limitations of the plugin API.</p>
|
||||
<p>In the end, turned out that this <a href="https://github.com/laurent22/joplin/issues/483#issuecomment-585655742">old idea</a> of adding a "deleted" property to each note and notebook was the easiest approach. With this it was simpler to get to a working solution relatively quickly, and then it was a matter of ensuring that deleted notes don't appear where they shouldn't, such as search results, etc.</p>
|
||||
<h3>Joplin Cloud multi-factor authentication<a name="joplin-cloud-multi-factor-authentication" href="#joplin-cloud-multi-factor-authentication" class="heading-anchor">🔗</a></h3>
|
||||
<p>Multi-factor authentication (MFA), also known as two-factor authentication (2FA) is a security process that requires you to provide two or more verification factors to gain access to a system or account. It typically includes something you know (password), something you have (security token), and something you are (biometric verification).</p>
|
||||
<p>To better secure your account, Joplin Cloud and all Joplin applications now support MFA. To enable it, go to your Joplin Cloud profile, click on "Enable multi-factor authentication" and follow the instructions. Please note that all your applications will then be disconnected, so you will need to login again (your data of course will remain on the app so you won't have to download it again).</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20240701-mfa.png" alt=""></p>
|
||||
<h3>Note list with multiple columns<a name="note-list-with-multiple-columns" href="#note-list-with-multiple-columns" class="heading-anchor">🔗</a></h3>
|
||||
<p>In this release we add support for multiple columns in the note list. You can display various properties of the notes, as well as sort the notes by these properties. As usual this feature can be controlled and customised by plugins so for example it should be possible to display custom columns, and display custom information including thumbnails.</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20240701-note-list-multi.png" alt=""></p>
|
||||
<h3>Plugin API enhancement<a name="plugin-api-enhancement" href="#plugin-api-enhancement" class="heading-anchor">🔗</a></h3>
|
||||
<p>The plugin API has received several updates to facilitate easy customisation of the app As mentioned above, it is now possible to customise the new note list. Besides this, we've added support for loading PDFs and creating images from them, which can for example be used to create thumbnails.</p>
|
||||
<p>Many other small enhancements have been made to the plugin API to help you tailor the app to your needs!</p>
|
||||
<h3>View OCR data<a name="view-ocr-data" href="#view-ocr-data" class="heading-anchor">🔗</a></h3>
|
||||
<p>Now when you right-click on an image or PDF you have an option to view the OCR (Optical character recognition) data associated with it. That will allow you for example to easily copy and paste the text.</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20240701-ocr-data.png" alt=""></p>
|
||||
<h2>Plugin support on mobile<a name="plugin-support-on-mobile" href="#plugin-support-on-mobile" class="heading-anchor">🔗</a></h2>
|
||||
<p>As always, most of the above changes also apply to mobile (iOS and Android), for example the trash folder and MFA support.</p>
|
||||
<p>Additionally the mobile application now adds support for plugins. To enable the feature, go to the settings then to the "Plugins" section. The feature is currently in Beta, in particular it means that some plugins do not work or only partially work. Normally the app should not offer you to install a non-working plugin but that may still happen. In general if you notice any issue with this beta feature please let me us know as we're keen to improve it.</p>
|
||||
<p>Support for cross-platform plugins in Joplin is great news as it means a lot of new features become available on mobile. As of now, we have checked the following plugins and can confirm that they work on mobile:</p>
|
||||
<ul>
|
||||
<li><a href="https://joplinapp.org/plugins/plugin/com.whatever.quick-links/">Quick Links</a></li>
|
||||
<li><a href="https://joplinapp.org/plugins/plugin/com.whatever.inline-tags/">Inline Tags</a></li>
|
||||
<li><a href="https://joplinapp.org/plugins/plugin/io.github.personalizedrefrigerator.codemirror6-settings/">CodeMirror 6 settings</a></li>
|
||||
<li><a href="https://joplinapp.org/plugins/plugin/com.hieuthi.joplin.function-plot/">Function plot</a></li>
|
||||
<li><a href="https://joplinapp.org/plugins/plugin/joplin.plugin.space-indenter/">Space indenter</a></li>
|
||||
<li><a href="https://joplinapp.org/plugins/plugin/joplin.plugin.alondmnt.tag-navigator/">Inline Tag Navigator</a></li>
|
||||
</ul>
|
||||
<p>Those are just some examples - many more are working!</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20240701-mobile-plugins.png" alt=""></p>
|
||||
<h1>Full changelogs<a name="full-changelogs" href="#full-changelogs" class="heading-anchor">🔗</a></h1>
|
||||
<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/20240701-release-3-0</link><guid isPermaLink="false">20240701-release-3-0</guid><pubDate>Mon, 01 Jul 2024 00:00:00 GMT</pubDate><twitter-text>What's new in Joplin 3.0</twitter-text></item><item><title><![CDATA[What's new in Joplin 2.14]]></title><description><![CDATA[<h2>OCR<a name="ocr" href="#ocr" class="heading-anchor">🔗</a></h2>
|
||||
<p>Optical Character Recognition (OCR) in Joplin enables the transformation of text-containing images into machine-readable text formats. From this version you can enable OCR in the Configuration screen under the "General" section. Once activated, Joplin scans images and PDFs, extracting text data for searchability.</p>
|
||||
<p>While OCR search is available on both desktop and mobile apps, document scanning is limited to the desktop due to resource demands. For more information head to the <a href="https://joplinapp.org/help/apps/ocr">OCR official documentation</a>!</p>
|
||||
<h2>Bundled plugins<a name="bundled-plugins" href="#bundled-plugins" class="heading-anchor">🔗</a></h2>
|
||||
@@ -394,7 +437,4 @@ sys 0m38.013s</p>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
]]></description><link>https://joplinapp.org/news/20220522-gsoc-contributors</link><guid isPermaLink="false">20220522-gsoc-contributors</guid><pubDate>Sun, 22 May 2022 00:00:00 GMT</pubDate><twitter-text>Joplin received 6 Contributor Projects for GSoC 2022! Welcome to our new contributors who will be working on these projects over summer!</twitter-text></item><item><title><![CDATA[GSoC "Contributor Proposals" phase is starting now!]]></title><description><![CDATA[<p>The "Contributor Proposals" phase of GSoC 2022 is starting today! If you would like to be a contributor, now is the time to choose your project idea, write your proposal, and upload it to <a href="https://summerofcode.withgoogle.com/">https://summerofcode.withgoogle.com/</a></p>
|
||||
<p>When it's done, please also let us know by posting an update on your forum introduction post.</p>
|
||||
<p>If you haven't created a pull request yet, it's still time to create one. Doing so will greatly increase your chances of being selected!</p>
|
||||
]]></description><link>https://joplinapp.org/news/20220405-gsoc-contributor-proposals</link><guid isPermaLink="false">20220405-gsoc-contributor-proposals</guid><pubDate>Tue, 05 Apr 2022 00:00:00 GMT</pubDate><twitter-text></twitter-text></item></channel></rss>
|
||||
]]></description><link>https://joplinapp.org/news/20220522-gsoc-contributors</link><guid isPermaLink="false">20220522-gsoc-contributors</guid><pubDate>Sun, 22 May 2022 00:00:00 GMT</pubDate><twitter-text>Joplin received 6 Contributor Projects for GSoC 2022! Welcome to our new contributors who will be working on these projects over summer!</twitter-text></item></channel></rss>
|
@@ -120,9 +120,10 @@ fi
|
||||
print "Checking dependencies..."
|
||||
## Check if libfuse2 is present.
|
||||
if [[ $(command -v ldconfig) ]]; then
|
||||
LIBFUSE=$(ldconfig -p | grep "libfuse.so.2" || echo '')
|
||||
else
|
||||
LIBFUSE=$(find /lib /usr/lib /lib64 /usr/lib64 /usr/local/lib -name "libfuse.so.2" 2>/dev/null | grep "libfuse.so.2" || echo '')
|
||||
LIBFUSE=$(ldconfig -p | grep "libfuse.so.2" || echo '')
|
||||
fi
|
||||
if [[ $LIBFUSE == "" ]]; then
|
||||
LIBFUSE=$(find /lib /usr/lib /lib64 /usr/lib64 /usr/local/lib -name "libfuse.so.2" 2>/dev/null | grep "libfuse.so.2" || echo '')
|
||||
fi
|
||||
if [[ $LIBFUSE == "" ]]; then
|
||||
print "${COLOR_RED}Error: Can't get libfuse2 on system, please install libfuse2${COLOR_RESET}"
|
||||
|
@@ -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://grundstueckspreise.info/"><img title="SP Software GmbH" width="256" src="https://joplinapp.org/images/sponsors/Grundstueckspreise.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://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://grundstueckspreise.info/"><img title="SP Software GmbH" width="256" src="https://joplinapp.org/images/sponsors/Grundstueckspreise.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>
|
||||
<!-- SPONSORS-ORG -->
|
||||
|
||||
* * *
|
||||
|
@@ -15,7 +15,6 @@
|
||||
# SLAVE_POSTGRES_USER=joplin
|
||||
# SLAVE_POSTGRES_PORT=5433
|
||||
# SLAVE_POSTGRES_HOST=localhost
|
||||
# USERS_WITH_REPLICATION=ID1,ID2,...
|
||||
|
||||
version: '2'
|
||||
|
||||
|
@@ -21,7 +21,8 @@ module.exports = {
|
||||
// See https://github.com/lint-staged/lint-staged/issues/934#issuecomment-743299357
|
||||
'*.{js,jsx,ts,tsx,task1}': 'yarn checkIgnoredFiles',
|
||||
'*.{js,jsx,ts,tsx,task2}': 'yarn spellcheck',
|
||||
'*.{js,jsx,ts,tsx,task3}': 'yarn packageJsonLint',
|
||||
'*.{js,jsx,ts,tsx,task4}': 'yarn linter-precommit',
|
||||
'*.{md,mdx}': 'yarn spellcheck',
|
||||
'*.{js,jsx,ts,tsx,task3}': 'yarn linter-precommit',
|
||||
'*.{json,task4}': 'yarn packageJsonLint',
|
||||
'*.{md,mdx,task5}': 'yarn spellcheck',
|
||||
'*.{md,mdx,task6}': 'yarn validateFilenames',
|
||||
};
|
||||
|
@@ -59,6 +59,7 @@
|
||||
"updateMarkdownDoc": "node ./packages/tools/updateMarkdownDoc",
|
||||
"updateNews": "node ./packages/tools/website/updateNews",
|
||||
"updatePluginTypes": "./packages/generator-joplin/updateTypes.sh",
|
||||
"validateFilenames": "node ./packages/tools/validateFilenames.js",
|
||||
"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\""
|
||||
},
|
||||
@@ -82,7 +83,7 @@
|
||||
"eslint-plugin-react": "7.33.2",
|
||||
"execa": "5.1.1",
|
||||
"fs-extra": "11.2.0",
|
||||
"glob": "10.3.10",
|
||||
"glob": "10.3.12",
|
||||
"gulp": "4.0.2",
|
||||
"husky": "3.1.0",
|
||||
"lerna": "3.22.1",
|
||||
@@ -109,6 +110,7 @@
|
||||
"@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"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import Setting, { SettingStorage } from '@joplin/lib/models/Setting';
|
||||
import Setting, { AppType, SettingStorage } from '@joplin/lib/models/Setting';
|
||||
import { SettingItemType } from '@joplin/lib/services/plugins/api/types';
|
||||
import shim from '@joplin/lib/shim';
|
||||
|
||||
@@ -61,7 +61,7 @@ class Command extends BaseCommand {
|
||||
|
||||
const description: string[] = [];
|
||||
if (md.label && md.label()) description.push(md.label());
|
||||
if (md.description && md.description('desktop')) description.push(md.description('desktop'));
|
||||
if (md.description && md.description(AppType.Desktop)) description.push(md.description(AppType.Desktop));
|
||||
|
||||
if (description.length) props.description = description.join('. ');
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
|
@@ -57,13 +57,13 @@
|
||||
"proper-lockfile": "4.1.2",
|
||||
"read-chunk": "2.1.0",
|
||||
"server-destroy": "1.0.1",
|
||||
"sharp": "0.33.2",
|
||||
"sharp": "0.33.3",
|
||||
"sprintf-js": "1.1.3",
|
||||
"sqlite3": "5.1.6",
|
||||
"string-padding": "1.0.2",
|
||||
"strip-ansi": "6.0.1",
|
||||
"tcp-port-used": "1.0.2",
|
||||
"terminal-kit": "3.0.2",
|
||||
"terminal-kit": "3.1.1",
|
||||
"tkwidgets": "0.5.27",
|
||||
"url-parse": "1.5.10",
|
||||
"word-wrap": "1.2.5",
|
||||
@@ -73,7 +73,7 @@
|
||||
"@joplin/tools": "~3.0",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@types/jest": "29.5.8",
|
||||
"@types/node": "18.19.26",
|
||||
"@types/node": "18.19.31",
|
||||
"@types/proper-lockfile": "^4.1.2",
|
||||
"gulp": "4.0.2",
|
||||
"jest": "29.7.0",
|
||||
|
@@ -2,13 +2,16 @@ import MdToHtml from '@joplin/renderer/MdToHtml';
|
||||
const { filename } = require('@joplin/lib/path-utils');
|
||||
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { RenderOptions } from '@joplin/renderer/types';
|
||||
import { isResourceUrl, resourceUrlToId } from '@joplin/lib/models/utils/resourceUtils';
|
||||
const { themeStyle } = require('@joplin/lib/theme');
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
function newTestMdToHtml(options: any = null) {
|
||||
options = {
|
||||
ResourceModel: {
|
||||
isResourceUrl: () => false,
|
||||
isResourceUrl: isResourceUrl,
|
||||
urlToId: resourceUrlToId,
|
||||
},
|
||||
fsDriver: shim.fsDriver(),
|
||||
...options,
|
||||
@@ -39,7 +42,7 @@ describe('MdToHtml', () => {
|
||||
// if (mdFilename !== 'sanitize_9.md') continue;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const mdToHtmlOptions: any = {
|
||||
const mdToHtmlOptions: RenderOptions = {
|
||||
bodyOnly: true,
|
||||
};
|
||||
|
||||
@@ -51,6 +54,8 @@ describe('MdToHtml', () => {
|
||||
};
|
||||
} else if (mdFilename.startsWith('sourcemap_')) {
|
||||
mdToHtmlOptions.mapsToLine = true;
|
||||
} else if (mdFilename.startsWith('resource_')) {
|
||||
mdToHtmlOptions.resources = {};
|
||||
}
|
||||
|
||||
const markdown = await shim.fsDriver().readFile(mdFilePath);
|
||||
|
48
packages/app-cli/tests/html_to_md/resource_placeholder.html
Normal file
@@ -0,0 +1,48 @@
|
||||
<p>Markdown images:</p>
|
||||
<ul>
|
||||
<li>
|
||||
With ALT and title:
|
||||
<div
|
||||
class="not-loaded-resource not-loaded-image-resource resource-status-test"
|
||||
data-original-alt="test"
|
||||
data-original-title="testing"
|
||||
data-resource-id="0415d61cc33e47afa6dde45948c3177f"
|
||||
>
|
||||
<img src="data:image/svg+xml;utf8,some-icon-here"/>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
With neither ALT nor title:
|
||||
<div
|
||||
class="not-loaded-resource not-loaded-image-resource resource-status-error"
|
||||
data-original-alt=""
|
||||
data-original-title=""
|
||||
data-resource-id="0a25d61cc33e57afa6dde45948c3177f"
|
||||
>
|
||||
<img src="data:image/svg+xml;utf8,some-icon-here"/>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>HTML images:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<div
|
||||
class="not-loaded-resource not-loaded-image-resource resource-status-error"
|
||||
data-original-before=" width="230""
|
||||
data-original-after=" style="border: 32px inset red;"/"
|
||||
data-resource-id="0415d61cc33e47afa6dde45948c3177f"
|
||||
>
|
||||
<img src="data:image/svg+xml;utf8,some-icon-here"/>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div
|
||||
class="not-loaded-resource not-loaded-image-resource resource-status-error"
|
||||
data-original-after="/"
|
||||
data-resource-id="0415d61cc33e47afa6dde45948c3177f"
|
||||
>
|
||||
<img src="data:image/svg+xml;utf8,some-icon-here"/>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
@@ -0,0 +1,9 @@
|
||||
Markdown images:
|
||||
|
||||
- With ALT and title:
|
||||
- With neither ALT nor title:
|
||||
|
||||
HTML images:
|
||||
|
||||
- <img width="230" src=":/0415d61cc33e47afa6dde45948c3177f" style="border: 32px inset red;"/>
|
||||
- <img src=":/0415d61cc33e47afa6dde45948c3177f" />
|
@@ -0,0 +1,15 @@
|
||||
<div class="not-loaded-resource not-loaded-image-resource resource-status-notDownloaded" data-resource-id="a1test2a1test2a1test2a1test22345" data-original-alt data-original-title="test" contenteditable="false"><img src="data:image/svg+xml;utf8,
|
||||
		<svg width="1700" height="1536" xmlns="http://www.w3.org/2000/svg">
|
||||
		 <path d="M1280 1344c0-35-29-64-64-64s-64 29-64 64 29 64 64 64 64-29 64-64zm256 0c0-35-29-64-64-64s-64 29-64 64 29 64 64 64 64-29 64-64zm128-224v320c0 53-43 96-96 96H96c-53 0-96-43-96-96v-320c0-53 43-96 96-96h465l135 136c37 36 85 56 136 56s99-20 136-56l136-136h464c53 0 96 43 96 96zm-325-569c10 24 5 52-14 70l-448 448c-12 13-29 19-45 19s-33-6-45-19L339 621c-19-18-24-46-14-70 10-23 33-39 59-39h256V64c0-35 29-64 64-64h256c35 0 64 29 64 64v448h256c26 0 49 16 59 39z"/>
|
||||
		</svg>
|
||||
	"/></div>
|
||||
<div class="not-loaded-resource not-loaded-image-resource resource-status-notDownloaded" data-resource-id="a1test2a1test2a1test2a1test22346" data-original-alt="test" data-original-title contenteditable="false"><img src="data:image/svg+xml;utf8,
|
||||
		<svg width="1700" height="1536" xmlns="http://www.w3.org/2000/svg">
|
||||
		 <path d="M1280 1344c0-35-29-64-64-64s-64 29-64 64 29 64 64 64 64-29 64-64zm256 0c0-35-29-64-64-64s-64 29-64 64 29 64 64 64 64-29 64-64zm128-224v320c0 53-43 96-96 96H96c-53 0-96-43-96-96v-320c0-53 43-96 96-96h465l135 136c37 36 85 56 136 56s99-20 136-56l136-136h464c53 0 96 43 96 96zm-325-569c10 24 5 52-14 70l-448 448c-12 13-29 19-45 19s-33-6-45-19L339 621c-19-18-24-46-14-70 10-23 33-39 59-39h256V64c0-35 29-64 64-64h256c35 0 64 29 64 64v448h256c26 0 49 16 59 39z"/>
|
||||
		</svg>
|
||||
	"/></div>
|
||||
<div class="not-loaded-resource not-loaded-image-resource resource-status-notDownloaded" data-resource-id="a1test2a1test2a1test2a1test22347" data-original-before=" " data-original-after=" class="jop-noMdConv"/" contenteditable="false"><img src="data:image/svg+xml;utf8,
|
||||
		<svg width="1700" height="1536" xmlns="http://www.w3.org/2000/svg">
|
||||
		 <path d="M1280 1344c0-35-29-64-64-64s-64 29-64 64 29 64 64 64 64-29 64-64zm256 0c0-35-29-64-64-64s-64 29-64 64 29 64 64 64 64-29 64-64zm128-224v320c0 53-43 96-96 96H96c-53 0-96-43-96-96v-320c0-53 43-96 96-96h465l135 136c37 36 85 56 136 56s99-20 136-56l136-136h464c53 0 96 43 96 96zm-325-569c10 24 5 52-14 70l-448 448c-12 13-29 19-45 19s-33-6-45-19L339 621c-19-18-24-46-14-70 10-23 33-39 59-39h256V64c0-35 29-64 64-64h256c35 0 64 29 64 64v448h256c26 0 49 16 59 39z"/>
|
||||
		</svg>
|
||||
	"/></div>
|
@@ -0,0 +1,3 @@
|
||||

|
||||

|
||||
<img src=":/a1test2a1test2a1test2a1test22347"/>
|
@@ -405,7 +405,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
return (
|
||||
<div key={key} style={rowStyle}>
|
||||
{label}
|
||||
{this.renderDescription(this.props.themeId, md.description ? md.description() : null)}
|
||||
{this.renderDescription(this.props.themeId, md.description ? md.description(AppType.Desktop) : null)}
|
||||
<SettingComponent
|
||||
metadata={md}
|
||||
value={value}
|
||||
@@ -667,7 +667,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
};
|
||||
|
||||
const label = [md.label()];
|
||||
if (md.unitLabel) label.push(`(${md.unitLabel()})`);
|
||||
if (md.unitLabel) label.push(`(${md.unitLabel(md.value)})`);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const inputStyle: any = { ...textInputBaseStyle };
|
||||
|
@@ -713,7 +713,7 @@ function useMenu(props: Props) {
|
||||
label: layoutButtonSequenceOptions[value],
|
||||
type: 'checkbox',
|
||||
click: () => {
|
||||
Setting.setValue('layoutButtonSequence', value);
|
||||
Setting.setValue('layoutButtonSequence', Number(value));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@@ -385,6 +385,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
ref={editorRef}
|
||||
settings={editorSettings}
|
||||
pluginStates={props.plugins}
|
||||
onPasteFile={null}
|
||||
onEvent={onEditorEvent}
|
||||
onLogMessage={logDebug}
|
||||
onEditorPaste={onEditorPaste}
|
||||
|
@@ -45,6 +45,9 @@
|
||||
<script src="./scrollmap.js"></script>
|
||||
|
||||
<script>
|
||||
// Fixes a warning when loading some TypeScript plugins generated with webpack.
|
||||
window.exports = {};
|
||||
|
||||
// This is function used internally to send message from the webview to
|
||||
// the host.
|
||||
const ipcProxySendToHost = (methodName, arg) => {
|
||||
|
@@ -51,6 +51,7 @@ test.describe('noteList', () => {
|
||||
|
||||
await activateMainMenuItem(electronApp, 'Note list', 'Focus');
|
||||
await expect(mainScreen.noteListContainer.getByText('test note 1')).toBeVisible();
|
||||
await expect(mainScreen.noteListContainer.getByText('test note 2')).toBeVisible();
|
||||
|
||||
await setMessageBoxResponse(electronApp, /^Delete/i);
|
||||
|
||||
@@ -61,9 +62,6 @@ test.describe('noteList', () => {
|
||||
await mainWindow.keyboard.up('Shift');
|
||||
};
|
||||
await pressShiftDelete();
|
||||
|
||||
await folderBHeader.click();
|
||||
await folderAHeader.click();
|
||||
await expect(mainScreen.noteListContainer.getByText('test note 2')).not.toBeVisible();
|
||||
|
||||
// Should not delete when the editor is focused
|
||||
|
@@ -126,14 +126,14 @@
|
||||
"@playwright/test": "1.42.1",
|
||||
"@testing-library/react-hooks": "8.0.1",
|
||||
"@types/jest": "29.5.8",
|
||||
"@types/node": "18.19.26",
|
||||
"@types/node": "18.19.31",
|
||||
"@types/react": "18.2.58",
|
||||
"@types/react-redux": "7.1.33",
|
||||
"@types/styled-components": "5.1.32",
|
||||
"@types/tesseract.js": "2.0.0",
|
||||
"electron": "29.1.0",
|
||||
"electron-builder": "24.13.3",
|
||||
"glob": "10.3.10",
|
||||
"glob": "10.3.12",
|
||||
"gulp": "4.0.2",
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
|
@@ -2,6 +2,9 @@
|
||||
const webviewApiPromises_ = {};
|
||||
let viewMessageHandler_ = () => {};
|
||||
|
||||
// This silences a warning when running plugins generated with Webpack.
|
||||
window.exports ??= {};
|
||||
|
||||
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
|
||||
const webviewApi = {
|
||||
postMessage: function(message) {
|
||||
|
@@ -60,7 +60,8 @@ export const setNotesSortOrder = (field?: string, reverse?: boolean) => {
|
||||
nextReverse = !!nextReverse;
|
||||
if (perFieldReverse[nextField] !== nextReverse) {
|
||||
perFieldReverse[nextField] = nextReverse;
|
||||
Setting.setValue('notes.perFieldReverse', { ...perFieldReverse });
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Partial refactor of old code before rule was applied
|
||||
Setting.setValue('notes.perFieldReverse', { ...perFieldReverse } as any);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import Setting, { AppType } from '@joplin/lib/models/Setting';
|
||||
import bridge from '../bridge';
|
||||
import processStartFlags from '@joplin/lib/utils/processStartFlags';
|
||||
import { safeModeFlagFilename } from '@joplin/lib/BaseApplication';
|
||||
@@ -13,7 +13,7 @@ const restartInSafeModeFromMain = async () => {
|
||||
// a large amount of other code) to the database.
|
||||
const appName = bridge().appName();
|
||||
Setting.setConstant('appId', `net.cozic.${appName}`);
|
||||
Setting.setConstant('appType', 'desktop');
|
||||
Setting.setConstant('appType', AppType.Desktop);
|
||||
Setting.setConstant('appName', appName);
|
||||
|
||||
// Load just enough for us to write a file in the profile directory
|
||||
|
12
packages/app-mobile/.gitignore
vendored
@@ -66,14 +66,10 @@ yarn-error.log
|
||||
lib/csstojs/
|
||||
lib/rnInjectedJs/
|
||||
dist/
|
||||
plugins/sources/*
|
||||
plugins/PluginRunner/**/*.bundle.js
|
||||
components/NoteBodyViewer/**/*.bundle.js
|
||||
components/NoteBodyViewer/**/*.bundle.js.LICENSE.txt
|
||||
components/NoteEditor/**/*.bundle.js
|
||||
components/NoteEditor/**/*.bundle.js.md5
|
||||
components/NoteEditor/**/*.bundle.min.js
|
||||
components/NoteEditor/**/*.bundle.js.LICENSE.txt
|
||||
components/**/*.bundle.js
|
||||
components/**/*.bundle.js.LICENSE.txt
|
||||
components/**/*.bundle.js.md5
|
||||
components/**/*.bundle.min.js
|
||||
|
||||
utils/fs-driver-android.js
|
||||
android/app/build-*
|
||||
|
@@ -0,0 +1,83 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import {
|
||||
forwardRef, Ref, useEffect, useImperativeHandle, useMemo, useRef,
|
||||
} from 'react';
|
||||
|
||||
import { View } from 'react-native';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import { Props, WebViewControl } from './types';
|
||||
import { JSDOM } from 'jsdom';
|
||||
|
||||
const logger = Logger.create('ExtendedWebView');
|
||||
|
||||
const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
|
||||
const dom = useMemo(() => {
|
||||
// Note: Adding `runScripts: 'dangerously'` to allow running inline <script></script>s.
|
||||
// Use with caution.
|
||||
return new JSDOM(props.html, { runScripts: 'dangerously' });
|
||||
}, [props.html]);
|
||||
|
||||
useImperativeHandle(ref, (): WebViewControl => {
|
||||
const result = {
|
||||
injectJS(js: string) {
|
||||
return dom.window.eval(js);
|
||||
},
|
||||
postMessage(message: unknown) {
|
||||
const messageEventContent = {
|
||||
data: message,
|
||||
source: 'react-native',
|
||||
};
|
||||
return dom.window.eval(`
|
||||
window.dispatchEvent(
|
||||
new MessageEvent('message', ${JSON.stringify(messageEventContent)}),
|
||||
);
|
||||
`);
|
||||
},
|
||||
};
|
||||
return result;
|
||||
}, [dom]);
|
||||
|
||||
const onMessageRef = useRef(props.onMessage);
|
||||
onMessageRef.current = props.onMessage;
|
||||
|
||||
// Don't re-load when injected JS changes. This should match the behavior of the native webview.
|
||||
const injectedJavaScriptRef = useRef(props.injectedJavaScript);
|
||||
injectedJavaScriptRef.current = props.injectedJavaScript;
|
||||
|
||||
useEffect(() => {
|
||||
dom.window.eval(`
|
||||
window.setWebViewApi = (api) => {
|
||||
window.ReactNativeWebView = api;
|
||||
};
|
||||
`);
|
||||
dom.window.setWebViewApi({
|
||||
postMessage: (message: unknown) => {
|
||||
logger.debug('Got message', message);
|
||||
onMessageRef.current({ nativeEvent: { data: message } });
|
||||
},
|
||||
});
|
||||
|
||||
dom.window.eval(injectedJavaScriptRef.current);
|
||||
}, [dom]);
|
||||
|
||||
|
||||
const onLoadEndRef = useRef(props.onLoadEnd);
|
||||
onLoadEndRef.current = props.onLoadEnd;
|
||||
const onLoadStartRef = useRef(props.onLoadStart);
|
||||
onLoadStartRef.current = props.onLoadStart;
|
||||
|
||||
useEffect(() => {
|
||||
logger.debug(`DOM at ${dom.window?.location?.href} is reloading.`);
|
||||
onLoadStartRef.current?.();
|
||||
onLoadEndRef.current?.();
|
||||
}, [dom]);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- HACK: Allow wrapper testing logic to access the DOM.
|
||||
const additionalProps: any = { document: dom?.window?.document };
|
||||
return (
|
||||
<View style={props.style} testID={props.testID} {...additionalProps}/>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(ExtendedWebView);
|
@@ -5,73 +5,17 @@ import * as React from 'react';
|
||||
import {
|
||||
forwardRef, Ref, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState,
|
||||
} from 'react';
|
||||
import { WebView, WebViewMessageEvent } from 'react-native-webview';
|
||||
import { WebViewErrorEvent, WebViewEvent, WebViewSource } from 'react-native-webview/lib/WebViewTypes';
|
||||
import { WebView } from 'react-native-webview';
|
||||
import { WebViewErrorEvent, WebViewSource } from 'react-native-webview/lib/WebViewTypes';
|
||||
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { StyleProp, ViewStyle } from 'react-native';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import { Props, WebViewControl } from './types';
|
||||
|
||||
const logger = Logger.create('ExtendedWebView');
|
||||
|
||||
export interface WebViewControl {
|
||||
// Evaluate the given [script] in the context of the page.
|
||||
// Unlike react-native-webview/WebView, this does not need to return true.
|
||||
injectJS(script: string): void;
|
||||
|
||||
// message must be convertible to JSON
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
postMessage(message: any): void;
|
||||
}
|
||||
|
||||
interface SourceFileUpdateEvent {
|
||||
uri: string;
|
||||
baseUrl: string;
|
||||
|
||||
filePath: string;
|
||||
}
|
||||
|
||||
export type OnMessageCallback = (event: WebViewMessageEvent)=> void;
|
||||
export type OnErrorCallback = (event: WebViewErrorEvent)=> void;
|
||||
export type OnLoadCallback = (event: WebViewEvent)=> void;
|
||||
type OnFileUpdateCallback = (event: SourceFileUpdateEvent)=> void;
|
||||
|
||||
interface Props {
|
||||
// A name to be associated with the WebView (e.g. NoteEditor)
|
||||
// This name should be unique.
|
||||
webviewInstanceId: string;
|
||||
|
||||
// If HTML is still being loaded, [html] should be an empty string.
|
||||
html: string;
|
||||
|
||||
// Allow a secure origin to load content from any other origin.
|
||||
// Defaults to 'never'.
|
||||
// See react-native-webview's prop with the same name.
|
||||
mixedContentMode?: 'never' | 'always';
|
||||
|
||||
allowFileAccessFromJs?: boolean;
|
||||
hasPluginScripts?: boolean;
|
||||
|
||||
// Initial javascript. Must evaluate to true.
|
||||
injectedJavaScript: string;
|
||||
|
||||
// iOS only: Scroll the outer content of the view. Set this to `false` if
|
||||
// the main view container doesn't scroll.
|
||||
scrollEnabled?: boolean;
|
||||
|
||||
style?: StyleProp<ViewStyle>;
|
||||
onMessage: OnMessageCallback;
|
||||
onError?: OnErrorCallback;
|
||||
onLoadStart?: OnLoadCallback;
|
||||
onLoadEnd?: OnLoadCallback;
|
||||
|
||||
// Triggered when the file containing [html] is overwritten with new content.
|
||||
onFileUpdate?: OnFileUpdateCallback;
|
||||
|
||||
// Defaults to the resource directory
|
||||
baseDirectory?: string;
|
||||
}
|
||||
export { WebViewControl, Props };
|
||||
|
||||
const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
|
||||
const webviewRef = useRef(null);
|
||||
@@ -124,11 +68,6 @@ const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
|
||||
baseUrl,
|
||||
};
|
||||
setSource(newSource);
|
||||
|
||||
props.onFileUpdate?.({
|
||||
...newSource,
|
||||
filePath: tempFile,
|
||||
});
|
||||
}
|
||||
|
||||
if (props.html && props.html.length > 0) {
|
||||
@@ -140,7 +79,7 @@ const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [props.html, props.webviewInstanceId, props.onFileUpdate, baseDirectory, baseUrl]);
|
||||
}, [props.html, props.webviewInstanceId, baseDirectory, baseUrl]);
|
||||
|
||||
const onError = useCallback((event: WebViewErrorEvent) => {
|
||||
logger.error('Error', event.nativeEvent.description);
|
46
packages/app-mobile/components/ExtendedWebView/types.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { StyleProp, ViewStyle } from 'react-native';
|
||||
import { WebViewErrorEvent } from 'react-native-webview/lib/WebViewTypes';
|
||||
|
||||
export interface WebViewControl {
|
||||
// Evaluate the given [script] in the context of the page.
|
||||
// Unlike react-native-webview/WebView, this does not need to return true.
|
||||
injectJS(script: string): void;
|
||||
|
||||
// message must be convertible to JSON
|
||||
postMessage(message: unknown): void;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Needs to interface with old code from before rule was applied.
|
||||
export type OnMessageEvent = { nativeEvent: { data: any } };
|
||||
|
||||
export type OnMessageCallback = (event: OnMessageEvent)=> void;
|
||||
export type OnErrorCallback = (event: WebViewErrorEvent)=> void;
|
||||
export type OnLoadCallback = ()=> void;
|
||||
|
||||
export interface Props {
|
||||
// A name to be associated with the WebView (e.g. NoteEditor)
|
||||
// This name should be unique.
|
||||
webviewInstanceId: string;
|
||||
testID?: string;
|
||||
hasPluginScripts?: boolean;
|
||||
|
||||
// Forwarded to RN WebView
|
||||
scrollEnabled?: boolean;
|
||||
allowFileAccessFromJs?: boolean;
|
||||
mixedContentMode?: 'never'|'always';
|
||||
|
||||
// If HTML is still being loaded, [html] should be an empty string.
|
||||
html: string;
|
||||
|
||||
// Initial javascript. Must evaluate to true.
|
||||
injectedJavaScript: string;
|
||||
|
||||
style?: StyleProp<ViewStyle>;
|
||||
onMessage: OnMessageCallback;
|
||||
onError?: OnErrorCallback;
|
||||
onLoadStart?: OnLoadCallback;
|
||||
onLoadEnd?: OnLoadCallback;
|
||||
|
||||
// Defaults to the resource directory
|
||||
baseDirectory?: string;
|
||||
}
|
@@ -0,0 +1,181 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { describe, it, beforeEach } from '@jest/globals';
|
||||
import { render, screen, waitFor } from '@testing-library/react-native';
|
||||
import '@testing-library/jest-native/extend-expect';
|
||||
|
||||
|
||||
import NoteBodyViewer from './NoteBodyViewer';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { MenuProvider } from 'react-native-popup-menu';
|
||||
import { resourceFetcher, setupDatabaseAndSynchronizer, supportDir, switchClient, synchronizerStart } from '@joplin/lib/testing/test-utils';
|
||||
import { MarkupLanguage } from '@joplin/renderer';
|
||||
import { HandleMessageCallback, OnMarkForDownloadCallback } from './hooks/useOnMessage';
|
||||
import Resource from '@joplin/lib/models/Resource';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
|
||||
interface WrapperProps {
|
||||
noteBody: string;
|
||||
highlightedKeywords?: string[];
|
||||
noteResources?: unknown;
|
||||
onJoplinLinkClick?: HandleMessageCallback;
|
||||
onScroll?: (percent: number)=> void;
|
||||
onMarkForDownload?: OnMarkForDownloadCallback;
|
||||
}
|
||||
|
||||
const emptyObject = {};
|
||||
const emptyArray: string[] = [];
|
||||
const noOpFunction = () => {};
|
||||
const WrappedNoteViewer: React.FC<WrapperProps> = (
|
||||
{
|
||||
noteBody,
|
||||
highlightedKeywords = emptyArray,
|
||||
noteResources = emptyObject,
|
||||
onJoplinLinkClick = noOpFunction,
|
||||
onScroll = noOpFunction,
|
||||
onMarkForDownload,
|
||||
}: WrapperProps,
|
||||
) => {
|
||||
return <MenuProvider>
|
||||
<NoteBodyViewer
|
||||
themeId={Setting.THEME_LIGHT}
|
||||
style={emptyObject}
|
||||
noteBody={noteBody}
|
||||
noteMarkupLanguage={MarkupLanguage.Markdown}
|
||||
highlightedKeywords={highlightedKeywords}
|
||||
noteResources={noteResources}
|
||||
paddingBottom={0}
|
||||
initialScroll={0}
|
||||
noteHash={''}
|
||||
onJoplinLinkClick={onJoplinLinkClick}
|
||||
onMarkForDownload={onMarkForDownload}
|
||||
onScroll={onScroll}
|
||||
pluginStates={emptyObject}
|
||||
/>
|
||||
</MenuProvider>;
|
||||
};
|
||||
|
||||
const getNoteViewerDom = async (): Promise<Document> => {
|
||||
const webviewContent = await screen.findByTestId('NoteBodyViewer');
|
||||
expect(webviewContent).toBeVisible();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(!!webviewContent.props.document).toBe(true);
|
||||
});
|
||||
|
||||
// Return the composite ExtendedWebView component
|
||||
// See https://callstack.github.io/react-native-testing-library/docs/advanced/testing-env#tree-navigation
|
||||
return webviewContent.props.document;
|
||||
};
|
||||
|
||||
describe('NoteBodyViewer', () => {
|
||||
beforeEach(async () => {
|
||||
await setupDatabaseAndSynchronizer(0);
|
||||
await switchClient(0);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
screen.unmount();
|
||||
});
|
||||
|
||||
it('should render markdown and re-render on change', async () => {
|
||||
render(<WrappedNoteViewer noteBody='# Test'/>);
|
||||
|
||||
const expectHeaderToBe = async (text: string) => {
|
||||
const noteViewer = await getNoteViewerDom();
|
||||
await waitFor(async () => {
|
||||
expect(noteViewer.querySelector('h1').textContent).toBe(text);
|
||||
});
|
||||
};
|
||||
|
||||
await expectHeaderToBe('Test');
|
||||
screen.rerender(<WrappedNoteViewer noteBody='# Test 2'/>);
|
||||
await expectHeaderToBe('Test 2');
|
||||
screen.rerender(<WrappedNoteViewer noteBody='# Test 3'/>);
|
||||
await expectHeaderToBe('Test 3');
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ keywords: ['match'], body: 'A match and another match. Both should be highlighted.', expectedMatchCount: 2 },
|
||||
{ keywords: ['test'], body: 'No match.', expectedMatchCount: 0 },
|
||||
{ keywords: ['a', 'b'], body: 'a, a, a, b, b, b', expectedMatchCount: 6 },
|
||||
])('should highlight search terms (case %#)', async ({ keywords, body, expectedMatchCount }) => {
|
||||
render(
|
||||
<WrappedNoteViewer
|
||||
highlightedKeywords={keywords}
|
||||
noteBody={body}
|
||||
/>,
|
||||
);
|
||||
|
||||
let noteViewerDom = await getNoteViewerDom();
|
||||
await waitFor(() => {
|
||||
expect(noteViewerDom.querySelectorAll('.highlighted-keyword')).toHaveLength(expectedMatchCount);
|
||||
});
|
||||
|
||||
// Should update highlights when the keywords change
|
||||
screen.rerender(
|
||||
<WrappedNoteViewer
|
||||
highlightedKeywords={[]}
|
||||
noteBody={body}
|
||||
/>,
|
||||
);
|
||||
noteViewerDom = await getNoteViewerDom();
|
||||
await waitFor(() => {
|
||||
expect(noteViewerDom.querySelectorAll('.highlighted-keyword')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('tapping on resource download icons should mark the resources for download', async () => {
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await switchClient(1);
|
||||
|
||||
let note1 = await Note.save({ title: 'Note 1', parent_id: '' });
|
||||
note1 = await shim.attachFileToNote(note1, `${supportDir}/photo.jpg`);
|
||||
|
||||
await synchronizerStart();
|
||||
await switchClient(0);
|
||||
Setting.setValue('sync.resourceDownloadMode', 'manual');
|
||||
await synchronizerStart();
|
||||
|
||||
const allResources = await Resource.all();
|
||||
expect(allResources.length).toBe(1);
|
||||
const localResource = allResources[0];
|
||||
const localState = await Resource.localState(localResource);
|
||||
expect(localState.fetch_status).toBe(Resource.FETCH_STATUS_IDLE);
|
||||
|
||||
const onMarkForDownload: OnMarkForDownloadCallback = jest.fn(({ resourceId }) => {
|
||||
return resourceFetcher().markForDownload([resourceId]);
|
||||
});
|
||||
render(
|
||||
<WrappedNoteViewer
|
||||
noteBody={note1.body}
|
||||
noteResources={{ [localResource.id]: { localState, item: localResource } }}
|
||||
onMarkForDownload={onMarkForDownload}
|
||||
/>,
|
||||
);
|
||||
|
||||
// The resource placeholder should have rendered
|
||||
const noteViewerDom = await getNoteViewerDom();
|
||||
let resourcePlaceholder: HTMLElement|null = null;
|
||||
await waitFor(() => {
|
||||
const placeholders = noteViewerDom.querySelectorAll<HTMLElement>(`[data-resource-id=${JSON.stringify(localResource.id)}]`);
|
||||
expect(placeholders).toHaveLength(1);
|
||||
resourcePlaceholder = placeholders[0];
|
||||
});
|
||||
|
||||
expect([...resourcePlaceholder.classList]).toContain('resource-status-notDownloaded');
|
||||
|
||||
// Clicking on the placeholder should download its resource
|
||||
await waitFor(() => {
|
||||
resourcePlaceholder.click();
|
||||
expect(onMarkForDownload).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
await resourceFetcher().waitForAllFinished();
|
||||
|
||||
await waitFor(async () => {
|
||||
expect(await Resource.localState(localResource.id)).toMatchObject({ fetch_status: Resource.FETCH_STATUS_DONE });
|
||||
});
|
||||
});
|
||||
});
|
@@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
|
||||
import useOnMessage, { HandleMessageCallback, OnMarkForDownloadCallback } from './hooks/useOnMessage';
|
||||
import { useRef, useCallback, useState, useMemo } from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { View, ViewStyle } from 'react-native';
|
||||
import BackButtonDialogBox from '../BackButtonDialogBox';
|
||||
import ExtendedWebView, { WebViewControl } from '../ExtendedWebView';
|
||||
import useOnResourceLongPress from './hooks/useOnResourceLongPress';
|
||||
@@ -14,13 +14,13 @@ import Setting from '@joplin/lib/models/Setting';
|
||||
import uuid from '@joplin/lib/uuid';
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import useContentScripts from './hooks/useContentScripts';
|
||||
import { MarkupLanguage } from '@joplin/renderer';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
style: any;
|
||||
style: ViewStyle;
|
||||
noteBody: string;
|
||||
noteMarkupLanguage: number;
|
||||
noteMarkupLanguage: MarkupLanguage;
|
||||
highlightedKeywords: string[];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
noteResources: any;
|
||||
@@ -111,6 +111,7 @@ export default function NoteBodyViewer(props: Props) {
|
||||
<ExtendedWebView
|
||||
ref={webviewRef}
|
||||
webviewInstanceId='NoteBodyViewer'
|
||||
testID='NoteBodyViewer'
|
||||
html={html}
|
||||
allowFileAccessFromJs={true}
|
||||
injectedJavaScript={injectedJs}
|
||||
|
@@ -1,3 +1,4 @@
|
||||
/** @jest-environment jsdom */
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import Renderer, { RendererSettings, RendererSetupOptions } from './Renderer';
|
||||
import shim from '@joplin/lib/shim';
|
||||
|
@@ -6,6 +6,7 @@ import Renderer from './Renderer';
|
||||
declare global {
|
||||
interface Window {
|
||||
rendererWebViewOptions: RendererWebViewOptions;
|
||||
webviewLib: { postMessage: (message: string)=> void };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +33,8 @@ webviewLib.initialize({
|
||||
messenger.remoteApi.onPostMessage(message);
|
||||
},
|
||||
});
|
||||
// Share the webview library globally so that the renderer can access it.
|
||||
window.webviewLib = webviewLib;
|
||||
|
||||
const renderer = new Renderer({
|
||||
...window.rendererWebViewOptions,
|
||||
@@ -58,5 +61,5 @@ const onMainContentScroll = () => {
|
||||
// scroll. However, window.addEventListener('scroll', callback) does.
|
||||
// - iOS needs a listener to be added to scrollingElement -- events aren't received when
|
||||
// the listener is added to window with window.addEventListener('scroll', ...).
|
||||
document.scrollingElement.addEventListener('scroll', onMainContentScroll);
|
||||
document.scrollingElement?.addEventListener('scroll', onMainContentScroll);
|
||||
window.addEventListener('scroll', onMainContentScroll);
|
||||
|
@@ -59,8 +59,8 @@ const useContentScripts = (pluginStates: PluginStates) => {
|
||||
if (event.cancelled) return;
|
||||
|
||||
const contentScriptModule = `(function () {
|
||||
const module = { exports: null };
|
||||
const exports = {};
|
||||
const module = { exports: exports };
|
||||
|
||||
${content}
|
||||
|
||||
|
@@ -19,7 +19,10 @@ const getEditIconSrc = (theme: Theme) => {
|
||||
// Copy in the background -- the edit icon popover script doesn't need the
|
||||
// icon immediately.
|
||||
void (async () => {
|
||||
await shim.fsDriver().copy(iconUri, destPath);
|
||||
// Can be '' in a testing environment.
|
||||
if (iconUri) {
|
||||
await shim.fsDriver().copy(iconUri, destPath);
|
||||
}
|
||||
})();
|
||||
|
||||
return destPath;
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Dispatch, RefObject, SetStateAction, useEffect, useMemo } from 'react';
|
||||
import { Dispatch, RefObject, SetStateAction, useEffect, useMemo, useRef } from 'react';
|
||||
import { WebViewControl } from '../../ExtendedWebView';
|
||||
import { OnScrollCallback, OnWebViewMessageHandler } from '../types';
|
||||
import RNToWebViewMessenger from '../../../utils/ipc/RNToWebViewMessenger';
|
||||
@@ -35,11 +35,17 @@ const onPostPluginMessage = async (contentScriptId: string, message: any) => {
|
||||
};
|
||||
|
||||
const useRenderer = (props: Props) => {
|
||||
const onScrollRef = useRef(props.onScroll);
|
||||
onScrollRef.current = props.onScroll;
|
||||
|
||||
const onPostMessageRef = useRef(props.onPostMessage);
|
||||
onPostMessageRef.current = props.onPostMessage;
|
||||
|
||||
const messenger = useMemo(() => {
|
||||
const fsDriver = shim.fsDriver();
|
||||
const localApi = {
|
||||
onScroll: props.onScroll,
|
||||
onPostMessage: props.onPostMessage,
|
||||
onScroll: (fraction: number) => onScrollRef.current?.(fraction),
|
||||
onPostMessage: (message: string) => onPostMessageRef.current?.(message),
|
||||
onPostPluginMessage,
|
||||
fsDriver: {
|
||||
writeFile: async (path: string, content: string, encoding?: string) => {
|
||||
@@ -58,7 +64,7 @@ const useRenderer = (props: Props) => {
|
||||
return new RNToWebViewMessenger<NoteViewerRemoteApi, NoteViewerLocalApi>(
|
||||
'note-viewer', props.webviewRef, localApi,
|
||||
);
|
||||
}, [props.onScroll, props.onPostMessage, props.webviewRef, props.tempDir]);
|
||||
}, [props.webviewRef, props.tempDir]);
|
||||
|
||||
useEffect(() => {
|
||||
props.setOnWebViewMessage(() => (event: WebViewMessageEvent) => {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { WebViewMessageEvent } from 'react-native-webview';
|
||||
import { OnMessageEvent } from '../ExtendedWebView/types';
|
||||
|
||||
export type OnScrollCallback = (scrollTop: number)=> void;
|
||||
export type OnWebViewMessageHandler = (event: WebViewMessageEvent)=> void;
|
||||
export type OnWebViewMessageHandler = (event: OnMessageEvent)=> void;
|
||||
|
@@ -27,6 +27,21 @@ export const initCodeMirror = (
|
||||
initialText,
|
||||
settings,
|
||||
|
||||
onPasteFile: async (data) => {
|
||||
const reader = new FileReader();
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
reader.onload = async () => {
|
||||
const dataUrl = reader.result as string;
|
||||
const base64 = dataUrl.replace(/^data:.*;base64,/, '');
|
||||
await messenger.remoteApi.onPasteFile(data.type, base64);
|
||||
resolve();
|
||||
};
|
||||
reader.onerror = () => reject(new Error('Failed to load file.'));
|
||||
|
||||
reader.readAsDataURL(data);
|
||||
});
|
||||
},
|
||||
|
||||
onLogMessage: message => {
|
||||
void messenger.remoteApi.logMessage(message);
|
||||
},
|
||||
|
@@ -7,11 +7,11 @@ import { themeStyle } from '@joplin/lib/theme';
|
||||
import { Theme } from '@joplin/lib/themes/type';
|
||||
import { MutableRefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Alert, BackHandler } from 'react-native';
|
||||
import { WebViewMessageEvent } from 'react-native-webview';
|
||||
import ExtendedWebView, { WebViewControl } from '../../ExtendedWebView';
|
||||
import { clearAutosave, writeAutosave } from './autosave';
|
||||
import { LocalizedStrings } from './js-draw/types';
|
||||
import VersionInfo from 'react-native-version-info';
|
||||
import { OnMessageEvent } from '../../ExtendedWebView/types';
|
||||
|
||||
|
||||
const logger = Logger.create('ImageEditor');
|
||||
@@ -280,7 +280,7 @@ const ImageEditor = (props: Props) => {
|
||||
})();`);
|
||||
}, [webviewRef, props.resourceFilename]);
|
||||
|
||||
const onMessage = useCallback(async (event: WebViewMessageEvent) => {
|
||||
const onMessage = useCallback(async (event: OnMessageEvent) => {
|
||||
const data = event.nativeEvent.data;
|
||||
if (data.startsWith('error:')) {
|
||||
logger.error('ImageEditor:', data);
|
||||
|
@@ -38,7 +38,7 @@ describe('NoteEditor', () => {
|
||||
onChange={()=>{}}
|
||||
onSelectionChange={()=>{}}
|
||||
onUndoRedoDepthChange={()=>{}}
|
||||
onAttach={()=>{}}
|
||||
onAttach={async ()=>{}}
|
||||
plugins={{}}
|
||||
/>
|
||||
</MenuProvider>,
|
||||
@@ -78,5 +78,7 @@ describe('NoteEditor', () => {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
wrappedNoteEditor.unmount();
|
||||
});
|
||||
});
|
||||
|
@@ -21,16 +21,19 @@ import { EditorCommandType, EditorKeymap, EditorLanguageType, SearchState } from
|
||||
import SelectionFormatting, { defaultSelectionFormatting } from '@joplin/editor/SelectionFormatting';
|
||||
import useCodeMirrorPlugins from './hooks/useCodeMirrorPlugins';
|
||||
import RNToWebViewMessenger from '../../utils/ipc/RNToWebViewMessenger';
|
||||
import { WebViewMessageEvent } from 'react-native-webview';
|
||||
import { WebViewErrorEvent } from 'react-native-webview/lib/RNCWebViewNativeComponent';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import useEditorCommandHandler from './hooks/useEditorCommandHandler';
|
||||
import { OnMessageEvent } from '../ExtendedWebView/types';
|
||||
import { join, dirname } from 'path';
|
||||
import * as mimeUtils from '@joplin/lib/mime-utils';
|
||||
import uuid from '@joplin/lib/uuid';
|
||||
|
||||
type ChangeEventHandler = (event: ChangeEvent)=> void;
|
||||
type UndoRedoDepthChangeHandler = (event: UndoRedoDepthChangeEvent)=> void;
|
||||
type SelectionChangeEventHandler = (event: SelectionRangeChangeEvent)=> void;
|
||||
type OnAttachCallback = ()=> void;
|
||||
type OnAttachCallback = (filePath?: string)=> Promise<void>;
|
||||
|
||||
const logger = Logger.create('NoteEditor');
|
||||
|
||||
@@ -50,7 +53,7 @@ interface Props {
|
||||
}
|
||||
|
||||
function fontFamilyFromSettings() {
|
||||
const font = editorFont(Setting.value('style.editor.fontFamily'));
|
||||
const font = editorFont(Setting.value('style.editor.fontFamily') as number);
|
||||
return font ? `${font}, sans-serif` : 'sans-serif';
|
||||
}
|
||||
|
||||
@@ -373,6 +376,9 @@ function NoteEditor(props: Props, ref: any) {
|
||||
|
||||
const onEditorEvent = useRef((_event: EditorEvent) => {});
|
||||
|
||||
const onAttachRef = useRef(props.onAttach);
|
||||
onAttachRef.current = props.onAttach;
|
||||
|
||||
const editorMessenger = useMemo(() => {
|
||||
const localApi: WebViewToEditorApi = {
|
||||
async onEditorEvent(event) {
|
||||
@@ -381,6 +387,16 @@ function NoteEditor(props: Props, ref: any) {
|
||||
async logMessage(message) {
|
||||
logger.debug('CodeMirror:', message);
|
||||
},
|
||||
async onPasteFile(type, data) {
|
||||
const tempFilePath = join(Setting.value('tempDir'), `paste.${uuid.createNano()}.${mimeUtils.toFileExtension(type)}`);
|
||||
await shim.fsDriver().mkdir(dirname(tempFilePath));
|
||||
try {
|
||||
await shim.fsDriver().writeFile(tempFilePath, data, 'base64');
|
||||
await onAttachRef.current(tempFilePath);
|
||||
} finally {
|
||||
await shim.fsDriver().remove(tempFilePath);
|
||||
}
|
||||
},
|
||||
};
|
||||
const messenger = new RNToWebViewMessenger<WebViewToEditorApi, EditorBodyControl>(
|
||||
'editor', webviewRef, localApi,
|
||||
@@ -450,7 +466,7 @@ function NoteEditor(props: Props, ref: any) {
|
||||
editorMessenger.onWebViewLoaded();
|
||||
}, [editorMessenger]);
|
||||
|
||||
const onMessage = useCallback((event: WebViewMessageEvent) => {
|
||||
const onMessage = useCallback((event: OnMessageEvent) => {
|
||||
const data = event.nativeEvent.data;
|
||||
|
||||
if (data.indexOf('error:') === 0) {
|
||||
|
@@ -1,3 +1,5 @@
|
||||
/** @jest-environment jsdom */
|
||||
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import useEditorCommandHandler from './useEditorCommandHandler';
|
||||
import commandDeclarations from '../commandDeclarations';
|
||||
|
@@ -57,4 +57,5 @@ export interface SelectionRange {
|
||||
export interface WebViewToEditorApi {
|
||||
onEditorEvent(event: EditorEvent): Promise<void>;
|
||||
logMessage(message: string): Promise<void>;
|
||||
onPasteFile(type: string, dataBase64: string): Promise<void>;
|
||||
}
|
||||
|
@@ -1,18 +1,18 @@
|
||||
import BasePluginRunner from '@joplin/lib/services/plugins/BasePluginRunner';
|
||||
import PluginApiGlobal from '@joplin/lib/services/plugins/api/Global';
|
||||
import Plugin from '@joplin/lib/services/plugins/Plugin';
|
||||
import { WebViewControl } from '../../components/ExtendedWebView';
|
||||
import { WebViewControl } from '../ExtendedWebView';
|
||||
import { RefObject } from 'react';
|
||||
import { WebViewMessageEvent } from 'react-native-webview';
|
||||
import RNToWebViewMessenger from '../../utils/ipc/RNToWebViewMessenger';
|
||||
import { PluginMainProcessApi, PluginWebViewApi } from './types';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import createOnLogHander from './utils/createOnLogHandler';
|
||||
import { OnMessageEvent } from '../ExtendedWebView/types';
|
||||
|
||||
const logger = Logger.create('PluginRunner');
|
||||
|
||||
type MessageEventListener = (event: WebViewMessageEvent)=> boolean;
|
||||
type MessageEventListener = (event: OnMessageEvent)=> boolean;
|
||||
|
||||
export default class PluginRunner extends BasePluginRunner {
|
||||
private messageEventListeners: MessageEventListener[] = [];
|
||||
@@ -71,7 +71,7 @@ export default class PluginRunner extends BasePluginRunner {
|
||||
`);
|
||||
}
|
||||
|
||||
public onWebviewMessage = (event: WebViewMessageEvent) => {
|
||||
public onWebviewMessage = (event: OnMessageEvent) => {
|
||||
this.messageEventListeners = this.messageEventListeners.filter(
|
||||
// Remove all listeners that return false
|
||||
listener => listener(event),
|
@@ -1,10 +1,9 @@
|
||||
|
||||
import * as React from 'react';
|
||||
import ExtendedWebView, { WebViewControl } from '../../components/ExtendedWebView';
|
||||
import ExtendedWebView, { WebViewControl } from '../ExtendedWebView';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import PluginRunner from './PluginRunner';
|
||||
import loadPlugins from '../loadPlugins';
|
||||
import loadPlugins from '@joplin/lib/services/plugins/loadPlugins';
|
||||
import { connect, useStore } from 'react-redux';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import { View } from 'react-native';
|
||||
@@ -14,6 +13,7 @@ import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
|
||||
import PluginDialogManager from './dialogs/PluginDialogManager';
|
||||
import { AppState } from '../../utils/types';
|
||||
import usePrevious from '@joplin/lib/hooks/usePrevious';
|
||||
import PlatformImplementation from '../../services/plugins/PlatformImplementation';
|
||||
|
||||
const logger = Logger.create('PluginRunnerWebView');
|
||||
|
||||
@@ -42,7 +42,14 @@ const usePlugins = (
|
||||
return;
|
||||
}
|
||||
|
||||
await loadPlugins({ pluginRunner, pluginSettings, store, reloadAll: reloadAllRef.current, cancelEvent: event });
|
||||
await loadPlugins({
|
||||
pluginRunner,
|
||||
pluginSettings,
|
||||
platformImplementation: PlatformImplementation.instance(),
|
||||
store,
|
||||
reloadAll: reloadAllRef.current,
|
||||
cancelEvent: event,
|
||||
});
|
||||
|
||||
// A full reload, if it was necessary, has been completed.
|
||||
if (!event.cancelled) {
|
@@ -1,3 +1,4 @@
|
||||
/** @jest-environment jsdom */
|
||||
import getFormData from './getFormData';
|
||||
|
||||
describe('getFormData', () => {
|
@@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { PluginHtmlContents, PluginStates, ViewInfo } from '@joplin/lib/services/plugins/reducer';
|
||||
import { StyleSheet, View, useWindowDimensions } from 'react-native';
|
||||
import usePlugin from '../../hooks/usePlugin';
|
||||
import usePlugin from '@joplin/lib/hooks/usePlugin';
|
||||
import { DialogContentSize, DialogWebViewApi } from '../types';
|
||||
import { Button } from 'react-native-paper';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
@@ -1,9 +1,9 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { PluginHtmlContents, ViewInfo } from '@joplin/lib/services/plugins/reducer';
|
||||
import ExtendedWebView, { WebViewControl } from '../../../components/ExtendedWebView';
|
||||
import ExtendedWebView, { WebViewControl } from '../../ExtendedWebView';
|
||||
import { ViewStyle } from 'react-native';
|
||||
import usePlugin from '../../hooks/usePlugin';
|
||||
import usePlugin from '@joplin/lib/hooks/usePlugin';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import useDialogMessenger from './hooks/useDialogMessenger';
|
||||
import useWebViewSetup from './hooks/useWebViewSetup';
|
@@ -1,7 +1,7 @@
|
||||
import { useMemo, RefObject } from 'react';
|
||||
import { DialogMainProcessApi, DialogWebViewApi } from '../../types';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import { WebViewControl } from '../../../../components/ExtendedWebView';
|
||||
import { WebViewControl } from '../../../ExtendedWebView';
|
||||
import createOnLogHander from '../../utils/createOnLogHandler';
|
||||
import RNToWebViewMessenger from '../../../../utils/ipc/RNToWebViewMessenger';
|
||||
import { SerializableData } from '@joplin/lib/utils/ipc/types';
|
@@ -475,7 +475,7 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
|
||||
}
|
||||
|
||||
const settingComp = this.settingToComponent(md.key, settings[md.key]);
|
||||
const relatedText = [md.label?.() ?? '', md.description?.() ?? ''];
|
||||
const relatedText = [md.label?.() ?? '', md.description?.(AppType.Mobile) ?? ''];
|
||||
addSettingComponent(
|
||||
settingComp,
|
||||
relatedText,
|
||||
@@ -830,8 +830,11 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
|
||||
}
|
||||
|
||||
let screenHeadingText = _('Configuration');
|
||||
let showSearchButton = true;
|
||||
|
||||
if (currentSectionName) {
|
||||
screenHeadingText = Setting.sectionNameToLabel(currentSectionName);
|
||||
showSearchButton = currentSectionName !== 'plugins';
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -839,7 +842,7 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
|
||||
<ScreenHeader
|
||||
title={screenHeadingText}
|
||||
showSaveButton={true}
|
||||
showSearchButton={true}
|
||||
showSearchButton={showSearchButton}
|
||||
showSideMenuButton={false}
|
||||
saveButtonDisabled={!this.hasUnsavedChanges()}
|
||||
onSaveButtonPress={this.saveButton_press}
|
||||
|
@@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
|
||||
import { UpdateSettingValueCallback } from './types';
|
||||
import { View, Text, TextInput } from 'react-native';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import Setting, { AppType } from '@joplin/lib/models/Setting';
|
||||
import Dropdown from '../../Dropdown';
|
||||
import { ConfigScreenStyles } from './configScreenStyles';
|
||||
import Slider from '@react-native-community/slider';
|
||||
@@ -32,7 +32,7 @@ const SettingComponent: React.FunctionComponent<Props> = props => {
|
||||
const output: any = null;
|
||||
|
||||
const md = Setting.settingMetadata(props.settingId);
|
||||
const settingDescription = md.description ? md.description() : '';
|
||||
const settingDescription = md.description ? md.description(AppType.Mobile) : '';
|
||||
|
||||
const styleSheet = props.styles.styleSheet;
|
||||
|
||||
|
@@ -52,6 +52,8 @@ const backlinksPluginId = 'joplin.plugin.ambrt.backlinksToNote';
|
||||
|
||||
describe('PluginStates.installed', () => {
|
||||
beforeEach(async () => {
|
||||
jest.useRealTimers();
|
||||
|
||||
await setupDatabaseAndSynchronizer(0);
|
||||
await switchClient(0);
|
||||
reduxStore = createMockReduxStore();
|
||||
@@ -60,6 +62,9 @@ describe('PluginStates.installed', () => {
|
||||
|
||||
await mockMobilePlatform('android');
|
||||
await mockRepositoryApiConstructor();
|
||||
|
||||
// Fake timers are necessary to prevent a warning.
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
afterEach(async () => {
|
||||
for (const pluginId of PluginService.instance().pluginIds) {
|
||||
@@ -228,7 +233,7 @@ describe('PluginStates.installed', () => {
|
||||
|
||||
// After updating, the update button should read "updated". Use a large
|
||||
// timeout because updating plugins can be slow, particularly in CI.
|
||||
const updatedButton = await screen.findByRole('button', { name: 'Updated', disabled: true, timeout: 16000 });
|
||||
const updatedButton = await screen.findByRole('button', { name: 'Updated', disabled: true }, { timeout: 16000 });
|
||||
expect(updatedButton).toBeVisible();
|
||||
|
||||
// Should be marked as updated.
|
||||
|
@@ -34,6 +34,7 @@ let reduxStore: Store<AppState>;
|
||||
|
||||
describe('PluginStates.search', () => {
|
||||
beforeEach(async () => {
|
||||
jest.useRealTimers();
|
||||
await setupDatabaseAndSynchronizer(0);
|
||||
await switchClient(0);
|
||||
reduxStore = createMockReduxStore();
|
||||
@@ -42,6 +43,7 @@ describe('PluginStates.search', () => {
|
||||
resetRepoApi();
|
||||
|
||||
await mockRepositoryApiConstructor();
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
it('should find results', async () => {
|
||||
|
@@ -2,7 +2,7 @@ import { PluginItem } from '@joplin/lib/components/shared/config/plugins/types';
|
||||
import { PluginSettings } from '@joplin/lib/services/plugins/PluginService';
|
||||
import { PluginManifest } from '@joplin/lib/services/plugins/utils/types';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import usePlugin from '../../../../../plugins/hooks/usePlugin';
|
||||
import usePlugin from '@joplin/lib/hooks/usePlugin';
|
||||
|
||||
// initialItem is used when the plugin is not installed. For example, if the plugin item is being
|
||||
// created from search results.
|
||||
|
@@ -6,10 +6,9 @@ import UndoRedoService from '@joplin/lib/services/UndoRedoService';
|
||||
import NoteBodyViewer from '../NoteBodyViewer/NoteBodyViewer';
|
||||
import checkPermissions from '../../utils/checkPermissions';
|
||||
import NoteEditor from '../NoteEditor/NoteEditor';
|
||||
import { Size } from '@joplin/utils/types';
|
||||
const FileViewer = require('react-native-file-viewer').default;
|
||||
const React = require('react');
|
||||
import { Keyboard, View, TextInput, StyleSheet, Linking, Image, Share, NativeSyntheticEvent } from 'react-native';
|
||||
import { Keyboard, View, TextInput, StyleSheet, Linking, Share, NativeSyntheticEvent } from 'react-native';
|
||||
import { Platform, PermissionsAndroid } from 'react-native';
|
||||
const { connect } = require('react-redux');
|
||||
// const { MarkdownEditor } = require('@joplin/lib/../MarkdownEditor/index.js');
|
||||
@@ -36,7 +35,6 @@ import { BaseScreenComponent } from '../base-screen';
|
||||
import { themeStyle, editorFont } from '../global-style';
|
||||
const { dialogs } = require('../../utils/dialogs.js');
|
||||
const DialogBox = require('react-native-dialogbox').default;
|
||||
import ImageResizer from '@bam.tech/react-native-image-resizer';
|
||||
import shared, { BaseNoteScreenComponent } from '@joplin/lib/components/shared/note-screen-shared';
|
||||
import { Asset, ImagePickerResponse, launchImageLibrary } from 'react-native-image-picker';
|
||||
import SelectDateTimeDialog from '../SelectDateTimeDialog';
|
||||
@@ -65,6 +63,8 @@ import debounce from '../../utils/debounce';
|
||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import * as urlUtils from '@joplin/lib/urlUtils';
|
||||
import getImageDimensions from '../../utils/image/getImageDimensions';
|
||||
import resizeImage from '../../utils/image/resizeImage';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const emptyArray: any[] = [];
|
||||
@@ -682,24 +682,9 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B
|
||||
return result;
|
||||
}
|
||||
|
||||
public async imageDimensions(uri: string): Promise<Size> {
|
||||
return new Promise((resolve, reject) => {
|
||||
Image.getSize(
|
||||
uri,
|
||||
(width: number, height: number) => {
|
||||
resolve({ width: width, height: height });
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
(error: any) => {
|
||||
reject(error);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public async resizeImage(localFilePath: string, targetPath: string, mimeType: string) {
|
||||
const maxSize = Resource.IMAGE_MAX_DIMENSION;
|
||||
const dimensions = await this.imageDimensions(localFilePath);
|
||||
const dimensions = await getImageDimensions(localFilePath);
|
||||
reg.logger().info('Original dimensions ', dimensions);
|
||||
|
||||
const saveOriginalImage = async () => {
|
||||
@@ -711,30 +696,14 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B
|
||||
dimensions.height = maxSize;
|
||||
reg.logger().info('New dimensions ', dimensions);
|
||||
|
||||
const format = mimeType === 'image/png' ? 'PNG' : 'JPEG';
|
||||
reg.logger().info(`Resizing image ${localFilePath}`);
|
||||
const resizedImage = await ImageResizer.createResizedImage(
|
||||
localFilePath,
|
||||
dimensions.width,
|
||||
dimensions.height,
|
||||
format,
|
||||
85, // quality
|
||||
undefined, // rotation
|
||||
undefined, // outputPath
|
||||
true, // keep metadata
|
||||
);
|
||||
|
||||
const resizedImagePath = resizedImage.uri;
|
||||
reg.logger().info('Resized image ', resizedImagePath);
|
||||
reg.logger().info(`Moving ${resizedImagePath} => ${targetPath}`);
|
||||
|
||||
await shim.fsDriver().copy(resizedImagePath, targetPath);
|
||||
|
||||
try {
|
||||
await shim.fsDriver().unlink(resizedImagePath);
|
||||
} catch (error) {
|
||||
reg.logger().warn('Error when unlinking cached file: ', error);
|
||||
}
|
||||
await resizeImage({
|
||||
inputPath: localFilePath,
|
||||
outputPath: targetPath,
|
||||
maxWidth: dimensions.width,
|
||||
maxHeight: dimensions.height,
|
||||
quality: 85,
|
||||
format: mimeType === 'image/png' ? 'PNG' : 'JPEG',
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -1140,11 +1109,19 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B
|
||||
|
||||
const buttonId = await dialogs.pop(this, _('Choose an option'), buttons);
|
||||
|
||||
if (buttonId === 'takePhoto') this.takePhoto_onPress();
|
||||
if (buttonId === 'attachFile') void this.attachFile_onPress();
|
||||
if (buttonId === 'attachPhoto') void this.attachPhoto_onPress();
|
||||
if (buttonId === 'takePhoto') await this.takePhoto_onPress();
|
||||
if (buttonId === 'attachFile') await this.attachFile_onPress();
|
||||
if (buttonId === 'attachPhoto') await this.attachPhoto_onPress();
|
||||
}
|
||||
|
||||
public onAttach = async (filePath?: string) => {
|
||||
if (filePath) {
|
||||
await this.attachFile({ uri: filePath }, 'all');
|
||||
} else {
|
||||
await this.showAttachMenu();
|
||||
}
|
||||
};
|
||||
|
||||
// private vosk_:Vosk;
|
||||
|
||||
// private async getVosk() {
|
||||
@@ -1585,7 +1562,7 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B
|
||||
onChange={this.onMarkdownEditorTextChange}
|
||||
onSelectionChange={this.onMarkdownEditorSelectionChange}
|
||||
onUndoRedoDepthChange={this.onUndoRedoDepthChange}
|
||||
onAttach={() => this.showAttachMenu()}
|
||||
onAttach={this.onAttach}
|
||||
readOnly={this.state.readOnly}
|
||||
plugins={this.props.plugins}
|
||||
style={{
|
||||
|
@@ -12,7 +12,7 @@ module.exports = {
|
||||
'\\.(ts|tsx)$': 'ts-jest',
|
||||
},
|
||||
|
||||
testEnvironment: 'jsdom',
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/*.test.(ts|tsx)'],
|
||||
|
||||
testPathIgnorePatterns: ['<rootDir>/node_modules/'],
|
||||
@@ -20,7 +20,7 @@ module.exports = {
|
||||
|
||||
// Do transform most packages in node_modules (transformations correct unrecognized
|
||||
// import syntax)
|
||||
transformIgnorePatterns: ['<rootDir>/node_modules/jest', '<rootDir>/node_modules/js-draw'],
|
||||
transformIgnorePatterns: ['<rootDir>/node_modules/jest', '<rootDir>/node_modules/js-draw', 'node_modules/jsdom'],
|
||||
|
||||
slowTestThreshold: 40,
|
||||
};
|
||||
|
@@ -1,9 +1,12 @@
|
||||
/* eslint-disable jest/require-top-level-describe */
|
||||
|
||||
const { afterEachCleanUp, afterAllCleanUp } = require('@joplin/lib/testing/test-utils.js');
|
||||
const shim = require('@joplin/lib/shim').default;
|
||||
const { shimInit } = require('@joplin/lib/shim-init-node.js');
|
||||
const injectedJs = require('./utils/injectedJs.js').default;
|
||||
const { mkdir, rm } = require('fs-extra');
|
||||
const path = require('path');
|
||||
const sharp = require('sharp');
|
||||
const { tmpdir } = require('os');
|
||||
const uuid = require('@joplin/lib/uuid').default;
|
||||
const sqlite3 = require('sqlite3');
|
||||
@@ -19,7 +22,14 @@ window.setImmediate = setImmediate;
|
||||
shimInit({
|
||||
nodeSqlite: sqlite3,
|
||||
React,
|
||||
sharp,
|
||||
});
|
||||
shim.injectedJs = (name) => {
|
||||
if (!(name in injectedJs)) {
|
||||
throw new Error(`Cannot find injected JS with ID ${name}`);
|
||||
}
|
||||
return injectedJs[name];
|
||||
};
|
||||
|
||||
// This library has the following error when running within Jest:
|
||||
// Invariant Violation: `new NativeEventEmitter()` requires a non-null argument.
|
||||
@@ -40,17 +50,27 @@ jest.doMock('react-native-version-info', () => {
|
||||
});
|
||||
|
||||
// react-native-webview expects native iOS/Android code so needs to be mocked.
|
||||
jest.mock('react-native-webview', () => {
|
||||
const { View } = require('react-native');
|
||||
return {
|
||||
WebView: View,
|
||||
};
|
||||
jest.mock('./components/ExtendedWebView', () => {
|
||||
return require('./components/ExtendedWebView/index.jest.js');
|
||||
});
|
||||
|
||||
jest.mock('@react-native-clipboard/clipboard', () => {
|
||||
return { default: { getString: jest.fn(), setString: jest.fn() } };
|
||||
});
|
||||
|
||||
jest.mock('react-native-share', () => {
|
||||
return { default: { } };
|
||||
});
|
||||
|
||||
// Used by the renderer
|
||||
jest.doMock('react-native-vector-icons/Ionicons', () => {
|
||||
return {
|
||||
default: class extends require('react-native').View {
|
||||
static getImageSourceSync = () => ({ uri: '' });
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// react-native-fs's CachesDirectoryPath export doesn't work in a testing environment.
|
||||
// Use a temporary folder instead.
|
||||
const tempDirectoryPath = path.join(tmpdir(), `appmobile-test-${uuid.createNano()}`);
|
||||
|
@@ -27,10 +27,10 @@
|
||||
"@joplin/utils": "~3.0",
|
||||
"@react-native-clipboard/clipboard": "1.14.1",
|
||||
"@react-native-community/datetimepicker": "8.0.0",
|
||||
"@react-native-community/geolocation": "3.1.0",
|
||||
"@react-native-community/geolocation": "3.2.1",
|
||||
"@react-native-community/netinfo": "11.3.1",
|
||||
"@react-native-community/push-notification-ios": "1.11.0",
|
||||
"@react-native-community/slider": "4.5.0",
|
||||
"@react-native-community/slider": "4.5.2",
|
||||
"assert-browserify": "2.0.0",
|
||||
"buffer": "6.0.3",
|
||||
"constants-browserify": "1.0.0",
|
||||
@@ -45,7 +45,7 @@
|
||||
"react": "18.2.0",
|
||||
"react-native": "0.74.1",
|
||||
"react-native-camera": "4.2.1",
|
||||
"react-native-device-info": "10.12.1",
|
||||
"react-native-device-info": "10.13.1",
|
||||
"react-native-dialogbox": "0.6.10",
|
||||
"react-native-document-picker": "9.1.1",
|
||||
"react-native-dropdownalert": "5.1.0",
|
||||
@@ -53,7 +53,7 @@
|
||||
"react-native-file-viewer": "2.1.5",
|
||||
"react-native-fingerprint-scanner": "6.0.0",
|
||||
"react-native-fs": "2.20.0",
|
||||
"react-native-get-random-values": "1.10.0",
|
||||
"react-native-get-random-values": "1.11.0",
|
||||
"react-native-image-picker": "7.1.1",
|
||||
"react-native-localize": "3.0.6",
|
||||
"react-native-modal-datetime-picker": "17.1.0",
|
||||
@@ -70,7 +70,7 @@
|
||||
"react-native-vector-icons": "10.1.0",
|
||||
"react-native-version-info": "1.1.1",
|
||||
"react-native-vosk": "0.1.12",
|
||||
"react-native-webview": "13.8.1",
|
||||
"react-native-webview": "13.8.6",
|
||||
"react-native-zip-archive": "6.1.0",
|
||||
"react-redux": "8.1.3",
|
||||
"redux": "4.2.1",
|
||||
@@ -108,10 +108,11 @@
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"jetifier": "2.0.0",
|
||||
"js-draw": "1.20.2",
|
||||
"jsdom": "23.0.1",
|
||||
"jsdom": "23.2.0",
|
||||
"nodemon": "3.0.3",
|
||||
"punycode": "2.3.1",
|
||||
"react-test-renderer": "18.2.0",
|
||||
"sharp": "0.33.2",
|
||||
"sqlite3": "5.1.6",
|
||||
"ts-jest": "29.1.1",
|
||||
"ts-loader": "9.5.1",
|
||||
|
@@ -14,7 +14,7 @@ import ResourceService from '@joplin/lib/services/ResourceService';
|
||||
import KvStore from '@joplin/lib/services/KvStore';
|
||||
import NoteScreen from './components/screens/Note';
|
||||
import UpgradeSyncTargetScreen from './components/screens/UpgradeSyncTargetScreen';
|
||||
import Setting, { Env } from '@joplin/lib/models/Setting';
|
||||
import Setting, { AppType, Env } from '@joplin/lib/models/Setting';
|
||||
import PoorManIntervals from '@joplin/lib/PoorManIntervals';
|
||||
import reducer, { NotesParent, parseNotesParent, serializeNotesParent } from '@joplin/lib/reducer';
|
||||
import ShareExtension from './utils/ShareExtension';
|
||||
@@ -41,7 +41,7 @@ const { BackButtonService } = require('./services/back-button.js');
|
||||
import NavService from '@joplin/lib/services/NavService';
|
||||
import { createStore, applyMiddleware, Dispatch } from 'redux';
|
||||
import reduxSharedMiddleware from '@joplin/lib/components/shared/reduxSharedMiddleware';
|
||||
const { shimInit } = require('./utils/shim-init-react.js');
|
||||
import shimInit from './utils/shim-init-react';
|
||||
const { AppNav } = require('./components/app-nav.js');
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
@@ -122,14 +122,15 @@ import { ReactNode } from 'react';
|
||||
import { parseShareCache } from '@joplin/lib/services/share/reducer';
|
||||
import autodetectTheme, { onSystemColorSchemeChange } from './utils/autodetectTheme';
|
||||
import runOnDeviceFsDriverTests from './utils/fs-driver/runOnDeviceTests';
|
||||
import PluginRunnerWebView from './plugins/PluginRunner/PluginRunnerWebView';
|
||||
import PluginRunnerWebView from './components/plugins/PluginRunnerWebView';
|
||||
import { refreshFolders, scheduleRefreshFolders } from '@joplin/lib/folders-screen-utils';
|
||||
import KeymapService from '@joplin/lib/services/KeymapService';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
import initializeCommandService from './utils/initializeCommandService';
|
||||
import PlatformImplementation from './plugins/PlatformImplementation';
|
||||
import PlatformImplementation from './services/plugins/PlatformImplementation';
|
||||
import ShareManager from './components/screens/ShareManager';
|
||||
import appDefaultState, { DEFAULT_ROUTE } from './utils/appDefaultState';
|
||||
import { setDateFormat, setTimeFormat, setTimeLocale } from '@joplin/utils/time';
|
||||
|
||||
type SideMenuPosition = 'left' | 'right';
|
||||
|
||||
@@ -186,10 +187,13 @@ const generalMiddleware = (store: any) => (next: any) => async (action: any) =>
|
||||
if ((action.type === 'SETTING_UPDATE_ONE' && (action.key === 'dateFormat' || action.key === 'timeFormat')) || (action.type === 'SETTING_UPDATE_ALL')) {
|
||||
time.setDateFormat(Setting.value('dateFormat'));
|
||||
time.setTimeFormat(Setting.value('timeFormat'));
|
||||
setDateFormat(Setting.value('dateFormat'));
|
||||
setTimeFormat(Setting.value('timeFormat'));
|
||||
}
|
||||
|
||||
if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'locale' || action.type === 'SETTING_UPDATE_ALL') {
|
||||
setLocale(Setting.value('locale'));
|
||||
setTimeLocale(Setting.value('locale'));
|
||||
}
|
||||
|
||||
if ((action.type === 'SETTING_UPDATE_ONE' && (action.key.indexOf('encryption.') === 0)) || (action.type === 'SETTING_UPDATE_ALL')) {
|
||||
@@ -501,9 +505,9 @@ async function initialize(dispatch: Dispatch) {
|
||||
value: profileConfig,
|
||||
});
|
||||
|
||||
Setting.setConstant('env', __DEV__ ? 'dev' : 'prod');
|
||||
Setting.setConstant('env', __DEV__ ? Env.Dev : Env.Prod);
|
||||
Setting.setConstant('appId', 'net.cozic.joplin-mobile');
|
||||
Setting.setConstant('appType', 'mobile');
|
||||
Setting.setConstant('appType', AppType.Mobile);
|
||||
Setting.setConstant('tempDir', await initializeTempDir());
|
||||
Setting.setConstant('cacheDir', `${getProfilesRootDir()}/cache`);
|
||||
const resourceDir = getResourceDir(currentProfile, isSubProfile);
|
||||
@@ -616,7 +620,7 @@ async function initialize(dispatch: Dispatch) {
|
||||
reg.logger().info(`First start: detected locale as ${detectedLocale}`);
|
||||
|
||||
Setting.skipDefaultMigrations();
|
||||
Setting.setValue('firstStart', 0);
|
||||
Setting.setValue('firstStart', false);
|
||||
} else {
|
||||
Setting.applyDefaultMigrations();
|
||||
}
|
||||
@@ -785,7 +789,7 @@ async function initialize(dispatch: Dispatch) {
|
||||
const pluginSettings = pluginService.unserializePluginSettings(Setting.value('plugins.states'));
|
||||
|
||||
const updatedSettings = pluginService.clearUpdateState(pluginSettings);
|
||||
Setting.setValue('plugins.states', pluginService.serializePluginSettings(updatedSettings));
|
||||
Setting.setValue('plugins.states', updatedSettings);
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Keep this below to test react-native-rsa-native
|
||||
|
@@ -16,7 +16,7 @@ const jsDrawBundle = new BundledFile(
|
||||
|
||||
const pluginBackgroundPageBundle = new BundledFile(
|
||||
'pluginBackgroundPage',
|
||||
`${mobileDir}/plugins/PluginRunner/backgroundPage/pluginRunnerBackgroundPage.ts`,
|
||||
`${mobileDir}/components/plugins/backgroundPage/pluginRunnerBackgroundPage.ts`,
|
||||
);
|
||||
|
||||
const noteViewerBundle = new BundledFile(
|
||||
|
18
packages/app-mobile/utils/image/getImageDimensions.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Size } from '@joplin/utils/types';
|
||||
import { Image as NativeImage } from 'react-native';
|
||||
|
||||
const getImageDimensions = async (uri: string): Promise<Size> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
NativeImage.getSize(
|
||||
uri,
|
||||
(width: number, height: number) => {
|
||||
resolve({ width: width, height: height });
|
||||
},
|
||||
(error: unknown) => {
|
||||
reject(error);
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export default getImageDimensions;
|
43
packages/app-mobile/utils/image/resizeImage.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import shim from '@joplin/lib/shim';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import ImageResizer from '@bam.tech/react-native-image-resizer';
|
||||
|
||||
const logger = Logger.create('resizeImage');
|
||||
|
||||
type OutputFormat = 'PNG' | 'JPEG';
|
||||
|
||||
interface Options {
|
||||
inputPath: string;
|
||||
outputPath: string;
|
||||
maxWidth: number;
|
||||
maxHeight: number;
|
||||
format: OutputFormat;
|
||||
quality: number;
|
||||
}
|
||||
|
||||
const resizeImage = async (options: Options) => {
|
||||
const resizedImage = await ImageResizer.createResizedImage(
|
||||
options.inputPath,
|
||||
options.maxWidth,
|
||||
options.maxHeight,
|
||||
options.format,
|
||||
options.quality, // quality
|
||||
undefined, // rotation
|
||||
undefined, // outputPath
|
||||
true, // keep metadata
|
||||
);
|
||||
|
||||
const resizedImagePath = resizedImage.uri;
|
||||
logger.info('Resized image ', resizedImagePath);
|
||||
logger.info(`Moving ${resizedImagePath} => ${options.outputPath}`);
|
||||
|
||||
await shim.fsDriver().copy(resizedImagePath, options.outputPath);
|
||||
|
||||
try {
|
||||
await shim.fsDriver().unlink(resizedImagePath);
|
||||
} catch (error) {
|
||||
logger.warn('Error when unlinking cached file: ', error);
|
||||
}
|
||||
};
|
||||
|
||||
export default resizeImage;
|
10
packages/app-mobile/utils/injectedJs.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
const injectedJs = {
|
||||
webviewLib: require('@joplin/lib/rnInjectedJs/webviewLib'),
|
||||
codeMirrorBundle: require('../lib/rnInjectedJs/codeMirrorBundle.bundle'),
|
||||
svgEditorBundle: require('../lib/rnInjectedJs/svgEditorBundle.bundle'),
|
||||
pluginBackgroundPage: require('../lib/rnInjectedJs/pluginBackgroundPage.bundle'),
|
||||
noteBodyViewerBundle: require('../lib/rnInjectedJs/noteBodyViewerBundle.bundle'),
|
||||
};
|
||||
|
||||
export default injectedJs;
|
@@ -1,9 +1,9 @@
|
||||
|
||||
import RemoteMessenger from '@joplin/lib/utils/ipc/RemoteMessenger';
|
||||
import { SerializableData } from '@joplin/lib/utils/ipc/types';
|
||||
import { WebViewMessageEvent } from 'react-native-webview';
|
||||
import { WebViewControl } from '../../components/ExtendedWebView';
|
||||
import { RefObject } from 'react';
|
||||
import { OnMessageEvent } from '../../components/ExtendedWebView/types';
|
||||
|
||||
export default class RNToWebViewMessenger<LocalInterface, RemoteInterface> extends RemoteMessenger<LocalInterface, RemoteInterface> {
|
||||
public constructor(channelId: string, private webviewControl: WebViewControl|RefObject<WebViewControl>, localApi: LocalInterface) {
|
||||
@@ -32,7 +32,7 @@ export default class RNToWebViewMessenger<LocalInterface, RemoteInterface> exten
|
||||
`);
|
||||
}
|
||||
|
||||
public onWebViewMessage = (event: WebViewMessageEvent) => {
|
||||
public onWebViewMessage = (event: OnMessageEvent) => {
|
||||
if (!this.hasBeenClosed()) {
|
||||
void this.onMessage(JSON.parse(event.nativeEvent.data));
|
||||
}
|
||||
|
@@ -1,28 +1,22 @@
|
||||
const shim = require('@joplin/lib/shim').default;
|
||||
import shim from '@joplin/lib/shim';
|
||||
const { GeolocationReact } = require('./geolocation-react.js');
|
||||
const PoorManIntervals = require('@joplin/lib/PoorManIntervals').default;
|
||||
const RNFetchBlob = require('rn-fetch-blob').default;
|
||||
const { generateSecureRandom } = require('react-native-securerandom');
|
||||
const FsDriverRN = require('./fs-driver/fs-driver-rn').default;
|
||||
const { Buffer } = require('buffer');
|
||||
const { Linking, Platform } = require('react-native');
|
||||
const showMessageBox = require('./showMessageBox.js').default;
|
||||
const mimeUtils = require('@joplin/lib/mime-utils.js');
|
||||
const { basename, fileExtension } = require('@joplin/lib/path-utils');
|
||||
const uuid = require('@joplin/lib/uuid').default;
|
||||
const Resource = require('@joplin/lib/models/Resource').default;
|
||||
const { getLocales } = require('react-native-localize');
|
||||
const { setLocale, defaultLocale, closestSupportedLocale } = require('@joplin/lib/locale');
|
||||
import PoorManIntervals from '@joplin/lib/PoorManIntervals';
|
||||
import RNFetchBlob from 'rn-fetch-blob';
|
||||
import { generateSecureRandom } from 'react-native-securerandom';
|
||||
import FsDriverRN from './fs-driver/fs-driver-rn';
|
||||
import { Buffer } from 'buffer';
|
||||
import { Linking, Platform } from 'react-native';
|
||||
import showMessageBox from './showMessageBox.js';
|
||||
import * as mimeUtils from '@joplin/lib/mime-utils';
|
||||
import { basename, fileExtension } from '@joplin/lib/path-utils';
|
||||
import uuid from '@joplin/lib/uuid';
|
||||
import Resource from '@joplin/lib/models/Resource';
|
||||
import { getLocales } from 'react-native-localize';
|
||||
import { setLocale, defaultLocale, closestSupportedLocale } from '@joplin/lib/locale';
|
||||
import type SettingType from '@joplin/lib/models/Setting';
|
||||
import injectedJs from './injectedJs';
|
||||
|
||||
const injectedJs = {
|
||||
webviewLib: require('@joplin/lib/rnInjectedJs/webviewLib'),
|
||||
codeMirrorBundle: require('../lib/rnInjectedJs/codeMirrorBundle.bundle'),
|
||||
svgEditorBundle: require('../lib/rnInjectedJs/svgEditorBundle.bundle'),
|
||||
pluginBackgroundPage: require('../lib/rnInjectedJs/pluginBackgroundPage.bundle'),
|
||||
noteBodyViewerBundle: require('../lib/rnInjectedJs/noteBodyViewerBundle.bundle'),
|
||||
};
|
||||
|
||||
function shimInit() {
|
||||
export default function shimInit() {
|
||||
shim.Geolocation = GeolocationReact;
|
||||
shim.sjclModule = require('@joplin/lib/vendor/sjcl-rn.js');
|
||||
|
||||
@@ -33,7 +27,7 @@ function shimInit() {
|
||||
return shim.fsDriver_;
|
||||
};
|
||||
|
||||
shim.randomBytes = async count => {
|
||||
shim.randomBytes = async (count: number) => {
|
||||
const randomBytes = await generateSecureRandom(count);
|
||||
const temp = [];
|
||||
for (const n in randomBytes) {
|
||||
@@ -91,7 +85,7 @@ function shimInit() {
|
||||
|
||||
/* eslint-enable */
|
||||
|
||||
shim.detectAndSetLocale = (Setting) => {
|
||||
shim.detectAndSetLocale = (Setting: typeof SettingType) => {
|
||||
// [
|
||||
// {
|
||||
// "countryCode": "US",
|
||||
@@ -179,7 +173,7 @@ function shimInit() {
|
||||
try {
|
||||
const response = await shim.fetchWithRetry(doFetchBlob, options);
|
||||
|
||||
// Returns an object that's roughtly compatible with a standard Response object
|
||||
// Returns an object that's roughly compatible with a standard Response object
|
||||
const output = {
|
||||
ok: response.respInfo.status < 400,
|
||||
path: response.data,
|
||||
@@ -212,7 +206,7 @@ function shimInit() {
|
||||
trusty: options.ignoreTlsErrors,
|
||||
}).fetch(method, url, headers, RNFetchBlob.wrap(options.path));
|
||||
|
||||
// Returns an object that's roughtly compatible with a standard Response object
|
||||
// Returns an object that's roughly compatible with a standard Response object
|
||||
return {
|
||||
ok: response.respInfo.status < 400,
|
||||
data: response.data,
|
||||
@@ -239,7 +233,7 @@ function shimInit() {
|
||||
shim.showMessageBox = showMessageBox;
|
||||
|
||||
shim.openUrl = url => {
|
||||
Linking.openURL(url);
|
||||
return Linking.openURL(url);
|
||||
};
|
||||
|
||||
shim.httpAgent = () => {
|
||||
@@ -247,7 +241,7 @@ function shimInit() {
|
||||
};
|
||||
|
||||
shim.waitForFrame = () => {
|
||||
return new Promise((resolve) => {
|
||||
return new Promise<void>((resolve) => {
|
||||
requestAnimationFrame(() => {
|
||||
resolve();
|
||||
});
|
||||
@@ -299,7 +293,7 @@ function shimInit() {
|
||||
|
||||
shim.injectedJs = function(name) {
|
||||
if (!(name in injectedJs)) throw new Error(`Cannot find injectedJs file (add it to "injectedJs" object): ${name}`);
|
||||
return injectedJs[name];
|
||||
return injectedJs[name as keyof typeof injectedJs];
|
||||
};
|
||||
|
||||
shim.setTimeout = (fn, interval) => {
|
||||
@@ -320,4 +314,3 @@ function shimInit() {
|
||||
|
||||
}
|
||||
|
||||
module.exports = { shimInit };
|
@@ -2,6 +2,6 @@
|
||||
"io.github.jackgruber.backup": {
|
||||
"cloneUrl": "https://github.com/JackGruber/joplin-plugin-backup.git",
|
||||
"branch": "master",
|
||||
"commit": "52d898315cab259da638698cc41120e9fae593ef"
|
||||
"commit": "5ba57c18ac0f24f20832f012e015a080b138f0c4"
|
||||
}
|
||||
}
|
||||
|
@@ -39,6 +39,7 @@ describe('createEditor', () => {
|
||||
settings: editorSettings,
|
||||
onEvent: _event => {},
|
||||
onLogMessage: _message => {},
|
||||
onPasteFile: null,
|
||||
});
|
||||
|
||||
// Force the generation of the syntax tree now.
|
||||
@@ -66,6 +67,7 @@ describe('createEditor', () => {
|
||||
settings: editorSettings,
|
||||
onEvent: _event => {},
|
||||
onLogMessage: _message => {},
|
||||
onPasteFile: null,
|
||||
});
|
||||
|
||||
const getContentScriptJs = jest.fn(async () => {
|
||||
@@ -133,6 +135,7 @@ describe('createEditor', () => {
|
||||
settings: editorSettings,
|
||||
onEvent: _event => {},
|
||||
onLogMessage: _message => {},
|
||||
onPasteFile: null,
|
||||
});
|
||||
|
||||
const getContentScriptJs = jest.fn(async () => {
|
||||
|
@@ -30,6 +30,7 @@ import configFromSettings from './configFromSettings';
|
||||
import getScrollFraction from './getScrollFraction';
|
||||
import CodeMirrorControl from './CodeMirrorControl';
|
||||
import insertLineAfter from './editorCommands/insertLineAfter';
|
||||
import handlePasteEvent from './utils/handlePasteEvent';
|
||||
|
||||
const createEditor = (
|
||||
parentElement: HTMLElement, props: EditorProps,
|
||||
@@ -257,6 +258,24 @@ const createEditor = (
|
||||
fraction: getScrollFraction(view),
|
||||
});
|
||||
},
|
||||
paste: (event, view) => {
|
||||
if (props.onPasteFile) {
|
||||
handlePasteEvent(event, view, props.onPasteFile);
|
||||
}
|
||||
},
|
||||
dragover: (event, _view) => {
|
||||
if (props.onPasteFile && event.dataTransfer.files.length) {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'copy';
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
drop: (event, view) => {
|
||||
if (props.onPasteFile) {
|
||||
handlePasteEvent(event, view, props.onPasteFile);
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
EditorState.tabSize.of(4),
|
||||
|
@@ -82,7 +82,7 @@ export default class PluginLoader {
|
||||
scriptElement.appendChild(document.createTextNode(`
|
||||
(async () => {
|
||||
const exports = {};
|
||||
const module = {};
|
||||
const module = { exports: exports };
|
||||
const require = window.__pluginLoaderRequireFunctions[${JSON.stringify(this.pluginLoaderId)}];
|
||||
const joplin = {
|
||||
require,
|
||||
@@ -90,7 +90,7 @@ export default class PluginLoader {
|
||||
|
||||
${js};
|
||||
|
||||
window.__pluginLoaderScriptLoadCallbacks[${JSON.stringify(scriptId)}](module.exports || exports);
|
||||
window.__pluginLoaderScriptLoadCallbacks[${JSON.stringify(scriptId)}](module.exports);
|
||||
})();
|
||||
`));
|
||||
|
||||
|
@@ -10,6 +10,7 @@ const createEditorControl = (initialText: string) => {
|
||||
settings: editorSettings,
|
||||
onEvent: _event => {},
|
||||
onLogMessage: _message => {},
|
||||
onPasteFile: null,
|
||||
});
|
||||
};
|
||||
|
||||
|
29
packages/editor/CodeMirror/utils/handlePasteEvent.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { PasteFileCallback } from '../../types';
|
||||
|
||||
const handlePasteEvent = (event: ClipboardEvent|DragEvent, _view: EditorView, onPaste: PasteFileCallback) => {
|
||||
const dataTransfer = 'clipboardData' in event ? event.clipboardData : event.dataTransfer;
|
||||
const files = dataTransfer.files;
|
||||
|
||||
let fileToPaste: File|null = null;
|
||||
|
||||
// Prefer image files, if available.
|
||||
for (const file of files) {
|
||||
if (['image/png', 'image/jpeg', 'image/svg+xml'].includes(file.type)) {
|
||||
fileToPaste = file;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to other files
|
||||
if (files.length && !fileToPaste) {
|
||||
fileToPaste = files[0];
|
||||
}
|
||||
|
||||
if (fileToPaste) {
|
||||
event.preventDefault();
|
||||
void onPaste(fileToPaste);
|
||||
}
|
||||
};
|
||||
|
||||
export default handlePasteEvent;
|
@@ -168,11 +168,14 @@ export interface EditorSettings {
|
||||
|
||||
export type LogMessageCallback = (message: string)=> void;
|
||||
export type OnEventCallback = (event: EditorEvent)=> void;
|
||||
export type PasteFileCallback = (data: File)=> Promise<void>;
|
||||
|
||||
export interface EditorProps {
|
||||
settings: EditorSettings;
|
||||
initialText: string;
|
||||
|
||||
// If null, paste and drag-and-drop will not work for resources unless handled elsewhere.
|
||||
onPasteFile: PasteFileCallback|null;
|
||||
onEvent: OnEventCallback;
|
||||
onLogMessage: LogMessageCallback;
|
||||
}
|
||||
|
@@ -46,7 +46,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "29.5.8",
|
||||
"@types/node": "18.19.26",
|
||||
"@types/node": "18.19.31",
|
||||
"@typescript-eslint/eslint-plugin": "6.8.0",
|
||||
"@typescript-eslint/parser": "6.8.0",
|
||||
"coveralls": "3.1.1",
|
||||
|
@@ -3,7 +3,7 @@
|
||||
"packageManager": "yarn@3.6.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"buildPluginDoc_": "typedoc --name 'Joplin Plugin API Documentation' --mode file -theme '../../Assets/PluginDocTheme/' --readme '../../Assets/PluginDocTheme/index.md' --excludeNotExported --excludeExternals --excludePrivate --excludeProtected --out ../../../joplin-website/docs/api/references/plugin_api ../lib/services/plugins/api/"
|
||||
"buildPluginDoc_": "typedoc --exclude '../lib/models/**' --name 'Joplin Plugin API Documentation' --mode file -theme '../../Assets/PluginDocTheme/' --readme '../../Assets/PluginDocTheme/index.md' --excludeNotExported --excludeExternals --excludePrivate --excludeProtected --out ../../../joplin-website/docs/api/references/plugin_api ../lib/services/plugins/api/"
|
||||
},
|
||||
"dependencies": {
|
||||
"typedoc": "0.17.8",
|
||||
|
@@ -21,6 +21,7 @@ import BaseItem from './models/BaseItem';
|
||||
import Note from './models/Note';
|
||||
import Tag from './models/Tag';
|
||||
import { splitCommandString } from '@joplin/utils';
|
||||
import { setDateFormat, setTimeFormat, setTimeLocale } from '@joplin/utils/time';
|
||||
import { reg } from './registry';
|
||||
import time from './time';
|
||||
import BaseSyncTarget from './BaseSyncTarget';
|
||||
@@ -357,6 +358,9 @@ export default class BaseApplication {
|
||||
const sideEffects: any = {
|
||||
'dateFormat': async () => {
|
||||
time.setLocale(Setting.value('locale'));
|
||||
setTimeLocale(Setting.value('locale'));
|
||||
setDateFormat(Setting.value('dateFormat'));
|
||||
setTimeFormat(Setting.value('timeFormat'));
|
||||
time.setDateFormat(Setting.value('dateFormat'));
|
||||
time.setTimeFormat(Setting.value('timeFormat'));
|
||||
},
|
||||
@@ -683,7 +687,7 @@ export default class BaseApplication {
|
||||
const tempDir = `${profileDir}/tmp`;
|
||||
const cacheDir = `${profileDir}/cache`;
|
||||
|
||||
Setting.setConstant('env', initArgs.env);
|
||||
Setting.setConstant('env', initArgs.env as Env);
|
||||
Setting.setConstant('resourceDirName', resourceDirName);
|
||||
Setting.setConstant('resourceDir', resourceDir);
|
||||
Setting.setConstant('tempDir', tempDir);
|
||||
@@ -785,12 +789,12 @@ export default class BaseApplication {
|
||||
Setting.skipDefaultMigrations();
|
||||
|
||||
if (Setting.value('env') === 'dev') {
|
||||
Setting.setValue('showTrayIcon', 0);
|
||||
Setting.setValue('autoUpdateEnabled', 0);
|
||||
Setting.setValue('showTrayIcon', false);
|
||||
Setting.setValue('autoUpdateEnabled', false);
|
||||
Setting.setValue('sync.interval', 3600);
|
||||
}
|
||||
|
||||
Setting.setValue('firstStart', 0);
|
||||
Setting.setValue('firstStart', false);
|
||||
} else {
|
||||
Setting.applyDefaultMigrations();
|
||||
Setting.applyUserSettingMigration();
|
||||
|