Compare commits
180 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 | ||
|
e49bca8315 | ||
|
636fbdf7d0 | ||
|
ee97434bb0 | ||
|
599cf5b86f | ||
|
2fd6a3a2fa | ||
|
a3e04103de | ||
|
731260926d | ||
|
a43635610a | ||
|
e307459652 | ||
|
c197a83de8 | ||
|
320d0df60d | ||
|
7e4533d811 | ||
|
f32fe63205 | ||
|
be117bca86 | ||
|
2b7bd902f3 | ||
|
3e0fb48e44 | ||
|
6d7fd19167 | ||
|
c3520d9eb1 | ||
|
5fd3cecc96 | ||
|
0d8666c946 | ||
|
4a475f1b53 | ||
|
8679cc5704 | ||
|
a48c4ba93f | ||
|
12db667128 | ||
|
6215de6080 | ||
|
7d2f384475 | ||
|
6ea1ac09a4 | ||
|
f2841a9a94 | ||
|
46ade2e0f8 | ||
|
d89be23069 | ||
|
337d50437b | ||
|
2479a8471e | ||
|
16e82b5462 | ||
|
1a82255865 | ||
|
a64d6e3270 | ||
|
ae1620dd50 | ||
|
d804e95d06 | ||
|
6c091910cd | ||
|
78d9a7e636 | ||
|
a074532497 | ||
|
5d2df358ac | ||
|
dfdc2fda27 | ||
|
a1f9c9c3d8 | ||
|
3270122419 | ||
|
838da6f161 | ||
|
e4b8976aa0 | ||
|
a86ee1d34e | ||
|
17e1eecb11 | ||
|
dd9a389711 | ||
|
55eaedb8b2 | ||
|
5f34a1bc92 | ||
|
f781face3a | ||
|
78ecd28d73 | ||
|
f8d772de87 | ||
|
77c39ac084 | ||
|
85e57a3953 | ||
|
95968f6690 | ||
|
f0b73ee916 | ||
|
a44412ae78 | ||
|
c7116b135f | ||
|
801d36c41f | ||
|
1d46adf801 | ||
|
94edaea210 | ||
|
3557138c84 | ||
|
77e74112ad | ||
|
5db88995c0 | ||
|
8eda8d3c84 | ||
|
1437dd5f27 | ||
|
9eb4944614 | ||
|
b4ef5abb88 | ||
|
4115e2054f | ||
|
8485277dcf | ||
|
fac9ea3b42 | ||
|
45f8e27d6a | ||
|
c8a478d970 | ||
|
75dfb0af5f | ||
|
5e592a3096 | ||
|
84e46ad874 | ||
|
0ec917bb96 | ||
|
818f9f58d1 | ||
|
e1abe0b4cb | ||
|
c972ce223e | ||
|
d9dadf28cb | ||
|
1fb392ff4e | ||
|
affa620983 | ||
|
ed31d8202b | ||
|
573ea6051c | ||
|
f1ec54532f | ||
|
5eb96d71e1 | ||
|
73251bac4a | ||
|
f40a0da195 | ||
|
8dc1ab2cc5 | ||
|
1b46c9f5e7 | ||
|
483ab55a36 | ||
|
502002f9f6 | ||
|
8d8014511f | ||
|
28569e652e | ||
|
9acf36d802 | ||
|
06d26767ed | ||
|
88858d4413 | ||
|
27309427a1 | ||
|
e83a18a907 | ||
|
9bd8b11f67 | ||
|
3a14c7ce2d | ||
|
fe4c9a2401 | ||
|
d2fb19cf6d | ||
|
1f8e3fb620 | ||
|
8bbe1d30b4 | ||
|
ec92f716de | ||
|
ab819d9210 | ||
|
e465b45d6e | ||
|
a4a4170d49 | ||
|
1dcf528443 | ||
|
8cf4ef88b5 | ||
|
bf634270be | ||
|
56437d3e1b | ||
|
d095ab2be7 | ||
|
4751b4dd74 | ||
|
ce22d8238c | ||
|
9e2b9e5b8d | ||
|
4952980e0a | ||
|
59989d2735 | ||
|
eb7f2855b0 |
@@ -515,16 +515,20 @@ packages/app-mobile/commands/scrollToHash.js
|
||||
packages/app-mobile/commands/util/goToNote.js
|
||||
packages/app-mobile/components/ActionButton.js
|
||||
packages/app-mobile/components/BackButtonDialogBox.js
|
||||
packages/app-mobile/components/BetaChip.js
|
||||
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
|
||||
@@ -595,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
|
||||
@@ -610,7 +635,8 @@ packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/expo
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/makeImportExportCacheDirectory.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SectionDescription.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SectionHeader.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SectionSelector.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SectionSelector/SectionTab.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SectionSelector/index.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingComponent.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingItem.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingsButton.js
|
||||
@@ -618,9 +644,10 @@ packages/app-mobile/components/screens/ConfigScreen/SettingsToggle.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/EnablePluginSupportPage.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/InstalledPluginBox.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChip.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChips.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginTitle.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/StyledChip.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/RecommendedBadge.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.js
|
||||
@@ -628,6 +655,7 @@ packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.search.
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginUploadButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/SearchPlugins.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/SectionLabel.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/buttons/ActionButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/buttons/InstallButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/WrappedPluginStates.js
|
||||
@@ -656,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
|
||||
@@ -713,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
|
||||
@@ -721,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
|
||||
@@ -765,6 +772,9 @@ packages/editor/CodeMirror/markdown/markdownCommands.toggleList.test.js
|
||||
packages/editor/CodeMirror/markdown/markdownCommands.js
|
||||
packages/editor/CodeMirror/markdown/markdownMathParser.test.js
|
||||
packages/editor/CodeMirror/markdown/markdownMathParser.js
|
||||
packages/editor/CodeMirror/markdown/utils/renumberSelectedLists.test.js
|
||||
packages/editor/CodeMirror/markdown/utils/renumberSelectedLists.js
|
||||
packages/editor/CodeMirror/markdown/utils/stripBlockquote.js
|
||||
packages/editor/CodeMirror/pluginApi/PluginLoader.js
|
||||
packages/editor/CodeMirror/pluginApi/codeMirrorRequire.js
|
||||
packages/editor/CodeMirror/pluginApi/customEditorCompletion.test.js
|
||||
@@ -777,10 +787,23 @@ packages/editor/CodeMirror/testUtil/loadLanguages.js
|
||||
packages/editor/CodeMirror/testUtil/pressReleaseKey.js
|
||||
packages/editor/CodeMirror/testUtil/typeText.js
|
||||
packages/editor/CodeMirror/theme.js
|
||||
packages/editor/CodeMirror/util/editorStateUtils.test.js
|
||||
packages/editor/CodeMirror/util/editorStateUtils.js
|
||||
packages/editor/CodeMirror/util/isInSyntaxNode.js
|
||||
packages/editor/CodeMirror/util/setupVim.js
|
||||
packages/editor/CodeMirror/utils/formatting/RegionSpec.js
|
||||
packages/editor/CodeMirror/utils/formatting/findInlineMatch.test.js
|
||||
packages/editor/CodeMirror/utils/formatting/findInlineMatch.js
|
||||
packages/editor/CodeMirror/utils/formatting/isIndentationEquivalent.js
|
||||
packages/editor/CodeMirror/utils/formatting/tabsToSpaces.test.js
|
||||
packages/editor/CodeMirror/utils/formatting/tabsToSpaces.js
|
||||
packages/editor/CodeMirror/utils/formatting/toggleInlineFormatGlobally.js
|
||||
packages/editor/CodeMirror/utils/formatting/toggleInlineRegionSurrounded.js
|
||||
packages/editor/CodeMirror/utils/formatting/toggleInlineSelectionFormat.js
|
||||
packages/editor/CodeMirror/utils/formatting/toggleRegionFormatGlobally.test.js
|
||||
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
|
||||
packages/editor/events.js
|
||||
packages/editor/types.js
|
||||
@@ -879,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
|
||||
@@ -925,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
|
||||
@@ -946,6 +972,7 @@ packages/lib/ntp.js
|
||||
packages/lib/onedrive-api.test.js
|
||||
packages/lib/onedrive-api.js
|
||||
packages/lib/path-utils.js
|
||||
packages/lib/reducer.test.js
|
||||
packages/lib/reducer.js
|
||||
packages/lib/registry.test.js
|
||||
packages/lib/registry.js
|
||||
@@ -1090,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
|
||||
@@ -1366,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"
|
4
.github/workflows/build-android.yml
vendored
@@ -47,5 +47,7 @@ jobs:
|
||||
|
||||
- name: Assemble Android Release
|
||||
run: |
|
||||
cd packages/app-mobile/android && ./gradlew assembleRelease
|
||||
cd packages/app-mobile/android
|
||||
sed -i -- 's/signingConfig signingConfigs.release/signingConfig signingConfigs.debug/' app/build.gradle
|
||||
./gradlew assembleRelease
|
||||
|
2
.github/workflows/cla.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
- name: "CLA Assistant"
|
||||
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
|
||||
# Beta Release
|
||||
uses: contributor-assistant/github-action@v2.3.1
|
||||
uses: contributor-assistant/github-action@v2.3.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# the below token should have repo scope and must be manually added by you in the repository's secret
|
||||
|
98
.gitignore
vendored
@@ -494,16 +494,20 @@ packages/app-mobile/commands/scrollToHash.js
|
||||
packages/app-mobile/commands/util/goToNote.js
|
||||
packages/app-mobile/components/ActionButton.js
|
||||
packages/app-mobile/components/BackButtonDialogBox.js
|
||||
packages/app-mobile/components/BetaChip.js
|
||||
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
|
||||
@@ -574,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
|
||||
@@ -589,7 +614,8 @@ packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/expo
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/makeImportExportCacheDirectory.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SectionDescription.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SectionHeader.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SectionSelector.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SectionSelector/SectionTab.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SectionSelector/index.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingComponent.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingItem.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingsButton.js
|
||||
@@ -597,9 +623,10 @@ packages/app-mobile/components/screens/ConfigScreen/SettingsToggle.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/EnablePluginSupportPage.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/InstalledPluginBox.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChip.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChips.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginTitle.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/StyledChip.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/RecommendedBadge.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.installed.test.js
|
||||
@@ -607,6 +634,7 @@ packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.search.
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginUploadButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/SearchPlugins.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/SectionLabel.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/buttons/ActionButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/buttons/InstallButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/WrappedPluginStates.js
|
||||
@@ -635,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
|
||||
@@ -692,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
|
||||
@@ -700,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
|
||||
@@ -744,6 +751,9 @@ packages/editor/CodeMirror/markdown/markdownCommands.toggleList.test.js
|
||||
packages/editor/CodeMirror/markdown/markdownCommands.js
|
||||
packages/editor/CodeMirror/markdown/markdownMathParser.test.js
|
||||
packages/editor/CodeMirror/markdown/markdownMathParser.js
|
||||
packages/editor/CodeMirror/markdown/utils/renumberSelectedLists.test.js
|
||||
packages/editor/CodeMirror/markdown/utils/renumberSelectedLists.js
|
||||
packages/editor/CodeMirror/markdown/utils/stripBlockquote.js
|
||||
packages/editor/CodeMirror/pluginApi/PluginLoader.js
|
||||
packages/editor/CodeMirror/pluginApi/codeMirrorRequire.js
|
||||
packages/editor/CodeMirror/pluginApi/customEditorCompletion.test.js
|
||||
@@ -756,10 +766,23 @@ packages/editor/CodeMirror/testUtil/loadLanguages.js
|
||||
packages/editor/CodeMirror/testUtil/pressReleaseKey.js
|
||||
packages/editor/CodeMirror/testUtil/typeText.js
|
||||
packages/editor/CodeMirror/theme.js
|
||||
packages/editor/CodeMirror/util/editorStateUtils.test.js
|
||||
packages/editor/CodeMirror/util/editorStateUtils.js
|
||||
packages/editor/CodeMirror/util/isInSyntaxNode.js
|
||||
packages/editor/CodeMirror/util/setupVim.js
|
||||
packages/editor/CodeMirror/utils/formatting/RegionSpec.js
|
||||
packages/editor/CodeMirror/utils/formatting/findInlineMatch.test.js
|
||||
packages/editor/CodeMirror/utils/formatting/findInlineMatch.js
|
||||
packages/editor/CodeMirror/utils/formatting/isIndentationEquivalent.js
|
||||
packages/editor/CodeMirror/utils/formatting/tabsToSpaces.test.js
|
||||
packages/editor/CodeMirror/utils/formatting/tabsToSpaces.js
|
||||
packages/editor/CodeMirror/utils/formatting/toggleInlineFormatGlobally.js
|
||||
packages/editor/CodeMirror/utils/formatting/toggleInlineRegionSurrounded.js
|
||||
packages/editor/CodeMirror/utils/formatting/toggleInlineSelectionFormat.js
|
||||
packages/editor/CodeMirror/utils/formatting/toggleRegionFormatGlobally.test.js
|
||||
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
|
||||
packages/editor/events.js
|
||||
packages/editor/types.js
|
||||
@@ -858,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
|
||||
@@ -904,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
|
||||
@@ -925,6 +951,7 @@ packages/lib/ntp.js
|
||||
packages/lib/onedrive-api.test.js
|
||||
packages/lib/onedrive-api.js
|
||||
packages/lib/path-utils.js
|
||||
packages/lib/reducer.test.js
|
||||
packages/lib/reducer.js
|
||||
packages/lib/registry.test.js
|
||||
packages/lib/registry.js
|
||||
@@ -1069,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
|
||||
@@ -1345,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
|
||||
|
@@ -35,7 +35,7 @@
|
||||
],
|
||||
"owner": "Laurent Cozic"
|
||||
},
|
||||
"version": "3.0.0",
|
||||
"version": "3.0.1",
|
||||
"bin": "./main.js",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
@@ -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.24",
|
||||
"@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"/>
|
@@ -204,7 +204,7 @@ class Application extends BaseApplication {
|
||||
public updateEditorFont() {
|
||||
const fontFamilies = [];
|
||||
if (Setting.value('style.editor.fontFamily')) fontFamilies.push(`"${Setting.value('style.editor.fontFamily')}"`);
|
||||
fontFamilies.push('Avenir, Arial, sans-serif');
|
||||
fontFamilies.push('\'Avenir Next\', Avenir, Arial, sans-serif');
|
||||
|
||||
// The '*' and '!important' parts are necessary to make sure Russian text is displayed properly
|
||||
// https://github.com/laurent22/joplin/issues/155
|
||||
@@ -420,6 +420,7 @@ class Application extends BaseApplication {
|
||||
AlarmService.setDriver(new AlarmServiceDriverNode({ appName: packageInfo.build.appId }));
|
||||
AlarmService.setLogger(reg.logger());
|
||||
|
||||
reg.setDispatch(this.dispatch.bind(this));
|
||||
reg.setShowErrorMessageBoxHandler((message: string) => { bridge().showErrorMessageBox(message); });
|
||||
|
||||
if (Setting.value('flagOpenDevTools')) {
|
||||
|
@@ -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 };
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import PluginService, { defaultPluginSetting, Plugins, PluginSetting, PluginSettings } from '@joplin/lib/services/plugins/PluginService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import styled from 'styled-components';
|
||||
@@ -214,8 +214,11 @@ export default function(props: Props) {
|
||||
props.onChange({ value: pluginService.serializePluginSettings(event.value) });
|
||||
}, [pluginService, props.onChange]);
|
||||
|
||||
const onDelete = useOnDeleteHandler(pluginSettings, onPluginSettingsChange, false);
|
||||
const onUpdate = useOnInstallHandler(setUpdatingPluginIds, pluginSettings, repoApi, onPluginSettingsChange, true);
|
||||
const pluginSettingsRef = useRef(pluginSettings);
|
||||
pluginSettingsRef.current = pluginSettings;
|
||||
|
||||
const onDelete = useOnDeleteHandler(pluginSettingsRef, onPluginSettingsChange, false);
|
||||
const onUpdate = useOnInstallHandler(setUpdatingPluginIds, pluginSettingsRef, repoApi, onPluginSettingsChange, true);
|
||||
|
||||
const onToolsClick = useCallback(async () => {
|
||||
const template = [
|
||||
|
@@ -40,7 +40,10 @@ export default function(props: Props) {
|
||||
const [installingPluginsIds, setInstallingPluginIds] = useState<Record<string, boolean>>({});
|
||||
const [searchResultCount, setSearchResultCount] = useState(null);
|
||||
|
||||
const onInstall = useOnInstallHandler(setInstallingPluginIds, props.pluginSettings, props.repoApi, props.onPluginSettingsChange, false);
|
||||
const pluginSettingsRef = useRef(props.pluginSettings);
|
||||
pluginSettingsRef.current = props.pluginSettings;
|
||||
|
||||
const onInstall = useOnInstallHandler(setInstallingPluginIds, pluginSettingsRef, props.repoApi, props.onPluginSettingsChange, false);
|
||||
|
||||
useEffect(() => {
|
||||
setSearchResultCount(null);
|
||||
|
@@ -9,6 +9,7 @@ import { Dispatch } from 'redux';
|
||||
import { reducer, defaultState, generateApplicationConfirmUrl, checkIfLoginWasSuccessful } from '@joplin/lib/services/joplinCloudUtils';
|
||||
import { AppState } from '../app.reducer';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
|
||||
const logger = Logger.create('JoplinCloudLoginScreen');
|
||||
const { connect } = require('react-redux');
|
||||
@@ -38,6 +39,7 @@ const JoplinCloudScreenComponent = (props: Props) => {
|
||||
if (response && response.success) {
|
||||
dispatch({ type: 'COMPLETED' });
|
||||
clearInterval(interval);
|
||||
void reg.scheduleSync(0);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
|
@@ -97,6 +97,7 @@ interface Props {
|
||||
notesSortOrderField: string;
|
||||
notesSortOrderReverse: boolean;
|
||||
notesColumns: NoteListColumns;
|
||||
showInvalidJoplinCloudCredential: boolean;
|
||||
}
|
||||
|
||||
interface ShareFolderDialogOptions {
|
||||
@@ -592,6 +593,13 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
});
|
||||
};
|
||||
|
||||
const onViewJoplinCloudLoginScreen = () => {
|
||||
this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'JoplinCloudLogin',
|
||||
});
|
||||
};
|
||||
|
||||
const onViewSyncSettingsScreen = () => {
|
||||
this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
@@ -684,6 +692,12 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
);
|
||||
} else if (this.props.mustUpgradeAppMessage) {
|
||||
msg = this.renderNotificationMessage(this.props.mustUpgradeAppMessage);
|
||||
} else if (this.props.showInvalidJoplinCloudCredential) {
|
||||
msg = this.renderNotificationMessage(
|
||||
_('Your Joplin Cloud credentials are invalid, please login.'),
|
||||
_('Login to Joplin Cloud.'),
|
||||
onViewJoplinCloudLoginScreen,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -705,7 +719,8 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
props.isSafeMode ||
|
||||
this.showShareInvitationNotification(props) ||
|
||||
this.props.needApiAuth ||
|
||||
!!this.props.mustUpgradeAppMessage;
|
||||
!!this.props.mustUpgradeAppMessage ||
|
||||
props.showInvalidJoplinCloudCredential;
|
||||
}
|
||||
|
||||
public registerCommands() {
|
||||
@@ -965,6 +980,7 @@ const mapStateToProps = (state: AppState) => {
|
||||
notesSortOrderField: state.settings['notes.sortOrder.field'],
|
||||
notesSortOrderReverse: state.settings['notes.sortOrder.reverse'],
|
||||
notesColumns: validateColumns(state.settings['notes.columns']),
|
||||
showInvalidJoplinCloudCredential: state.settings['sync.target'] === 10 && state.mustAuthenticate,
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -713,7 +713,7 @@ function useMenu(props: Props) {
|
||||
label: layoutButtonSequenceOptions[value],
|
||||
type: 'checkbox',
|
||||
click: () => {
|
||||
Setting.setValue('layoutButtonSequence', value);
|
||||
Setting.setValue('layoutButtonSequence', Number(value));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@@ -4,7 +4,7 @@ import KeymapService, { KeymapItem } from '@joplin/lib/services/KeymapService';
|
||||
import { EditorCommand } from '../../../../utils/types';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import setupVim from '@joplin/editor/CodeMirror/util/setupVim';
|
||||
import setupVim from '@joplin/editor/CodeMirror/utils/setupVim';
|
||||
import { EventName } from '@joplin/lib/eventManager';
|
||||
import normalizeAccelerator from '../../utils/normalizeAccelerator';
|
||||
import { CodeMirrorVersion } from '../../utils/types';
|
||||
|
@@ -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}
|
||||
|
@@ -8,7 +8,7 @@ import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import { ContentScriptType } from '@joplin/lib/services/plugins/api/types';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
import setupVim from '@joplin/editor/CodeMirror/util/setupVim';
|
||||
import setupVim from '@joplin/editor/CodeMirror/utils/setupVim';
|
||||
import { dirname } from 'path';
|
||||
import useKeymap from './utils/useKeymap';
|
||||
import useEditorSearch from '../utils/useEditorSearchExtension';
|
||||
|
@@ -6,7 +6,7 @@ import Resource from '@joplin/lib/models/Resource';
|
||||
const bridge = require('@electron/remote').require('./bridge').default;
|
||||
import ResourceFetcher from '@joplin/lib/services/ResourceFetcher';
|
||||
import htmlUtils from '@joplin/lib/htmlUtils';
|
||||
import rendererHtmlUtils, { extractHtmlBody } from '@joplin/renderer/htmlUtils';
|
||||
import rendererHtmlUtils, { extractHtmlBody, removeWrappingParagraphAndTrailingEmptyElements } from '@joplin/renderer/htmlUtils';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import { fileUriToPath } from '@joplin/utils/url';
|
||||
import { MarkupLanguage } from '@joplin/renderer';
|
||||
@@ -220,6 +220,13 @@ export async function processPastedHtml(html: string, htmlToMd: HtmlToMarkdownHa
|
||||
if (htmlToMd && mdToHtml) {
|
||||
const md = await htmlToMd(MarkupLanguage.Markdown, html, '');
|
||||
html = (await mdToHtml(MarkupLanguage.Markdown, md, markupRenderOptions({ bodyOnly: true }))).html;
|
||||
|
||||
// When plugins that add to the end of rendered content are installed, bodyOnly can
|
||||
// fail to remove the wrapping paragraph. This works around that issue by removing
|
||||
// the wrapping paragraph in more cases. See issue #10061.
|
||||
if (!md.trim().includes('\n')) {
|
||||
html = removeWrappingParagraphAndTrailingEmptyElements(html);
|
||||
}
|
||||
}
|
||||
|
||||
return extractHtmlBody(rendererHtmlUtils.sanitizeHtml(html, {
|
||||
|
@@ -104,10 +104,17 @@ const useOnKeyDown = (
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
if (noteIds.length && (key === 'Delete' || (key === 'Backspace' && event.metaKey))) {
|
||||
event.preventDefault();
|
||||
if (CommandService.instance().isEnabled('deleteNote')) {
|
||||
void CommandService.instance().execute('deleteNote', noteIds);
|
||||
if (noteIds.length) {
|
||||
if (key === 'Delete' && event.shiftKey) {
|
||||
event.preventDefault();
|
||||
if (CommandService.instance().isEnabled('permanentlyDeleteNote')) {
|
||||
void CommandService.instance().execute('permanentlyDeleteNote', noteIds);
|
||||
}
|
||||
} else if (key === 'Delete' || (key === 'Backspace' && event.metaKey)) {
|
||||
event.preventDefault();
|
||||
if (CommandService.instance().isEnabled('deleteNote')) {
|
||||
void CommandService.instance().execute('deleteNote', noteIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { useMemo } from 'react';
|
||||
import { FolderListItem, HeaderId, HeaderListItem, ListItem, ListItemType, TagListItem } from '../types';
|
||||
import { FolderEntity, TagsWithNoteCountEntity } from '@joplin/lib/services/database/types';
|
||||
import { renderFolders, renderTags } from '@joplin/lib/components/shared/side-menu-shared';
|
||||
import { buildFolderTree, renderFolders, renderTags } from '@joplin/lib/components/shared/side-menu-shared';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
@@ -35,10 +35,13 @@ const useSidebarListData = (props: Props): ListItem[] => {
|
||||
});
|
||||
}, [props.tags]);
|
||||
|
||||
const folderTree = useMemo(() => {
|
||||
return buildFolderTree(props.folders);
|
||||
}, [props.folders]);
|
||||
|
||||
const folderItems = useMemo(() => {
|
||||
const renderProps = {
|
||||
folders: props.folders,
|
||||
folderTree,
|
||||
collapsedFolderIds: props.collapsedFolderIds,
|
||||
};
|
||||
return renderFolders<ListItem>(renderProps, (folder, hasChildren, depth): FolderListItem => {
|
||||
@@ -50,7 +53,7 @@ const useSidebarListData = (props: Props): ListItem[] => {
|
||||
key: folder.id,
|
||||
};
|
||||
});
|
||||
}, [props.folders, props.collapsedFolderIds]);
|
||||
}, [folderTree, props.collapsedFolderIds]);
|
||||
|
||||
return useMemo(() => {
|
||||
const foldersHeader: HeaderListItem = {
|
||||
|
@@ -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) => {
|
||||
|
@@ -1,5 +1,7 @@
|
||||
import { test, expect } from './util/test';
|
||||
import MainScreen from './models/MainScreen';
|
||||
import activateMainMenuItem from './util/activateMainMenuItem';
|
||||
import setMessageBoxResponse from './util/setMessageBoxResponse';
|
||||
|
||||
test.describe('noteList', () => {
|
||||
test('should be possible to edit notes in a different notebook when searching', async ({ mainWindow }) => {
|
||||
@@ -35,4 +37,40 @@ test.describe('noteList', () => {
|
||||
// Updating the title should force the sidebar to update sooner
|
||||
await expect(editor.noteTitleInput).toHaveValue('note-1');
|
||||
});
|
||||
|
||||
test('shift-delete should ask to permanently delete notes, but only when the note list is focused', async ({ electronApp, mainWindow }) => {
|
||||
const mainScreen = new MainScreen(mainWindow);
|
||||
const sidebar = mainScreen.sidebar;
|
||||
|
||||
const folderBHeader = await sidebar.createNewFolder('Folder B');
|
||||
const folderAHeader = await sidebar.createNewFolder('Folder A');
|
||||
await expect(folderAHeader).toBeVisible();
|
||||
|
||||
await mainScreen.createNewNote('test note 1');
|
||||
await mainScreen.createNewNote('test note 2');
|
||||
|
||||
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);
|
||||
|
||||
const pressShiftDelete = async () => {
|
||||
await mainWindow.keyboard.press('Shift');
|
||||
await mainWindow.keyboard.press('Delete');
|
||||
await mainWindow.keyboard.up('Delete');
|
||||
await mainWindow.keyboard.up('Shift');
|
||||
};
|
||||
await pressShiftDelete();
|
||||
await expect(mainScreen.noteListContainer.getByText('test note 2')).not.toBeVisible();
|
||||
|
||||
// Should not delete when the editor is focused
|
||||
await mainScreen.noteEditor.focusCodeMirrorEditor();
|
||||
await mainWindow.keyboard.type('test');
|
||||
await pressShiftDelete();
|
||||
|
||||
await folderBHeader.click();
|
||||
await folderAHeader.click();
|
||||
await expect(mainScreen.noteListContainer.getByText('test note 1')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "3.0.9",
|
||||
"version": "3.0.13",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"private": true,
|
||||
@@ -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.24",
|
||||
"@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-*
|
||||
|
@@ -79,8 +79,8 @@ android {
|
||||
applicationId "net.cozic.joplin"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 2097743
|
||||
versionName "3.0.4"
|
||||
versionCode 2097747
|
||||
versionName "3.0.8"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
|
@@ -92,6 +92,15 @@
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
<!-- /SHARE EXTENSION -->
|
||||
|
||||
<!-- EXTERNAL LINK -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="joplin" />
|
||||
</intent-filter>
|
||||
<!-- /EXTERNAL LINK -->
|
||||
</activity>
|
||||
|
||||
</application>
|
||||
|
40
packages/app-mobile/components/BetaChip.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import * as React from 'react';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import { themeStyle } from './global-style';
|
||||
import { useMemo } from 'react';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { connect } from 'react-redux';
|
||||
import { AppState } from '../utils/types';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
const useStyles = (themeId: number, size: number) => {
|
||||
return useMemo(() => {
|
||||
const theme = themeStyle(themeId);
|
||||
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
borderColor: theme.color4,
|
||||
color: theme.color4,
|
||||
fontSize: size,
|
||||
opacity: 0.8,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
padding: 4,
|
||||
flexGrow: 0,
|
||||
textAlign: 'center',
|
||||
textAlignVertical: 'center',
|
||||
},
|
||||
});
|
||||
}, [themeId, size]);
|
||||
};
|
||||
|
||||
const BetaChip: React.FC<Props> = props => {
|
||||
const styles = useStyles(props.themeId, props.size);
|
||||
return <View><Text style={styles.container}>{_('Beta')}</Text></View>;
|
||||
};
|
||||
|
||||
export default connect((state: AppState) => ({ themeId: state.settings.theme }))(BetaChip);
|
@@ -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;
|
||||
|
@@ -14,6 +14,7 @@ import createEditor from '@joplin/editor/CodeMirror/createEditor';
|
||||
import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
|
||||
import WebViewToRNMessenger from '../../../utils/ipc/WebViewToRNMessenger';
|
||||
import { WebViewToEditorApi } from '../types';
|
||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||
|
||||
export const initCodeMirror = (
|
||||
parentElement: HTMLElement,
|
||||
@@ -26,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);
|
||||
},
|
||||
@@ -47,6 +63,15 @@ export const initCodeMirror = (
|
||||
}
|
||||
});
|
||||
|
||||
// Note: Just adding an onclick listener seems sufficient to focus the editor when its background
|
||||
// is tapped.
|
||||
parentElement.addEventListener('click', (event) => {
|
||||
const activeElement = document.querySelector(':focus');
|
||||
if (!parentElement.contains(activeElement) && event.target === parentElement) {
|
||||
focus('initial editor focus', control);
|
||||
}
|
||||
});
|
||||
|
||||
messenger.setLocalInterface(control);
|
||||
return control;
|
||||
};
|
||||
|
@@ -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';
|
||||
}
|
||||
|
||||
@@ -101,10 +104,6 @@ function useHtml(initialCss: string): string {
|
||||
scrolling. */
|
||||
.cm-scroller {
|
||||
overflow: none;
|
||||
|
||||
/* Ensure that the editor can be focused by clicking on the lower half of the screen.
|
||||
Don't use 100vh to prevent a scrollbar being present for empty notes. */
|
||||
min-height: 80vh;
|
||||
}
|
||||
</style>
|
||||
<style class=${JSON.stringify(themeStyleSheetClassName)}>
|
||||
@@ -377,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) {
|
||||
@@ -385,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,
|
||||
@@ -454,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>;
|
||||
}
|
||||
|
@@ -16,6 +16,7 @@ interface WrapperProps {
|
||||
mustUpgradeAppMessage?: string;
|
||||
shareInvitations?: ShareInvitation[];
|
||||
processingShareInvitationResponse?: boolean;
|
||||
showInvalidJoplinCloudCredential?: boolean;
|
||||
}
|
||||
|
||||
const WarningBannerWrapper: React.FC<WrapperProps> = props => {
|
||||
@@ -29,6 +30,7 @@ const WarningBannerWrapper: React.FC<WrapperProps> = props => {
|
||||
mustUpgradeAppMessage={props.mustUpgradeAppMessage ?? ''}
|
||||
shareInvitations={props.shareInvitations ?? []}
|
||||
processingShareInvitationResponse={props.processingShareInvitationResponse ?? false}
|
||||
showInvalidJoplinCloudCredential={props.showInvalidJoplinCloudCredential ?? false}
|
||||
/>;
|
||||
};
|
||||
|
||||
|
@@ -19,6 +19,7 @@ interface Props {
|
||||
mustUpgradeAppMessage: string;
|
||||
shareInvitations: ShareInvitation[];
|
||||
processingShareInvitationResponse: boolean;
|
||||
showInvalidJoplinCloudCredential: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +51,9 @@ export const WarningBannerComponent: React.FC<Props> = props => {
|
||||
if (props.hasDisabledEncryptionItems) {
|
||||
warningComps.push(renderWarningBox('Status', _('Some items cannot be decrypted.')));
|
||||
}
|
||||
if (props.showInvalidJoplinCloudCredential) {
|
||||
warningComps.push(renderWarningBox('JoplinCloudLogin', _('Your Joplin Cloud credentials are invalid, please login.')));
|
||||
}
|
||||
|
||||
const shareInvitation = props.shareInvitations.find(inv => inv.status === ShareUserStatus.Waiting);
|
||||
if (
|
||||
@@ -85,5 +89,6 @@ export default connect((state: AppState) => {
|
||||
mustUpgradeAppMessage: state.mustUpgradeAppMessage,
|
||||
shareInvitations: state.shareService.shareInvitations,
|
||||
processingShareInvitationResponse: state.shareService.processingShareInvitationResponse,
|
||||
showInvalidJoplinCloudCredential: state.settings['sync.target'] === 10 && state.mustAuthenticate,
|
||||
};
|
||||
})(WarningBannerComponent);
|
||||
|
@@ -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}
|
||||
|
@@ -1,127 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import Setting, { AppType, SettingMetadataSection, SettingSectionSource } from '@joplin/lib/models/Setting';
|
||||
import { FunctionComponent, useEffect, useMemo, useState } from 'react';
|
||||
import { ConfigScreenStyles } from './configScreenStyles';
|
||||
import { FlatList, Text, View, ViewStyle } from 'react-native';
|
||||
import { settingsSections } from '@joplin/lib/components/shared/config/config-shared';
|
||||
import Icon from '../../Icon';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { TouchableRipple } from 'react-native-paper';
|
||||
|
||||
interface Props {
|
||||
styles: ConfigScreenStyles;
|
||||
|
||||
width: number|undefined;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
settings: any;
|
||||
selectedSectionName: string|null;
|
||||
openSection: (sectionName: string)=> void;
|
||||
}
|
||||
|
||||
const SectionSelector: FunctionComponent<Props> = props => {
|
||||
const sections = useMemo(() => {
|
||||
return settingsSections({ device: AppType.Mobile, settings: props.settings });
|
||||
}, [props.settings]);
|
||||
const styles = props.styles.styleSheet;
|
||||
|
||||
const itemHeight = styles.sidebarButton.height;
|
||||
|
||||
const onRenderButton = ({ item }: { item: SettingMetadataSection }) => {
|
||||
const section = item;
|
||||
const selected = props.selectedSectionName === section.name;
|
||||
const icon = Setting.sectionNameToIcon(section.name, AppType.Mobile);
|
||||
const label = Setting.sectionNameToLabel(section.name);
|
||||
const shortDescription = Setting.sectionMetadataToSummary(section);
|
||||
const isPlugin = item.source === SettingSectionSource.Plugin;
|
||||
|
||||
const titleStyle = selected ? styles.sidebarSelectedButtonText : styles.sidebarButtonMainText;
|
||||
|
||||
const sourceIcon = isPlugin ? (
|
||||
<Icon
|
||||
name='fas fa-puzzle-piece'
|
||||
accessibilityLabel={_('From a plugin')}
|
||||
style={titleStyle}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<TouchableRipple
|
||||
key={section.name}
|
||||
role='tab'
|
||||
aria-selected={selected}
|
||||
onPress={() => props.openSection(section.name)}
|
||||
>
|
||||
<View
|
||||
style={selected ? styles.selectedSidebarButton : styles.sidebarButton}
|
||||
>
|
||||
<Icon
|
||||
name={icon}
|
||||
accessibilityLabel={null}
|
||||
style={styles.sidebarIcon}
|
||||
/>
|
||||
<View style={{ display: 'flex', flexDirection: 'column', flex: 1 }}>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={titleStyle}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
<Text
|
||||
style={styles.sidebarButtonDescriptionText}
|
||||
numberOfLines={1}
|
||||
ellipsizeMode='tail'
|
||||
>
|
||||
{shortDescription ?? ''}
|
||||
</Text>
|
||||
</View>
|
||||
{sourceIcon}
|
||||
</View>
|
||||
</TouchableRipple>
|
||||
);
|
||||
};
|
||||
|
||||
const [flatListRef, setFlatListRef] = useState<FlatList|null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (flatListRef && props.selectedSectionName) {
|
||||
let selectedIndex = 0;
|
||||
for (const section of sections) {
|
||||
if (section.name === props.selectedSectionName) {
|
||||
break;
|
||||
}
|
||||
selectedIndex ++;
|
||||
}
|
||||
|
||||
flatListRef.scrollToIndex({
|
||||
index: selectedIndex,
|
||||
viewPosition: 0.5,
|
||||
});
|
||||
}
|
||||
}, [props.selectedSectionName, flatListRef, sections]);
|
||||
|
||||
const containerStyle: ViewStyle = useMemo(() => ({
|
||||
width: props.width,
|
||||
maxWidth: props.width,
|
||||
minWidth: props.width,
|
||||
flex: 1,
|
||||
}), [props.width]);
|
||||
|
||||
return (
|
||||
<View style={containerStyle}>
|
||||
<FlatList
|
||||
role='tablist'
|
||||
ref={setFlatListRef}
|
||||
data={sections}
|
||||
renderItem={onRenderButton}
|
||||
keyExtractor={item => item.name}
|
||||
getItemLayout={(_data, index) => ({
|
||||
length: itemHeight, offset: itemHeight * index, index,
|
||||
})}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionSelector;
|
@@ -0,0 +1,66 @@
|
||||
import * as React from 'react';
|
||||
import { ConfigScreenStyles } from '../configScreenStyles';
|
||||
import Icon from '../../../Icon';
|
||||
import BetaChip from '../../../BetaChip';
|
||||
import { TouchableRipple, Text } from 'react-native-paper';
|
||||
import { View } from 'react-native';
|
||||
import Setting, { AppType, SettingMetadataSection } from '@joplin/lib/models/Setting';
|
||||
|
||||
interface Props {
|
||||
selected: boolean;
|
||||
section: SettingMetadataSection;
|
||||
styles: ConfigScreenStyles;
|
||||
onPress: ()=> void;
|
||||
}
|
||||
|
||||
const SectionTab: React.FC<Props> = ({ styles, onPress, selected, section }) => {
|
||||
const icon = Setting.sectionNameToIcon(section.name, AppType.Mobile);
|
||||
const label = Setting.sectionNameToLabel(section.name);
|
||||
const shortDescription = Setting.sectionMetadataToSummary(section);
|
||||
|
||||
const styleSheet = styles.styleSheet;
|
||||
const titleStyle = selected ? styleSheet.sidebarSelectedButtonText : styleSheet.sidebarButtonMainText;
|
||||
|
||||
const isBeta = section.name === 'plugins';
|
||||
const betaChip = isBeta ? <BetaChip size={10}/> : null;
|
||||
|
||||
return (
|
||||
<TouchableRipple
|
||||
key={section.name}
|
||||
role='tab'
|
||||
aria-selected={selected}
|
||||
onPress={onPress}
|
||||
>
|
||||
<View
|
||||
style={selected ? styleSheet.selectedSidebarButton : styleSheet.sidebarButton}
|
||||
>
|
||||
<Icon
|
||||
name={icon}
|
||||
accessibilityLabel={null}
|
||||
style={styleSheet.sidebarIcon}
|
||||
/>
|
||||
<View style={{ display: 'flex', flexDirection: 'column', flex: 1 }}>
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={titleStyle}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
|
||||
{betaChip}
|
||||
</View>
|
||||
<Text
|
||||
style={styleSheet.sidebarButtonDescriptionText}
|
||||
numberOfLines={1}
|
||||
ellipsizeMode='tail'
|
||||
>
|
||||
{shortDescription ?? ''}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableRipple>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionTab;
|
@@ -0,0 +1,116 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { AppType, SettingMetadataSection, SettingSectionSource } from '@joplin/lib/models/Setting';
|
||||
import { FunctionComponent, useEffect, useMemo, useState } from 'react';
|
||||
import { ConfigScreenStyles } from '../configScreenStyles';
|
||||
import { FlatList, View, ViewStyle } from 'react-native';
|
||||
import { Text } from 'react-native-paper';
|
||||
import { settingsSections } from '@joplin/lib/components/shared/config/config-shared';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import SectionTab from './SectionTab';
|
||||
|
||||
interface Props {
|
||||
styles: ConfigScreenStyles;
|
||||
|
||||
width: number|undefined;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
settings: Record<string, any>;
|
||||
selectedSectionName: string|null;
|
||||
openSection: (sectionName: string)=> void;
|
||||
}
|
||||
type SectionDivider = { divider: boolean; label: string; name: string; source?: string };
|
||||
|
||||
const SectionSelector: FunctionComponent<Props> = props => {
|
||||
type SectionListType = (SectionDivider|SettingMetadataSection)[];
|
||||
const sections = useMemo((): SectionListType => {
|
||||
const allSections = settingsSections({ device: AppType.Mobile, settings: props.settings });
|
||||
|
||||
const builtInSections = [];
|
||||
const pluginSections = [];
|
||||
for (const section of allSections) {
|
||||
if (section.source === SettingSectionSource.Plugin) {
|
||||
pluginSections.push(section);
|
||||
} else {
|
||||
builtInSections.push(section);
|
||||
}
|
||||
}
|
||||
|
||||
let result: SectionListType = builtInSections;
|
||||
if (pluginSections.length > 0) {
|
||||
result = result.concat([
|
||||
{ label: _('Plugins'), name: 'plugins-divider', divider: true },
|
||||
...pluginSections,
|
||||
]);
|
||||
}
|
||||
return result;
|
||||
}, [props.settings]);
|
||||
|
||||
const styles = props.styles.styleSheet;
|
||||
const onRenderButton = ({ item }: { item: SettingMetadataSection|SectionDivider }) => {
|
||||
const section = item;
|
||||
const selected = props.selectedSectionName === section.name;
|
||||
|
||||
if ('divider' in item && item.divider) {
|
||||
return (
|
||||
<View style={styles.sidebarHeader}>
|
||||
<Text variant='labelLarge' style={styles.sidebarHeaderText}>{item.label}</Text>
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<SectionTab
|
||||
selected={selected}
|
||||
section={section as SettingMetadataSection}
|
||||
styles={props.styles}
|
||||
onPress={() => props.openSection(section.name)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const [flatListRef, setFlatListRef] = useState<FlatList|null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (flatListRef && props.selectedSectionName) {
|
||||
let selectedIndex = 0;
|
||||
for (const section of sections) {
|
||||
if (section.name === props.selectedSectionName) {
|
||||
break;
|
||||
}
|
||||
selectedIndex ++;
|
||||
}
|
||||
|
||||
flatListRef.scrollToIndex({
|
||||
index: selectedIndex,
|
||||
viewPosition: 0.5,
|
||||
});
|
||||
}
|
||||
}, [props.selectedSectionName, flatListRef, sections]);
|
||||
|
||||
const containerStyle: ViewStyle = useMemo(() => ({
|
||||
width: props.width,
|
||||
maxWidth: props.width,
|
||||
minWidth: props.width,
|
||||
flex: 1,
|
||||
}), [props.width]);
|
||||
|
||||
return (
|
||||
<View style={containerStyle}>
|
||||
<FlatList
|
||||
role='tablist'
|
||||
ref={setFlatListRef}
|
||||
data={sections}
|
||||
renderItem={onRenderButton}
|
||||
keyExtractor={item => item.name}
|
||||
onScrollToIndexFailed={({ index, averageItemLength }) => {
|
||||
// Scrolling to a particular index can fail if the item at that index hasn't been rendered yet.
|
||||
// This shouldn't happen often, so a guess should be sufficient.
|
||||
flatListRef.scrollToOffset({ offset: (index + 0.5) * averageItemLength });
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionSelector;
|
@@ -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;
|
||||
|
||||
|
@@ -33,6 +33,8 @@ export interface ConfigScreenStyleSheet {
|
||||
sidebarButtonMainText: TextStyle;
|
||||
sidebarSelectedButtonText: TextStyle;
|
||||
sidebarButtonDescriptionText: TextStyle;
|
||||
sidebarHeader: ViewStyle;
|
||||
sidebarHeaderText: TextStyle;
|
||||
|
||||
settingControl: TextStyle;
|
||||
}
|
||||
@@ -204,6 +206,18 @@ const configScreenStyles = (themeId: number): ConfigScreenStyles => {
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
sidebarButtonDescriptionText,
|
||||
sidebarHeader: {
|
||||
paddingLeft: 12,
|
||||
height: sidebarButtonHeight / 2,
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: theme.oddBackgroundColor,
|
||||
},
|
||||
sidebarHeaderText: {
|
||||
color: theme.color,
|
||||
fontWeight: 'bold',
|
||||
fontSize: theme.fontSize,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
|
@@ -0,0 +1,57 @@
|
||||
import * as React from 'react';
|
||||
import { Chip, ChipProps } from 'react-native-paper';
|
||||
import { useMemo } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { AppState } from '../../../../../utils/types';
|
||||
import { themeStyle } from '../../../../global-style';
|
||||
|
||||
type Props = {
|
||||
themeId: number;
|
||||
color?: string;
|
||||
faded?: boolean;
|
||||
onPress?: ()=> void;
|
||||
icon?: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const fadedStyle = { opacity: 0.87 };
|
||||
|
||||
const PluginChip: React.FC<Props> = props => {
|
||||
const themeOverride = useMemo(() => {
|
||||
const theme = themeStyle(props.themeId);
|
||||
const foreground = props.color ?? theme.color;
|
||||
const background = theme.backgroundColor;
|
||||
return {
|
||||
colors: {
|
||||
secondaryContainer: background,
|
||||
onSecondaryContainer: foreground,
|
||||
primary: foreground,
|
||||
|
||||
outline: foreground,
|
||||
onPrimary: foreground,
|
||||
onSurfaceVariant: foreground,
|
||||
},
|
||||
};
|
||||
}, [props.themeId, props.color]);
|
||||
|
||||
const accessibilityProps: Partial<ChipProps> = {};
|
||||
if (!props.onPress) {
|
||||
// Note: May have no effect until a future version of RN Paper.
|
||||
// See https://github.com/callstack/react-native-paper/pull/4327
|
||||
accessibilityProps.accessibilityRole = 'text';
|
||||
}
|
||||
|
||||
return <Chip
|
||||
theme={themeOverride}
|
||||
style={props.faded ? fadedStyle : null}
|
||||
mode='outlined'
|
||||
{...accessibilityProps}
|
||||
{...props}
|
||||
/>;
|
||||
};
|
||||
|
||||
export default connect((state: AppState) => {
|
||||
return {
|
||||
themeId: state.settings.theme,
|
||||
};
|
||||
})(PluginChip);
|
@@ -2,10 +2,10 @@ import { PluginItem } from '@joplin/lib/components/shared/config/plugins/types';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import * as React from 'react';
|
||||
import { Alert, Linking, View, ViewStyle } from 'react-native';
|
||||
import { View, ViewStyle } from 'react-native';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { PluginCallback } from '../utils/usePluginCallbacks';
|
||||
import StyledChip from './StyledChip';
|
||||
import PluginChip from './PluginChip';
|
||||
import { themeStyle } from '../../../../global-style';
|
||||
|
||||
interface Props {
|
||||
@@ -19,23 +19,6 @@ interface Props {
|
||||
onShowPluginLog?: PluginCallback;
|
||||
}
|
||||
|
||||
const onRecommendedPress = () => {
|
||||
Alert.alert(
|
||||
'',
|
||||
_('The Joplin team has vetted this plugin and it meets our standards for security and performance.'),
|
||||
[
|
||||
{
|
||||
text: _('Learn more'),
|
||||
onPress: () => Linking.openURL('https://github.com/joplin/plugins/blob/master/readme/recommended.md'),
|
||||
},
|
||||
{
|
||||
text: _('OK'),
|
||||
},
|
||||
],
|
||||
{ cancelable: true },
|
||||
);
|
||||
};
|
||||
|
||||
const containerStyle: ViewStyle = {
|
||||
flexDirection: 'row',
|
||||
gap: 4,
|
||||
@@ -54,43 +37,28 @@ const PluginChips: React.FC<Props> = props => {
|
||||
if (!props.hasErrors) return null;
|
||||
|
||||
return (
|
||||
<StyledChip
|
||||
background={theme.backgroundColor2}
|
||||
foreground={theme.colorError2}
|
||||
<PluginChip
|
||||
color={theme.colorError2}
|
||||
icon='alert'
|
||||
mode='flat'
|
||||
onPress={() => props.onShowPluginLog({ item })}
|
||||
>
|
||||
{_('Error')}
|
||||
</StyledChip>
|
||||
</PluginChip>
|
||||
);
|
||||
};
|
||||
|
||||
const renderRecommendedChip = () => {
|
||||
if (!props.item.manifest._recommended || !props.isCompatible) {
|
||||
return null;
|
||||
}
|
||||
return <StyledChip
|
||||
background={theme.searchMarkerBackgroundColor}
|
||||
foreground={theme.searchMarkerColor}
|
||||
icon='crown'
|
||||
onPress={onRecommendedPress}
|
||||
>{_('Recommended')}</StyledChip>;
|
||||
};
|
||||
|
||||
const renderBuiltInChip = () => {
|
||||
if (!props.item.builtIn) {
|
||||
return null;
|
||||
}
|
||||
return <StyledChip icon='code-tags-check'>{_('Built-in')}</StyledChip>;
|
||||
return <PluginChip icon='code-tags-check'>{_('Built-in')}</PluginChip>;
|
||||
};
|
||||
|
||||
const renderIncompatibleChip = () => {
|
||||
if (props.isCompatible) return null;
|
||||
return (
|
||||
<StyledChip
|
||||
background={theme.backgroundColor3}
|
||||
foreground={theme.color3}
|
||||
<PluginChip
|
||||
color={theme.color3}
|
||||
icon='alert'
|
||||
onPress={() => {
|
||||
void shim.showMessageBox(
|
||||
@@ -98,7 +66,7 @@ const PluginChips: React.FC<Props> = props => {
|
||||
{ buttons: [_('OK')] },
|
||||
);
|
||||
}}
|
||||
>{_('Incompatible')}</StyledChip>
|
||||
>{_('Incompatible')}</PluginChip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -106,7 +74,7 @@ const PluginChips: React.FC<Props> = props => {
|
||||
if (!props.isCompatible || !props.canUpdate) return null;
|
||||
|
||||
return (
|
||||
<StyledChip>{_('Update available')}</StyledChip>
|
||||
<PluginChip>{_('Update available')}</PluginChip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -114,21 +82,20 @@ const PluginChips: React.FC<Props> = props => {
|
||||
if (props.item.enabled || !props.item.installed) {
|
||||
return null;
|
||||
}
|
||||
return <StyledChip>{_('Disabled')}</StyledChip>;
|
||||
return <PluginChip faded={true}>{_('Disabled')}</PluginChip>;
|
||||
};
|
||||
|
||||
const renderInstalledChip = () => {
|
||||
if (!props.showInstalledChip) {
|
||||
return null;
|
||||
}
|
||||
return <StyledChip>{_('Installed')}</StyledChip>;
|
||||
return <PluginChip faded={true}>{_('Installed')}</PluginChip>;
|
||||
};
|
||||
|
||||
return <View style={containerStyle}>
|
||||
{renderIncompatibleChip()}
|
||||
{renderInstalledChip()}
|
||||
{renderErrorsChip()}
|
||||
{renderRecommendedChip()}
|
||||
{renderBuiltInChip()}
|
||||
{renderUpdatableChip()}
|
||||
{renderDisabledChip()}
|
||||
|
@@ -0,0 +1,74 @@
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { PluginManifest } from '@joplin/lib/services/plugins/utils/types';
|
||||
import * as React from 'react';
|
||||
import IconButton from '../../../../IconButton';
|
||||
import { Alert, Linking, StyleSheet } from 'react-native';
|
||||
import { themeStyle } from '../../../../global-style';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
const onRecommendedPress = () => {
|
||||
Alert.alert(
|
||||
'',
|
||||
_('The Joplin team has vetted this plugin and it meets our standards for security and performance.'),
|
||||
[
|
||||
{
|
||||
text: _('Learn more'),
|
||||
onPress: () => Linking.openURL('https://github.com/joplin/plugins/blob/master/readme/recommended.md'),
|
||||
},
|
||||
{
|
||||
text: _('OK'),
|
||||
},
|
||||
],
|
||||
{ cancelable: true },
|
||||
);
|
||||
};
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
manifest: PluginManifest;
|
||||
isCompatible: boolean;
|
||||
}
|
||||
|
||||
const useStyles = (themeId: number) => {
|
||||
return useMemo(() => {
|
||||
const theme = themeStyle(themeId);
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
opacity: 0.8,
|
||||
},
|
||||
wrapper: {
|
||||
borderColor: theme.colorWarn,
|
||||
borderWidth: 1,
|
||||
borderRadius: 20,
|
||||
justifyContent: 'center',
|
||||
height: 32,
|
||||
width: 32,
|
||||
textAlign: 'center',
|
||||
},
|
||||
icon: {
|
||||
fontSize: 14,
|
||||
color: theme.colorWarn,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
},
|
||||
});
|
||||
}, [themeId]);
|
||||
};
|
||||
|
||||
const RecommendedBadge: React.FC<Props> = props => {
|
||||
const styles = useStyles(props.themeId);
|
||||
|
||||
if (!props.manifest._recommended || !props.isCompatible) return null;
|
||||
|
||||
return <IconButton
|
||||
onPress={onRecommendedPress}
|
||||
iconName='fas fa-crown'
|
||||
containerStyle={styles.container}
|
||||
contentWrapperStyle={styles.wrapper}
|
||||
iconStyle={styles.icon}
|
||||
themeId={props.themeId}
|
||||
description={_('Recommended')}
|
||||
/>;
|
||||
};
|
||||
|
||||
export default RecommendedBadge;
|
@@ -1,39 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { Chip, ChipProps } from 'react-native-paper';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
type Props = ({
|
||||
foreground: string;
|
||||
background: string;
|
||||
}|{
|
||||
foreground?: undefined;
|
||||
background?: undefined;
|
||||
}) & ChipProps;
|
||||
|
||||
const RecommendedChip: React.FC<Props> = props => {
|
||||
const themeOverride = useMemo(() => {
|
||||
if (!props.foreground) return {};
|
||||
return {
|
||||
colors: {
|
||||
secondaryContainer: props.background,
|
||||
onSecondaryContainer: props.foreground,
|
||||
primary: props.foreground,
|
||||
},
|
||||
};
|
||||
}, [props.foreground, props.background]);
|
||||
|
||||
const accessibilityProps: Partial<Props> = {};
|
||||
if (!props.onPress) {
|
||||
// Note: May have no effect until a future version of RN Paper.
|
||||
// See https://github.com/callstack/react-native-paper/pull/4327
|
||||
accessibilityProps.accessibilityRole = 'text';
|
||||
}
|
||||
|
||||
return <Chip
|
||||
theme={themeOverride}
|
||||
{...accessibilityProps}
|
||||
{...props}
|
||||
/>;
|
||||
};
|
||||
|
||||
export default RecommendedChip;
|