Compare commits
269 Commits
android-v3
...
plugin-gen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1aec4a9f7 | ||
|
|
cab1525589 | ||
|
|
a52f3fea9e | ||
|
|
dfbd5eb8ed | ||
|
|
3131f36033 | ||
|
|
dc5b2cfa21 | ||
|
|
cad0f35fcc | ||
|
|
38ea92ff57 | ||
|
|
830deada22 | ||
|
|
38cd4033ea | ||
|
|
02900752d9 | ||
|
|
091e9813b5 | ||
|
|
e61e5ac32a | ||
|
|
414970c9a1 | ||
|
|
d4ed49ff23 | ||
|
|
8751d5d152 | ||
|
|
2e846fe15d | ||
|
|
e54b7696d9 | ||
|
|
553c61d628 | ||
|
|
6a15db3a36 | ||
|
|
6f1d0a4b90 | ||
|
|
33b995672c | ||
|
|
8ee46bb4e7 | ||
|
|
b35d9a64cf | ||
|
|
64ef74dd01 | ||
|
|
53035839a5 | ||
|
|
af5287de99 | ||
|
|
45a7554774 | ||
|
|
b06ffe3d25 | ||
|
|
53ea51b758 | ||
|
|
820acdc1f0 | ||
|
|
ef0a79666e | ||
|
|
d096a90c0e | ||
|
|
191775310e | ||
|
|
4fc351b861 | ||
|
|
396decd26c | ||
|
|
01f8fa7bef | ||
|
|
c40856ac7e | ||
|
|
d869cce413 | ||
|
|
a83e8311d8 | ||
|
|
aa884fcb39 | ||
|
|
be2a4c3e24 | ||
|
|
520eec555b | ||
|
|
1281fdb9d2 | ||
|
|
6029353fd1 | ||
|
|
8d1d1be79e | ||
|
|
fd180ae0b4 | ||
|
|
6fdfd6eae6 | ||
|
|
cd5bb575c8 | ||
|
|
2df56530ae | ||
|
|
7987137470 | ||
|
|
a1dcd2fd8f | ||
|
|
7826dc064a | ||
|
|
eedf083bfd | ||
|
|
d4aa1f8f8d | ||
|
|
738e749d51 | ||
|
|
8fe818c0b0 | ||
|
|
e603452fad | ||
|
|
3827637b54 | ||
|
|
1da7c54e5f | ||
|
|
e24ebffba6 | ||
|
|
e5bd77836a | ||
|
|
8f5e628303 | ||
|
|
6850c8128b | ||
|
|
8a797fdf23 | ||
|
|
1ae550c0aa | ||
|
|
e7e0529f52 | ||
|
|
2381e44c7f | ||
|
|
a59e975f73 | ||
|
|
2d703b6292 | ||
|
|
b8db70f707 | ||
|
|
c91513b6b5 | ||
|
|
a57ada97ef | ||
|
|
d8677a70dd | ||
|
|
15839a19fd | ||
|
|
8f1d55c1fc | ||
|
|
98c18711f7 | ||
|
|
24ff4612fb | ||
|
|
f832eb38ff | ||
|
|
91dc23c23f | ||
|
|
d1913493ab | ||
|
|
fd2b22ed68 | ||
|
|
14b56f19df | ||
|
|
0b082a985b | ||
|
|
53dcac22d0 | ||
|
|
2c721a76b7 | ||
|
|
b68cfd6d9e | ||
|
|
affebedc4b | ||
|
|
a714ef4807 | ||
|
|
596f99aad3 | ||
|
|
c530d35b36 | ||
|
|
5a5c734e2a | ||
|
|
f7eb483d9a | ||
|
|
7f3c7e807c | ||
|
|
a50fc02b32 | ||
|
|
63702e9e34 | ||
|
|
92c67aab4e | ||
|
|
91535870a2 | ||
|
|
d4bb277417 | ||
|
|
90f87d1496 | ||
|
|
b07752b3ab | ||
|
|
98effef4c5 | ||
|
|
32a919eb81 | ||
|
|
e124fd5c9f | ||
|
|
c5f9290402 | ||
|
|
c80cdadc99 | ||
|
|
d96dcef109 | ||
|
|
33b889ca38 | ||
|
|
fa78ea0173 | ||
|
|
6705712f80 | ||
|
|
2785b7f7d9 | ||
|
|
f04831406e | ||
|
|
fdffc81834 | ||
|
|
6f113df2d6 | ||
|
|
8b8b6fbe36 | ||
|
|
1ef8fd529b | ||
|
|
9547a459cb | ||
|
|
be1d092cab | ||
|
|
517669ee27 | ||
|
|
72fc97116f | ||
|
|
77ca6b3447 | ||
|
|
b227d337d0 | ||
|
|
a6e671d45b | ||
|
|
47c82a7e75 | ||
|
|
bafa1576f2 | ||
|
|
48956df439 | ||
|
|
4716065295 | ||
|
|
f801bbfb27 | ||
|
|
4a043f68ad | ||
|
|
cac93e9f9c | ||
|
|
e1e5c9aeb0 | ||
|
|
382cb257ab | ||
|
|
6f375be8b9 | ||
|
|
a118615e06 | ||
|
|
912bf7463f | ||
|
|
cfc29832a2 | ||
|
|
737fd132e3 | ||
|
|
9fc76f4e4c | ||
|
|
981f15d85c | ||
|
|
a59594db3b | ||
|
|
8c8190e2e9 | ||
|
|
d7e7ff77e8 | ||
|
|
e33c142c5a | ||
|
|
97d3a8243d | ||
|
|
f1716a3edb | ||
|
|
1436f5867d | ||
|
|
d754b8fe0c | ||
|
|
4f58055cc1 | ||
|
|
98697e1db4 | ||
|
|
8ac65a08c1 | ||
|
|
2b86d83290 | ||
|
|
09cafe99d1 | ||
|
|
6fce844cbf | ||
|
|
52de8c071f | ||
|
|
537543cc8a | ||
|
|
ff16453299 | ||
|
|
210deec495 | ||
|
|
e96baea005 | ||
|
|
ae24b91f25 | ||
|
|
f2e5118bf5 | ||
|
|
72698ec573 | ||
|
|
68abc27c6a | ||
|
|
1acb3d0726 | ||
|
|
5bf97dc3b8 | ||
|
|
e0e04fbc91 | ||
|
|
625cd1221c | ||
|
|
110d5bde2d | ||
|
|
93a85b3207 | ||
|
|
ff305f42fd | ||
|
|
99ba854ee1 | ||
|
|
38b368e997 | ||
|
|
f9ffe6c4e6 | ||
|
|
5adc0170fc | ||
|
|
f54c364b4d | ||
|
|
9f541b9b9d | ||
|
|
bd0af08c57 | ||
|
|
ac06c6750d | ||
|
|
23b07094b7 | ||
|
|
7eefc016de | ||
|
|
c002be76cd | ||
|
|
2cd29aaaea | ||
|
|
4cb6b01c71 | ||
|
|
91c79b9488 | ||
|
|
fc516d05b3 | ||
|
|
2769c9586c | ||
|
|
fd15d5a6d3 | ||
|
|
7237d7faa7 | ||
|
|
3025d62568 | ||
|
|
5b5dcf34a1 | ||
|
|
9e8500c148 | ||
|
|
4f1999f921 | ||
|
|
6ee9571069 | ||
|
|
10663b1494 | ||
|
|
f25db9bbd7 | ||
|
|
44ac261304 | ||
|
|
eac995a209 | ||
|
|
15c973e885 | ||
|
|
1762f9485f | ||
|
|
7777f8428f | ||
|
|
948aa9db4f | ||
|
|
fdde04ee85 | ||
|
|
f77a20f5d5 | ||
|
|
d43aa2a3e6 | ||
|
|
04d5ce13c2 | ||
|
|
3b764ba06a | ||
|
|
5492ce55fa | ||
|
|
f6b3f9860c | ||
|
|
88f687ba6a | ||
|
|
1f0a98999f | ||
|
|
69135c3bea | ||
|
|
c27d542a4b | ||
|
|
bd1c2534c5 | ||
|
|
72513b520c | ||
|
|
ec0f9ef9bc | ||
|
|
818bc3218a | ||
|
|
82760a5b6a | ||
|
|
5ba9a16cfd | ||
|
|
68fc91fdc7 | ||
|
|
bdc4687327 | ||
|
|
3a9f57e13f | ||
|
|
b72c48c693 | ||
|
|
f1e42f3bac | ||
|
|
93c908286d | ||
|
|
4eb8777ed0 | ||
|
|
5e1909cee0 | ||
|
|
2e7b312415 | ||
|
|
7735a59fc1 | ||
|
|
41d6e912a7 | ||
|
|
4c2fae8423 | ||
|
|
b72c134890 | ||
|
|
58a9c229bb | ||
|
|
d8c203bb8a | ||
|
|
9020c07825 | ||
|
|
e884da8312 | ||
|
|
d134ea8bfe | ||
|
|
faa44468f3 | ||
|
|
85585d16d2 | ||
|
|
b9c5b8f187 | ||
|
|
da8e638359 | ||
|
|
6482ab5a4e | ||
|
|
ec74abe754 | ||
|
|
859bc8d88e | ||
|
|
56ed471a2f | ||
|
|
650594ecea | ||
|
|
3e9bb914e5 | ||
|
|
f75e911a4e | ||
|
|
78fb07d4c7 | ||
|
|
6390ef43ed | ||
|
|
78c5c4d7c3 | ||
|
|
0d1d50768b | ||
|
|
57093b35ea | ||
|
|
cba5cf660b | ||
|
|
0024722c79 | ||
|
|
bc2832e78f | ||
|
|
424cc96d36 | ||
|
|
56fd5d828f | ||
|
|
03843b087a | ||
|
|
b179509dd3 | ||
|
|
f6851314d2 | ||
|
|
eaec45cb3f | ||
|
|
9be954496c | ||
|
|
ac289c5198 | ||
|
|
98ef5e619b | ||
|
|
62faa48aac | ||
|
|
5daa7a1f4c | ||
|
|
32be071601 | ||
|
|
0dc63dd306 | ||
|
|
78ed58187a | ||
|
|
b8b8dd8011 |
@@ -9,6 +9,7 @@ API_KEY=random-string
|
||||
QUEUE_TTL=900000
|
||||
QUEUE_RETRY_COUNT=2
|
||||
QUEUE_MAINTENANCE_INTERVAL=30000
|
||||
IMAGE_MAX_DIMENSION=400
|
||||
|
||||
HTR_CLI_DOCKER_IMAGE=joplin/htr-cli:latest
|
||||
# Fullpath to images folder e.g.:
|
||||
|
||||
@@ -96,6 +96,7 @@ packages/onenote-converter/pkg/onenote_converter.js
|
||||
packages/app-cli/app/LinkSelector.js
|
||||
packages/app-cli/app/app.js
|
||||
packages/app-cli/app/base-command.js
|
||||
packages/app-cli/app/cli-integration-tests.js
|
||||
packages/app-cli/app/command-apidoc.js
|
||||
packages/app-cli/app/command-attach.js
|
||||
packages/app-cli/app/command-batch.js
|
||||
@@ -676,6 +677,8 @@ packages/app-mobile/components/ExtendedWebView/index.js
|
||||
packages/app-mobile/components/ExtendedWebView/index.web.js
|
||||
packages/app-mobile/components/ExtendedWebView/types.js
|
||||
packages/app-mobile/components/ExtendedWebView/utils/useCss.js
|
||||
packages/app-mobile/components/FeedbackBanner.test.js
|
||||
packages/app-mobile/components/FeedbackBanner.js
|
||||
packages/app-mobile/components/FolderPicker.js
|
||||
packages/app-mobile/components/Icon.js
|
||||
packages/app-mobile/components/IconButton.js
|
||||
@@ -722,6 +725,8 @@ packages/app-mobile/components/SearchInput.js
|
||||
packages/app-mobile/components/SelectDateTimeDialog.js
|
||||
packages/app-mobile/components/SideMenu.js
|
||||
packages/app-mobile/components/SideMenuContentNote.js
|
||||
packages/app-mobile/components/SyncWizard/JoplinCloudIcon.js
|
||||
packages/app-mobile/components/SyncWizard/SyncWizard.js
|
||||
packages/app-mobile/components/TagEditor.test.js
|
||||
packages/app-mobile/components/TagEditor.js
|
||||
packages/app-mobile/components/TextInput.js
|
||||
@@ -738,6 +743,7 @@ packages/app-mobile/components/base-screen.js
|
||||
packages/app-mobile/components/biometrics/BiometricPopup.js
|
||||
packages/app-mobile/components/biometrics/biometricAuthenticate.js
|
||||
packages/app-mobile/components/biometrics/sensorInfo.js
|
||||
packages/app-mobile/components/buttons/CardButton.js
|
||||
packages/app-mobile/components/buttons/FloatingActionButton.js
|
||||
packages/app-mobile/components/buttons/LabelledIconButton.js
|
||||
packages/app-mobile/components/buttons/MultiTouchableOpacity.js
|
||||
@@ -813,7 +819,6 @@ packages/app-mobile/components/screens/ConfigScreen/plugins/buttons/InstallButto
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/WrappedPluginStates.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/mockRepositoryApiConstructor.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/newRepoApi.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/pluginServiceSetup.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/utils/openWebsiteForPlugin.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/utils/usePluginCallbacks.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/utils/usePluginItem.js
|
||||
@@ -839,6 +844,7 @@ packages/app-mobile/components/screens/NoteTagsDialog.js
|
||||
packages/app-mobile/components/screens/Notes/NewNoteButton.test.js
|
||||
packages/app-mobile/components/screens/Notes/NewNoteButton.js
|
||||
packages/app-mobile/components/screens/Notes/Notes.js
|
||||
packages/app-mobile/components/screens/SearchScreen/SearchResults.test.js
|
||||
packages/app-mobile/components/screens/SearchScreen/SearchResults.js
|
||||
packages/app-mobile/components/screens/SearchScreen/index.js
|
||||
packages/app-mobile/components/screens/ShareManager/AcceptedShareItem.js
|
||||
@@ -901,14 +907,13 @@ packages/app-mobile/services/AlarmServiceDriver.web.js
|
||||
packages/app-mobile/services/BackButtonService.js
|
||||
packages/app-mobile/services/commands/stateToWhenClauseContext.js
|
||||
packages/app-mobile/services/e2ee/RSA.react-native.js
|
||||
packages/app-mobile/services/e2ee/RSA.react-native.web.js
|
||||
packages/app-mobile/services/e2ee/crypto.js
|
||||
packages/app-mobile/services/plugins/PlatformImplementation.js
|
||||
packages/app-mobile/services/profiles/index.js
|
||||
packages/app-mobile/services/voiceTyping/VoiceTyping.js
|
||||
packages/app-mobile/services/voiceTyping/utils/unzip.android.js
|
||||
packages/app-mobile/services/voiceTyping/utils/unzip.js
|
||||
packages/app-mobile/services/voiceTyping/vosk.android.js
|
||||
packages/app-mobile/services/voiceTyping/vosk.js
|
||||
packages/app-mobile/services/voiceTyping/whisper.test.js
|
||||
packages/app-mobile/services/voiceTyping/whisper.js
|
||||
packages/app-mobile/setupQuickActions.js
|
||||
@@ -922,6 +927,7 @@ packages/app-mobile/utils/ShareUtils.test.js
|
||||
packages/app-mobile/utils/ShareUtils.js
|
||||
packages/app-mobile/utils/TlsUtils.js
|
||||
packages/app-mobile/utils/appDefaultState.js
|
||||
packages/app-mobile/utils/appReducer.js
|
||||
packages/app-mobile/utils/autodetectTheme.js
|
||||
packages/app-mobile/utils/buildStartupTasks.js
|
||||
packages/app-mobile/utils/checkPermissions.js
|
||||
@@ -961,6 +967,7 @@ packages/app-mobile/utils/pickDocument.js
|
||||
packages/app-mobile/utils/polyfills/bufferPolyfill.js
|
||||
packages/app-mobile/utils/polyfills/crypto-polyfill/index.js
|
||||
packages/app-mobile/utils/polyfills/index.js
|
||||
packages/app-mobile/utils/polyfills/index.web.js
|
||||
packages/app-mobile/utils/setupNotifications.js
|
||||
packages/app-mobile/utils/shareFile.js
|
||||
packages/app-mobile/utils/shareHandler.js
|
||||
@@ -971,6 +978,7 @@ packages/app-mobile/utils/shim-init-react/shimInitShared.js
|
||||
packages/app-mobile/utils/testing/createMockReduxStore.js
|
||||
packages/app-mobile/utils/testing/getWebViewDomById.js
|
||||
packages/app-mobile/utils/testing/getWebViewWindowById.js
|
||||
packages/app-mobile/utils/testing/mockPluginServiceSetup.js
|
||||
packages/app-mobile/utils/testing/setupGlobalStore.js
|
||||
packages/app-mobile/utils/testing/testingLibrary.js
|
||||
packages/app-mobile/utils/types.js
|
||||
@@ -1042,6 +1050,7 @@ packages/editor/CodeMirror/extensions/rendering/types.js
|
||||
packages/editor/CodeMirror/extensions/rendering/utils/makeBlockReplaceExtension.js
|
||||
packages/editor/CodeMirror/extensions/rendering/utils/makeInlineReplaceExtension.js
|
||||
packages/editor/CodeMirror/extensions/rendering/utils/nodeIntersectsSelection.js
|
||||
packages/editor/CodeMirror/extensions/searchExtension.test.js
|
||||
packages/editor/CodeMirror/extensions/searchExtension.js
|
||||
packages/editor/CodeMirror/extensions/selectedNoteIdExtension.js
|
||||
packages/editor/CodeMirror/getScrollFraction.js
|
||||
@@ -1088,12 +1097,15 @@ packages/editor/CodeMirror/utils/markdown/renumberSelectedLists.js
|
||||
packages/editor/CodeMirror/utils/markdown/stripBlockquote.js
|
||||
packages/editor/CodeMirror/utils/markdown/toggleCheckboxAt.js
|
||||
packages/editor/CodeMirror/utils/setupVim.js
|
||||
packages/editor/CodeMirror/vendor/announceSearchMatch.js
|
||||
packages/editor/ProseMirror/commands.test.js
|
||||
packages/editor/ProseMirror/commands.js
|
||||
packages/editor/ProseMirror/createEditor.js
|
||||
packages/editor/ProseMirror/index.js
|
||||
packages/editor/ProseMirror/plugins/detailsPlugin.test.js
|
||||
packages/editor/ProseMirror/plugins/detailsPlugin.js
|
||||
packages/editor/ProseMirror/plugins/imagePlugin.test.js
|
||||
packages/editor/ProseMirror/plugins/imagePlugin.js
|
||||
packages/editor/ProseMirror/plugins/inputRulesPlugin.js
|
||||
packages/editor/ProseMirror/plugins/joplinEditablePlugin/createEditorDialog.js
|
||||
packages/editor/ProseMirror/plugins/joplinEditablePlugin/joplinEditablePlugin.test.js
|
||||
@@ -1105,12 +1117,15 @@ packages/editor/ProseMirror/plugins/linkTooltipPlugin.test.js
|
||||
packages/editor/ProseMirror/plugins/linkTooltipPlugin.js
|
||||
packages/editor/ProseMirror/plugins/listPlugin.js
|
||||
packages/editor/ProseMirror/plugins/originalMarkupPlugin.js
|
||||
packages/editor/ProseMirror/plugins/resourcePlaceholderPlugin.js
|
||||
packages/editor/ProseMirror/plugins/searchPlugin.js
|
||||
packages/editor/ProseMirror/plugins/utils/createExternalEditorPlugin.js
|
||||
packages/editor/ProseMirror/plugins/utils/createFloatingButtonPlugin.js
|
||||
packages/editor/ProseMirror/schema.js
|
||||
packages/editor/ProseMirror/styles.js
|
||||
packages/editor/ProseMirror/testing/createTestEditor.js
|
||||
packages/editor/ProseMirror/testing/createTestEditorWithSerializer.js
|
||||
packages/editor/ProseMirror/types.js
|
||||
packages/editor/ProseMirror/utils/SelectableNodeView.js
|
||||
packages/editor/ProseMirror/utils/UndoStackSynchronizer.js
|
||||
packages/editor/ProseMirror/utils/canReplaceSelectionWith.js
|
||||
packages/editor/ProseMirror/utils/computeSelectionFormatting.js
|
||||
@@ -1118,6 +1133,7 @@ packages/editor/ProseMirror/utils/dom/createButton.js
|
||||
packages/editor/ProseMirror/utils/dom/createTextArea.js
|
||||
packages/editor/ProseMirror/utils/dom/createTextNode.js
|
||||
packages/editor/ProseMirror/utils/dom/createUniqueId.js
|
||||
packages/editor/ProseMirror/utils/dom/showModal.js
|
||||
packages/editor/ProseMirror/utils/extractSelectedLinesTo.test.js
|
||||
packages/editor/ProseMirror/utils/extractSelectedLinesTo.js
|
||||
packages/editor/ProseMirror/utils/forEachHeading.js
|
||||
@@ -1128,6 +1144,7 @@ packages/editor/ProseMirror/utils/postprocessEditorOutput.js
|
||||
packages/editor/ProseMirror/utils/preprocessEditorInput.test.js
|
||||
packages/editor/ProseMirror/utils/preprocessEditorInput.js
|
||||
packages/editor/ProseMirror/utils/sanitizeHtml.js
|
||||
packages/editor/ProseMirror/utils/selectFirstInstanceOfNode.js
|
||||
packages/editor/ProseMirror/utils/trimEmptyParagraphs.js
|
||||
packages/editor/ProseMirror/vendor/changedDescendants.js
|
||||
packages/editor/ProseMirror/vendor/splitBlockAs.js
|
||||
@@ -1404,14 +1421,19 @@ packages/lib/services/database/types.js
|
||||
packages/lib/services/debug/populateDatabase.js
|
||||
packages/lib/services/e2ee/EncryptionService.test.js
|
||||
packages/lib/services/e2ee/EncryptionService.js
|
||||
packages/lib/services/e2ee/RSA.node.js
|
||||
packages/lib/services/e2ee/crypto.test.js
|
||||
packages/lib/services/e2ee/crypto.js
|
||||
packages/lib/services/e2ee/cryptoShared.js
|
||||
packages/lib/services/e2ee/cryptoTestUtils.js
|
||||
packages/lib/services/e2ee/ppk.test.js
|
||||
packages/lib/services/e2ee/ppk.js
|
||||
packages/lib/services/e2ee/ppkTestUtils.js
|
||||
packages/lib/services/e2ee/ppk/RSA.node.js
|
||||
packages/lib/services/e2ee/ppk/ppk.test.js
|
||||
packages/lib/services/e2ee/ppk/ppk.js
|
||||
packages/lib/services/e2ee/ppk/ppkTestUtils.js
|
||||
packages/lib/services/e2ee/ppk/webCrypto/LongDataWrapper.js
|
||||
packages/lib/services/e2ee/ppk/webCrypto/StringToBufferWrapper.js
|
||||
packages/lib/services/e2ee/ppk/webCrypto/WebCryptoRsa.js
|
||||
packages/lib/services/e2ee/ppk/webCrypto/buildRsaCryptoProvider.test.js
|
||||
packages/lib/services/e2ee/ppk/webCrypto/buildRsaCryptoProvider.js
|
||||
packages/lib/services/e2ee/types.js
|
||||
packages/lib/services/e2ee/utils.test.js
|
||||
packages/lib/services/e2ee/utils.js
|
||||
@@ -1516,6 +1538,7 @@ packages/lib/services/plugins/utils/createViewHandle.js
|
||||
packages/lib/services/plugins/utils/executeSandboxCall.js
|
||||
packages/lib/services/plugins/utils/getActivePluginEditorView.js
|
||||
packages/lib/services/plugins/utils/getActivePluginEditorViews.js
|
||||
packages/lib/services/plugins/utils/getPluginHelpUrl.js
|
||||
packages/lib/services/plugins/utils/getPluginIssueReportUrl.test.js
|
||||
packages/lib/services/plugins/utils/getPluginIssueReportUrl.js
|
||||
packages/lib/services/plugins/utils/getPluginNamespacedSettingKey.js
|
||||
@@ -1720,6 +1743,7 @@ packages/plugin-repo-cli/lib/gitCompareUrl.test.js
|
||||
packages/plugin-repo-cli/lib/gitCompareUrl.js
|
||||
packages/plugin-repo-cli/lib/overrideUtils.test.js
|
||||
packages/plugin-repo-cli/lib/overrideUtils.js
|
||||
packages/plugin-repo-cli/lib/searchPlugins.js
|
||||
packages/plugin-repo-cli/lib/types.js
|
||||
packages/plugin-repo-cli/lib/updateReadme.test.js
|
||||
packages/plugin-repo-cli/lib/updateReadme.js
|
||||
@@ -1835,6 +1859,8 @@ packages/tools/updateMarkdownDoc.js
|
||||
packages/tools/utils/discourse.test.js
|
||||
packages/tools/utils/discourse.js
|
||||
packages/tools/utils/loadSponsors.js
|
||||
packages/tools/utils/parsePluralLocalizationForm.js
|
||||
packages/tools/utils/parsePlurallLocalizationForm.test.js
|
||||
packages/tools/utils/translation.js
|
||||
packages/tools/validateFilenames.js
|
||||
packages/tools/website/build.js
|
||||
|
||||
27
.github/workflows/build-android.yml
vendored
@@ -40,4 +40,29 @@ jobs:
|
||||
cd packages/app-mobile/android
|
||||
sed -i -- 's/signingConfig signingConfigs.release/signingConfig signingConfigs.debug/' app/build.gradle
|
||||
./gradlew assembleRelease
|
||||
|
||||
|
||||
- name: Verify alignment
|
||||
run: |
|
||||
cd packages/app-mobile/android/app
|
||||
APK_FILE="./build/outputs/apk/release/app-release.apk"
|
||||
if test ! -f "$APK_FILE" ; then
|
||||
echo "APK file not found."
|
||||
exit 1
|
||||
else
|
||||
echo "APK file found at: $APK_FILE"
|
||||
fi
|
||||
|
||||
BUILD_TOOLS_PATH="$ANDROID_HOME/build-tools/"
|
||||
if test ! -d "$BUILD_TOOLS_PATH" ; then
|
||||
echo "Build tools not found at $BUILD_TOOLS_PATH ($ANDROID_HOME, $BUILD_TOOLS_VERSION)"
|
||||
exit 1
|
||||
fi
|
||||
# The build-tools/ directory contains different subdirectories
|
||||
# for each build tools version. As a result, there may be multiple
|
||||
# zipalign tools. Select the most recent (biggest two-digit version number):
|
||||
ZIPALIGN_PATH="$(find $BUILD_TOOLS_PATH -name "zipalign" -print | sort | tail -n1)"
|
||||
if test ! -x "$ZIPALIGN_PATH" ; then
|
||||
echo "zipalign not found (searching in $BUILD_TOOLS_PATH, candidate: $ZIPALIGN_PATH)"
|
||||
exit 1
|
||||
fi
|
||||
"$ZIPALIGN_PATH" -c -P 16 -v 4 "$APK_FILE"
|
||||
|
||||
50
.github/workflows/github-actions-main.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
matrix:
|
||||
# Do not use unbuntu-latest because it causes `The operation was canceled` failures:
|
||||
# https://github.com/actions/runner-images/issues/6709
|
||||
os: [macos-13, ubuntu-22.04, windows-2025, ubuntu-22.04-arm]
|
||||
os: [macos-15-intel, ubuntu-22.04, windows-2025, ubuntu-22.04-arm]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -31,6 +31,16 @@ jobs:
|
||||
sudo apt-get update || true
|
||||
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin
|
||||
|
||||
- name: Free disk space
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo rm -rf /usr/local/lib/android || true
|
||||
sudo rm -rf /usr/share/dotnet || true
|
||||
sudo rm -rf /opt/ghc || true
|
||||
docker system prune -af || true
|
||||
docker builder prune -af || true
|
||||
sudo rm -rf /var/lib/docker/tmp/* || true
|
||||
|
||||
# Login to Docker only if we're on a server release tag. If we run this on
|
||||
# a pull request it will fail because the PR doesn't have access to
|
||||
# secrets
|
||||
@@ -40,6 +50,22 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
# - name: Test Windows app signing
|
||||
# if: runner.os == 'Windows'
|
||||
# env:
|
||||
# GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
# IS_CONTINUOUS_INTEGRATION: 1
|
||||
# BUILD_SEQUENCIAL: 1
|
||||
# SSL_ESIGNER_USER_NAME: ${{ secrets.SSL_ESIGNER_USER_NAME }}
|
||||
# SSL_ESIGNER_USER_PASSWORD: ${{ secrets.SSL_ESIGNER_USER_PASSWORD }}
|
||||
# SSL_ESIGNER_CREDENTIAL_ID: ${{ secrets.SSL_ESIGNER_CREDENTIAL_ID }}
|
||||
# SSL_ESIGNER_USER_TOTP: ${{ secrets.SSL_ESIGNER_USER_TOTP }}
|
||||
# SIGN_APPLICATION: 1
|
||||
# # To ensure that the operations stop on failure, all commands
|
||||
# # should be on one line with "&&" in between.
|
||||
# run: |
|
||||
# yarn install && cd packages/app-desktop && yarn dist
|
||||
|
||||
- name: Run tests, build and publish Linux and macOS apps
|
||||
if: runner.os == 'Linux' || runner.os == 'macOs'
|
||||
env:
|
||||
@@ -61,11 +87,14 @@ jobs:
|
||||
- name: Build and publish Windows app
|
||||
if: runner.os == 'Windows' && startsWith(github.ref, 'refs/tags/v')
|
||||
env:
|
||||
CSC_KEY_PASSWORD: ${{ secrets.WINDOWS_CSC_KEY_PASSWORD }}
|
||||
CSC_LINK: ${{ secrets.WINDOWS_CSC_LINK }}
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
IS_CONTINUOUS_INTEGRATION: 1
|
||||
BUILD_SEQUENCIAL: 1
|
||||
SSL_ESIGNER_USER_NAME: ${{ secrets.SSL_ESIGNER_USER_NAME }}
|
||||
SSL_ESIGNER_USER_PASSWORD: ${{ secrets.SSL_ESIGNER_USER_PASSWORD }}
|
||||
SSL_ESIGNER_CREDENTIAL_ID: ${{ secrets.SSL_ESIGNER_CREDENTIAL_ID }}
|
||||
SSL_ESIGNER_USER_TOTP: ${{ secrets.SSL_ESIGNER_USER_TOTP }}
|
||||
SIGN_APPLICATION: 1
|
||||
# To ensure that the operations stop on failure, all commands
|
||||
# should be on one line with "&&" in between.
|
||||
run: |
|
||||
@@ -122,6 +151,16 @@ jobs:
|
||||
with:
|
||||
node-version: '18'
|
||||
|
||||
- name: Free disk space
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo rm -rf /usr/local/lib/android || true
|
||||
sudo rm -rf /usr/share/dotnet || true
|
||||
sudo rm -rf /opt/ghc || true
|
||||
docker system prune -af || true
|
||||
docker builder prune -af || true
|
||||
sudo rm -rf /var/lib/docker/tmp/* || true
|
||||
|
||||
- name: Install Yarn
|
||||
run: |
|
||||
# https://yarnpkg.com/getting-started/install
|
||||
@@ -149,7 +188,7 @@ jobs:
|
||||
- name: Check HTTP request
|
||||
run: |
|
||||
# Need to pass environment variables:
|
||||
docker run -p 22300:22300 joplin/server:$(dpkg --print-architecture)-0.0.0 node dist/app.js --env dev &
|
||||
docker run --env MAX_TIME_DRIFT=0 --publish 22300:22300 joplin/server:$(dpkg --print-architecture)-0.0.0 node dist/app.js --env dev &
|
||||
|
||||
# Wait for server to start
|
||||
sleep 120
|
||||
@@ -175,5 +214,4 @@ jobs:
|
||||
if [[ "$actual_body" != "$expected_body" ]]; then
|
||||
echo 'Failed while checking the body response after request to /api/ping'
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
fi
|
||||
42
.gitignore
vendored
@@ -69,6 +69,7 @@ docs/**/*.mustache
|
||||
packages/app-cli/app/LinkSelector.js
|
||||
packages/app-cli/app/app.js
|
||||
packages/app-cli/app/base-command.js
|
||||
packages/app-cli/app/cli-integration-tests.js
|
||||
packages/app-cli/app/command-apidoc.js
|
||||
packages/app-cli/app/command-attach.js
|
||||
packages/app-cli/app/command-batch.js
|
||||
@@ -649,6 +650,8 @@ packages/app-mobile/components/ExtendedWebView/index.js
|
||||
packages/app-mobile/components/ExtendedWebView/index.web.js
|
||||
packages/app-mobile/components/ExtendedWebView/types.js
|
||||
packages/app-mobile/components/ExtendedWebView/utils/useCss.js
|
||||
packages/app-mobile/components/FeedbackBanner.test.js
|
||||
packages/app-mobile/components/FeedbackBanner.js
|
||||
packages/app-mobile/components/FolderPicker.js
|
||||
packages/app-mobile/components/Icon.js
|
||||
packages/app-mobile/components/IconButton.js
|
||||
@@ -695,6 +698,8 @@ packages/app-mobile/components/SearchInput.js
|
||||
packages/app-mobile/components/SelectDateTimeDialog.js
|
||||
packages/app-mobile/components/SideMenu.js
|
||||
packages/app-mobile/components/SideMenuContentNote.js
|
||||
packages/app-mobile/components/SyncWizard/JoplinCloudIcon.js
|
||||
packages/app-mobile/components/SyncWizard/SyncWizard.js
|
||||
packages/app-mobile/components/TagEditor.test.js
|
||||
packages/app-mobile/components/TagEditor.js
|
||||
packages/app-mobile/components/TextInput.js
|
||||
@@ -711,6 +716,7 @@ packages/app-mobile/components/base-screen.js
|
||||
packages/app-mobile/components/biometrics/BiometricPopup.js
|
||||
packages/app-mobile/components/biometrics/biometricAuthenticate.js
|
||||
packages/app-mobile/components/biometrics/sensorInfo.js
|
||||
packages/app-mobile/components/buttons/CardButton.js
|
||||
packages/app-mobile/components/buttons/FloatingActionButton.js
|
||||
packages/app-mobile/components/buttons/LabelledIconButton.js
|
||||
packages/app-mobile/components/buttons/MultiTouchableOpacity.js
|
||||
@@ -786,7 +792,6 @@ packages/app-mobile/components/screens/ConfigScreen/plugins/buttons/InstallButto
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/WrappedPluginStates.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/mockRepositoryApiConstructor.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/newRepoApi.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/testUtils/pluginServiceSetup.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/utils/openWebsiteForPlugin.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/utils/usePluginCallbacks.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/utils/usePluginItem.js
|
||||
@@ -812,6 +817,7 @@ packages/app-mobile/components/screens/NoteTagsDialog.js
|
||||
packages/app-mobile/components/screens/Notes/NewNoteButton.test.js
|
||||
packages/app-mobile/components/screens/Notes/NewNoteButton.js
|
||||
packages/app-mobile/components/screens/Notes/Notes.js
|
||||
packages/app-mobile/components/screens/SearchScreen/SearchResults.test.js
|
||||
packages/app-mobile/components/screens/SearchScreen/SearchResults.js
|
||||
packages/app-mobile/components/screens/SearchScreen/index.js
|
||||
packages/app-mobile/components/screens/ShareManager/AcceptedShareItem.js
|
||||
@@ -874,14 +880,13 @@ packages/app-mobile/services/AlarmServiceDriver.web.js
|
||||
packages/app-mobile/services/BackButtonService.js
|
||||
packages/app-mobile/services/commands/stateToWhenClauseContext.js
|
||||
packages/app-mobile/services/e2ee/RSA.react-native.js
|
||||
packages/app-mobile/services/e2ee/RSA.react-native.web.js
|
||||
packages/app-mobile/services/e2ee/crypto.js
|
||||
packages/app-mobile/services/plugins/PlatformImplementation.js
|
||||
packages/app-mobile/services/profiles/index.js
|
||||
packages/app-mobile/services/voiceTyping/VoiceTyping.js
|
||||
packages/app-mobile/services/voiceTyping/utils/unzip.android.js
|
||||
packages/app-mobile/services/voiceTyping/utils/unzip.js
|
||||
packages/app-mobile/services/voiceTyping/vosk.android.js
|
||||
packages/app-mobile/services/voiceTyping/vosk.js
|
||||
packages/app-mobile/services/voiceTyping/whisper.test.js
|
||||
packages/app-mobile/services/voiceTyping/whisper.js
|
||||
packages/app-mobile/setupQuickActions.js
|
||||
@@ -895,6 +900,7 @@ packages/app-mobile/utils/ShareUtils.test.js
|
||||
packages/app-mobile/utils/ShareUtils.js
|
||||
packages/app-mobile/utils/TlsUtils.js
|
||||
packages/app-mobile/utils/appDefaultState.js
|
||||
packages/app-mobile/utils/appReducer.js
|
||||
packages/app-mobile/utils/autodetectTheme.js
|
||||
packages/app-mobile/utils/buildStartupTasks.js
|
||||
packages/app-mobile/utils/checkPermissions.js
|
||||
@@ -934,6 +940,7 @@ packages/app-mobile/utils/pickDocument.js
|
||||
packages/app-mobile/utils/polyfills/bufferPolyfill.js
|
||||
packages/app-mobile/utils/polyfills/crypto-polyfill/index.js
|
||||
packages/app-mobile/utils/polyfills/index.js
|
||||
packages/app-mobile/utils/polyfills/index.web.js
|
||||
packages/app-mobile/utils/setupNotifications.js
|
||||
packages/app-mobile/utils/shareFile.js
|
||||
packages/app-mobile/utils/shareHandler.js
|
||||
@@ -944,6 +951,7 @@ packages/app-mobile/utils/shim-init-react/shimInitShared.js
|
||||
packages/app-mobile/utils/testing/createMockReduxStore.js
|
||||
packages/app-mobile/utils/testing/getWebViewDomById.js
|
||||
packages/app-mobile/utils/testing/getWebViewWindowById.js
|
||||
packages/app-mobile/utils/testing/mockPluginServiceSetup.js
|
||||
packages/app-mobile/utils/testing/setupGlobalStore.js
|
||||
packages/app-mobile/utils/testing/testingLibrary.js
|
||||
packages/app-mobile/utils/types.js
|
||||
@@ -1015,6 +1023,7 @@ packages/editor/CodeMirror/extensions/rendering/types.js
|
||||
packages/editor/CodeMirror/extensions/rendering/utils/makeBlockReplaceExtension.js
|
||||
packages/editor/CodeMirror/extensions/rendering/utils/makeInlineReplaceExtension.js
|
||||
packages/editor/CodeMirror/extensions/rendering/utils/nodeIntersectsSelection.js
|
||||
packages/editor/CodeMirror/extensions/searchExtension.test.js
|
||||
packages/editor/CodeMirror/extensions/searchExtension.js
|
||||
packages/editor/CodeMirror/extensions/selectedNoteIdExtension.js
|
||||
packages/editor/CodeMirror/getScrollFraction.js
|
||||
@@ -1061,12 +1070,15 @@ packages/editor/CodeMirror/utils/markdown/renumberSelectedLists.js
|
||||
packages/editor/CodeMirror/utils/markdown/stripBlockquote.js
|
||||
packages/editor/CodeMirror/utils/markdown/toggleCheckboxAt.js
|
||||
packages/editor/CodeMirror/utils/setupVim.js
|
||||
packages/editor/CodeMirror/vendor/announceSearchMatch.js
|
||||
packages/editor/ProseMirror/commands.test.js
|
||||
packages/editor/ProseMirror/commands.js
|
||||
packages/editor/ProseMirror/createEditor.js
|
||||
packages/editor/ProseMirror/index.js
|
||||
packages/editor/ProseMirror/plugins/detailsPlugin.test.js
|
||||
packages/editor/ProseMirror/plugins/detailsPlugin.js
|
||||
packages/editor/ProseMirror/plugins/imagePlugin.test.js
|
||||
packages/editor/ProseMirror/plugins/imagePlugin.js
|
||||
packages/editor/ProseMirror/plugins/inputRulesPlugin.js
|
||||
packages/editor/ProseMirror/plugins/joplinEditablePlugin/createEditorDialog.js
|
||||
packages/editor/ProseMirror/plugins/joplinEditablePlugin/joplinEditablePlugin.test.js
|
||||
@@ -1078,12 +1090,15 @@ packages/editor/ProseMirror/plugins/linkTooltipPlugin.test.js
|
||||
packages/editor/ProseMirror/plugins/linkTooltipPlugin.js
|
||||
packages/editor/ProseMirror/plugins/listPlugin.js
|
||||
packages/editor/ProseMirror/plugins/originalMarkupPlugin.js
|
||||
packages/editor/ProseMirror/plugins/resourcePlaceholderPlugin.js
|
||||
packages/editor/ProseMirror/plugins/searchPlugin.js
|
||||
packages/editor/ProseMirror/plugins/utils/createExternalEditorPlugin.js
|
||||
packages/editor/ProseMirror/plugins/utils/createFloatingButtonPlugin.js
|
||||
packages/editor/ProseMirror/schema.js
|
||||
packages/editor/ProseMirror/styles.js
|
||||
packages/editor/ProseMirror/testing/createTestEditor.js
|
||||
packages/editor/ProseMirror/testing/createTestEditorWithSerializer.js
|
||||
packages/editor/ProseMirror/types.js
|
||||
packages/editor/ProseMirror/utils/SelectableNodeView.js
|
||||
packages/editor/ProseMirror/utils/UndoStackSynchronizer.js
|
||||
packages/editor/ProseMirror/utils/canReplaceSelectionWith.js
|
||||
packages/editor/ProseMirror/utils/computeSelectionFormatting.js
|
||||
@@ -1091,6 +1106,7 @@ packages/editor/ProseMirror/utils/dom/createButton.js
|
||||
packages/editor/ProseMirror/utils/dom/createTextArea.js
|
||||
packages/editor/ProseMirror/utils/dom/createTextNode.js
|
||||
packages/editor/ProseMirror/utils/dom/createUniqueId.js
|
||||
packages/editor/ProseMirror/utils/dom/showModal.js
|
||||
packages/editor/ProseMirror/utils/extractSelectedLinesTo.test.js
|
||||
packages/editor/ProseMirror/utils/extractSelectedLinesTo.js
|
||||
packages/editor/ProseMirror/utils/forEachHeading.js
|
||||
@@ -1101,6 +1117,7 @@ packages/editor/ProseMirror/utils/postprocessEditorOutput.js
|
||||
packages/editor/ProseMirror/utils/preprocessEditorInput.test.js
|
||||
packages/editor/ProseMirror/utils/preprocessEditorInput.js
|
||||
packages/editor/ProseMirror/utils/sanitizeHtml.js
|
||||
packages/editor/ProseMirror/utils/selectFirstInstanceOfNode.js
|
||||
packages/editor/ProseMirror/utils/trimEmptyParagraphs.js
|
||||
packages/editor/ProseMirror/vendor/changedDescendants.js
|
||||
packages/editor/ProseMirror/vendor/splitBlockAs.js
|
||||
@@ -1377,14 +1394,19 @@ packages/lib/services/database/types.js
|
||||
packages/lib/services/debug/populateDatabase.js
|
||||
packages/lib/services/e2ee/EncryptionService.test.js
|
||||
packages/lib/services/e2ee/EncryptionService.js
|
||||
packages/lib/services/e2ee/RSA.node.js
|
||||
packages/lib/services/e2ee/crypto.test.js
|
||||
packages/lib/services/e2ee/crypto.js
|
||||
packages/lib/services/e2ee/cryptoShared.js
|
||||
packages/lib/services/e2ee/cryptoTestUtils.js
|
||||
packages/lib/services/e2ee/ppk.test.js
|
||||
packages/lib/services/e2ee/ppk.js
|
||||
packages/lib/services/e2ee/ppkTestUtils.js
|
||||
packages/lib/services/e2ee/ppk/RSA.node.js
|
||||
packages/lib/services/e2ee/ppk/ppk.test.js
|
||||
packages/lib/services/e2ee/ppk/ppk.js
|
||||
packages/lib/services/e2ee/ppk/ppkTestUtils.js
|
||||
packages/lib/services/e2ee/ppk/webCrypto/LongDataWrapper.js
|
||||
packages/lib/services/e2ee/ppk/webCrypto/StringToBufferWrapper.js
|
||||
packages/lib/services/e2ee/ppk/webCrypto/WebCryptoRsa.js
|
||||
packages/lib/services/e2ee/ppk/webCrypto/buildRsaCryptoProvider.test.js
|
||||
packages/lib/services/e2ee/ppk/webCrypto/buildRsaCryptoProvider.js
|
||||
packages/lib/services/e2ee/types.js
|
||||
packages/lib/services/e2ee/utils.test.js
|
||||
packages/lib/services/e2ee/utils.js
|
||||
@@ -1489,6 +1511,7 @@ packages/lib/services/plugins/utils/createViewHandle.js
|
||||
packages/lib/services/plugins/utils/executeSandboxCall.js
|
||||
packages/lib/services/plugins/utils/getActivePluginEditorView.js
|
||||
packages/lib/services/plugins/utils/getActivePluginEditorViews.js
|
||||
packages/lib/services/plugins/utils/getPluginHelpUrl.js
|
||||
packages/lib/services/plugins/utils/getPluginIssueReportUrl.test.js
|
||||
packages/lib/services/plugins/utils/getPluginIssueReportUrl.js
|
||||
packages/lib/services/plugins/utils/getPluginNamespacedSettingKey.js
|
||||
@@ -1693,6 +1716,7 @@ packages/plugin-repo-cli/lib/gitCompareUrl.test.js
|
||||
packages/plugin-repo-cli/lib/gitCompareUrl.js
|
||||
packages/plugin-repo-cli/lib/overrideUtils.test.js
|
||||
packages/plugin-repo-cli/lib/overrideUtils.js
|
||||
packages/plugin-repo-cli/lib/searchPlugins.js
|
||||
packages/plugin-repo-cli/lib/types.js
|
||||
packages/plugin-repo-cli/lib/updateReadme.test.js
|
||||
packages/plugin-repo-cli/lib/updateReadme.js
|
||||
@@ -1808,6 +1832,8 @@ packages/tools/updateMarkdownDoc.js
|
||||
packages/tools/utils/discourse.test.js
|
||||
packages/tools/utils/discourse.js
|
||||
packages/tools/utils/loadSponsors.js
|
||||
packages/tools/utils/parsePluralLocalizationForm.js
|
||||
packages/tools/utils/parsePlurallLocalizationForm.test.js
|
||||
packages/tools/utils/translation.js
|
||||
packages/tools/validateFilenames.js
|
||||
packages/tools/website/build.js
|
||||
|
||||
36
.yarn/patches/depd-npm-1.1.2-b0c8414da7.patch
Normal file
@@ -0,0 +1,36 @@
|
||||
# Patch to remove eval. This allows using depd in an environment with
|
||||
# a strict Content-Security-Policy.
|
||||
# Ref: https://github.com/dougwilson/nodejs-depd/pull/33
|
||||
diff --git a/index.js b/index.js
|
||||
index d758d3c8f58a60bf27ef377ad77639bf10ce7854..2bad40d4eeba553d3bcfb206873eac059067ae3b 100644
|
||||
--- a/index.js
|
||||
+++ b/index.js
|
||||
@@ -399,19 +399,20 @@ function wrapfunction (fn, message) {
|
||||
throw new TypeError('argument fn must be a function')
|
||||
}
|
||||
|
||||
- var args = createArgumentsString(fn.length)
|
||||
- var deprecate = this // eslint-disable-line no-unused-vars
|
||||
var stack = getStack()
|
||||
var site = callSiteLocation(stack[1])
|
||||
|
||||
site.name = fn.name
|
||||
|
||||
- // eslint-disable-next-line no-eval
|
||||
- var deprecatedfn = eval('(function (' + args + ') {\n' +
|
||||
- '"use strict"\n' +
|
||||
- 'log.call(deprecate, message, site)\n' +
|
||||
- 'return fn.apply(this, arguments)\n' +
|
||||
- '})')
|
||||
+ var deprecatedfn
|
||||
+ var self = this
|
||||
+ deprecatedfn = function () {
|
||||
+ 'use strict'
|
||||
+ log.call(self, message, site)
|
||||
+ return fn.apply(this, arguments)
|
||||
+ }
|
||||
+ Object.defineProperty(deprecatedfn, 'length', { value: fn.length })
|
||||
+ Object.defineProperty(deprecatedfn, 'name', { value: fn.name })
|
||||
|
||||
return deprecatedfn
|
||||
}
|
||||
35
.yarn/patches/depd-npm-2.0.0-b6c51a4b43.patch
Normal file
@@ -0,0 +1,35 @@
|
||||
# Patch to remove eval. This allows using depd in an environment with
|
||||
# a strict Content-Security-Policy.
|
||||
# Ref: https://github.com/dougwilson/nodejs-depd/pull/33
|
||||
diff --git a/index.js b/index.js
|
||||
index 1bf2fcfdeffc984e5ad792eec08744c29d4a4590..1b24aa2414458bc651abfdded81b103c131efeaa 100644
|
||||
--- a/index.js
|
||||
+++ b/index.js
|
||||
@@ -415,19 +415,19 @@ function wrapfunction (fn, message) {
|
||||
throw new TypeError('argument fn must be a function')
|
||||
}
|
||||
|
||||
- var args = createArgumentsString(fn.length)
|
||||
var stack = getStack()
|
||||
var site = callSiteLocation(stack[1])
|
||||
|
||||
site.name = fn.name
|
||||
|
||||
- // eslint-disable-next-line no-new-func
|
||||
- var deprecatedfn = new Function('fn', 'log', 'deprecate', 'message', 'site',
|
||||
- '"use strict"\n' +
|
||||
- 'return function (' + args + ') {' +
|
||||
- 'log.call(deprecate, message, site)\n' +
|
||||
- 'return fn.apply(this, arguments)\n' +
|
||||
- '}')(fn, log, this, message, site)
|
||||
+ var self = this
|
||||
+ var deprecatedfn = function () {
|
||||
+ 'use strict'
|
||||
+ log.call(self, message, site)
|
||||
+ return fn.apply(this, arguments)
|
||||
+ }
|
||||
+ Object.defineProperty(deprecatedfn, 'length', { value: fn.length })
|
||||
+ Object.defineProperty(deprecatedfn, 'name', { value: fn.name })
|
||||
|
||||
return deprecatedfn
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
diff --git a/android/build.gradle b/android/build.gradle
|
||||
index 6afcbbf0cc8ca2d69dd78077d61e59a90b2136bb..9f8d72b4ec5b2b3d290975d6a255917c95300854 100644
|
||||
--- a/android/build.gradle
|
||||
+++ b/android/build.gradle
|
||||
@@ -67,19 +67,19 @@ repositories {
|
||||
}
|
||||
|
||||
// Generate UUIDs for each models contained in android/src/main/assets/
|
||||
-tasks.register('genUUID') {
|
||||
- doLast {
|
||||
- fileTree(dir: "$rootDir/app/src/main/assets", exclude: ['*/*']).visit { fileDetails ->
|
||||
- if (fileDetails.directory) {
|
||||
- def odir = file("$rootDir/app/src/main/assets/$fileDetails.relativePath")
|
||||
- def ofile = file("$odir/uuid")
|
||||
- mkdir odir
|
||||
- ofile.text = UUID.randomUUID().toString()
|
||||
- }
|
||||
- }
|
||||
- }
|
||||
-}
|
||||
-preBuild.dependsOn genUUID
|
||||
+// tasks.register('genUUID') {
|
||||
+// doLast {
|
||||
+// fileTree(dir: "$rootDir/app/src/main/assets", exclude: ['*/*']).visit { fileDetails ->
|
||||
+// if (fileDetails.directory) {
|
||||
+// def odir = file("$rootDir/app/src/main/assets/$fileDetails.relativePath")
|
||||
+// def ofile = file("$odir/uuid")
|
||||
+// mkdir odir
|
||||
+// ofile.text = UUID.randomUUID().toString()
|
||||
+// }
|
||||
+// }
|
||||
+// }
|
||||
+// }
|
||||
+// preBuild.dependsOn genUUID
|
||||
|
||||
def kotlin_version = getExtOrDefault('kotlinVersion')
|
||||
|
||||
diff --git a/android/src/main/java/com/reactnativevosk/VoskModule.kt b/android/src/main/java/com/reactnativevosk/VoskModule.kt
|
||||
index 0e2b6595b1b2cf1ee01c6c64239c4b0ea37fce19..5a8539b9cce8951967640dba755e29a4e3ff404a 100644
|
||||
--- a/android/src/main/java/com/reactnativevosk/VoskModule.kt
|
||||
+++ b/android/src/main/java/com/reactnativevosk/VoskModule.kt
|
||||
@@ -19,13 +19,25 @@ class VoskModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
|
||||
return "Vosk"
|
||||
}
|
||||
|
||||
+ @ReactMethod
|
||||
+ fun addListener(type: String?) {
|
||||
+ // Keep: Required for RN built in Event Emitter Calls.
|
||||
+ }
|
||||
+
|
||||
+ @ReactMethod
|
||||
+ fun removeListeners(type: Int?) {
|
||||
+ // Keep: Required for RN built in Event Emitter Calls.
|
||||
+ }
|
||||
+
|
||||
override fun onResult(hypothesis: String) {
|
||||
// Get text data from string object
|
||||
val text = getHypothesisText(hypothesis)
|
||||
|
||||
// Stop recording if data found
|
||||
if (text != null && text.isNotEmpty()) {
|
||||
- cleanRecognizer();
|
||||
+ // Don't auto-stop the recogniser - we want to do that when the user
|
||||
+ // presses on "stop" only.
|
||||
+ // cleanRecognizer();
|
||||
sendEvent("onResult", text)
|
||||
}
|
||||
}
|
||||
@@ -93,12 +105,11 @@ class VoskModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
|
||||
@ReactMethod
|
||||
fun loadModel(path: String, promise: Promise) {
|
||||
cleanModel();
|
||||
- StorageService.unpack(context, path, "models",
|
||||
- { model: Model? ->
|
||||
- this.model = model
|
||||
- promise.resolve("Model successfully loaded")
|
||||
- }
|
||||
- ) { e: IOException ->
|
||||
+
|
||||
+ try {
|
||||
+ this.model = Model(path);
|
||||
+ promise.resolve("Model successfully loaded")
|
||||
+ } catch (e: IOException) {
|
||||
this.model = null
|
||||
promise.reject(e)
|
||||
}
|
||||
@@ -153,6 +164,25 @@ class VoskModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
|
||||
cleanRecognizer();
|
||||
}
|
||||
|
||||
+ @ReactMethod
|
||||
+ fun stopOnly() {
|
||||
+ if (speechService != null) {
|
||||
+ speechService!!.stop()
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ @ReactMethod
|
||||
+ fun cleanup() {
|
||||
+ if (speechService != null) {
|
||||
+ speechService!!.shutdown();
|
||||
+ speechService = null
|
||||
+ }
|
||||
+ if (recognizer != null) {
|
||||
+ recognizer!!.close();
|
||||
+ recognizer = null;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
@ReactMethod
|
||||
fun unload() {
|
||||
cleanRecognizer();
|
||||
diff --git a/lib/typescript/index.d.ts b/lib/typescript/index.d.ts
|
||||
index 441e41cc402cca3a60b34978ef4fea976076259c..a173acebb4b314402550442ad471e0f7c706e3c4 100644
|
||||
--- a/lib/typescript/index.d.ts
|
||||
+++ b/lib/typescript/index.d.ts
|
||||
@@ -10,6 +10,8 @@ export default class Vosk {
|
||||
currentRegisteredEvents: EmitterSubscription[];
|
||||
start: (grammar?: string[] | null) => Promise<String>;
|
||||
stop: () => void;
|
||||
+ stopOnly: () => void;
|
||||
+ cleanup: () => void;
|
||||
unload: () => void;
|
||||
onResult: (onResult: (e: VoskEvent) => void) => EventSubscription;
|
||||
onFinalResult: (onFinalResult: (e: VoskEvent) => void) => EventSubscription;
|
||||
diff --git a/package.json b/package.json
|
||||
index 707eddb8d68007f93071ac659c5b087c935c5f01..90ebe20f224eeec472c377df1fef9b15f2ff8200 100644
|
||||
--- a/package.json
|
||||
+++ b/package.json
|
||||
@@ -11,12 +11,9 @@
|
||||
"src",
|
||||
"lib",
|
||||
"android",
|
||||
- "ios",
|
||||
"cpp",
|
||||
- "react-native-vosk.podspec",
|
||||
"!lib/typescript/example",
|
||||
"!android/build",
|
||||
- "!ios/build",
|
||||
"!**/__tests__",
|
||||
"!**/__fixtures__",
|
||||
"!**/__mocks__"
|
||||
diff --git a/react-native-vosk.podspec b/react-native-vosk.podspec
|
||||
deleted file mode 100644
|
||||
index e3d41b90c5eef890c7a5108aaf16ac07d34a698b..0000000000000000000000000000000000000000
|
||||
--- a/react-native-vosk.podspec
|
||||
+++ /dev/null
|
||||
@@ -1,41 +0,0 @@
|
||||
-require "json"
|
||||
-
|
||||
-package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
||||
-folly_version = '2021.06.28.00-v2'
|
||||
-folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'
|
||||
-
|
||||
-Pod::Spec.new do |s|
|
||||
- s.name = "react-native-vosk"
|
||||
- s.version = package["version"]
|
||||
- s.summary = package["description"]
|
||||
- s.homepage = package["homepage"]
|
||||
- s.license = package["license"]
|
||||
- s.authors = package["author"]
|
||||
-
|
||||
- s.platforms = { :ios => "10.0" }
|
||||
- s.source = { :git => "https://github.com/riderodd/react-native-vosk.git", :tag => "#{s.version}" }
|
||||
-
|
||||
- s.source_files = "ios/**/*.{h,m,mm,swift}"
|
||||
- s.resource_bundles = { 'Vosk' => ['ios/Vosk/*'] }
|
||||
-
|
||||
- s.dependency "React-Core"
|
||||
- s.frameworks = "Accelerate"
|
||||
- s.library = "c++"
|
||||
- s.vendored_frameworks = "ios/libvosk.xcframework"
|
||||
- s.requires_arc = true
|
||||
-
|
||||
- # Don't install the dependencies when we run `pod install` in the old architecture.
|
||||
- if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then
|
||||
- s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1"
|
||||
- s.pod_target_xcconfig = {
|
||||
- "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"",
|
||||
- "CLANG_CXX_LANGUAGE_STANDARD" => "c++17"
|
||||
- }
|
||||
-
|
||||
- s.dependency "React-Codegen"
|
||||
- s.dependency "RCT-Folly", folly_version
|
||||
- s.dependency "RCTRequired"
|
||||
- s.dependency "RCTTypeSafety"
|
||||
- s.dependency "ReactCommon/turbomodule/core"
|
||||
- end
|
||||
-end
|
||||
diff --git a/src/index.tsx b/src/index.tsx
|
||||
index d9f90c921d89b1b4d85e145443ed3376546a368a..29e4068dbd7500828a73145bd25497a52c9bf638 100644
|
||||
--- a/src/index.tsx
|
||||
+++ b/src/index.tsx
|
||||
@@ -69,6 +69,15 @@ export default class Vosk {
|
||||
VoskModule.stop();
|
||||
};
|
||||
|
||||
+ stopOnly = () => {
|
||||
+ VoskModule.stopOnly();
|
||||
+ };
|
||||
+
|
||||
+ cleanup = () => {
|
||||
+ this.cleanListeners();
|
||||
+ VoskModule.cleanup();
|
||||
+ };
|
||||
+
|
||||
unload = () => {
|
||||
this.cleanListeners();
|
||||
VoskModule.unload();
|
||||
BIN
Assets/WebsiteAssets/images/news/20250922-mobile-rte.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
Assets/WebsiteAssets/images/news/20250922-note-history.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
Assets/WebsiteAssets/images/news/20250922-publish-notes.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
Assets/WebsiteAssets/images/news/20250922-scan-notebook.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
Assets/WebsiteAssets/images/news/20250922-tag-editor.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
Assets/WebsiteAssets/images/sponsors/DoMyEssay.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
Assets/WebsiteAssets/images/sponsors/NotGamStop.jpg
Normal file
|
After Width: | Height: | Size: 109 KiB |
BIN
Assets/WebsiteAssets/images/sponsors/PokiesLab.png
Normal file
|
After Width: | Height: | Size: 378 KiB |
BIN
Assets/WebsiteAssets/images/sponsors/Pokiesman.png
Normal file
|
After Width: | Height: | Size: 295 KiB |
BIN
Assets/WebsiteAssets/images/sponsors/SocialKings.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
Assets/WebsiteAssets/images/sponsors/WriteMyEssay.png
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
@@ -1,4 +1,77 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Joplin]]></title><description><![CDATA[Joplin, the open source note-taking application]]></description><link>https://joplinapp.org</link><generator>RSS for Node</generator><lastBuildDate>Mon, 28 Apr 2025 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Mon, 28 Apr 2025 00:00:00 GMT</pubDate><item><title><![CDATA[What's new in Joplin 3.3]]></title><description><![CDATA[<h2>Desktop application<a name="desktop-application" href="#desktop-application" class="heading-anchor">🔗</a></h2>
|
||||
<?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, 22 Sep 2025 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Mon, 22 Sep 2025 00:00:00 GMT</pubDate><item><title><![CDATA[What's new in Joplin 3.4]]></title><description><![CDATA[<p>Joplin 3.4 includes many bug fixes and improvements, with a focus on the mobile app.</p>
|
||||
<h2>Mobile<a name="mobile" href="#mobile" class="heading-anchor">🔗</a></h2>
|
||||
<h3>Rich Text Editor<a name="rich-text-editor" href="#rich-text-editor" class="heading-anchor">🔗</a></h3>
|
||||
<p>The mobile app now includes a beta <a href="https://joplinapp.org/help/apps/rich_text_editor">Rich Text Editor</a>! The new editor renders formatting/math/images within the editor:</p>
|
||||
<img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20250922-mobile-rte.png" width="400" alt="screenshot: Mobile Rich Text Editor editing the welcome notes. Images, headings, etc are rendering."/>
|
||||
<p>To try it, 1) open a note in the default Markdown editor 2) open the note actions menu (the three vertical dots) for the note and 3) click “Edit as Rich Text”.</p>
|
||||
<p>Be aware that this editor is still in active development and <a href="https://github.com/laurent22/joplin/issues/12840">has a number of known limitations and issues</a>. The Rich Text editor is based on <a href="https://prosemirror.net/">ProseMirror</a> and will behave differently from the desktop Rich Text Editor in many cases.</p>
|
||||
<h3>Support for publishing notes with Joplin Cloud and Server<a name="support-for-publishing-notes-with-joplin-cloud-and-server" href="#support-for-publishing-notes-with-joplin-cloud-and-server" class="heading-anchor">🔗</a></h3>
|
||||
<p>It's now possible to <a href="https://joplinapp.org/help/apps/publish_note">publish notes</a> from the mobile app! To do so, open the “Properties” menu for a note, then click “Publish/unpublish”:</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20250922-publish-notes.png" alt="screenshot: A Publish/unpublish note action is shown in the "Note properties" sidebar, just below a "Previous versions" button"></p>
|
||||
<p>Next, in the “publish note” dialog, click “Copy shareable link”. Notes can later be unpublished by clicking "Unpublish" in the publication dialog.</p>
|
||||
<h3>Viewing note history<a name="viewing-note-history" href="#viewing-note-history" class="heading-anchor">🔗</a></h3>
|
||||
<p>It is now possible to view and restore previous note versions from the mobile app. Like the "publish note" feature, previous note versions can be accessed from the note properties menu.</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20250922-note-history.png" alt="screenshot: The note history page"></p>
|
||||
<p>As on desktop, the note history feature can be configured from the “Note History” tab in settings.</p>
|
||||
<h3>Updated tag dialog<a name="updated-tag-dialog" href="#updated-tag-dialog" class="heading-anchor">🔗</a></h3>
|
||||
<p>The tag dialog has been redesigned, with a new UI for adding, removing, and creating new tags:<br>
|
||||
<img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20250922-tag-editor.png" width="500" alt="screenshot: Tag dialog now consists of three sections: Added tags, Add new tags, Actions."/></p>
|
||||
<h3>Android: Improved voice typing<a name="android-improved-voice-typing" href="#android-improved-voice-typing" class="heading-anchor">🔗</a></h3>
|
||||
<p>The voice typing feature on Android has been updated with <a href="https://github.com/laurent22/joplin/pull/12404">improved silence detection</a> and a new “<a href="https://github.com/laurent22/joplin/pull/12370">custom glossary</a>” setting. Voice typing also now <a href="https://github.com/laurent22/joplin/pull/12352">defaults to a more accurate (but somewhat slower) model</a>.</p>
|
||||
<h3>Quickly creating a note from multiple photos<a name="quickly-creating-a-note-from-multiple-photos" href="#quickly-creating-a-note-from-multiple-photos" class="heading-anchor">🔗</a></h3>
|
||||
<p>A “scan notebook” action has been added to the “New note” menu:</p>
|
||||
<img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20250922-scan-notebook.png" width="500"/>
|
||||
<p>This action allows quickly creating a new note with multiple pictures taken from the camera.</p>
|
||||
<h2>Desktop<a name="desktop" href="#desktop" class="heading-anchor">🔗</a></h2>
|
||||
<h3>More Markdown Editor settings<a name="more-markdown-editor-settings" href="#more-markdown-editor-settings" class="heading-anchor">🔗</a></h3>
|
||||
<p>The "Note" tab in settings now includes new settings for the Markdown editor, including:</p>
|
||||
<ul>
|
||||
<li>An option to render headers, lists, and certain other formatting within the editor.</li>
|
||||
<li>An option to render images in the editor.</li>
|
||||
</ul>
|
||||
<p>When enabled, these settings bring the Markdown editor closer to the Rich Text Editor, without <a href="https://joplinapp.org/help/apps/rich_text_editor">some of the Rich Text Editor's limitations</a>.</p>
|
||||
<p>These settings are also available on mobile.</p>
|
||||
<h3>Smaller application size and faster startup<a name="smaller-application-size-and-faster-startup" href="#smaller-application-size-and-faster-startup" class="heading-anchor">🔗</a></h3>
|
||||
<p>We've made the desktop application roughly 33% smaller! In addition to faster application startup, this means that the desktop app should be faster to download take up less space.</p>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Joplin version</th>
|
||||
<th>Previous size (v3.3.13)</th>
|
||||
<th>New size (v3.4.12)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Joplin for MacOS (ARM)</td>
|
||||
<td>211 MB</td>
|
||||
<td>141 MB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Joplin for Windows (installer)</td>
|
||||
<td>321 MB</td>
|
||||
<td>219 MB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Joplin for Windows (portable)</td>
|
||||
<td>320 MB</td>
|
||||
<td>219 MB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Joplin for Linux (AppImage)</td>
|
||||
<td>219 MB</td>
|
||||
<td>147 MB</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h2>Terminal app<a name="terminal-app" href="#terminal-app" class="heading-anchor">🔗</a></h2>
|
||||
<h3>Collapsible folders<a name="collapsible-folders" href="#collapsible-folders" class="heading-anchor">🔗</a></h3>
|
||||
<p>The <a href="https://joplinapp.org/help/apps/terminal/">terminal application</a> now supports expanding and collapsing folders by pressing <kbd>z</kbd>. For additional information, see <a href="https://github.com/laurent22/joplin/pull/12718">the original pull request</a>.</p>
|
||||
<h3>Managing shared notebooks and published notes<a name="managing-shared-notebooks-and-published-notes" href="#managing-shared-notebooks-and-published-notes" class="heading-anchor">🔗</a></h3>
|
||||
<p>New commands have been added to the terminal app, including <code>publish</code>, <code>unpublish</code>, and <code>share</code>. This allows the terminal app to manage shared folders and published notes.</p>
|
||||
<h2>Bug fixes<a name="bug-fixes" href="#bug-fixes" class="heading-anchor">🔗</a></h2>
|
||||
<p>For the full list of changes, see <a href="https://joplinapp.org/help/about/changelog/desktop/">the desktop changelog</a> and <a href="https://joplinapp.org/help/about/changelog/android/">the mobile changelog</a>.</p>
|
||||
]]></description><link>https://joplinapp.org/news/20250922-release-3-4</link><guid isPermaLink="false">20250922-release-3-4</guid><pubDate>Mon, 22 Sep 2025 00:00:00 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[What's new in Joplin 3.3]]></title><description><![CDATA[<h2>Desktop application<a name="desktop-application" href="#desktop-application" class="heading-anchor">🔗</a></h2>
|
||||
<h3>Accessibility improvements<a name="accessibility-improvements" href="#accessibility-improvements" class="heading-anchor">🔗</a></h3>
|
||||
<p>The Joplin 3.3 release introduces significant accessibility enhancements designed to make the application more inclusive and user-friendly. Users can now benefit from improved keyboard navigation, thanks to newly added shortcuts and clearer labels that streamline interaction across the interface. We've also added a "go to viewer" menu item that moves focus from the note editor to the note viewer. Focus is moved to the location in the viewer corresponding to the location of the cursor in the editor.</p>
|
||||
<p>Screen reader support has been bolstered, ensuring elements like the note list and sidebar are easier to toggle and interact with. These updates make the application more usable for individuals relying on assistive technologies.</p>
|
||||
@@ -446,10 +519,4 @@ sys 0m38.013s</p>
|
||||
<p>Unfortunately we cannot publish the Android version because it is based on a framework version that Google does not accept. To upgrade the app a lot of changes are needed and another round of pre-releases, and therefore there will not be a 2.9 version for Google Play. You may however download the official APK directly from there: <a href="https://github.com/laurent22/joplin-android/releases/tag/android-v2.9.8">Android 2.9 Official Release</a></p>
|
||||
<p>This is the reality of app stores in general - small developers being imposed never ending new requirements by all-powerful companies, and by the time a version is finally ready we can't even publish it because yet more requirements are in place.</p>
|
||||
<p>For the record the current 2.9 app works perfectly fine. It targets Android 11, which is only 2 years old and is still supported (and installed on millions of phones). Google requires us to target Android 12 which only came out last year.</p>
|
||||
]]></description><link>https://joplinapp.org/news/20221216-release-2-9</link><guid isPermaLink="false">20221216-release-2-9</guid><pubDate>Fri, 16 Dec 2022 00:00:00 GMT</pubDate><twitter-text>What's new in Joplin 2.9</twitter-text></item><item><title><![CDATA[Joplin is hiring!]]></title><description><![CDATA[<p>Joplin is an open source note-taking app. Capture your thoughts and securely access them from any device.</p>
|
||||
<p>We are looking to hire two JavaScript software developers to work on the desktop, mobile, and server applications. All those are built using modern technologies, including React, React Native and Electron with a strong focus on test units.</p>
|
||||
<p>You need to demonstrate some experience with at least some of these technologies, and willing to learn more and touch various different projects.</p>
|
||||
<p>You will be part of a small team, so you will have an opportunity for a high-impact role, targeting hundreds of thousands of users.</p>
|
||||
<p>If you're interested please contact us at job-AT-joplin.cloud</p>
|
||||
<p>No agencies please.</p>
|
||||
]]></description><link>https://joplinapp.org/news/20221209-job</link><guid isPermaLink="false">20221209-job</guid><pubDate>Fri, 09 Dec 2022 00:00:00 GMT</pubDate><twitter-text>Joplin is hiring!</twitter-text></item></channel></rss>
|
||||
]]></description><link>https://joplinapp.org/news/20221216-release-2-9</link><guid isPermaLink="false">20221216-release-2-9</guid><pubDate>Fri, 16 Dec 2022 00:00:00 GMT</pubDate><twitter-text>What's new in Joplin 2.9</twitter-text></item></channel></rss>
|
||||
@@ -63,11 +63,24 @@ FROM node:18-slim
|
||||
ARG user=joplin
|
||||
RUN useradd --create-home --shell /bin/bash $user
|
||||
|
||||
# Install PM2 and set home directory. Setting the PM2 data dir so modules/config persist regardless
|
||||
# of user home.
|
||||
RUN npm i -g pm2@5.4.3 && mkdir -p /opt/pm2 && chown -R $user:$user /opt/pm2
|
||||
ENV PM2_HOME=/opt/pm2
|
||||
|
||||
USER $user
|
||||
|
||||
COPY --chown=$user:$user --from=builder /build/packages /home/$user/packages
|
||||
COPY --chown=$user:$user --from=builder /usr/bin/tini /usr/local/bin/tini
|
||||
|
||||
# We download a specific version of the plugin to prevent pm2 from fetching the latest, since it may
|
||||
# not have been properly audited (that fact was used to spread malware at some point). Ref:
|
||||
# https://github.com/laurent22/joplin/issues/12754
|
||||
RUN pm2 install https://registry.npmjs.org/pm2-logrotate/-/pm2-logrotate-3.0.0.tgz \
|
||||
&& pm2 set pm2-logrotate:max_size 100MB \
|
||||
&& pm2 set pm2-logrotate:retain 5 \
|
||||
&& pm2 set pm2-logrotate:compress true
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV RUNNING_IN_DOCKER=1
|
||||
EXPOSE ${APP_PORT}
|
||||
|
||||
@@ -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://citricsheep.com"><img title="Citric Sheep" width="256" src="https://joplinapp.org/images/sponsors/CitricSheep.png"/></a> <a href="https://sorted.travel/?utm_source=joplinapp"><img title="Sorted Travel" width="256" src="https://joplinapp.org/images/sponsors/SortedTravel.png"/></a> <a href="https://celebian.com"><img title="Celebian" width="256" src="https://joplinapp.org/images/sponsors/Celebian.png"/></a> <a href="https://bestkru.com"><img title="BestKru" width="256" src="https://joplinapp.org/images/sponsors/BestKru.png"/></a> <a href="https://www.socialfollowers.uk/buy-tiktok-followers/"><img title="Social Followers" width="256" src="https://joplinapp.org/images/sponsors/SocialFollowers.png"/></a> <a href="https://stormlikes.com/"><img title="Stormlikes" width="256" src="https://joplinapp.org/images/sponsors/Stormlikes.png"/></a> <a href="https://route4me.com"><img title="Route4Me" width="256" src="https://joplinapp.org/images/sponsors/Route4Me.png"/></a> <a href="https://topagency.webflow.io"><img title="WebDesignAgency" width="256" src="https://joplinapp.org/images/sponsors/WebDesignAgency.png" alt="topagency"/></a> <a href="https://www.slotozilla.com/nz/no-deposit-bonus"><img title="casino without making any upfront cost" width="256" src="https://joplinapp.org/images/sponsors/Slotozilla.png" alt="casino without making any upfront cost"/></a> <a href="https://essayservice.com"><img title="quick and reliable service to write my paper for me" width="256" src="https://joplinapp.org/images/sponsors/EssayService.png" alt="quick and reliable service to write my paper for me"/></a> <a href="https://writepaper.com/"><img title="best service to write my paper for me" width="256" src="https://joplinapp.org/images/sponsors/WritePaper.png" alt="best service to write my paper for me"/></a> <a href="https://paperwriter.com/"><img title="high-quality paper writing service PaperWriter" width="256" src="https://joplinapp.org/images/sponsors/PaperWriter.png" alt="high-quality paper writing service PaperWriter"/></a> <a href="https://www.bestetf.net/"><img title="BestETF" width="256" src="https://joplinapp.org/images/sponsors/BestEtf.png" alt="BestETF"/></a> <a href="https://freespinny.io/free-spins-no-deposit/"><img title="Freespinny.io Free Spins Bonus site" width="256" src="https://joplinapp.org/images/sponsors/Freespinny.png" alt="Freespinny.io Free Spins Bonus site"/></a> <a href="https://damangameplay.in"><img title="Daman Game" width="256" src="https://joplinapp.org/images/sponsors/DamanGame.png" alt="Daman Game"/></a> <a href="https://essayshark.com"><img title="EssayShark - essay writers for hire" width="256" src="https://joplinapp.org/images/sponsors/EssayShark.png" alt="EssayShark - essay writers for hire"/></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://citricsheep.com"><img title="Citric Sheep" width="256" src="https://joplinapp.org/images/sponsors/CitricSheep.png"/></a> <a href="https://sorted.travel/?utm_source=joplinapp"><img title="Sorted Travel" width="256" src="https://joplinapp.org/images/sponsors/SortedTravel.png"/></a> <a href="https://celebian.com"><img title="Celebian" width="256" src="https://joplinapp.org/images/sponsors/Celebian.png"/></a> <a href="https://bestkru.com"><img title="BestKru" width="256" src="https://joplinapp.org/images/sponsors/BestKru.png"/></a> <a href="https://www.socialfollowers.uk/buy-tiktok-followers/"><img title="Social Followers" width="256" src="https://joplinapp.org/images/sponsors/SocialFollowers.png"/></a> <a href="https://stormlikes.com/"><img title="Stormlikes" width="256" src="https://joplinapp.org/images/sponsors/Stormlikes.png"/></a> <a href="https://route4me.com"><img title="Route4Me" width="256" src="https://joplinapp.org/images/sponsors/Route4Me.png"/></a> <a href="https://topagency.webflow.io"><img title="WebDesignAgency" width="256" src="https://joplinapp.org/images/sponsors/WebDesignAgency.png" alt="topagency"/></a> <a href="https://www.slotozilla.com/nz/no-deposit-bonus"><img title="casino without making any upfront cost" width="256" src="https://joplinapp.org/images/sponsors/Slotozilla.png" alt="casino without making any upfront cost"/></a> <a href="https://writepaper.com/"><img title="best service to write my paper for me" width="256" src="https://joplinapp.org/images/sponsors/WritePaper.png" alt="best service to write my paper for me"/></a> <a href="https://paperwriter.com/"><img title="high-quality paper writing service PaperWriter" width="256" src="https://joplinapp.org/images/sponsors/PaperWriter.png" alt="high-quality paper writing service PaperWriter"/></a> <a href="https://www.bestetf.net/"><img title="BestETF" width="256" src="https://joplinapp.org/images/sponsors/BestEtf.png" alt="BestETF"/></a> <a href="https://freespinny.io/free-spins-no-deposit/"><img title="Freespinny.io Free Spins Bonus site" width="256" src="https://joplinapp.org/images/sponsors/Freespinny.png" alt="Freespinny.io Free Spins Bonus site"/></a> <a href="https://essayshark.com"><img title="EssayShark - essay writers for hire" width="256" src="https://joplinapp.org/images/sponsors/EssayShark.png" alt="EssayShark - essay writers for hire"/></a> <a href="https://pokieslab1.com/real-money-pokies/"><img title="Australian Real Money Pokies" width="256" src="https://joplinapp.org/images/sponsors/PokiesLab.png" alt="Australian Real Money Pokies"/></a> <a href="https://pokiesman1.net/real-money-pokies/"><img title="Australian Real Money Pokies" width="256" src="https://joplinapp.org/images/sponsors/Pokiesman.png" alt="Australian Real Money Pokies"/></a> <a href="https://domyessay.com"><img title="Essay writers DoMyEssay are dedicated to providing top-notch, custom-written papers that meet your academic requirements" width="256" src="https://joplinapp.org/images/sponsors/DoMyEssay.png" alt="DoMyEssay"/></a> <a href="https://essaypro.com/"><img title="best essay writing service" width="256" src="https://joplinapp.org/images/sponsors/EssayPro.png" alt="best essay writing service"/></a> <a href="https://socialkings.online"><img title="Boost your reach and buy real followers" width="256" src="https://joplinapp.org/images/sponsors/SocialKings.png" alt="Boost your reach and buy real followers"/></a> <a href="https://uk.notgamstop.com/bonuses/free-spins-no-deposit-no-gamstop/"><img title="free spins no deposit at NotGamstop" width="256" src="https://joplinapp.org/images/sponsors/NotGamStop.jpg" alt="free spins no deposit at NotGamstop"/></a> <a href="https://www.writemyessay.com/"><img title="writing service for students WriteMyEssay" width="256" src="https://joplinapp.org/images/sponsors/WriteMyEssay.png" alt="writing service for students WriteMyEssay"/></a>
|
||||
<!-- SPONSORS-ORG -->
|
||||
|
||||
* * *
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"vips.dev": {
|
||||
"platforms": ["aarch64-darwin"],
|
||||
},
|
||||
"nodejs": "23.10.0",
|
||||
"nodejs": "23.11.0",
|
||||
"pkg-config": "latest",
|
||||
"darwin.apple_sdk.frameworks.Foundation": { // satisfies missing CoreText/CoreText.h
|
||||
// https://github.com/NixOS/nixpkgs/blob/master/pkgs/os-specific/darwin/apple-sdk/default.nix
|
||||
|
||||
@@ -16,12 +16,10 @@
|
||||
# SLAVE_POSTGRES_PORT=5433
|
||||
# SLAVE_POSTGRES_HOST=localhost
|
||||
|
||||
version: '2'
|
||||
|
||||
services:
|
||||
|
||||
postgresql-master:
|
||||
image: 'bitnami/postgresql:17.3.0'
|
||||
image: 'bitnamilegacy/postgresql:17.4.0'
|
||||
ports:
|
||||
- '5432:5432'
|
||||
environment:
|
||||
@@ -38,7 +36,7 @@ services:
|
||||
- POSTGRESQL_EXTRA_FLAGS=-c work_mem=100000 -c log_statement=all
|
||||
|
||||
postgresql-slave:
|
||||
image: 'bitnami/postgresql:17.3.0'
|
||||
image: 'bitnamilegacy/postgresql:17.4.0'
|
||||
ports:
|
||||
- '5433:5432'
|
||||
depends_on:
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
# This compose file can be used in development to run both the database and app
|
||||
# within Docker.
|
||||
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
# - This would typically be mapped to port to 443 (TLS) with a reverse proxy.
|
||||
# - If Joplin Server does not need to be accessible over the internet, the port can be mapped to 22300.
|
||||
|
||||
version: '3'
|
||||
|
||||
networks:
|
||||
app-network:
|
||||
transcribe-network:
|
||||
|
||||
15
package.json
@@ -79,17 +79,17 @@
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-jest": "27.9.0",
|
||||
"eslint-plugin-promise": "6.6.0",
|
||||
"eslint-plugin-react": "7.37.4",
|
||||
"eslint-plugin-react": "7.37.5",
|
||||
"execa": "5.1.1",
|
||||
"fs-extra": "11.2.0",
|
||||
"glob": "11.0.2",
|
||||
"glob": "11.0.3",
|
||||
"gulp": "4.0.2",
|
||||
"husky": "9.1.7",
|
||||
"lerna": "3.22.1",
|
||||
"lint-staged": "15.5.2",
|
||||
"madge": "8.0.0",
|
||||
"npm-package-json-lint": "8.0.0",
|
||||
"typescript": "5.8.2"
|
||||
"typescript": "5.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/fs-extra": "11.0.4",
|
||||
@@ -101,7 +101,6 @@
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"resolutions": {
|
||||
"react-native-camera@4.2.1": "patch:react-native-camera@npm%3A4.2.1#./.yarn/patches/react-native-camera-npm-4.2.1-24b2600a7e.patch",
|
||||
"react-native-vosk@0.1.12": "patch:react-native-vosk@npm%3A0.1.12#./.yarn/patches/react-native-vosk-npm-0.1.12-76b1caaae8.patch",
|
||||
"eslint": "patch:eslint@8.57.1#./.yarn/patches/eslint-npm-8.39.0-d92bace04d.patch",
|
||||
"app-builder-lib@24.4.0": "patch:app-builder-lib@npm%3A24.4.0#./.yarn/patches/app-builder-lib-npm-24.4.0-05322ff057.patch",
|
||||
"nanoid": "patch:nanoid@npm%3A3.3.7#./.yarn/patches/nanoid-npm-3.3.7-98824ba130.patch",
|
||||
@@ -118,6 +117,12 @@
|
||||
"pdfjs-dist@*": "patch:pdfjs-dist@npm%3A3.11.174#./.yarn/patches/pdfjs-dist-npm-3.11.174-67f2fee6d6.patch",
|
||||
"pdfjs-dist@3.11.174": "patch:pdfjs-dist@npm%3A3.11.174#./.yarn/patches/pdfjs-dist-npm-3.11.174-67f2fee6d6.patch",
|
||||
"canvas@npm:^2.11.2": "link:./.yarn/joplin-empty-package/",
|
||||
"node-gyp@npm:^9.0.0": "11.2.0"
|
||||
"node-gyp@npm:^9.0.0": "11.2.0",
|
||||
"depd@npm:^2.0.0": "patch:depd@npm%3A2.0.0#~/.yarn/patches/depd-npm-2.0.0-b6c51a4b43.patch",
|
||||
"depd@npm:~2.0.0": "patch:depd@npm%3A2.0.0#~/.yarn/patches/depd-npm-2.0.0-b6c51a4b43.patch",
|
||||
"depd@npm:~1.1.2": "patch:depd@npm%3A2.0.0#~/.yarn/patches/depd-npm-2.0.0-b6c51a4b43.patch",
|
||||
"depd@npm:2.0.0": "patch:depd@npm%3A2.0.0#~/.yarn/patches/depd-npm-2.0.0-b6c51a4b43.patch",
|
||||
"depd@npm:^1.1.2": "patch:depd@npm%3A2.0.0#~/.yarn/patches/depd-npm-2.0.0-b6c51a4b43.patch",
|
||||
"depd@npm:^1.1.0": "patch:depd@npm%3A2.0.0#~/.yarn/patches/depd-npm-2.0.0-b6c51a4b43.patch"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -417,8 +417,10 @@ class Application extends BaseApplication {
|
||||
if (argv.length) {
|
||||
this.gui_ = this.dummyGui();
|
||||
|
||||
const initialFolder = await Folder.load(Setting.value('activeFolderId'));
|
||||
await this.switchCurrentFolder(initialFolder);
|
||||
await this.applySettingsSideEffects();
|
||||
await this.refreshCurrentFolder();
|
||||
|
||||
try {
|
||||
await this.execCommand(argv);
|
||||
} catch (error) {
|
||||
@@ -432,6 +434,7 @@ class Application extends BaseApplication {
|
||||
}
|
||||
|
||||
await Setting.saveAll();
|
||||
await this.database_.close();
|
||||
|
||||
// Need to call exit() explicitly, otherwise Node wait for any timeout to complete
|
||||
// https://stackoverflow.com/questions/18050095
|
||||
|
||||
@@ -2,33 +2,44 @@
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
const fs = require('fs-extra');
|
||||
const Logger = require('@joplin/utils/Logger').default;
|
||||
const { dirname } = require('@joplin/lib/path-utils');
|
||||
import * as fs from 'fs-extra';
|
||||
import Logger, { TargetType } from '@joplin/utils/Logger';
|
||||
import { dirname } from '@joplin/lib/path-utils';
|
||||
const { DatabaseDriverNode } = require('@joplin/lib/database-driver-node.js');
|
||||
const JoplinDatabase = require('@joplin/lib/JoplinDatabase').default;
|
||||
const BaseModel = require('@joplin/lib/BaseModel').default;
|
||||
const Folder = require('@joplin/lib/models/Folder').default;
|
||||
const Note = require('@joplin/lib/models/Note').default;
|
||||
const Setting = require('@joplin/lib/models/Setting').default;
|
||||
import JoplinDatabase from '@joplin/lib/JoplinDatabase';
|
||||
import BaseModel from '@joplin/lib/BaseModel';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
const { sprintf } = require('sprintf-js');
|
||||
const exec = require('child_process').exec;
|
||||
const nodeSqlite = require('sqlite3');
|
||||
const { loadKeychainServiceAndSettings } = require('@joplin/lib/services/SettingUtils');
|
||||
const { default: shimInitCli } = require('./utils/shimInitCli');
|
||||
|
||||
const baseDir = `${dirname(__dirname)}/tests/cli-integration`;
|
||||
const joplinAppPath = `${__dirname}/main.js`;
|
||||
|
||||
shimInitCli({ nodeSqlite, appVersion: () => require('../package.json').version, keytar: null });
|
||||
require('@joplin/lib/testing/test-utils');
|
||||
|
||||
const logger = new Logger();
|
||||
logger.addTarget('console');
|
||||
logger.addTarget(TargetType.Console);
|
||||
logger.setLevel(Logger.LEVEL_ERROR);
|
||||
|
||||
const dbLogger = new Logger();
|
||||
dbLogger.addTarget('console');
|
||||
dbLogger.addTarget(TargetType.Console);
|
||||
dbLogger.setLevel(Logger.LEVEL_INFO);
|
||||
|
||||
const db = new JoplinDatabase(new DatabaseDriverNode());
|
||||
db.setLogger(dbLogger);
|
||||
|
||||
function createClient(id) {
|
||||
interface Client {
|
||||
id: number;
|
||||
profileDir: string;
|
||||
}
|
||||
|
||||
function createClient(id: number): Client {
|
||||
return {
|
||||
id: id,
|
||||
profileDir: `${baseDir}/client${id}`,
|
||||
@@ -37,13 +48,13 @@ function createClient(id) {
|
||||
|
||||
const client = createClient(1);
|
||||
|
||||
function execCommand(client, command) {
|
||||
function execCommand(client: Client, command: string) {
|
||||
const exePath = `node ${joplinAppPath}`;
|
||||
const cmd = `${exePath} --update-geolocation-disabled --env dev --profile ${client.profileDir} ${command}`;
|
||||
logger.info(`${client.id}: ${command}`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(cmd, (error, stdout, stderr) => {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
exec(cmd, (error: string, stdout: string, stderr: string) => {
|
||||
if (error) {
|
||||
logger.error(stderr);
|
||||
reject(error);
|
||||
@@ -54,17 +65,17 @@ function execCommand(client, command) {
|
||||
});
|
||||
}
|
||||
|
||||
function assertTrue(v) {
|
||||
function assertTrue(v: unknown) {
|
||||
if (!v) throw new Error(sprintf('Expected "true", got "%s"."', v));
|
||||
process.stdout.write('.');
|
||||
}
|
||||
|
||||
function assertFalse(v) {
|
||||
function assertFalse(v: unknown) {
|
||||
if (v) throw new Error(sprintf('Expected "false", got "%s"."', v));
|
||||
process.stdout.write('.');
|
||||
}
|
||||
|
||||
function assertEquals(expected, real) {
|
||||
function assertEquals(expected: unknown, real: unknown) {
|
||||
if (expected !== real) throw new Error(sprintf('Expecting "%s", got "%s"', expected, real));
|
||||
process.stdout.write('.');
|
||||
}
|
||||
@@ -73,7 +84,7 @@ async function clearDatabase() {
|
||||
await db.transactionExecBatch(['DELETE FROM folders', 'DELETE FROM notes', 'DELETE FROM tags', 'DELETE FROM note_tags', 'DELETE FROM resources', 'DELETE FROM deleted_items']);
|
||||
}
|
||||
|
||||
const testUnits = {};
|
||||
const testUnits: Record<string, ()=> Promise<void>> = {};
|
||||
|
||||
testUnits.testFolders = async () => {
|
||||
await execCommand(client, 'mkbook nb1');
|
||||
@@ -85,10 +96,16 @@ testUnits.testFolders = async () => {
|
||||
await execCommand(client, 'mkbook nb1');
|
||||
|
||||
folders = await Folder.all();
|
||||
assertEquals(1, folders.length);
|
||||
assertEquals(2, folders.length);
|
||||
assertEquals('nb1', folders[0].title);
|
||||
assertEquals('nb1', folders[1].title);
|
||||
|
||||
await execCommand(client, 'rm -r -f nb1');
|
||||
await execCommand(client, 'rmbook -p -f nb1');
|
||||
|
||||
folders = await Folder.all();
|
||||
assertEquals(1, folders.length);
|
||||
|
||||
await execCommand(client, 'rmbook -p -f nb1');
|
||||
|
||||
folders = await Folder.all();
|
||||
assertEquals(0, folders.length);
|
||||
@@ -102,7 +119,7 @@ testUnits.testNotes = async () => {
|
||||
assertEquals(1, notes.length);
|
||||
assertEquals('n1', notes[0].title);
|
||||
|
||||
await execCommand(client, 'rm -f n1');
|
||||
await execCommand(client, 'rmnote -p -f n1');
|
||||
notes = await Note.all();
|
||||
assertEquals(0, notes.length);
|
||||
|
||||
@@ -112,12 +129,19 @@ testUnits.testNotes = async () => {
|
||||
notes = await Note.all();
|
||||
assertEquals(2, notes.length);
|
||||
|
||||
await execCommand(client, 'rm -f \'blabla*\'');
|
||||
// Should fail to delete a non-existent note
|
||||
let failed = false;
|
||||
try {
|
||||
await execCommand(client, 'rmnote -f \'blabla*\'');
|
||||
} catch (error) {
|
||||
failed = true;
|
||||
}
|
||||
assertEquals(failed, true);
|
||||
|
||||
notes = await Note.all();
|
||||
assertEquals(2, notes.length);
|
||||
|
||||
await execCommand(client, 'rm -f \'n*\'');
|
||||
await execCommand(client, 'rmnote -f -p \'n*\'');
|
||||
|
||||
notes = await Note.all();
|
||||
assertEquals(0, notes.length);
|
||||
@@ -140,10 +164,12 @@ testUnits.testCat = async () => {
|
||||
|
||||
testUnits.testConfig = async () => {
|
||||
await execCommand(client, 'config editor vim');
|
||||
await Setting.reset();
|
||||
await Setting.load();
|
||||
assertEquals('vim', Setting.value('editor'));
|
||||
|
||||
await execCommand(client, 'config editor subl');
|
||||
await Setting.reset();
|
||||
await Setting.load();
|
||||
assertEquals('subl', Setting.value('editor'));
|
||||
|
||||
@@ -201,15 +227,47 @@ testUnits.testMv = async () => {
|
||||
await execCommand(client, 'mknote note2');
|
||||
await execCommand(client, 'mknote note3');
|
||||
await execCommand(client, 'mknote blabla');
|
||||
await execCommand(client, 'mv \'note*\' nb2');
|
||||
|
||||
notes1 = await Note.previews(f1.id);
|
||||
notes2 = await Note.previews(f2.id);
|
||||
|
||||
assertEquals(4, notes1.length);
|
||||
assertEquals(1, notes2.length);
|
||||
|
||||
await execCommand(client, 'mv \'note*\' nb2');
|
||||
|
||||
notes2 = await Note.previews(f2.id);
|
||||
notes1 = await Note.previews(f1.id);
|
||||
|
||||
assertEquals(1, notes1.length);
|
||||
assertEquals(4, notes2.length);
|
||||
};
|
||||
|
||||
testUnits.testUse = async () => {
|
||||
await execCommand(client, 'mkbook nb1');
|
||||
await execCommand(client, 'mkbook nb2');
|
||||
await execCommand(client, 'mknote n1');
|
||||
await execCommand(client, 'mknote n2');
|
||||
|
||||
const f1 = await Folder.loadByTitle('nb1');
|
||||
const f2 = await Folder.loadByTitle('nb2');
|
||||
let notes1 = await Note.previews(f1.id);
|
||||
let notes2 = await Note.previews(f2.id);
|
||||
|
||||
assertEquals(0, notes1.length);
|
||||
assertEquals(2, notes2.length);
|
||||
|
||||
await execCommand(client, 'use nb1');
|
||||
await execCommand(client, 'mknote note2');
|
||||
await execCommand(client, 'mknote note3');
|
||||
|
||||
notes1 = await Note.previews(f1.id);
|
||||
notes2 = await Note.previews(f2.id);
|
||||
|
||||
assertEquals(2, notes1.length);
|
||||
assertEquals(2, notes2.length);
|
||||
};
|
||||
|
||||
async function main() {
|
||||
await fs.remove(baseDir);
|
||||
|
||||
@@ -217,7 +275,9 @@ async function main() {
|
||||
|
||||
await db.open({ name: `${client.profileDir}/database.sqlite` });
|
||||
BaseModel.setDb(db);
|
||||
await Setting.load();
|
||||
Setting.setConstant('rootProfileDir', client.profileDir);
|
||||
Setting.setConstant('profileDir', client.profileDir);
|
||||
await loadKeychainServiceAndSettings([]);
|
||||
|
||||
let onlyThisTest = 'testMv';
|
||||
onlyThisTest = '';
|
||||
@@ -234,7 +294,7 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
main(process.argv).catch(error => {
|
||||
main().catch(error => {
|
||||
console.info('');
|
||||
logger.error(error);
|
||||
});
|
||||
@@ -35,15 +35,15 @@
|
||||
],
|
||||
"owner": "Laurent Cozic"
|
||||
},
|
||||
"version": "3.4.0",
|
||||
"version": "3.5.0",
|
||||
"bin": "./main.js",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@joplin/lib": "~3.4",
|
||||
"@joplin/renderer": "~3.4",
|
||||
"@joplin/utils": "~3.4",
|
||||
"@joplin/lib": "~3.5",
|
||||
"@joplin/renderer": "~3.5",
|
||||
"@joplin/utils": "~3.5",
|
||||
"aws-sdk": "2.1340.0",
|
||||
"chalk": "4.1.2",
|
||||
"compare-version": "0.1.2",
|
||||
@@ -57,7 +57,7 @@
|
||||
"proper-lockfile": "4.1.2",
|
||||
"redux": "4.2.1",
|
||||
"server-destroy": "1.0.1",
|
||||
"sharp": "0.34.2",
|
||||
"sharp": "0.34.3",
|
||||
"sprintf-js": "1.1.3",
|
||||
"sqlite3": "5.1.6",
|
||||
"string-padding": "1.0.2",
|
||||
@@ -70,14 +70,14 @@
|
||||
"yargs-parser": "21.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@joplin/tools": "~3.4",
|
||||
"@joplin/tools": "~3.5",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/node": "18.19.103",
|
||||
"@types/node": "18.19.119",
|
||||
"@types/proper-lockfile": "^4.1.2",
|
||||
"gulp": "4.0.2",
|
||||
"jest": "29.7.0",
|
||||
"temp": "0.9.4",
|
||||
"typescript": "5.8.2"
|
||||
"typescript": "5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import PluginRunner from '../../../app/services/plugins/PluginRunner';
|
||||
import PluginService, { PluginSettings, defaultPluginSetting } from '@joplin/lib/services/plugins/PluginService';
|
||||
import { ContentScriptType } from '@joplin/lib/services/plugins/api/types';
|
||||
import MdToHtml from '@joplin/renderer/MdToHtml';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import shim, { MobilePlatform } from '@joplin/lib/shim';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import * as fs from 'fs-extra';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
@@ -310,7 +310,7 @@ describe('services_PluginService', () => {
|
||||
|
||||
let resetPlatformMock = () => {};
|
||||
if (!isDesktop) {
|
||||
resetPlatformMock = mockMobilePlatform('android').reset;
|
||||
resetPlatformMock = mockMobilePlatform(MobilePlatform.Android).reset;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
content
|
||||
@@ -0,0 +1 @@
|
||||
content
|
||||
@@ -0,0 +1 @@
|
||||
content
|
||||
@@ -0,0 +1,3 @@
|
||||
1. File without extension and leading `./`: [file1](./file1). Gets imported, but filename is converted to extension, like `<internal_id>.file1`
|
||||
2. File without extension: [file2](file2). Not imported at all.
|
||||
3. File with extension: [file3](file3.text). Gets imported properly.
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Joplin Web Clipper [DEV]",
|
||||
"version": "3.4.0",
|
||||
"version": "3.5.0",
|
||||
"description": "Capture and save web pages and screenshots from your browser to Joplin.",
|
||||
"homepage_url": "https://joplinapp.org",
|
||||
"content_security_policy": {
|
||||
|
||||
@@ -407,7 +407,17 @@ export default class ElectronAppWrapper {
|
||||
isGoingToExit = true;
|
||||
} else {
|
||||
event.preventDefault();
|
||||
this.hide();
|
||||
|
||||
const w = this.win_;
|
||||
if (!w) return;
|
||||
|
||||
if (w.isFullScreen()) {
|
||||
// leave fullscreen, then hide
|
||||
w.once('leave-full-screen', () => w.hide());
|
||||
w.setFullScreen(false);
|
||||
} else {
|
||||
w.hide();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const hasBackgroundWindows = this.secondaryWindows_.size > 0;
|
||||
@@ -612,7 +622,11 @@ export default class ElectronAppWrapper {
|
||||
console.warn('The window object was not available during the click event from tray icon');
|
||||
return;
|
||||
}
|
||||
this.mainWindow().show();
|
||||
if (!this.mainWindow().isVisible()) {
|
||||
this.mainWindow().show();
|
||||
} else {
|
||||
this.mainWindow().hide();
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Cannot create tray', error);
|
||||
|
||||
@@ -86,8 +86,14 @@ export default class InteropServiceHelper {
|
||||
// pdfs.
|
||||
// https://github.com/laurent22/joplin/issues/6254.
|
||||
await win.webContents.executeJavaScript('document.querySelectorAll(\'details\').forEach(el=>el.setAttribute(\'open\',\'\'))');
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const data = await win.webContents.printToPDF(options as any);
|
||||
const data = await win.webContents.printToPDF({
|
||||
...options,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Partially refactored old code before rule was applied
|
||||
pageSize: options.pageSize as any,
|
||||
// Allows users to override the CSS page size.
|
||||
// See https://github.com/laurent22/joplin/issues/13096
|
||||
preferCSSPageSize: true,
|
||||
});
|
||||
resolve(data);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
|
||||
@@ -63,6 +63,8 @@ import { refreshFolders } from '@joplin/lib/folders-screen-utils';
|
||||
import initializeCommandService from './utils/initializeCommandService';
|
||||
import OcrDriverBase from '@joplin/lib/services/ocr/OcrDriverBase';
|
||||
import PerformanceLogger from '@joplin/lib/PerformanceLogger';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import Resource from '@joplin/lib/models/Resource';
|
||||
|
||||
const perfLogger = PerformanceLogger.create();
|
||||
|
||||
@@ -683,6 +685,11 @@ class Application extends BaseApplication {
|
||||
debug: new DebugService(reg.db()),
|
||||
resourceService: ResourceService.instance(),
|
||||
searchEngine: SearchEngine.instance(),
|
||||
shim,
|
||||
Note,
|
||||
Folder,
|
||||
Resource,
|
||||
Setting,
|
||||
ocrService: () => this.ocrService_,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -225,7 +225,7 @@ const Button = React.forwardRef(({
|
||||
animation={iconAnimation}
|
||||
mr={iconOnly ? '0' : '6px'}
|
||||
color={color}
|
||||
className={iconName}
|
||||
className={`${iconName} icon`}
|
||||
role='img'
|
||||
/>;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { PluginManifest } from '@joplin/lib/services/plugins/utils/types';
|
||||
import bridge from '../../../../services/bridge';
|
||||
import { ItemEvent, PluginItem } from '@joplin/lib/components/shared/config/plugins/types';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
import getPluginHelpUrl from '@joplin/lib/services/plugins/utils/getPluginHelpUrl';
|
||||
|
||||
export enum InstallState {
|
||||
NotInstalled = 1,
|
||||
@@ -150,8 +151,7 @@ export default function(props: Props) {
|
||||
|
||||
const onNameClick = useCallback(() => {
|
||||
const manifest = item.manifest;
|
||||
if (!manifest.homepage_url) return;
|
||||
void bridge().openExternal(manifest.homepage_url);
|
||||
void bridge().openExternal(getPluginHelpUrl(manifest.id));
|
||||
}, [item]);
|
||||
|
||||
const onRecommendedClick = useCallback(() => {
|
||||
|
||||
@@ -15,7 +15,7 @@ import { connect } from 'react-redux';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import { PublicPrivateKeyPair } from '@joplin/lib/services/e2ee/ppk';
|
||||
import { PublicPrivateKeyPair } from '@joplin/lib/services/e2ee/ppk/ppk';
|
||||
import ToggleAdvancedSettingsButton from '../ConfigScreen/controls/ToggleAdvancedSettingsButton';
|
||||
import MacOSMissingPasswordHelpLink from '../ConfigScreen/controls/MissingPasswordHelpLink';
|
||||
|
||||
|
||||
@@ -67,6 +67,11 @@ import 'codemirror/mode/diff/diff';
|
||||
import 'codemirror/mode/erlang/erlang';
|
||||
import 'codemirror/mode/sql/sql';
|
||||
|
||||
interface ExtendedWindow {
|
||||
CodeMirror?: unknown;
|
||||
}
|
||||
declare const window: ExtendedWindow;
|
||||
|
||||
|
||||
export interface EditorProps {
|
||||
value: string;
|
||||
@@ -100,6 +105,14 @@ function Editor(props: EditorProps, ref: any) {
|
||||
const editorParent = useRef(null);
|
||||
const lastEditTime = useRef(NaN);
|
||||
|
||||
useEffect(() => {
|
||||
window.CodeMirror = CodeMirror;
|
||||
|
||||
return () => {
|
||||
window.CodeMirror = undefined;
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Codemirror plugins add new commands to codemirror (or change it's behavior)
|
||||
// This command adds the smartListIndent function which will be bound to tab
|
||||
useListIdent(CodeMirror);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import bridge from '../../../../../../services/bridge';
|
||||
import { contentScriptsToCodeMirrorPlugin } from '@joplin/lib/services/plugins/utils/loadContentScripts';
|
||||
import { extname } from 'path';
|
||||
import shim from '@joplin/lib/shim';
|
||||
@@ -7,6 +8,18 @@ import uuid from '@joplin/lib/uuid';
|
||||
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
|
||||
const addPluginDependency = (path: string) => {
|
||||
const id = `content-script-${encodeURIComponent(path)}`;
|
||||
if (document.getElementById(id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const element = document.createElement('script');
|
||||
element.setAttribute('id', id);
|
||||
element.setAttribute('src', path);
|
||||
document.head.appendChild(element);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
export default function useExternalPlugins(CodeMirror: any, plugins: PluginStates) {
|
||||
const [options, setOptions] = useState({});
|
||||
@@ -23,7 +36,14 @@ export default function useExternalPlugins(CodeMirror: any, plugins: PluginState
|
||||
if (mod.codeMirrorResources) {
|
||||
for (const asset of mod.codeMirrorResources) {
|
||||
try {
|
||||
require(`codemirror/${asset}`);
|
||||
let assetPath = shim.fsDriver().resolveRelativePathWithinDir(`${bridge().vendorDir()}/lib/codemirror/`, asset);
|
||||
|
||||
// Compatibility with old versions of Joplin, where the file extension was automatically added by require().
|
||||
if (extname(assetPath) === '') {
|
||||
assetPath += '.js';
|
||||
}
|
||||
|
||||
addPluginDependency(assetPath);
|
||||
} catch (error) {
|
||||
error.message = `${asset} is not a valid CodeMirror asset, keymap or mode. You can find a list of valid assets here: https://codemirror.net/doc/manual.html#addons`;
|
||||
throw error;
|
||||
|
||||
@@ -30,6 +30,7 @@ import useEditorSearchHandler from '../utils/useEditorSearchHandler';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import useRefocusOnVisiblePaneChange from './utils/useRefocusOnVisiblePaneChange';
|
||||
import { WindowIdContext } from '../../../../NewWindowOrIFrame';
|
||||
import eventManager, { EventName, ResourceChangeEvent } from '@joplin/lib/eventManager';
|
||||
|
||||
const logger = Logger.create('CodeMirror6');
|
||||
const logDebug = (message: string) => logger.debug(message);
|
||||
@@ -272,6 +273,17 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
props.noteId, props.useCustomPdfViewer,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = (event: ResourceChangeEvent) => {
|
||||
editorRef.current?.onResourceChanged(event.id);
|
||||
};
|
||||
|
||||
eventManager.on(EventName.ResourceChange, listener);
|
||||
return () => {
|
||||
eventManager.off(EventName.ResourceChange, listener);
|
||||
};
|
||||
}, [props.resourceInfos]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!webviewReady) return;
|
||||
|
||||
|
||||
@@ -110,12 +110,12 @@ const Editor = (props: Props, ref: ForwardedRef<CodeMirrorControl>) => {
|
||||
|
||||
const editor = createEditor(editorContainerRef.current, {
|
||||
...editorProps,
|
||||
resolveImageSrc: async src => {
|
||||
resolveImageSrc: async (src, reloadCounter) => {
|
||||
const url = parseResourceUrl(src);
|
||||
if (!url.itemId) return null;
|
||||
const item = await Resource.load(url.itemId);
|
||||
if (!item) return null;
|
||||
return `${getResourceBaseUrl()}/${resourceFilename(item)}`;
|
||||
return `${getResourceBaseUrl()}/${resourceFilename(item)}${reloadCounter ? `?r=${reloadCounter}` : ''}`;
|
||||
},
|
||||
});
|
||||
editor.addStyles({
|
||||
|
||||
@@ -1387,16 +1387,18 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
async function onCopy(event: any) {
|
||||
const copiedContent = editor.selection.getContent();
|
||||
if (!copiedContent) return;
|
||||
copyHtmlToClipboard(copiedContent);
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
async function onCut(event: any) {
|
||||
event.preventDefault();
|
||||
const selectedContent = editor.selection.getContent();
|
||||
if (!selectedContent) return;
|
||||
copyHtmlToClipboard(selectedContent);
|
||||
editor.insertContent('');
|
||||
event.preventDefault();
|
||||
onChangeHandler();
|
||||
}
|
||||
|
||||
@@ -1444,7 +1446,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
|
||||
// `compositionend` means that a user has finished entering a Chinese
|
||||
// (or other languages that require IME) character.
|
||||
editor.on(TinyMceEditorEvents.CompositionEnd, onChangeHandler);
|
||||
editor.on(TinyMceEditorEvents.Cut, onCut);
|
||||
editor.on(TinyMceEditorEvents.Cut, onCut, true);
|
||||
editor.on(TinyMceEditorEvents.JoplinChange, onChangeHandler);
|
||||
editor.on(TinyMceEditorEvents.Undo, onChangeHandler);
|
||||
editor.on(TinyMceEditorEvents.Redo, onChangeHandler);
|
||||
|
||||
@@ -15,6 +15,7 @@ const joplinRendererUtils = require('@joplin/renderer').utils;
|
||||
const { clipboard } = require('electron');
|
||||
import * as mimeUtils from '@joplin/lib/mime-utils';
|
||||
import bridge from '../../../services/bridge';
|
||||
import { getCollator, getCollatorLocale } from '@joplin/lib/models/utils/getCollator';
|
||||
const md5 = require('md5');
|
||||
const path = require('path');
|
||||
|
||||
@@ -43,22 +44,30 @@ export async function commandAttachFileToBody(body: string, filePaths: string[]
|
||||
if (!filePaths || !filePaths.length) return null;
|
||||
}
|
||||
|
||||
const collatorLocale = getCollatorLocale();
|
||||
const collator = getCollator(collatorLocale);
|
||||
filePaths = filePaths.sort((a, b) => {
|
||||
return collator.compare(a, b);
|
||||
});
|
||||
|
||||
let pos = options.position ?? 0;
|
||||
|
||||
for (let i = 0; i < filePaths.length; i++) {
|
||||
const filePath = filePaths[i];
|
||||
const beforeLen = body.length;
|
||||
try {
|
||||
logger.info(`Attaching ${filePath}`);
|
||||
const newBody = await shim.attachFileToNoteBody(body, filePath, options.position, {
|
||||
const newBody = await shim.attachFileToNoteBody(body, filePath, pos, {
|
||||
createFileURL: options.createFileURL,
|
||||
resizeLargeImages: Setting.value('imageResizing'),
|
||||
markupLanguage: options.markupLanguage,
|
||||
resourceSuffix: i > 0 ? ' ' : '',
|
||||
resourcePrefix: i > 0 ? ' ' : '',
|
||||
});
|
||||
|
||||
if (!newBody) {
|
||||
logger.info('File attachment was cancelled');
|
||||
return null;
|
||||
}
|
||||
|
||||
pos += newBody.length - beforeLen;
|
||||
body = newBody;
|
||||
logger.info('File was attached.');
|
||||
} catch (error) {
|
||||
@@ -66,7 +75,6 @@ export async function commandAttachFileToBody(body: string, filePaths: string[]
|
||||
bridge().showErrorMessageBox(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import { MarkupToHtmlOptions } from '../../hooks/useMarkupToHtml';
|
||||
import { ScrollbarSize } from '@joplin/lib/models/settings/builtInMetadata';
|
||||
import { RefObject, SetStateAction } from 'react';
|
||||
import * as React from 'react';
|
||||
import { ResourceEntity, ResourceLocalStateEntity } from '@joplin/lib/services/database/types';
|
||||
|
||||
export interface AllAssetsOptions {
|
||||
contentMaxWidthTarget?: string;
|
||||
@@ -214,10 +215,8 @@ export function defaultFormNote(): FormNote {
|
||||
}
|
||||
|
||||
export interface ResourceInfo {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
localState: any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
item: any;
|
||||
localState: ResourceLocalStateEntity;
|
||||
item: ResourceEntity;
|
||||
}
|
||||
|
||||
export interface ResourceInfos {
|
||||
|
||||
@@ -251,8 +251,6 @@ export default class PromptDialog extends React.Component<Props, any> {
|
||||
} else {
|
||||
onClose(true);
|
||||
}
|
||||
} else if (event.key === 'Escape') {
|
||||
onClose(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -309,7 +307,7 @@ export default class PromptDialog extends React.Component<Props, any> {
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog className='prompt-dialog' contentStyle={styles.dialog}>
|
||||
<Dialog className='prompt-dialog' contentStyle={styles.dialog} onCancel={() => onClose(false, 'cancel')}>
|
||||
<label style={styles.label}>{this.props.label ? this.props.label : ''}</label>
|
||||
<div style={{ display: 'inline-block', color: 'black', backgroundColor: theme.backgroundColor }}>
|
||||
{inputComp}
|
||||
|
||||
@@ -25,14 +25,13 @@ interface Props {
|
||||
const SidebarComponent = (props: Props) => {
|
||||
const renderSynchronizeButton = (type: string) => {
|
||||
const label = type === 'sync' ? _('Synchronise') : _('Cancel');
|
||||
const iconAnimation = type !== 'sync' ? 'icon-infinite-rotation 1s linear infinite' : '';
|
||||
|
||||
return (
|
||||
<StyledSynchronizeButton
|
||||
level={ButtonLevel.SidebarSecondary}
|
||||
className={`sidebar-sync-button ${type === 'sync' ? '' : '-syncing'}`}
|
||||
iconName="icon-sync"
|
||||
key="sync_button"
|
||||
iconAnimation={iconAnimation}
|
||||
title={label}
|
||||
onClick={() => {
|
||||
void CommandService.instance().execute('synchronize', type !== 'sync');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { StyledHeader, StyledHeaderIcon, StyledHeaderLabel } from '../styles';
|
||||
import { HeaderId, HeaderListItem } from '../types';
|
||||
import bridge from '../../../services/bridge';
|
||||
@@ -25,6 +25,8 @@ const HeaderItem: React.FC<Props> = props => {
|
||||
const item = props.item;
|
||||
const onItemClick = item.onClick;
|
||||
const itemId = item.id;
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const expanded = item.expanded;
|
||||
|
||||
const onClick: React.MouseEventHandler<HTMLElement> = useCallback(event => {
|
||||
if (onItemClick) {
|
||||
@@ -44,6 +46,14 @@ const HeaderItem: React.FC<Props> = props => {
|
||||
}
|
||||
}, [itemId]);
|
||||
|
||||
const handleMouseEnter = useCallback(() => {
|
||||
setIsHovered(true);
|
||||
}, []);
|
||||
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
setIsHovered(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ListItemWrapper
|
||||
containerRef={props.anchorRef}
|
||||
@@ -58,8 +68,12 @@ const HeaderItem: React.FC<Props> = props => {
|
||||
{...item.extraProps}
|
||||
onDrop={props.onDrop}
|
||||
>
|
||||
<StyledHeader onClick={onClick}>
|
||||
<StyledHeaderIcon aria-hidden='true' role='img' className={item.iconName}/>
|
||||
<StyledHeader
|
||||
onClick={onClick}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<StyledHeaderIcon aria-hidden='true' role='img' className={isHovered ? `fas ${expanded ? 'fa-caret-down' : 'fa-caret-right'}` : item.iconName}/>
|
||||
<StyledHeaderLabel>{item.label}</StyledHeaderLabel>
|
||||
</StyledHeader>
|
||||
</ListItemWrapper>
|
||||
|
||||
@@ -5,4 +5,5 @@
|
||||
@use 'styles/sidebar-expand-link.scss';
|
||||
@use 'styles/sidebar-header-container.scss';
|
||||
@use 'styles/sidebar-spacer-item.scss';
|
||||
@use 'styles/sidebar-header-button.scss';
|
||||
@use 'styles/sidebar-header-button.scss';
|
||||
@use 'styles/sidebar-sync-button.scss';
|
||||
@@ -28,6 +28,11 @@ export const StyledHeader = styled.div`
|
||||
user-select: none;
|
||||
text-transform: uppercase;
|
||||
//cursor: pointer;
|
||||
cursor: default;
|
||||
transition: background 0.2s;
|
||||
&:hover {
|
||||
background: ${(props: StyleProps) => props.theme.backgroundColorHover2};
|
||||
}
|
||||
`;
|
||||
|
||||
export const StyledHeaderIcon = styled.i`
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
|
||||
@keyframes icon-infinite-rotation {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-sync-button {
|
||||
&.-syncing > .icon {
|
||||
animation: icon-infinite-rotation 1s linear infinite;
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -143,7 +143,7 @@ export default class NoteListUtils {
|
||||
|
||||
menu.append(new MenuItem({ type: 'separator' }));
|
||||
|
||||
if ([9, 10].includes(Setting.value('sync.target'))) {
|
||||
if ([9, 10, 11].includes(Setting.value('sync.target'))) {
|
||||
menu.append(
|
||||
new MenuItem(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
|
||||
@@ -2,11 +2,19 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<!--
|
||||
No CPS because we need to allow everything due to some dependencies (eg. depd, which comes from maybe Node or Electron
|
||||
uses 'eval'.
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' 'unsafe-eval'">
|
||||
-->
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="
|
||||
default-src 'self' joplin-content://* ;
|
||||
connect-src 'self' * http://* https://* joplin-content://* blob: ;
|
||||
style-src 'unsafe-inline' 'self' blob: joplin-content://* https://* http://* ;
|
||||
child-src 'self' joplin-content://* ;
|
||||
script-src 'self' 'unsafe-inline' joplin-content://* ;
|
||||
media-src 'self' * blob: data: https://* http://* joplin-content://* ;
|
||||
img-src 'self' blob: data: http://* https://* joplin-content://* ;
|
||||
font-src 'self' http://* https://* blob: data: joplin-content://* ;
|
||||
"
|
||||
/>
|
||||
<title>Joplin</title>
|
||||
<!-- Note: Add new dynamic CSS imports to style.scss to allow them to be included in secondary windows. -->
|
||||
<link rel="stylesheet" href="style.min.css">
|
||||
|
||||
@@ -8,6 +8,7 @@ import getMainWindow from './util/getMainWindow';
|
||||
import setFilePickerResponse from './util/setFilePickerResponse';
|
||||
import setMessageBoxResponse from './util/setMessageBoxResponse';
|
||||
import getImageSourceSize from './util/getImageSourceSize';
|
||||
import setSettingValue from './util/setSettingValue';
|
||||
|
||||
|
||||
test.describe('main', () => {
|
||||
@@ -19,6 +20,13 @@ test.describe('main', () => {
|
||||
await mainPage.waitFor();
|
||||
});
|
||||
|
||||
test('app should support French localization', async ({ mainWindow, electronApp }) => {
|
||||
await setSettingValue(electronApp, mainWindow, 'locale', 'fr_FR');
|
||||
// The "Notebooks" header should be localized
|
||||
const localizedText = mainWindow.getByText('Carnets').first();
|
||||
await expect(localizedText).toBeAttached();
|
||||
});
|
||||
|
||||
test('should be able to create and edit a new note', async ({ mainWindow }) => {
|
||||
const mainScreen = await new MainScreen(mainWindow).setup();
|
||||
const editor = await mainScreen.createNewNote('Test note');
|
||||
|
||||
@@ -72,4 +72,10 @@ export default class MainScreen {
|
||||
await setFilePickerResponse(electronApp, [path]);
|
||||
await activateMainMenuItem(electronApp, 'HTML - HTML document (Directory)', 'Import');
|
||||
}
|
||||
|
||||
public async pluginPanelLocator(pluginId: string) {
|
||||
return this.page.locator(
|
||||
`iframe[id^=${JSON.stringify(`plugin-view-${pluginId}`)}]`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,41 @@ test.describe('pluginApi', () => {
|
||||
}));
|
||||
});
|
||||
|
||||
test('should report the correct visibility state for dialogs', async ({ startAppWithPlugins }) => {
|
||||
const { app, mainWindow } = await startAppWithPlugins(['resources/test-plugins/dialogs.js']);
|
||||
const mainScreen = await new MainScreen(mainWindow).setup();
|
||||
await mainScreen.createNewNote('Dialog test note');
|
||||
|
||||
const editor = mainScreen.noteEditor;
|
||||
const expectVisible = async (visible: boolean) => {
|
||||
// Check UI visibility
|
||||
if (visible) {
|
||||
await expect(mainScreen.dialog).toBeVisible();
|
||||
} else {
|
||||
await expect(mainScreen.dialog).not.toBeVisible();
|
||||
}
|
||||
|
||||
// Check visibility reported through the plugin API
|
||||
await expect.poll(async () => {
|
||||
await mainScreen.goToAnything.runCommand(app, 'getTestDialogVisibility');
|
||||
|
||||
const editorContent = await editor.contentLocator();
|
||||
return editorContent.textContent();
|
||||
}).toBe(JSON.stringify({
|
||||
visible: visible,
|
||||
active: visible,
|
||||
}));
|
||||
};
|
||||
await expectVisible(false);
|
||||
|
||||
await mainScreen.goToAnything.runCommand(app, 'showTestDialog');
|
||||
await expectVisible(true);
|
||||
|
||||
// Submitting the dialog should include form data in the output
|
||||
await mainScreen.dialog.getByRole('button', { name: 'Okay' }).click();
|
||||
await expectVisible(false);
|
||||
});
|
||||
|
||||
test('should be possible to create multiple toasts with the same text from a plugin', async ({ startAppWithPlugins }) => {
|
||||
const { app, mainWindow } = await startAppWithPlugins(['resources/test-plugins/showToast.js']);
|
||||
const mainScreen = await new MainScreen(mainWindow).setup();
|
||||
@@ -122,5 +157,30 @@ test.describe('pluginApi', () => {
|
||||
await msleep(Second);
|
||||
await expect(noteEditor.codeMirrorEditor).toHaveText(expectedUpdatedText);
|
||||
});
|
||||
|
||||
test('should support hiding and showing panels', async ({ startAppWithPlugins }) => {
|
||||
const { mainWindow, app } = await startAppWithPlugins(['resources/test-plugins/panels.js']);
|
||||
const mainScreen = await new MainScreen(mainWindow).setup();
|
||||
await mainScreen.createNewNote('Test note (panels)');
|
||||
|
||||
const panelLocator = await mainScreen.pluginPanelLocator('org.joplinapp.plugins.example.panels');
|
||||
|
||||
const noteEditor = mainScreen.noteEditor;
|
||||
await mainScreen.goToAnything.runCommand(app, 'testShowPanel');
|
||||
await expect(noteEditor.codeMirrorEditor).toHaveText('visible');
|
||||
|
||||
// Panel should be visible
|
||||
await expect(panelLocator).toBeVisible();
|
||||
// The panel should have the expected content
|
||||
const panelContent = panelLocator.contentFrame();
|
||||
await expect(
|
||||
panelContent.getByRole('heading', { name: 'Panel content' }),
|
||||
).toBeAttached();
|
||||
|
||||
await mainScreen.goToAnything.runCommand(app, 'testHidePanel');
|
||||
await expect(noteEditor.codeMirrorEditor).toHaveText('hidden');
|
||||
|
||||
await expect(panelLocator).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -47,5 +47,22 @@ joplin.plugins.register({
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
await joplin.commands.register({
|
||||
name: 'getTestDialogVisibility',
|
||||
label: 'Returns the dialog visibility state',
|
||||
execute: async () => {
|
||||
// panels.visible should also work for dialogs.
|
||||
const visible = await joplin.views.panels.visible(dialogHandle);
|
||||
// For dialogs, isActive should return the visibility.
|
||||
// (Prefer panels.visible for dialogs).
|
||||
const active = await joplin.views.panels.isActive(dialogHandle);
|
||||
|
||||
await joplin.commands.execute('editor.setText', JSON.stringify({
|
||||
visible,
|
||||
active,
|
||||
}));
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// Allows referencing the Joplin global:
|
||||
/* eslint-disable no-undef */
|
||||
|
||||
// Allows the `joplin-manifest` block comment:
|
||||
/* eslint-disable multiline-comment-style */
|
||||
|
||||
/* joplin-manifest:
|
||||
{
|
||||
"id": "org.joplinapp.plugins.example.panels",
|
||||
"manifest_version": 1,
|
||||
"app_min_version": "3.1",
|
||||
"name": "JS Bundle test",
|
||||
"description": "JS Bundle Test plugin",
|
||||
"version": "1.0.0",
|
||||
"author": "",
|
||||
"homepage_url": "https://joplinapp.org"
|
||||
}
|
||||
*/
|
||||
|
||||
const waitFor = async (condition) => {
|
||||
const wait = () => {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => resolve(), 100);
|
||||
});
|
||||
};
|
||||
for (let i = 0; i < 100; i++) {
|
||||
if (await condition()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Pause for a brief delay
|
||||
await wait();
|
||||
}
|
||||
|
||||
throw new Error('Condition was never true');
|
||||
};
|
||||
|
||||
joplin.plugins.register({
|
||||
onStart: async function() {
|
||||
const panels = joplin.views.panels;
|
||||
const view = await panels.create('panelTestView');
|
||||
await panels.setHtml(view, '<h1>Panel content</h1><p>Test</p>');
|
||||
await panels.hide(view);
|
||||
|
||||
|
||||
await joplin.commands.register({
|
||||
name: 'testShowPanel',
|
||||
label: 'Test panel visibility',
|
||||
execute: async () => {
|
||||
await panels.show(view);
|
||||
await waitFor(async () => {
|
||||
return await panels.visible(view);
|
||||
});
|
||||
await joplin.commands.execute('editor.setText', 'visible');
|
||||
},
|
||||
});
|
||||
|
||||
await joplin.commands.register({
|
||||
name: 'testHidePanel',
|
||||
label: 'Test: Hide the panel',
|
||||
execute: async () => {
|
||||
await panels.hide(view);
|
||||
await waitFor(async () => {
|
||||
return !await panels.visible(view);
|
||||
});
|
||||
|
||||
await joplin.commands.execute('editor.setText', 'hidden');
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -97,12 +97,6 @@ a {
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
@keyframes icon-infinite-rotation{
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.rdtPicker {
|
||||
min-width: 250px;
|
||||
width: auto !important;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "3.4.8",
|
||||
"version": "3.5.4",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.bundle.js",
|
||||
"private": true,
|
||||
@@ -46,6 +46,7 @@
|
||||
"asar": true,
|
||||
"asarUnpack": "./node_modules/node-notifier/vendor/**",
|
||||
"win": {
|
||||
"sign": "./sign.js",
|
||||
"rfc3161TimeStampServer": "http://timestamp.digicert.com",
|
||||
"icon": "../../Assets/ImageSources/Joplin.ico",
|
||||
"target": [
|
||||
@@ -131,28 +132,27 @@
|
||||
"homepage": "https://github.com/laurent22/joplin#readme",
|
||||
"devDependencies": {
|
||||
"7zip-bin": "5.2.0",
|
||||
"@axe-core/playwright": "4.10.1",
|
||||
"@axe-core/playwright": "4.10.2",
|
||||
"@electron/notarize": "2.5.0",
|
||||
"@electron/rebuild": "3.7.2",
|
||||
"@fortawesome/fontawesome-free": "5.15.4",
|
||||
"@joeattardi/emoji-button": "4.6.4",
|
||||
"@joplin/default-plugins": "~3.4",
|
||||
"@joplin/editor": "~3.4",
|
||||
"@joplin/lib": "~3.4",
|
||||
"@joplin/renderer": "~3.4",
|
||||
"@joplin/tools": "~3.4",
|
||||
"@joplin/utils": "~3.4",
|
||||
"@playwright/test": "1.52.0",
|
||||
"@joplin/default-plugins": "~3.5",
|
||||
"@joplin/editor": "~3.5",
|
||||
"@joplin/lib": "~3.5",
|
||||
"@joplin/renderer": "~3.5",
|
||||
"@joplin/tools": "~3.5",
|
||||
"@joplin/utils": "~3.5",
|
||||
"@playwright/test": "1.53.2",
|
||||
"@sentry/electron": "4.24.0",
|
||||
"@testing-library/react-hooks": "8.0.1",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/mustache": "4.2.6",
|
||||
"@types/node": "18.19.103",
|
||||
"@types/node": "18.19.119",
|
||||
"@types/react": "18.3.23",
|
||||
"@types/react-dom": "18.3.7",
|
||||
"@types/react-redux": "7.1.33",
|
||||
"@types/styled-components": "5.1.32",
|
||||
"@types/tesseract.js": "2.0.0",
|
||||
"async-mutex": "0.5.0",
|
||||
"axios": "^1.7.7",
|
||||
"codemirror": "5.65.9",
|
||||
@@ -160,13 +160,13 @@
|
||||
"compare-versions": "6.1.1",
|
||||
"countable": "3.0.1",
|
||||
"debounce": "1.2.1",
|
||||
"electron": "35.7.5",
|
||||
"electron": "37.4.0",
|
||||
"electron-builder": "24.13.3",
|
||||
"electron-updater": "6.6.2",
|
||||
"electron-window-state": "5.0.3",
|
||||
"esbuild": "^0.25.3",
|
||||
"formatcoords": "1.1.3",
|
||||
"glob": "11.0.2",
|
||||
"glob": "11.0.3",
|
||||
"gulp": "4.0.2",
|
||||
"highlight.js": "11.11.1",
|
||||
"immer": "9.0.21",
|
||||
@@ -187,7 +187,7 @@
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-redux": "8.1.3",
|
||||
"react-select": "5.10.1",
|
||||
"react-select": "5.10.2",
|
||||
"react-test-renderer": "18.3.1",
|
||||
"react-toggle-button": "2.2.0",
|
||||
"react-tooltip": "4.5.1",
|
||||
@@ -199,15 +199,15 @@
|
||||
"styled-components": "5.3.11",
|
||||
"styled-system": "5.1.5",
|
||||
"taboverride": "4.0.3",
|
||||
"tesseract.js": "5.1.1",
|
||||
"tesseract.js": "6.0.1",
|
||||
"tinymce": "6.8.5",
|
||||
"ts-jest": "29.3.1",
|
||||
"ts-jest": "29.3.4",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.8.2"
|
||||
"typescript": "5.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron/remote": "2.1.2",
|
||||
"@joplin/onenote-converter": "~3.4",
|
||||
"@joplin/onenote-converter": "~3.5",
|
||||
"fs-extra": "11.2.0",
|
||||
"keytar": "7.9.0",
|
||||
"node-fetch": "2.6.7",
|
||||
|
||||
109
packages/app-desktop/sign.js
Normal file
@@ -0,0 +1,109 @@
|
||||
/* eslint-disable no-console */
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
const { chdir, cwd } = require('process');
|
||||
const { mkdirpSync, moveSync, pathExists } = require('fs-extra');
|
||||
const { readdirSync, writeFileSync } = require('fs');
|
||||
const { dirname } = require('path');
|
||||
|
||||
const signToolName = 'CodeSignTool.bat';
|
||||
|
||||
const getTempDir = () => {
|
||||
if (process.env.RUNNER_TEMP) return process.env.RUNNER_TEMP;
|
||||
if (process.env.GITHUB_WORKSPACE) return process.env.GITHUB_WORKSPACE;
|
||||
|
||||
const output = `${dirname(dirname(__dirname))}/temp`;
|
||||
mkdirpSync(output);
|
||||
return output;
|
||||
};
|
||||
|
||||
const tempDir = getTempDir();
|
||||
|
||||
const downloadSignTool = async () => {
|
||||
const signToolUrl = 'https://www.ssl.com/download/codesigntool-for-windows/';
|
||||
const downloadDir = `${tempDir}/signToolDownloadTemp`;
|
||||
const extractDir = `${tempDir}/signToolExtractTemp`;
|
||||
|
||||
if (await pathExists(`${extractDir}/${signToolName}`)) {
|
||||
console.info('sign.js: Sign tool has already been downloaded - skipping');
|
||||
return extractDir;
|
||||
}
|
||||
|
||||
mkdirpSync(downloadDir);
|
||||
mkdirpSync(extractDir);
|
||||
|
||||
const response = await fetch(signToolUrl);
|
||||
if (!response.ok) throw new Error(`sign.js: HTTP error ${response.status}: ${response.statusText}`);
|
||||
|
||||
const zipPath = `${downloadDir}/codeSignTool.zip`;
|
||||
|
||||
const buffer = Buffer.from(await response.arrayBuffer());
|
||||
writeFileSync(zipPath, buffer);
|
||||
|
||||
console.info('sign.js: Downloaded sign tool zip:', readdirSync(downloadDir));
|
||||
|
||||
mkdirpSync(extractDir);
|
||||
|
||||
execSync(
|
||||
`powershell -Command "Expand-Archive -Path '${zipPath}' -DestinationPath '${extractDir}' -Force"`,
|
||||
{ stdio: 'inherit' },
|
||||
);
|
||||
|
||||
console.info('sign.js: Extracted sign tool zip:', readdirSync(extractDir));
|
||||
|
||||
return extractDir;
|
||||
};
|
||||
|
||||
exports.default = async (configuration) => {
|
||||
const inputFilePath = configuration.path;
|
||||
|
||||
const {
|
||||
SSL_ESIGNER_USER_NAME,
|
||||
SSL_ESIGNER_USER_PASSWORD,
|
||||
SSL_ESIGNER_CREDENTIAL_ID,
|
||||
SSL_ESIGNER_USER_TOTP,
|
||||
SIGN_APPLICATION,
|
||||
} = process.env;
|
||||
|
||||
console.info('sign.js: File to sign:', inputFilePath);
|
||||
|
||||
console.info('sign.js: Using temp dir:', tempDir);
|
||||
|
||||
if (SIGN_APPLICATION !== '1') {
|
||||
console.info('sign.js: SIGN_APPLICATION != 1 - not signing application');
|
||||
return;
|
||||
}
|
||||
|
||||
console.info('sign.js: SIGN_APPLICATION = 1 - signing application');
|
||||
|
||||
const signToolDir = await downloadSignTool();
|
||||
const signToolOutDir = `${tempDir}/signedToolOutDir`;
|
||||
mkdirpSync(signToolOutDir);
|
||||
|
||||
const previousDir = cwd();
|
||||
chdir(signToolDir);
|
||||
|
||||
try {
|
||||
const cmd = [
|
||||
`${signToolName} sign`,
|
||||
`-input_file_path="${inputFilePath}"`,
|
||||
`-output_dir_path="${signToolOutDir}"`,
|
||||
`-credential_id="${SSL_ESIGNER_CREDENTIAL_ID}"`,
|
||||
`-username="${SSL_ESIGNER_USER_NAME}"`,
|
||||
`-password="${SSL_ESIGNER_USER_PASSWORD}"`,
|
||||
`-totp_secret="${SSL_ESIGNER_USER_TOTP}"`,
|
||||
];
|
||||
|
||||
execSync(cmd.join(' '));
|
||||
|
||||
const createdFiles = readdirSync(signToolOutDir);
|
||||
console.info('sign.js: Created files:', createdFiles);
|
||||
|
||||
moveSync(`${signToolOutDir}/${createdFiles[0]}`, inputFilePath, { overwrite: true });
|
||||
} catch (error) {
|
||||
console.error('sign.js: Could not sign file:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
chdir(previousDir);
|
||||
}
|
||||
};
|
||||
@@ -82,7 +82,7 @@ async function main() {
|
||||
const files = [
|
||||
'@fortawesome/fontawesome-free/css/all.min.css',
|
||||
'@joeattardi/emoji-button/dist/index.js',
|
||||
'codemirror/addon/dialog/dialog.css',
|
||||
'codemirror/addon/',
|
||||
'codemirror/lib/codemirror.css',
|
||||
'mark.js/dist/mark.min.js',
|
||||
'roboto-fontface/css/roboto/roboto-fontface.css',
|
||||
|
||||
@@ -89,8 +89,8 @@ android {
|
||||
applicationId "net.cozic.joplin"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 2097778
|
||||
versionName "3.4.5"
|
||||
versionCode 2097780
|
||||
versionName "3.5.0"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
@@ -100,6 +100,8 @@ android {
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
cppFlags '-DCMAKE_BUILD_TYPE=Release'
|
||||
// For 16 KB pages. This should be removable after upgrading to NDK r28
|
||||
arguments "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,8 +38,9 @@ add_library(${CMAKE_PROJECT_NAME} SHARED
|
||||
set(WHISPER_LIB_DIR ${CMAKE_SOURCE_DIR}/../../../../vendor/whisper.cpp)
|
||||
|
||||
# Based on the Whisper.cpp Android example:
|
||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O3 ")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3 -fvisibility=hidden -fvisibility-inlines-hidden -ffunction-sections -fdata-sections")
|
||||
set(SHARED_FLAGS "-O3 ")
|
||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${SHARED_FLAGS} ")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${SHARED_FLAGS} -fvisibility=hidden -fvisibility-inlines-hidden -ffunction-sections -fdata-sections")
|
||||
|
||||
# Whisper: See https://stackoverflow.com/a/76290722
|
||||
add_subdirectory(${WHISPER_LIB_DIR} ./whisper)
|
||||
|
||||
@@ -24,29 +24,8 @@ buildscript {
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
|
||||
// Seems to be required for react-native-vosk, otherwise the lib looks for it at "https://maven.aliyun.com/repository/jcenter/com/alphacephei/vosk-android/0.3.46/vosk-android-0.3.46.aar" but it's not there. And we get this error:
|
||||
//
|
||||
// Execution failed for task ':app:checkDebugAarMetadata'.
|
||||
// > Could not resolve all files for configuration ':app:debugRuntimeClasspath'.
|
||||
// > Failed to transform vosk-android-0.3.46.aar (com.alphacephei:vosk-android:0.3.46) to match attributes {artifactType=android-aar-metadata, org.gradle.status=release}.
|
||||
// > Could not find vosk-android-0.3.46.aar (com.alphacephei:vosk-android:0.3.46).
|
||||
// Searched in the following locations:
|
||||
// https://maven.aliyun.com/repository/jcenter/com/alphacephei/vosk-android/0.3.46/vosk-android-0.3.46.aar
|
||||
//
|
||||
// But according to this page, the lib is on the Apache repository:
|
||||
//
|
||||
// https://search.maven.org/artifact/com.alphacephei/vosk-android/0.3.46/aar
|
||||
maven { url "https://maven.apache.org" }
|
||||
|
||||
// Also required for react-native-vosk?
|
||||
maven { url "https://maven.google.com" }
|
||||
|
||||
// Maybe still needed to fetch above package?
|
||||
|
||||
google()
|
||||
maven { url 'https://www.jitpack.io' }
|
||||
mavenCentral()
|
||||
|
||||
maven {
|
||||
// expo-camera bundles a custom com.google.android:cameraview
|
||||
|
||||
@@ -54,6 +54,14 @@ const Camera = (props: Props, ref: ForwardedRef<CameraRef>) => {
|
||||
logger.error(message);
|
||||
}, []);
|
||||
|
||||
const isReadyRef = useRef(false);
|
||||
const onCameraReady = useCallback(() => {
|
||||
if (isReadyRef.current) return; // Already emitted
|
||||
|
||||
isReadyRef.current = true;
|
||||
props.onCameraReady();
|
||||
}, [props.onCameraReady]);
|
||||
|
||||
useAsyncEffect(async (event) => {
|
||||
// iOS issue workaround: Since upgrading to Expo SDK 52, closing and reopening the camera on iOS
|
||||
// never emits onCameraReady. As a workaround, call .resumePreview and wait for it to resolve,
|
||||
@@ -63,16 +71,16 @@ const Camera = (props: Props, ref: ForwardedRef<CameraRef>) => {
|
||||
// Instead, wait for the preview to start using resumePreview:
|
||||
await camera.resumePreview();
|
||||
if (event.cancelled) return;
|
||||
props.onCameraReady();
|
||||
onCameraReady();
|
||||
}
|
||||
}, [camera, props.onCameraReady]);
|
||||
}, [camera, onCameraReady]);
|
||||
|
||||
return hasPermission?.granted ? <CameraView
|
||||
ref={setCamera}
|
||||
style={props.style}
|
||||
facing={props.cameraType === CameraDirection.Front ? 'front' : 'back'}
|
||||
ratio={props.ratio as CameraRatio}
|
||||
onCameraReady={Platform.OS === 'android' ? props.onCameraReady : undefined}
|
||||
onCameraReady={onCameraReady}
|
||||
onMountError={onMountError}
|
||||
animateShutter={false}
|
||||
barcodeScannerSettings={barcodeScannerSettings}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Platform, ScrollView, StyleSheet, View } from 'react-native';
|
||||
import { BarcodeScanner } from './utils/useBarcodeScanner';
|
||||
import { LinkButton, PrimaryButton } from '../buttons';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import DismissibleDialog, { DialogSize } from '../DismissibleDialog';
|
||||
import DismissibleDialog, { DialogVariant } from '../DismissibleDialog';
|
||||
import { Chip, Text } from 'react-native-paper';
|
||||
import { isCallbackUrl, parseCallbackUrl } from '@joplin/lib/callbackUrlUtils';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
@@ -84,7 +84,7 @@ const ScannedBarcodes: React.FC<Props> = props => {
|
||||
visible={dialogVisible}
|
||||
onDismiss={onHideDialog}
|
||||
themeId={props.themeId}
|
||||
size={DialogSize.Small}
|
||||
size={DialogVariant.Small}
|
||||
>
|
||||
<ScrollView>
|
||||
<Text variant='titleMedium' role='heading'>{_('Scanned code')}</Text>
|
||||
|
||||
@@ -66,12 +66,12 @@ describe('ComboBox', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
test('changing the search query should limit which items are visible', () => {
|
||||
test('changing the search query should limit which items are visible and be case insensitive', () => {
|
||||
const testItems = [
|
||||
{ title: 'a' },
|
||||
{ title: 'b' },
|
||||
{ title: 'c' },
|
||||
{ title: 'aa' },
|
||||
{ title: 'Aa' },
|
||||
];
|
||||
const { unmount } = render(
|
||||
<WrappedComboBox items={testItems}/>,
|
||||
@@ -82,7 +82,7 @@ describe('ComboBox', () => {
|
||||
|
||||
const updatedResults = getSearchResults();
|
||||
expect(updatedResults[0]).toHaveTextContent('a');
|
||||
expect(updatedResults[1]).toHaveTextContent('aa');
|
||||
expect(updatedResults[1]).toHaveTextContent('Aa');
|
||||
expect(updatedResults).toHaveLength(2);
|
||||
|
||||
unmount();
|
||||
|
||||
@@ -12,7 +12,7 @@ import focusView from '../utils/focusView';
|
||||
import AsyncActionQueue from '@joplin/lib/AsyncActionQueue';
|
||||
import NestableFlatList, { NestableFlatListControl } from './NestableFlatList';
|
||||
import useKeyboardState from '../utils/hooks/useKeyboardState';
|
||||
const naturalCompare = require('string-natural-compare');
|
||||
import { getCollator, getCollatorLocale } from '@joplin/lib/models/utils/getCollator';
|
||||
|
||||
|
||||
export interface Option {
|
||||
@@ -64,17 +64,20 @@ interface UseSearchResultsOptions {
|
||||
const useSearchResults = ({
|
||||
search, setSearch, options, onAddItem, canAddItem,
|
||||
}: UseSearchResultsOptions) => {
|
||||
const collatorLocale = getCollatorLocale();
|
||||
const results = useMemo(() => {
|
||||
const collator = getCollator(collatorLocale);
|
||||
const lowerSearch = search?.toLowerCase();
|
||||
return options
|
||||
.filter(option => option.title.startsWith(search))
|
||||
.filter(option => option.title.toLowerCase().includes(lowerSearch))
|
||||
.sort((a, b) => {
|
||||
if (a.title === b.title) return 0;
|
||||
// Full matches should go first
|
||||
if (a.title === search) return -1;
|
||||
if (b.title === search) return 1;
|
||||
return naturalCompare(a.title, b.title);
|
||||
if (a.title.toLowerCase() === lowerSearch) return -1;
|
||||
if (b.title.toLowerCase() === lowerSearch) return 1;
|
||||
return collator.compare(a.title, b.title);
|
||||
});
|
||||
}, [search, options]);
|
||||
}, [search, options, collatorLocale]);
|
||||
|
||||
const canAdd = (
|
||||
!!onAddItem
|
||||
@@ -254,6 +257,8 @@ const SearchResult: React.FC<SearchResultProps> = ({
|
||||
<View style={[styles.optionContent, selected && styles.optionContentSelected]}>
|
||||
{icon}
|
||||
<Text
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
style={styles.optionLabel}
|
||||
>{text}</Text>
|
||||
</View>
|
||||
@@ -458,10 +463,11 @@ const useInputEventHandlers = ({
|
||||
} else if (key === 'ArrowUp') {
|
||||
selectedIndexControl.onPreviousResult();
|
||||
event.preventDefault();
|
||||
} else if (key === 'Enter') {
|
||||
} else if (key === 'Enter' && Platform.OS === 'web') {
|
||||
// This case is necessary on web to prevent the
|
||||
// search input from becoming defocused after
|
||||
// pressing "enter".
|
||||
// pressing "enter". Enter key behavior is handled
|
||||
// elsewhere for other platforms.
|
||||
event.preventDefault();
|
||||
onSubmit();
|
||||
setSearch('');
|
||||
@@ -584,6 +590,7 @@ const ComboBox: React.FC<Props> = ({
|
||||
onChangeText={setSearch}
|
||||
onKeyPress={onKeyPress}
|
||||
onSubmitEditing={onSubmit}
|
||||
submitBehavior='submit'
|
||||
placeholder={placeholder}
|
||||
aria-activedescendant={showSearchResults ? activeId : undefined}
|
||||
aria-controls={`menuBox-${baseId}`}
|
||||
|
||||
@@ -6,7 +6,10 @@ import { themeStyle } from './global-style';
|
||||
import Modal from './Modal';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
|
||||
export enum DialogSize {
|
||||
export enum DialogVariant {
|
||||
// Small width, auto-determined height
|
||||
SmallResize = 'small-resize',
|
||||
|
||||
Small = 'small',
|
||||
|
||||
// Ideal for panels and dialogs that should be fullscreen even on large devices
|
||||
@@ -20,34 +23,58 @@ interface Props {
|
||||
containerStyle?: ViewStyle;
|
||||
children: React.ReactNode;
|
||||
heading?: string;
|
||||
scrollOverflow?: boolean;
|
||||
|
||||
size: DialogSize;
|
||||
size: DialogVariant;
|
||||
}
|
||||
|
||||
const useStyles = (themeId: number, containerStyle: ViewStyle, size: DialogSize) => {
|
||||
const useStyles = (themeId: number, containerStyle: ViewStyle, size: DialogVariant) => {
|
||||
const windowSize = useWindowDimensions();
|
||||
|
||||
return useMemo(() => {
|
||||
const theme = themeStyle(themeId);
|
||||
|
||||
const maxWidth = size === DialogSize.Large ? windowSize.width : 500;
|
||||
const maxHeight = size === DialogSize.Large ? windowSize.height : 700;
|
||||
const maxWidth = size === DialogVariant.Large ? windowSize.width : 500;
|
||||
const maxHeight = size === DialogVariant.Large ? windowSize.height : 700;
|
||||
|
||||
const dialogSizing: ViewStyle = {
|
||||
width: '100%',
|
||||
|
||||
...(size !== DialogVariant.SmallResize ? {
|
||||
height: '100%',
|
||||
} : { }),
|
||||
};
|
||||
|
||||
return StyleSheet.create({
|
||||
closeButtonContainer: {
|
||||
closeButtonRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignContent: 'center',
|
||||
marginBottom: 8,
|
||||
},
|
||||
closeButtonRowWithHeading: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
closeButton: {
|
||||
margin: 0,
|
||||
},
|
||||
// Ensure that the close button is aligned with the center of the header:
|
||||
// Make its container smaller and center it.
|
||||
closeButtonWrapper: {
|
||||
height: 18,
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignSelf: 'center',
|
||||
},
|
||||
heading: {
|
||||
alignSelf: 'center',
|
||||
},
|
||||
modalBackground: {
|
||||
justifyContent: 'center',
|
||||
},
|
||||
dialogContainer: {
|
||||
maxHeight,
|
||||
maxWidth,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
flexShrink: 1,
|
||||
...dialogSizing,
|
||||
|
||||
// Center
|
||||
marginLeft: 'auto',
|
||||
@@ -58,11 +85,11 @@ const useStyles = (themeId: number, containerStyle: ViewStyle, size: DialogSize)
|
||||
...containerStyle,
|
||||
},
|
||||
dialogSurface: {
|
||||
borderRadius: 12,
|
||||
borderRadius: 24,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
padding: 10,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 24,
|
||||
...dialogSizing,
|
||||
},
|
||||
});
|
||||
}, [themeId, windowSize.width, windowSize.height, containerStyle, size]);
|
||||
@@ -76,13 +103,16 @@ const DismissibleDialog: React.FC<Props> = props => {
|
||||
<Text variant='headlineSmall' role='heading' style={styles.heading}>{props.heading}</Text>
|
||||
) : null;
|
||||
const closeButtonRow = (
|
||||
<View style={styles.closeButtonContainer}>
|
||||
<View style={[styles.closeButtonRow, !!props.heading && styles.closeButtonRowWithHeading]}>
|
||||
{heading ?? <View/>}
|
||||
<IconButton
|
||||
icon='close'
|
||||
accessibilityLabel={_('Close')}
|
||||
onPress={props.onDismiss}
|
||||
/>
|
||||
<View style={styles.closeButtonWrapper}>
|
||||
<IconButton
|
||||
icon='close'
|
||||
accessibilityLabel={_('Close')}
|
||||
onPress={props.onDismiss}
|
||||
style={styles.closeButton}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -92,9 +122,13 @@ const DismissibleDialog: React.FC<Props> = props => {
|
||||
onDismiss={props.onDismiss}
|
||||
onRequestClose={props.onDismiss}
|
||||
containerStyle={styles.dialogContainer}
|
||||
modalBackgroundStyle={styles.modalBackground}
|
||||
animationType='fade'
|
||||
backgroundColor={theme.backgroundColorTransparent2}
|
||||
transparent={true}
|
||||
scrollOverflow={props.scrollOverflow}
|
||||
// Allows the modal background to extend under the statusbar
|
||||
statusBarTranslucent
|
||||
>
|
||||
<Surface style={styles.dialogSurface} elevation={1}>
|
||||
{closeButtonRow}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import { TouchableOpacity, TouchableWithoutFeedback, Dimensions, Text, Modal, View, LayoutRectangle, ViewStyle, TextStyle, FlatList } from 'react-native';
|
||||
import { TouchableOpacity, TouchableWithoutFeedback, Dimensions, Text, Modal, View, LayoutRectangle, ViewStyle, TextStyle, FlatList, Platform } from 'react-native';
|
||||
import { Component, ReactElement } from 'react';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { EdgeInsets, SafeAreaInsetsContext } from 'react-native-safe-area-context';
|
||||
|
||||
type ValueType = string;
|
||||
export interface DropdownListItem {
|
||||
@@ -56,25 +57,43 @@ class Dropdown extends Component<DropdownProps, DropdownState> {
|
||||
};
|
||||
}
|
||||
|
||||
private updateHeaderCoordinates = () => {
|
||||
private updateHeaderCoordinates = (insets: EdgeInsets) => {
|
||||
if (!this.headerRef) return;
|
||||
|
||||
// https://stackoverflow.com/questions/30096038/react-native-getting-the-position-of-an-element
|
||||
this.headerRef.measure((_fx, _fy, width, height, px, py) => {
|
||||
const lastLayout = this.state.headerSize;
|
||||
let offsetX = 0;
|
||||
let offsetY = 0;
|
||||
|
||||
// The opening position of the dropdown must be offset to cater for insets, on newer versions of Android which use edge to edge by default
|
||||
// If the dropdown fills the full height of the screen, the offset gets ignored and does not cause anything to be truncated
|
||||
if (Platform.OS === 'android' && Platform.Version >= 35) {
|
||||
const windowHeight = Dimensions.get('window').height;
|
||||
const windowWidth = Dimensions.get('window').width;
|
||||
const isLandscape = windowWidth > windowHeight;
|
||||
|
||||
if (isLandscape) {
|
||||
offsetX = insets.left;
|
||||
offsetY = insets.top;
|
||||
} else {
|
||||
offsetY = insets.top;
|
||||
}
|
||||
}
|
||||
|
||||
if (px !== lastLayout.x || py !== lastLayout.y || width !== lastLayout.width || height !== lastLayout.height) {
|
||||
this.setState({
|
||||
headerSize: { x: px, y: py, width: width, height: height },
|
||||
headerSize: { x: px - offsetX, y: py - offsetY, width: width, height: height },
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
private onOpenList = () => {
|
||||
private onOpenList = (insets: EdgeInsets) => {
|
||||
// On iOS, we need to re-measure just before opening the list. Measurements from just after
|
||||
// onLayout can be inaccurate in some cases (in the past, this had caused the menu to be
|
||||
// drawn far offscreen).
|
||||
this.updateHeaderCoordinates();
|
||||
this.updateHeaderCoordinates(insets);
|
||||
this.setState({ listVisible: true });
|
||||
};
|
||||
private onCloseList = () => {
|
||||
@@ -92,10 +111,16 @@ class Dropdown extends Component<DropdownProps, DropdownState> {
|
||||
}
|
||||
};
|
||||
|
||||
public render() {
|
||||
private renderWithInsets(insets: EdgeInsets) {
|
||||
let offsetHeight = 0;
|
||||
|
||||
if (Platform.OS === 'android' && Platform.Version >= 35) {
|
||||
offsetHeight = insets.bottom;
|
||||
}
|
||||
|
||||
const items = this.props.items;
|
||||
const itemHeight = 60;
|
||||
const windowHeight = Dimensions.get('window').height - 50;
|
||||
const windowHeight = Dimensions.get('window').height - 50 - offsetHeight;
|
||||
const windowWidth = Dimensions.get('window').width;
|
||||
|
||||
// Dimensions doesn't return quite the right dimensions so leave an extra gap to make
|
||||
@@ -205,13 +230,13 @@ class Dropdown extends Component<DropdownProps, DropdownState> {
|
||||
<View style={{ flex: 1, flexDirection: 'column' }}>
|
||||
<View
|
||||
style={{ flexDirection: 'row', flex: 1, alignItems: 'center' }}
|
||||
onLayout={this.updateHeaderCoordinates}
|
||||
onLayout={() => this.updateHeaderCoordinates(insets)}
|
||||
ref={ref => { this.headerRef = ref; } }
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={headerWrapperStyle}
|
||||
disabled={this.props.disabled}
|
||||
onPress={this.onOpenList}
|
||||
onPress={() => this.onOpenList(insets)}
|
||||
accessibilityRole='button'
|
||||
accessibilityHint={[this.props.accessibilityHint, _('Opens dropdown')].join(' ')}
|
||||
>
|
||||
@@ -268,6 +293,14 @@ class Dropdown extends Component<DropdownProps, DropdownState> {
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<SafeAreaInsetsContext.Consumer>
|
||||
{(insets) => this.renderWithInsets(insets)}
|
||||
</SafeAreaInsetsContext.Consumer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Dropdown;
|
||||
|
||||
@@ -12,7 +12,7 @@ import { AppState } from '../../utils/types';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import allToolbarCommandNamesFromState from './utils/allToolbarCommandNamesFromState';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import DismissibleDialog, { DialogSize } from '../DismissibleDialog';
|
||||
import DismissibleDialog, { DialogVariant } from '../DismissibleDialog';
|
||||
import selectedCommandNamesFromState from './utils/selectedCommandNamesFromState';
|
||||
import stateToWhenClauseContext from '../../services/commands/stateToWhenClauseContext';
|
||||
import { DeleteButton } from '../buttons';
|
||||
@@ -158,7 +158,7 @@ const ToolbarEditorScreen: React.FC<EditorDialogProps> = props => {
|
||||
|
||||
return (
|
||||
<DismissibleDialog
|
||||
size={DialogSize.Small}
|
||||
size={DialogVariant.Small}
|
||||
themeId={props.themeId}
|
||||
visible={props.visible}
|
||||
onDismiss={props.onDismiss}
|
||||
|
||||
133
packages/app-mobile/components/FeedbackBanner.test.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import * as React from 'react';
|
||||
import { Store } from 'redux';
|
||||
import { AppState } from '../utils/types';
|
||||
import TestProviderStack from './testing/TestProviderStack';
|
||||
import { switchClient, setupDatabase, mockMobilePlatform, mockFetch, waitFor } from '@joplin/lib/testing/test-utils';
|
||||
import createMockReduxStore from '../utils/testing/createMockReduxStore';
|
||||
import setupGlobalStore from '../utils/testing/setupGlobalStore';
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react-native';
|
||||
import FeedbackBanner from './FeedbackBanner';
|
||||
import { MobilePlatform } from '@joplin/lib/shim';
|
||||
|
||||
interface WrapperProps { }
|
||||
|
||||
let store: Store<AppState>;
|
||||
const WrappedFeedbackBanner: React.FC<WrapperProps> = () => {
|
||||
return <TestProviderStack store={store}>
|
||||
<FeedbackBanner/>
|
||||
</TestProviderStack>;
|
||||
};
|
||||
|
||||
const getFeedbackButton = (positive: boolean) => {
|
||||
return screen.getByRole('button', { name: positive ? 'Useful' : 'Not useful' });
|
||||
};
|
||||
|
||||
const getSurveyLink = () => {
|
||||
return screen.getByRole('button', { name: 'Take survey' });
|
||||
};
|
||||
|
||||
const mockFeedbackServer = (surveyName = 'web-app-test') => {
|
||||
let helpfulCount = 0;
|
||||
let unhelpfulCount = 0;
|
||||
|
||||
const { reset } = mockFetch((request) => {
|
||||
const surveyBaseUrls = [
|
||||
'https://objects.joplinusercontent.com/',
|
||||
'http://localhost:3430/',
|
||||
];
|
||||
const isSurveyRequest = surveyBaseUrls.some(url => request.url.startsWith(url));
|
||||
if (!isSurveyRequest) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const url = new URL(request.url);
|
||||
|
||||
if (url.pathname === `/r/survey--${surveyName}--helpful`) {
|
||||
helpfulCount ++;
|
||||
} else if (url.pathname === `/r/survey--${surveyName}--unhelpful`) {
|
||||
unhelpfulCount ++;
|
||||
} else {
|
||||
return new Response('Not found', { status: 404 });
|
||||
}
|
||||
|
||||
// The feedback server always redirects to another URL after a
|
||||
// successful request. Mock this by always redirecting to the
|
||||
// same URL.
|
||||
return new Response('', {
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/302
|
||||
status: 302,
|
||||
statusText: 'Found',
|
||||
headers: [
|
||||
['location', 'https://joplinapp.org'],
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
reset,
|
||||
get helpfulCount() {
|
||||
return helpfulCount;
|
||||
},
|
||||
get unhelpfulCount() {
|
||||
return unhelpfulCount;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
describe('FeedbackBanner', () => {
|
||||
const resetMobilePlatform = ()=>{};
|
||||
|
||||
beforeEach(async () => {
|
||||
await setupDatabase(0);
|
||||
await switchClient(0);
|
||||
|
||||
store = createMockReduxStore();
|
||||
setupGlobalStore(store);
|
||||
|
||||
jest.useFakeTimers({ advanceTimers: true });
|
||||
mockMobilePlatform(MobilePlatform.Web);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
screen.unmount();
|
||||
resetMobilePlatform();
|
||||
});
|
||||
|
||||
test.each([
|
||||
{ platform: MobilePlatform.Android, shouldShow: false },
|
||||
{ platform: MobilePlatform.Web, shouldShow: true },
|
||||
{ platform: MobilePlatform.Ios, shouldShow: false },
|
||||
])('should correctly show/hide the feedback banner on %s', ({ platform, shouldShow }) => {
|
||||
mockMobilePlatform(platform);
|
||||
|
||||
render(<WrappedFeedbackBanner />);
|
||||
|
||||
const header = screen.queryByRole('header', { name: 'Feedback' });
|
||||
if (shouldShow) {
|
||||
expect(header).toBeVisible();
|
||||
} else {
|
||||
expect(header).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
test('clicking the "Useful" button should submit the response and show the "take survey" link', async () => {
|
||||
const feedbackServerMock = mockFeedbackServer();
|
||||
render(<WrappedFeedbackBanner />);
|
||||
|
||||
try {
|
||||
const usefulButton = getFeedbackButton(true);
|
||||
fireEvent.press(usefulButton);
|
||||
|
||||
await act(() => waitFor(async () => {
|
||||
expect(getSurveyLink()).toBeVisible();
|
||||
}));
|
||||
|
||||
expect(feedbackServerMock).toMatchObject({
|
||||
helpfulCount: 1,
|
||||
unhelpfulCount: 0,
|
||||
});
|
||||
} finally {
|
||||
feedbackServerMock.reset();
|
||||
}
|
||||
});
|
||||
});
|
||||
216
packages/app-mobile/components/FeedbackBanner.tsx
Normal file
@@ -0,0 +1,216 @@
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import * as React from 'react';
|
||||
import { View, StyleSheet, useWindowDimensions, TextStyle, Linking } from 'react-native';
|
||||
import { Portal, Text } from 'react-native-paper';
|
||||
import IconButton from './IconButton';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { Dispatch } from 'redux';
|
||||
import { themeStyle } from './global-style';
|
||||
import { AppState } from '../utils/types';
|
||||
import { connect } from 'react-redux';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { LinkButton } from './buttons';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import { SurveyProgress } from '@joplin/lib/models/settings/builtInMetadata';
|
||||
|
||||
const logger = Logger.create('FeedbackBanner');
|
||||
|
||||
interface Props {
|
||||
dispatch: Dispatch;
|
||||
progress: SurveyProgress;
|
||||
surveyKey: string;
|
||||
themeId: number;
|
||||
}
|
||||
|
||||
const useStyles = (themeId: number, sentFeedback: boolean) => {
|
||||
const { width: windowWidth } = useWindowDimensions();
|
||||
return useMemo(() => {
|
||||
const theme = themeStyle(themeId);
|
||||
|
||||
const iconBaseStyle: TextStyle = {
|
||||
fontSize: 24,
|
||||
color: theme.color3,
|
||||
};
|
||||
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
backgroundColor: theme.backgroundColor3,
|
||||
borderTopRightRadius: 16,
|
||||
display: 'flex',
|
||||
flexGrow: 1,
|
||||
flexWrap: 'wrap',
|
||||
flexDirection: 'row',
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
maxWidth: windowWidth - 50,
|
||||
gap: 18,
|
||||
padding: 12,
|
||||
},
|
||||
contentRight: {
|
||||
display: sentFeedback ? 'none' : 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 16,
|
||||
},
|
||||
header: {
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
iconUseful: {
|
||||
...iconBaseStyle,
|
||||
color: theme.colorCorrect,
|
||||
},
|
||||
iconNotUseful: {
|
||||
...iconBaseStyle,
|
||||
color: theme.colorWarn,
|
||||
},
|
||||
dismissButtonIcon: {
|
||||
fontSize: 16,
|
||||
color: theme.color2,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
},
|
||||
dismissButton: {
|
||||
backgroundColor: theme.backgroundColor2,
|
||||
borderColor: theme.backgroundColor,
|
||||
borderWidth: 2,
|
||||
width: 29,
|
||||
height: 29,
|
||||
borderRadius: 14,
|
||||
position: 'absolute',
|
||||
top: -16,
|
||||
right: -16,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
dismissButtonContent: {
|
||||
flexShrink: 1,
|
||||
},
|
||||
});
|
||||
}, [themeId, windowWidth, sentFeedback]);
|
||||
};
|
||||
|
||||
const useSurveyUrl = (surveyKey: string) => {
|
||||
return useMemo(() => {
|
||||
let baseUrl = 'https://objects.joplinusercontent.com/';
|
||||
|
||||
// For testing with a locally-hosted server:
|
||||
const useLocalServer = false;
|
||||
if (Setting.value('env') === 'dev' && useLocalServer) {
|
||||
baseUrl = 'http://localhost:3430/';
|
||||
}
|
||||
|
||||
return `${baseUrl}r/survey--${encodeURIComponent(surveyKey)}`;
|
||||
}, [surveyKey]);
|
||||
};
|
||||
|
||||
const setProgress = (progress: SurveyProgress) => {
|
||||
Setting.setValue('survey.webClientEval2025.progress', progress);
|
||||
};
|
||||
|
||||
const onDismiss = () => {
|
||||
setProgress(SurveyProgress.Dismissed);
|
||||
};
|
||||
|
||||
const FeedbackBanner: React.FC<Props> = props => {
|
||||
const surveyUrl = useSurveyUrl(props.surveyKey);
|
||||
const sentFeedback = props.progress === SurveyProgress.Started;
|
||||
|
||||
const sendSurveyResponse = useCallback(async (surveyResponse: string) => {
|
||||
const fetchUrl = `${surveyUrl}--${encodeURIComponent(surveyResponse)}`;
|
||||
logger.debug('sending response to', fetchUrl);
|
||||
const showError = (message: string) => {
|
||||
logger.error('Error', message);
|
||||
void shim.showErrorDialog(
|
||||
_('An error occurred while sending the response. This can happen if the app is offline or cannot connect to the server.\nError: %s', message),
|
||||
);
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await shim.fetch(fetchUrl);
|
||||
// The server currently redirects (status 302) in response
|
||||
// to many survey-related requests. This may be returned by
|
||||
// the web app service worker as a 200 OK response, however. Support both:
|
||||
if (response.ok || response.status === 302) {
|
||||
setProgress(SurveyProgress.Started);
|
||||
} else {
|
||||
const body = await response.text();
|
||||
showError(`Server error: ${response.status} ${body}`);
|
||||
}
|
||||
} catch (error) {
|
||||
showError(error);
|
||||
}
|
||||
}, [surveyUrl]);
|
||||
|
||||
const onSurveyLinkClick = useCallback(() => {
|
||||
void Linking.openURL(surveyUrl);
|
||||
onDismiss();
|
||||
}, [surveyUrl]);
|
||||
|
||||
const onNotUsefulClick = useCallback(() => {
|
||||
void sendSurveyResponse('unhelpful');
|
||||
}, [sendSurveyResponse]);
|
||||
|
||||
const onUsefulClick = useCallback(() => {
|
||||
void sendSurveyResponse('helpful');
|
||||
}, [sendSurveyResponse]);
|
||||
|
||||
const styles = useStyles(props.themeId, sentFeedback);
|
||||
|
||||
const renderStatusMessage = () => {
|
||||
if (sentFeedback) {
|
||||
return <View>
|
||||
<Text>{_('Thank you for the feedback!\nDo you have time to complete a short survey?')}</Text>
|
||||
<LinkButton onPress={onSurveyLinkClick}>{_('Take survey')}</LinkButton>
|
||||
</View>;
|
||||
} else {
|
||||
return <Text>{_('Do you find the Joplin web app useful?')}</Text>;
|
||||
}
|
||||
};
|
||||
|
||||
if (shim.mobilePlatform() !== 'web' || props.progress === SurveyProgress.Dismissed) return null;
|
||||
|
||||
return <Portal>
|
||||
<View style={styles.container} role='complementary'>
|
||||
<View>
|
||||
<Text
|
||||
accessibilityRole='header'
|
||||
variant='titleMedium'
|
||||
style={styles.header}
|
||||
>{_('Feedback')}</Text>
|
||||
<Text>{renderStatusMessage()}</Text>
|
||||
</View>
|
||||
<View style={styles.contentRight}>
|
||||
<IconButton
|
||||
iconName='fas times'
|
||||
themeId={props.themeId}
|
||||
onPress={onNotUsefulClick}
|
||||
description={_('Not useful')}
|
||||
iconStyle={styles.iconNotUseful}
|
||||
/>
|
||||
<IconButton
|
||||
iconName='fas check'
|
||||
themeId={props.themeId}
|
||||
onPress={onUsefulClick}
|
||||
description={_('Useful')}
|
||||
iconStyle={styles.iconUseful}
|
||||
/>
|
||||
</View>
|
||||
<IconButton
|
||||
iconName='fas times'
|
||||
themeId={props.themeId}
|
||||
onPress={onDismiss}
|
||||
description={_('Dismiss')}
|
||||
iconStyle={styles.dismissButtonIcon}
|
||||
contentWrapperStyle={styles.dismissButtonContent}
|
||||
containerStyle={styles.dismissButton}
|
||||
/>
|
||||
</View>
|
||||
</Portal>;
|
||||
};
|
||||
|
||||
export default connect((state: AppState) => ({
|
||||
themeId: state.settings.theme,
|
||||
surveyKey: 'web-app-test',
|
||||
progress: state.settings['survey.webClientEval2025.progress'],
|
||||
}))(FeedbackBanner);
|
||||
@@ -87,6 +87,14 @@ const IconButton = (props: ButtonProps) => {
|
||||
props.preventKeyboardDismiss, props.onPress, props.disabled,
|
||||
);
|
||||
|
||||
let icon = <Icon
|
||||
name={props.iconName}
|
||||
style={props.iconStyle}
|
||||
accessibilityLabel={null}
|
||||
/>;
|
||||
// Include browser-provided tooltips on web.
|
||||
icon = Platform.OS === 'web' ? <span title={props.description}>{icon}</span> : icon;
|
||||
|
||||
const button = (
|
||||
<Pressable
|
||||
ref={props.pressableRef}
|
||||
@@ -115,11 +123,7 @@ const IconButton = (props: ButtonProps) => {
|
||||
opacity: fadeAnim,
|
||||
...props.contentWrapperStyle,
|
||||
}}>
|
||||
<Icon
|
||||
name={props.iconName}
|
||||
style={props.iconStyle}
|
||||
accessibilityLabel={null}
|
||||
/>
|
||||
{icon}
|
||||
</Animated.View>
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
@@ -27,11 +27,23 @@ export interface ModalElementProps extends ModalProps {
|
||||
const useStyles = (hasScrollView: boolean, backgroundColor: string|undefined) => {
|
||||
const safeAreaPadding = useSafeAreaPadding();
|
||||
return useMemo(() => {
|
||||
// On Android, the top-level container seems to need to be absolutely positioned
|
||||
// to prevent it from being larger than the screen size:
|
||||
const absoluteFill = {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
} satisfies ViewStyle;
|
||||
|
||||
return StyleSheet.create({
|
||||
modalBackground: {
|
||||
...safeAreaPadding,
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
...(hasScrollView ? {
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
} : absoluteFill),
|
||||
|
||||
// When hasScrollView, the modal background is wrapped in a ScrollView. In this case, it's
|
||||
// possible to scroll content outside the background into view. To prevent the edge of the
|
||||
@@ -40,6 +52,7 @@ const useStyles = (hasScrollView: boolean, backgroundColor: string|undefined) =>
|
||||
backgroundColor: hasScrollView ? null : backgroundColor,
|
||||
},
|
||||
keyboardAvoidingView: {
|
||||
...absoluteFill,
|
||||
flex: 1,
|
||||
},
|
||||
modalScrollView: {
|
||||
|
||||
@@ -84,7 +84,7 @@ const NestableFlatList = function<T>({
|
||||
}, []);
|
||||
|
||||
const bufferSize = 10;
|
||||
const visibleStartIndex = Math.floor(scroll / itemHeight);
|
||||
const visibleStartIndex = Math.min(Math.floor(scroll / itemHeight), data.length);
|
||||
const visibleEndIndex = Math.ceil((scroll + listHeight) / itemHeight);
|
||||
const startIndex = Math.max(0, visibleStartIndex - bufferSize);
|
||||
const maximumIndex = data.length - 1;
|
||||
|
||||
@@ -16,6 +16,12 @@ import { ResourceInfo } from './hooks/useRerenderHandler';
|
||||
import getWebViewDomById from '../../utils/testing/getWebViewDomById';
|
||||
import TestProviderStack from '../testing/TestProviderStack';
|
||||
import createMockReduxStore from '../../utils/testing/createMockReduxStore';
|
||||
import Plugin from '@joplin/lib/services/plugins/Plugin';
|
||||
import { Store } from 'redux';
|
||||
import { ContentScriptType } from '@joplin/lib/services/plugins/api/types';
|
||||
import { basename, dirname, join } from 'path';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
import mockPluginServiceSetup from '../../utils/testing/mockPluginServiceSetup';
|
||||
|
||||
interface WrapperProps {
|
||||
noteBody: string;
|
||||
@@ -28,7 +34,7 @@ interface WrapperProps {
|
||||
const emptyObject = {};
|
||||
const emptyArray: string[] = [];
|
||||
const noOpFunction = () => {};
|
||||
const testStore = createMockReduxStore();
|
||||
let testStore: Store;
|
||||
const WrappedNoteViewer: React.FC<WrapperProps> = (
|
||||
{
|
||||
noteBody,
|
||||
@@ -58,10 +64,34 @@ const getNoteViewerDom = async () => {
|
||||
return await getWebViewDomById('NoteBodyViewer');
|
||||
};
|
||||
|
||||
const loadTestContentScript = async (path: string, pluginId: string) => {
|
||||
const plugin = new Plugin(
|
||||
dirname(path),
|
||||
{
|
||||
manifest_version: 1,
|
||||
id: pluginId,
|
||||
name: 'Test plugin',
|
||||
version: '1',
|
||||
app_min_version: '1',
|
||||
},
|
||||
'',
|
||||
testStore.dispatch,
|
||||
'',
|
||||
);
|
||||
await PluginService.instance().runPlugin(plugin);
|
||||
await plugin.registerContentScript(
|
||||
ContentScriptType.MarkdownItPlugin,
|
||||
`${pluginId}-markdown-it`,
|
||||
basename(path),
|
||||
);
|
||||
};
|
||||
|
||||
describe('NoteBodyViewer', () => {
|
||||
beforeEach(async () => {
|
||||
await setupDatabaseAndSynchronizer(0);
|
||||
await switchClient(0);
|
||||
testStore = createMockReduxStore();
|
||||
mockPluginServiceSetup(testStore);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -85,6 +115,17 @@ describe('NoteBodyViewer', () => {
|
||||
await expectHeaderToBe('Test 3');
|
||||
});
|
||||
|
||||
it('should support basic renderer plugins', async () => {
|
||||
await loadTestContentScript(join(supportDir, 'plugins', 'markdownItTestPlugin.js'), 'test-plugin');
|
||||
|
||||
render(<WrappedNoteViewer noteBody={'```justtesting\ntest\n```\n'}/>);
|
||||
|
||||
const noteViewer = await getNoteViewerDom();
|
||||
await waitFor(async () => {
|
||||
expect(noteViewer.querySelector('div.just-testing')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ keywords: ['match'], body: 'A match and another match. Both should be highlighted.', expectedMatchCount: 2 },
|
||||
{ keywords: ['test'], body: 'No match.', expectedMatchCount: 0 },
|
||||
|
||||
@@ -65,7 +65,7 @@ function NoteBodyViewer(props: Props) {
|
||||
onResourceLongPress,
|
||||
});
|
||||
|
||||
const { api: renderer, pageSetup, webViewEventHandlers } = useWebViewSetup({
|
||||
const { api: renderer, pageSetup, webViewEventHandlers, hasPluginScripts } = useWebViewSetup({
|
||||
webviewRef,
|
||||
onBodyScroll: onScroll,
|
||||
onPostMessage,
|
||||
@@ -106,6 +106,7 @@ function NoteBodyViewer(props: Props) {
|
||||
mixedContentMode="always"
|
||||
onLoadEnd={onLoadEnd}
|
||||
onMessage={webViewEventHandlers.onMessage}
|
||||
hasPluginScripts={hasPluginScripts}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -33,6 +33,9 @@ import { toFileExtension } from '@joplin/lib/mime-utils';
|
||||
import { MarkupLanguage } from '@joplin/renderer';
|
||||
import WarningBanner from './WarningBanner';
|
||||
import useIsScreenReaderEnabled from '../../utils/hooks/useIsScreenReaderEnabled';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
|
||||
const logger = Logger.create('NoteEditor');
|
||||
|
||||
type ChangeEventHandler = (event: ChangeEvent)=> void;
|
||||
type UndoRedoDepthChangeHandler = (event: UndoRedoDepthChangeEvent)=> void;
|
||||
@@ -87,6 +90,8 @@ function editorTheme(themeId: number) {
|
||||
};
|
||||
}
|
||||
|
||||
const noteEditorSearchChangeSource = 'joplin.noteEditor.setSearchState';
|
||||
|
||||
type OnSetVisibleCallback = (visible: boolean)=> void;
|
||||
type OnSearchStateChangeCallback = (state: SearchState)=> void;
|
||||
const useEditorControl = (
|
||||
@@ -101,7 +106,7 @@ const useEditorControl = (
|
||||
};
|
||||
|
||||
const setSearchStateCallback = (state: SearchState) => {
|
||||
editorRef.current.setSearchState(state);
|
||||
editorRef.current.setSearchState(state, noteEditorSearchChangeSource);
|
||||
setSearchState(state);
|
||||
};
|
||||
|
||||
@@ -111,6 +116,7 @@ const useEditorControl = (
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
execCommand(command, ...args: any[]) {
|
||||
logger.debug('execCommand', command);
|
||||
return editorRef.current.execCommand(command, ...args);
|
||||
},
|
||||
|
||||
@@ -230,8 +236,8 @@ const useEditorControl = (
|
||||
setSearchState: setSearchStateCallback,
|
||||
},
|
||||
|
||||
onResourceDownloaded: (id: string) => {
|
||||
editorRef.current.onResourceDownloaded(id);
|
||||
onResourceChanged: (id: string) => {
|
||||
editorRef.current.onResourceChanged(id);
|
||||
},
|
||||
|
||||
remove: () => {
|
||||
@@ -306,15 +312,26 @@ function NoteEditor(props: Props) {
|
||||
case EditorEventType.FollowLink:
|
||||
void CommandService.instance().execute('openItem', event.link);
|
||||
break;
|
||||
case EditorEventType.UpdateSearchDialog:
|
||||
setSearchState(event.searchState);
|
||||
case EditorEventType.UpdateSearchDialog: {
|
||||
const hasExternalChange = (
|
||||
event.changeSources.length !== 1
|
||||
|| event.changeSources[0] !== noteEditorSearchChangeSource
|
||||
);
|
||||
|
||||
if (event.searchState.dialogVisible) {
|
||||
editorControl.searchControl.showSearch();
|
||||
} else {
|
||||
editorControl.searchControl.hideSearch();
|
||||
// If the change to the search was done by this editor, it was already applied to the
|
||||
// search state. Skipping the update in this case also helps avoid overwriting the
|
||||
// search state with an older value.
|
||||
if (hasExternalChange) {
|
||||
setSearchState(event.searchState);
|
||||
|
||||
if (event.searchState.dialogVisible) {
|
||||
editorControl.searchControl.showSearch();
|
||||
} else {
|
||||
editorControl.searchControl.hideSearch();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case EditorEventType.Remove:
|
||||
case EditorEventType.Scroll:
|
||||
// Not handled
|
||||
@@ -342,10 +359,18 @@ function NoteEditor(props: Props) {
|
||||
const isDownloaded = (resourceInfos: ResourceInfos, resourceId: string) => {
|
||||
return resourceInfos[resourceId]?.localState?.fetch_status === Resource.FETCH_STATUS_DONE;
|
||||
};
|
||||
const isEncrypted = (resourceInfos: ResourceInfos, resourceId: string) => {
|
||||
return resourceInfos[resourceId]?.item?.encryption_blob_encrypted === 1;
|
||||
};
|
||||
for (const key in props.noteResources) {
|
||||
const wasDownloaded = isDownloaded(lastNoteResources.current, key);
|
||||
if (!wasDownloaded && isDownloaded(props.noteResources, key)) {
|
||||
editorControl.onResourceDownloaded(key);
|
||||
const hasDownloaded = !wasDownloaded && isDownloaded(props.noteResources, key);
|
||||
|
||||
const wasEncrypted = isEncrypted(lastNoteResources.current, key);
|
||||
const hasDecrypted = wasEncrypted && !isEncrypted(props.noteResources, key);
|
||||
|
||||
if (hasDownloaded || hasDecrypted) {
|
||||
editorControl.onResourceChanged(key);
|
||||
}
|
||||
}
|
||||
}, [props.noteResources, editorControl]);
|
||||
|
||||
@@ -23,6 +23,7 @@ import { MarkupLanguage } from '@joplin/renderer';
|
||||
import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||
import { EditorSettings } from './types';
|
||||
import { pregQuote } from '@joplin/lib/string-utils';
|
||||
import { join } from 'path';
|
||||
|
||||
|
||||
interface WrapperProps {
|
||||
@@ -103,8 +104,8 @@ const mockTyping = (window: EditorWindow, text: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const mockSelectionMovement = (window: EditorWindow, position: number) => {
|
||||
getEditorControl(window).select(position, position);
|
||||
const mockSelectionMovement = (window: EditorWindow, from: number, to?: number) => {
|
||||
getEditorControl(window).select(from, to ?? from);
|
||||
};
|
||||
|
||||
const findElement = async function<ElementType extends Element = Element>(selector: string) {
|
||||
@@ -257,7 +258,7 @@ describe('RichTextEditor', () => {
|
||||
ref={editorRef}
|
||||
/>,
|
||||
);
|
||||
editorRef.current.onResourceDownloaded(localResource.id);
|
||||
editorRef.current.onResourceChanged(localResource.id);
|
||||
|
||||
expect(
|
||||
await findElement(`img[data-resource-id=${JSON.stringify(localResource.id)}]`),
|
||||
@@ -333,7 +334,7 @@ describe('RichTextEditor', () => {
|
||||
const editorContent = body.trim();
|
||||
if (markupLanguage === MarkupLanguage.Html) {
|
||||
expect(editorContent).toMatch(
|
||||
new RegExp(`^<p><img src=":/${pregQuote(resource.id)}" alt="${pregQuote(renderedImage.alt)}"[^>]*> test</p>$`),
|
||||
new RegExp(`^<p><img[^>]* src=":/${pregQuote(resource.id)}" alt="${pregQuote(renderedImage.alt)}"[^>]*> test</p>$`),
|
||||
);
|
||||
} else {
|
||||
expect(editorContent).toBe(` test`);
|
||||
@@ -341,6 +342,29 @@ describe('RichTextEditor', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should preserve non-image attachments on edit', async () => {
|
||||
const { note, resource } = await createNoteAndResource({ path: join(supportDir, 'sample.txt') });
|
||||
let body = note.body;
|
||||
|
||||
const resources = await attachedResources(body);
|
||||
render(<WrappedEditor
|
||||
noteBody={note.body}
|
||||
note={note}
|
||||
onBodyChange={newBody => { body = newBody; }}
|
||||
noteResources={resources}
|
||||
/>);
|
||||
|
||||
const window = await getEditorWindow();
|
||||
mockTyping(window, ' test');
|
||||
|
||||
await waitFor(async () => {
|
||||
const editorContent = body.trim();
|
||||
// TODO: At present, the resource title may be included in the final Markdown
|
||||
// (e.g. as [sample.txt](:/id-here "sample.txt")).
|
||||
expect(editorContent).toMatch(new RegExp(`^\\[sample\\.txt\\]\\(:/${pregQuote(resource.id)}.*\\) test$`));
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ useValidSyntax: false },
|
||||
{ useValidSyntax: true },
|
||||
@@ -390,14 +414,18 @@ describe('RichTextEditor', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should be possible show an editor for math blocks', async () => {
|
||||
it('should be possible to show an editor for math blocks', async () => {
|
||||
let body = 'Test:\n\n$$3^2 + 4^2 = 5^2$$';
|
||||
render(<WrappedEditor
|
||||
noteBody={body}
|
||||
onBodyChange={newBody => { body = newBody; }}
|
||||
/>);
|
||||
|
||||
const editButton = await findElement<HTMLButtonElement>('button.edit');
|
||||
const window = await getEditorWindow();
|
||||
// Select the math block to show the "edit" button.
|
||||
mockSelectionMovement(window, '<Test:>'.length, '<Test:>$'.length);
|
||||
|
||||
const editButton = await findElement<HTMLButtonElement>('button.edit-button');
|
||||
editButton.click();
|
||||
|
||||
const editor = await findElement('dialog .cm-editor');
|
||||
@@ -452,6 +480,8 @@ describe('RichTextEditor', () => {
|
||||
'==highlight==ed',
|
||||
'<sup>Super</sup>script',
|
||||
'<sub>Sub</sub>script',
|
||||
'',
|
||||
'<img width="120" src="data:image/svg+xml;utf8,test">',
|
||||
])('should preserve inline markup on edit (case %#)', async (initialBody) => {
|
||||
initialBody += 'test'; // Ensure that typing will add new content outside the formatting
|
||||
let body = initialBody;
|
||||
|
||||
@@ -44,6 +44,13 @@ function useCss(themeId: number, editorCss: string): string {
|
||||
font-size: 13pt;
|
||||
font-family: ${JSON.stringify(theme.fontFamily)}, sans-serif;
|
||||
}
|
||||
|
||||
.RichTextEditor {
|
||||
/* Relatively positioning the editor container causes absolutely-positioned
|
||||
elements to be positioned relative to Rich Text Editor's container,
|
||||
rather than the body. This fixes an alignment issue involving button overlays. */
|
||||
position: relative;
|
||||
}
|
||||
`;
|
||||
}, [themeId, editorCss]);
|
||||
}
|
||||
|
||||
@@ -158,8 +158,7 @@ export const SearchPanel = (props: SearchPanelProps) => {
|
||||
const state = props.searchState;
|
||||
const control = props.searchControl;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const updateSearchState = (changedData: any) => {
|
||||
const updateSearchState = (changedData: Partial<SearchState>) => {
|
||||
const newState = { ...state, ...changedData };
|
||||
control.setSearchState(newState);
|
||||
};
|
||||
|
||||
@@ -37,6 +37,7 @@ const useStyles = (themeId: number) => {
|
||||
|
||||
const listItemPressable: ViewStyle = {
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
alignSelf: 'stretch',
|
||||
};
|
||||
const listItemPressableWithCheckbox: ViewStyle = {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { saveProfileConfig, switchProfile } from '../../services/profiles';
|
||||
import { themeStyle } from '../global-style';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { DialogContext } from '../DialogManager';
|
||||
import { FAB, List, Portal } from 'react-native-paper';
|
||||
import { FAB, List } from 'react-native-paper';
|
||||
import { TextStyle } from 'react-native';
|
||||
import useOnLongPressProps from '../../utils/hooks/useOnLongPressProps';
|
||||
import { Dispatch } from 'redux';
|
||||
@@ -206,19 +206,17 @@ export default (props: Props) => {
|
||||
extraData={extraListItemData}
|
||||
/>
|
||||
</View>
|
||||
<Portal>
|
||||
<FAB
|
||||
icon="plus"
|
||||
accessibilityLabel={_('New profile')}
|
||||
style={style.fab}
|
||||
onPress={() => {
|
||||
props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'ProfileEditor',
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Portal>
|
||||
<FAB
|
||||
icon="plus"
|
||||
accessibilityLabel={_('New profile')}
|
||||
style={style.fab}
|
||||
onPress={() => {
|
||||
props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'ProfileEditor',
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,6 +7,8 @@ import AccessibleView from '../accessibility/AccessibleView';
|
||||
import debounce from '../../utils/debounce';
|
||||
import FocusControl from '../accessibility/FocusControl/FocusControl';
|
||||
import { ModalState } from '../accessibility/FocusControl/types';
|
||||
import useKeyboardState from '../../utils/hooks/useKeyboardState';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
interface MenuOptionDivider {
|
||||
isDivider: true;
|
||||
@@ -29,7 +31,9 @@ interface Props {
|
||||
}
|
||||
|
||||
const useStyles = (themeId: number) => {
|
||||
const { height: windowHeight } = useWindowDimensions();
|
||||
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
|
||||
const safeAreaInsets = useSafeAreaInsets();
|
||||
const { dockedKeyboardHeight: keyboardHeight } = useKeyboardState();
|
||||
|
||||
return useMemo(() => {
|
||||
const theme = themeStyle(themeId);
|
||||
@@ -46,6 +50,20 @@ const useStyles = (themeId: number) => {
|
||||
fontSize: theme.fontSize,
|
||||
};
|
||||
|
||||
const isLandscape = windowWidth > windowHeight;
|
||||
const extraPadding = isLandscape ? 25 : 50;
|
||||
|
||||
// When a docked on-screen keyboard is showing, we want to maximise the height of the menu as much as possible, due to the limited available space.
|
||||
// However, when the on-screen keyboard is hidden or floating in either portrait or landscape orientation, it is less of an issue to have excess in the amount
|
||||
// of padding, to ensure nothing is cut off on all varieties of supported mobile platforms with different input and navigation bar settings. In particular,
|
||||
// on Android it is not possible to distinguish between a floating keyboard and a horizontal input bar which is docked, but the latter requires a larger
|
||||
// reduction in height. For this reason we use a fixed value for insetOrExtraFullscreenPadding when the keyboard height is zero. However, Android versions
|
||||
// earlier than 15 have an IME toolbar in addition to the input toolbar when using an external keyboard, so to cater for this scenario, we can use the fixed
|
||||
// value if the keyboardHeight is <= 25 (as any proper on-screen keyboard would be much taller than this). If the keyboard height is larger than this, we can assume
|
||||
// a docked keyboard is visible, so we only need cater for the insets in addition to the fixed extraPadding required for compatibility across Android versions
|
||||
const insetOrExtraFullscreenPadding = keyboardHeight <= 25 ? 70 : safeAreaInsets.top + safeAreaInsets.bottom;
|
||||
const maxMenuHeight = windowHeight - keyboardHeight - extraPadding - insetOrExtraFullscreenPadding;
|
||||
|
||||
return StyleSheet.create({
|
||||
divider: {
|
||||
borderBottomWidth: 1,
|
||||
@@ -66,13 +84,13 @@ const useStyles = (themeId: number) => {
|
||||
opacity: 0.5,
|
||||
},
|
||||
menuContentScroller: {
|
||||
maxHeight: windowHeight - 50,
|
||||
maxHeight: maxMenuHeight,
|
||||
},
|
||||
contextMenuButton: {
|
||||
padding: 0,
|
||||
},
|
||||
});
|
||||
}, [themeId, windowHeight]);
|
||||
}, [themeId, windowWidth, windowHeight, safeAreaInsets, keyboardHeight]);
|
||||
};
|
||||
|
||||
const MenuComponent: React.FC<Props> = props => {
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import * as React from 'react';
|
||||
import { Linking, TextStyle, View, ViewStyle } from 'react-native';
|
||||
import { Linking, StyleSheet, TextStyle, View, ViewStyle } from 'react-native';
|
||||
import { Text } from 'react-native-paper';
|
||||
import IconButton from '../IconButton';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { useCallback, useState } from 'react';
|
||||
import DismissibleDialog, { DialogSize } from '../DismissibleDialog';
|
||||
import { LinkButton } from '../buttons';
|
||||
import NavService from '@joplin/lib/services/NavService';
|
||||
import DismissibleDialog, { DialogVariant } from '../DismissibleDialog';
|
||||
import { LinkButton, PrimaryButton } from '../buttons';
|
||||
import makeDiscourseDebugUrl from '@joplin/lib/makeDiscourseDebugUrl';
|
||||
import getPackageInfo from '../../utils/getPackageInfo';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
|
||||
interface Props {
|
||||
wrapperStyle: ViewStyle;
|
||||
@@ -15,10 +18,27 @@ interface Props {
|
||||
}
|
||||
|
||||
const onLeaveFeedback = () => {
|
||||
void Linking.openURL('https://discourse.joplinapp.org/t/web-client-running-joplin-mobile-in-a-web-browser-with-react-native-web/38749');
|
||||
void Linking.openURL('https://forms.gle/B5YGDNzsUYBnoPx19');
|
||||
};
|
||||
|
||||
const feedbackContainerStyles: ViewStyle = { flexGrow: 1, justifyContent: 'flex-end' };
|
||||
const onReportBug = () => {
|
||||
void Linking.openURL(
|
||||
makeDiscourseDebugUrl('', '', [], getPackageInfo(), PluginService.instance(), Setting.value('plugins.states')),
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
feedbackContainer: {
|
||||
flexGrow: 1,
|
||||
flexDirection: 'row',
|
||||
gap: 16,
|
||||
justifyContent: 'flex-end',
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
paragraph: {
|
||||
paddingBottom: 7,
|
||||
},
|
||||
});
|
||||
|
||||
const WebBetaButton: React.FC<Props> = props => {
|
||||
const [dialogVisible, setDialogVisible] = useState(false);
|
||||
@@ -31,6 +51,10 @@ const WebBetaButton: React.FC<Props> = props => {
|
||||
setDialogVisible(false);
|
||||
}, []);
|
||||
|
||||
const renderParagraph = (content: string) => {
|
||||
return <Text variant='bodyLarge' style={styles.paragraph}>{content}</Text>;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
@@ -44,15 +68,18 @@ const WebBetaButton: React.FC<Props> = props => {
|
||||
/>
|
||||
<DismissibleDialog
|
||||
heading={_('Beta')}
|
||||
size={DialogSize.Small}
|
||||
size={DialogVariant.SmallResize}
|
||||
themeId={props.themeId}
|
||||
visible={dialogVisible}
|
||||
onDismiss={onHideDialog}
|
||||
>
|
||||
<Text>{'At present, the web client is in beta. In the future, it may change significantly, or be removed.'}</Text>
|
||||
<View style={feedbackContainerStyles}>
|
||||
<LinkButton onPress={onLeaveFeedback}>{'Give feedback'}</LinkButton>
|
||||
<LinkButton onPress={() => NavService.go('DocumentScanner')}>{'Test work-in-progress feature: Document scanner'}</LinkButton>
|
||||
{renderParagraph('Welcome to the beta version of the Joplin Web App!')}
|
||||
{renderParagraph('Thank you for participating in the beta version of the Joplin Web App.')}
|
||||
{renderParagraph('The Joplin Web App is available for a limited time in open beta and may later join the Joplin Cloud plans.')}
|
||||
{renderParagraph('Feel free to use it and let us know if have any questions or notice any issues!')}
|
||||
<View style={styles.feedbackContainer}>
|
||||
<LinkButton onPress={onReportBug}>{'Report bug'}</LinkButton>
|
||||
<PrimaryButton onPress={onLeaveFeedback}>{'Give feedback'}</PrimaryButton>
|
||||
</View>
|
||||
</DismissibleDialog>
|
||||
</>
|
||||
|
||||
@@ -101,12 +101,24 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const styleObject: any = {
|
||||
container: {
|
||||
outerContainer: {
|
||||
flexDirection: 'column',
|
||||
},
|
||||
innerContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.backgroundColor2,
|
||||
shadowColor: '#000000',
|
||||
elevation: 5,
|
||||
},
|
||||
// A small border above the header: Covers the part of the shadow that would otherwise
|
||||
// be shown above the header on Android.
|
||||
aboveHeader: {
|
||||
backgroundColor: theme.backgroundColor2,
|
||||
paddingBottom: 6,
|
||||
marginTop: -6,
|
||||
zIndex: 2,
|
||||
},
|
||||
sideMenuButton: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
@@ -678,8 +690,9 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={this.styles().container}>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
<View style={this.styles().outerContainer}>
|
||||
<View style={this.styles().aboveHeader}/>
|
||||
<View style={this.styles().innerContainer}>
|
||||
{sideMenuComp}
|
||||
{backButtonComp}
|
||||
{renderUndoButton()}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import * as React from 'react';
|
||||
import Svg, { SvgProps, G, Path, Defs, LinearGradient, Stop, ClipPath, Rect } from 'react-native-svg';
|
||||
|
||||
const JoplinCloudIcon: React.FC<SvgProps> = props => {
|
||||
return <Svg
|
||||
viewBox='0 0 84 84'
|
||||
fill='none'
|
||||
{...props}
|
||||
>
|
||||
<G clipPath='url(#a)'>
|
||||
<Path fill='url(#b)' d='M0 0h84v84H0z'/>
|
||||
<Path
|
||||
fill='#fff'
|
||||
d='M73.706 49.825c0 3.732-1.534 7.148-4.007 9.592a13.714 13.714 0 0 1-9.675 3.973h-8.199v-7.065h8.2c1.818 0 3.436-.723 4.635-1.904 1.19-1.188 1.92-2.784 1.92-4.596 0-1.804-.73-3.408-1.92-4.597a6.539 6.539 0 0 0-4.636-1.903h-6.933l.386-3.882c.042-.4.059-.79.059-1.197 0-3.3-1.342-6.251-3.513-8.412a11.964 11.964 0 0 0-8.484-3.483c-3.328 0-6.304 1.33-8.484 3.483-2.18 2.16-3.513 5.112-3.513 8.412 0 .399.017.798.06 1.197l.385 3.882h-6.154c-1.819 0-3.437.723-4.636 1.903-1.19 1.189-1.92 2.785-1.92 4.597 0 1.803.73 3.408 1.92 4.596a6.539 6.539 0 0 0 4.636 1.904h9.935a8.854 8.854 0 0 0 4.82-2.452 8.705 8.705 0 0 0 2.59-6.201v-7.523h-7.217v-2.726c0-2.32 1.903-4.215 4.25-4.215h9.968v14.464c0 4.14-1.685 8.187-4.636 11.105-2.6 2.585-6.078 4.19-9.733 4.53l-.923.083h-9.062c-3.764 0-7.21-1.52-9.674-3.973a13.483 13.483 0 0 1 0-19.185 13.673 13.673 0 0 1 8.35-3.906c.452-4.464 2.48-8.487 5.507-11.488a19.154 19.154 0 0 1 13.523-5.552 19.16 19.16 0 0 1 13.522 5.552 18.842 18.842 0 0 1 5.5 11.446c3.554.141 6.782 1.613 9.129 3.948a13.428 13.428 0 0 1 4.024 9.593z'
|
||||
strokeWidth={1.3}
|
||||
/>
|
||||
</G>
|
||||
<Defs>
|
||||
<LinearGradient
|
||||
id='b'
|
||||
x1={3}
|
||||
x2={78}
|
||||
y1={4}
|
||||
y2={79}
|
||||
gradientUnits='userSpaceOnUse'
|
||||
>
|
||||
<Stop offset={0.14} stopColor='#3873DB'/>
|
||||
<Stop offset={0.974} stopColor='#163467'/>
|
||||
</LinearGradient>
|
||||
<ClipPath id='a'>
|
||||
<Rect width={84} height={84} fill='#fff' rx={20} />
|
||||
</ClipPath>
|
||||
</Defs>
|
||||
</Svg>;
|
||||
};
|
||||
|
||||
export default JoplinCloudIcon;
|
||||
138
packages/app-mobile/components/SyncWizard/SyncWizard.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import * as React from 'react';
|
||||
import DismissibleDialog, { DialogVariant } from '../DismissibleDialog';
|
||||
import { AppState } from '../../utils/types';
|
||||
import { connect } from 'react-redux';
|
||||
import { Dispatch } from 'redux';
|
||||
import { useCallback } from 'react';
|
||||
import { Icon, Text } from 'react-native-paper';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import JoplinCloudIcon from './JoplinCloudIcon';
|
||||
import NavService from '@joplin/lib/services/NavService';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import CardButton from '../buttons/CardButton';
|
||||
|
||||
interface Props {
|
||||
dispatch: Dispatch;
|
||||
visible: boolean;
|
||||
themeId: number;
|
||||
}
|
||||
|
||||
const iconSize = 24;
|
||||
const styles = StyleSheet.create({
|
||||
titleContainer: {
|
||||
flexDirection: 'row',
|
||||
gap: 8,
|
||||
paddingBottom: 6,
|
||||
alignItems: 'center',
|
||||
},
|
||||
subheading: {
|
||||
marginBottom: 24,
|
||||
},
|
||||
cardContent: {
|
||||
padding: 12,
|
||||
borderRadius: 14,
|
||||
},
|
||||
syncProviderList: {
|
||||
gap: 8,
|
||||
},
|
||||
featuresList: {
|
||||
marginTop: 4,
|
||||
},
|
||||
listItem: {
|
||||
flexDirection: 'row',
|
||||
gap: 8,
|
||||
marginVertical: 6,
|
||||
verticalAlign: 'middle',
|
||||
},
|
||||
});
|
||||
|
||||
interface SyncProviderProps {
|
||||
title: string;
|
||||
icon: ()=> React.ReactNode;
|
||||
description: string;
|
||||
onPress: ()=> void;
|
||||
featuresList: string[];
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
const SyncProvider: React.FC<SyncProviderProps> = props => {
|
||||
return <CardButton
|
||||
disabled={props.disabled}
|
||||
onPress={props.onPress}
|
||||
testID='sync-provider-card'
|
||||
>
|
||||
<View style={styles.cardContent}>
|
||||
<View style={styles.titleContainer}>
|
||||
{props.icon()}
|
||||
<Text variant='titleMedium'>{props.title}{props.disabled ? ' (Not supported)' : ''}</Text>
|
||||
</View>
|
||||
{props.description && <Text variant='bodyMedium'>{props.description}</Text>}
|
||||
<View style={styles.featuresList}>
|
||||
{props.featuresList.map((feature, index) => (
|
||||
<View key={`feature-${index}`} style={styles.listItem}>
|
||||
<Icon size={14} source='check'/><Text>{feature}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</CardButton>;
|
||||
};
|
||||
|
||||
const SyncWizard: React.FC<Props> = ({ themeId, visible, dispatch }) => {
|
||||
const onDismiss = useCallback(() => {
|
||||
dispatch({
|
||||
type: 'SYNC_WIZARD_VISIBLE_CHANGE',
|
||||
visible: false,
|
||||
});
|
||||
}, [dispatch]);
|
||||
|
||||
const onSelectJoplinCloud = useCallback(async () => {
|
||||
onDismiss();
|
||||
await NavService.go('JoplinCloudLogin');
|
||||
}, [onDismiss]);
|
||||
|
||||
const onSelectOtherTarget = useCallback(async () => {
|
||||
onDismiss();
|
||||
await NavService.go('Config', { sectionName: 'sync' });
|
||||
}, [onDismiss]);
|
||||
|
||||
return <DismissibleDialog
|
||||
themeId={themeId}
|
||||
visible={visible}
|
||||
onDismiss={onDismiss}
|
||||
size={DialogVariant.SmallResize}
|
||||
scrollOverflow={true}
|
||||
heading={_('Sync')}
|
||||
>
|
||||
<Text variant='bodyLarge' role='heading' style={styles.subheading}>{
|
||||
_('Joplin can synchronise your notes using various providers. Select one from the list below.')
|
||||
}</Text>
|
||||
<View style={styles.syncProviderList}>
|
||||
<SyncProvider
|
||||
title={_('Joplin Cloud')}
|
||||
description={_('Joplin\'s own sync service. Also gives access to Joplin-specific features such as publishing notes or collaborating on notebooks with others.')}
|
||||
featuresList={[
|
||||
_('Sync your notes'),
|
||||
_('Publish notes to the internet'),
|
||||
_('Collaborate on notebooks with others'),
|
||||
]}
|
||||
icon={() => <JoplinCloudIcon width={iconSize} height={iconSize}/>}
|
||||
onPress={onSelectJoplinCloud}
|
||||
disabled={false}
|
||||
/>
|
||||
<SyncProvider
|
||||
title={_('Other')}
|
||||
description={_('Select one of the other supported sync targets.')}
|
||||
icon={() => <Icon size={iconSize} source='dots-horizontal-circle'/>}
|
||||
featuresList={[]}
|
||||
onPress={onSelectOtherTarget}
|
||||
disabled={false}
|
||||
/>
|
||||
</View>
|
||||
</DismissibleDialog>;
|
||||
};
|
||||
|
||||
export default connect((state: AppState) => ({
|
||||
visible: state.syncWizardVisible,
|
||||
themeId: state.settings.theme,
|
||||
}))(SyncWizard);
|
||||
@@ -89,7 +89,7 @@ describe('TagEditor', () => {
|
||||
|
||||
const searchResult = screen.getByRole('button', { name: 'new tag 1' });
|
||||
fireEvent.press(searchResult);
|
||||
expect(currentTags).toEqual(['test', 'new tag 1']);
|
||||
expect(currentTags).toEqual(['new tag 1', 'test']);
|
||||
|
||||
// Manually unmount to prevent warnings
|
||||
unmount();
|
||||
@@ -115,7 +115,7 @@ describe('TagEditor', () => {
|
||||
|
||||
const addNewButton = screen.getByRole('button', { name: 'Add new' });
|
||||
fireEvent.press(addNewButton);
|
||||
expect(currentTags).toEqual(['test', 'create']);
|
||||
expect(currentTags).toEqual(['create', 'test']);
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import { TagEntity } from '@joplin/lib/services/database/types';
|
||||
import { Divider } from 'react-native-paper';
|
||||
import focusView from '../utils/focusView';
|
||||
import { msleep } from '@joplin/utils/time';
|
||||
import { getCollator, getCollatorLocale } from '@joplin/lib/models/utils/getCollator';
|
||||
|
||||
export enum TagEditorMode {
|
||||
Large,
|
||||
@@ -38,11 +39,13 @@ const useStyles = (themeId: number, headerStyle: TextStyle|undefined) => {
|
||||
color: theme.color3,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
maxWidth: '100%',
|
||||
gap: 4,
|
||||
},
|
||||
tagText: {
|
||||
color: theme.color3,
|
||||
fontSize: theme.fontSize,
|
||||
flexShrink: 1,
|
||||
},
|
||||
removeTagButton: {
|
||||
color: theme.color3,
|
||||
@@ -122,7 +125,11 @@ const TagCard: React.FC<TagChipProps> = props => {
|
||||
style={props.styles.tag}
|
||||
role='listitem'
|
||||
>
|
||||
<Text style={props.styles.tagText}>{props.title}</Text>
|
||||
<Text
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
style={props.styles.tagText}
|
||||
>{props.title}</Text>
|
||||
<IconButton
|
||||
pressableRef={removeButtonRef}
|
||||
themeId={props.themeId}
|
||||
@@ -144,23 +151,32 @@ interface TagsBoxProps {
|
||||
}
|
||||
|
||||
const TagsBox: React.FC<TagsBoxProps> = props => {
|
||||
const collatorLocale = getCollatorLocale();
|
||||
const collator = useMemo(() => {
|
||||
return getCollator(collatorLocale);
|
||||
}, [collatorLocale]);
|
||||
|
||||
const onRemoveTag = useCallback((tag: string) => {
|
||||
props.onRemoveTag(tag);
|
||||
}, [props.onRemoveTag]);
|
||||
|
||||
const renderContent = () => {
|
||||
if (props.tags.length) {
|
||||
return props.tags.map(tag => (
|
||||
<TagCard
|
||||
key={`tag-${tag}`}
|
||||
title={tag}
|
||||
styles={props.styles}
|
||||
themeId={props.themeId}
|
||||
onRemove={onRemoveTag}
|
||||
autofocus={props.autofocusTag === tag}
|
||||
onAutoFocusComplete={props.onAutoFocusComplete}
|
||||
/>
|
||||
));
|
||||
return props.tags
|
||||
.sort((a, b) => {
|
||||
return collator.compare(a, b);
|
||||
})
|
||||
.map(tag => (
|
||||
<TagCard
|
||||
key={`tag-${tag}`}
|
||||
title={tag}
|
||||
styles={props.styles}
|
||||
themeId={props.themeId}
|
||||
onRemove={onRemoveTag}
|
||||
autofocus={props.autofocusTag === tag}
|
||||
onAutoFocusComplete={props.onAutoFocusComplete}
|
||||
/>
|
||||
));
|
||||
} else {
|
||||
return <Text
|
||||
style={props.styles.noTagsLabel}
|
||||
@@ -189,15 +205,13 @@ const TagsBox: React.FC<TagsBoxProps> = props => {
|
||||
</View>;
|
||||
};
|
||||
|
||||
const normalizeTag = (tagText: string) => tagText.trim().toLowerCase();
|
||||
|
||||
const TagEditor: React.FC<Props> = props => {
|
||||
const styles = useStyles(props.themeId, props.headerStyle);
|
||||
|
||||
const comboBoxItems = useMemo(() => {
|
||||
return props.allTags
|
||||
// Exclude tags already associated with the note
|
||||
.filter(tag => !props.tags.includes(tag.title))
|
||||
.filter(tag => !props.tags.some(o => o.toLowerCase() === tag.title?.toLowerCase()))
|
||||
.map((tag): Option => {
|
||||
const title = tag.title ?? 'Untitled';
|
||||
return {
|
||||
@@ -217,11 +231,13 @@ const TagEditor: React.FC<Props> = props => {
|
||||
|
||||
const onAddTag = useCallback((title: string) => {
|
||||
AccessibilityInfo.announceForAccessibility(_('Added tag: %s', title));
|
||||
props.onTagsChange([...props.tags, normalizeTag(title)]);
|
||||
props.onTagsChange([...props.tags, title.trim()]);
|
||||
}, [props.tags, props.onTagsChange]);
|
||||
|
||||
const onRemoveTag = useCallback(async (title: string) => {
|
||||
const previousTagIndex = props.tags.indexOf(title);
|
||||
if (!title) return;
|
||||
const lowercaseTitle = title.toLowerCase();
|
||||
const previousTagIndex = props.tags.findIndex(item => item.toLowerCase() === lowercaseTitle);
|
||||
const targetTag = props.tags[previousTagIndex + 1] ?? props.tags[previousTagIndex - 1];
|
||||
setAutofocusTag(targetTag);
|
||||
|
||||
@@ -229,7 +245,7 @@ const TagEditor: React.FC<Props> = props => {
|
||||
// prevent focus from occasionally jumping away from the tag box.
|
||||
await msleep(100);
|
||||
AccessibilityInfo.announceForAccessibility(_('Removed tag: %s', title));
|
||||
props.onTagsChange(props.tags.filter(tag => tag !== title));
|
||||
props.onTagsChange(props.tags.filter(tag => tag.toLowerCase() !== lowercaseTitle));
|
||||
}, [props.tags, props.onTagsChange]);
|
||||
|
||||
const onComboBoxSelect = useCallback((item: { title: string }) => {
|
||||
@@ -237,16 +253,16 @@ const TagEditor: React.FC<Props> = props => {
|
||||
return { willRemove: true };
|
||||
}, [onAddTag]);
|
||||
|
||||
const allTagsSet = useMemo(() => {
|
||||
const allTagsSetNormalized = useMemo(() => {
|
||||
return new Set([
|
||||
...props.allTags.map(tag => tag.title),
|
||||
...props.tags,
|
||||
...props.allTags.map(tag => tag.title?.trim()?.toLowerCase()),
|
||||
...props.tags.map(tag => tag.trim().toLowerCase()),
|
||||
]);
|
||||
}, [props.allTags, props.tags]);
|
||||
|
||||
const onCanAddTag = useCallback((tag: string) => {
|
||||
return !allTagsSet.has(normalizeTag(tag));
|
||||
}, [allTagsSet]);
|
||||
return !allTagsSetNormalized.has(tag.trim().toLowerCase());
|
||||
}, [allTagsSetNormalized]);
|
||||
|
||||
const showAssociatedTags = props.mode === TagEditorMode.Large || props.tags.length > 0;
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { themeStyle } from './global-style';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import useKeyboardState from '../utils/hooks/useKeyboardState';
|
||||
import usePrevious from '@joplin/lib/hooks/usePrevious';
|
||||
import FeedbackBanner from './FeedbackBanner';
|
||||
|
||||
interface Props {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
@@ -67,6 +68,7 @@ const AppNavComponent: React.FC<Props> = (props) => {
|
||||
<NotesScreen visible={notesScreenVisible} />
|
||||
{searchScreenLoaded && <SearchScreen visible={searchScreenVisible} />}
|
||||
{!notesScreenVisible && !searchScreenVisible && <Screen navigation={{ state: route }} themeId={props.themeId} dispatch={props.dispatch} />}
|
||||
{notesScreenVisible ? <FeedbackBanner/> : null}
|
||||
<View style={{ height: autocompletionBarPadding }} />
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
|
||||
72
packages/app-mobile/components/buttons/CardButton.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import * as React from 'react';
|
||||
import { Card, TouchableRipple } from 'react-native-paper';
|
||||
import { useMemo } from 'react';
|
||||
import { StyleSheet, View, ViewStyle } from 'react-native';
|
||||
|
||||
export enum InstallState {
|
||||
NotInstalled,
|
||||
Installing,
|
||||
Installed,
|
||||
}
|
||||
|
||||
interface Props {
|
||||
onPress: ()=> void;
|
||||
disabled: boolean;
|
||||
children: React.ReactNode;
|
||||
style?: ViewStyle;
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
const useStyles = (disabled: boolean) => {
|
||||
return useMemo(() => {
|
||||
// For the TouchableRipple to work on Android, the card needs a transparent background.
|
||||
const baseCard = { backgroundColor: 'transparent' };
|
||||
return StyleSheet.create({
|
||||
cardOuterWrapper: {
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
cardInnerWrapper: {
|
||||
width: '100%',
|
||||
},
|
||||
card: disabled ? {
|
||||
...baseCard,
|
||||
opacity: 0.7,
|
||||
} : baseCard,
|
||||
content: {
|
||||
gap: 5,
|
||||
},
|
||||
});
|
||||
}, [disabled]);
|
||||
};
|
||||
|
||||
|
||||
const CardButton: React.FC<Props> = props => {
|
||||
const containerIsButton = !!props.onPress;
|
||||
const styles = useStyles(props.disabled);
|
||||
|
||||
const CardWrapper = containerIsButton ? TouchableRipple : View;
|
||||
return (
|
||||
<View style={[styles.cardOuterWrapper, props.style]}>
|
||||
<CardWrapper
|
||||
accessibilityRole={containerIsButton ? 'button' : null}
|
||||
accessible={containerIsButton}
|
||||
onPress={props.onPress}
|
||||
disabled={props.disabled}
|
||||
style={styles.cardInnerWrapper}
|
||||
testID={props.testID}
|
||||
>
|
||||
<Card
|
||||
mode='outlined'
|
||||
style={styles.card}
|
||||
>
|
||||
{props.children}
|
||||
</Card>
|
||||
</CardWrapper>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardButton;
|
||||
@@ -6,11 +6,12 @@ import createMockReduxStore from '../../utils/testing/createMockReduxStore';
|
||||
import setupGlobalStore from '../../utils/testing/setupGlobalStore';
|
||||
import PluginRunnerWebView from './PluginRunnerWebView';
|
||||
import TestProviderStack from '../testing/TestProviderStack';
|
||||
import { render, waitFor } from '../../utils/testing/testingLibrary';
|
||||
import { act, render, screen, waitFor } from '../../utils/testing/testingLibrary';
|
||||
import createTestPlugin from '@joplin/lib/testing/plugins/createTestPlugin';
|
||||
import getWebViewDomById from '../../utils/testing/getWebViewDomById';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
|
||||
let store: Store<AppState>;
|
||||
|
||||
@@ -30,6 +31,16 @@ const defaultManifestProperties = {
|
||||
name: 'Some plugin name',
|
||||
};
|
||||
|
||||
type PluginSlice = { manifest: { id: string } };
|
||||
const waitForPluginToLoad = (plugin: PluginSlice) => {
|
||||
return waitFor(async () => {
|
||||
expect(PluginService.instance().pluginById(plugin.manifest.id)).toBeTruthy();
|
||||
});
|
||||
};
|
||||
|
||||
const webViewId = 'joplin__PluginDialogWebView';
|
||||
const getUserWebViewDom = () => getWebViewDomById(webViewId);
|
||||
|
||||
describe('PluginRunnerWebView', () => {
|
||||
beforeEach(async () => {
|
||||
await setupDatabaseAndSynchronizer(0);
|
||||
@@ -56,16 +67,68 @@ describe('PluginRunnerWebView', () => {
|
||||
`,
|
||||
});
|
||||
render(<WrappedPluginRunnerWebView/>);
|
||||
|
||||
// Should load the plugin
|
||||
await waitFor(async () => {
|
||||
expect(PluginService.instance().pluginById(testPlugin.manifest.id)).toBeTruthy();
|
||||
});
|
||||
await waitForPluginToLoad(testPlugin);
|
||||
|
||||
// Should show the dialog
|
||||
await waitFor(async () => {
|
||||
const dom = await getWebViewDomById('joplin__PluginDialogWebView');
|
||||
const dom = await getUserWebViewDom();
|
||||
expect(dom.querySelector('h1').textContent).toBe('Test!');
|
||||
});
|
||||
});
|
||||
|
||||
test('should load a plugin that adds a panel', async () => {
|
||||
const testPlugin = await createTestPlugin({
|
||||
...defaultManifestProperties,
|
||||
id: 'org.joplinapp.panel-test',
|
||||
}, {
|
||||
onStart: `
|
||||
const panels = joplin.views.panels;
|
||||
const handle = await panels.create('test-panel');
|
||||
await panels.setHtml(
|
||||
handle,
|
||||
'<h1>Panel content</h1><p>Test</p>',
|
||||
);
|
||||
|
||||
const commands = joplin.commands;
|
||||
await commands.register({
|
||||
name: 'hideTestPanel',
|
||||
label: 'Hide the test plugin panel',
|
||||
execute: async () => {
|
||||
await panels.hide(handle);
|
||||
},
|
||||
});
|
||||
|
||||
await commands.register({
|
||||
name: 'showTestPanel',
|
||||
execute: async () => {
|
||||
await panels.show(handle);
|
||||
},
|
||||
});
|
||||
`,
|
||||
});
|
||||
render(<WrappedPluginRunnerWebView/>);
|
||||
await waitForPluginToLoad(testPlugin);
|
||||
|
||||
act(() => {
|
||||
store.dispatch({ type: 'SET_PLUGIN_PANELS_DIALOG_VISIBLE', visible: true });
|
||||
});
|
||||
|
||||
const expectPanelVisible = async () => {
|
||||
const dom = await getUserWebViewDom();
|
||||
await waitFor(async () => {
|
||||
expect(dom.querySelector('h1').textContent).toBe('Panel content');
|
||||
});
|
||||
};
|
||||
await expectPanelVisible();
|
||||
|
||||
// Should hide the panel
|
||||
await act(() => CommandService.instance().execute('hideTestPanel'));
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('webViewId')).toBeNull();
|
||||
});
|
||||
|
||||
// Should show the panel again
|
||||
await act(() => CommandService.instance().execute('showTestPanel'));
|
||||
await expectPanelVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ import PluginUserWebView from './PluginUserWebView';
|
||||
import { View, StyleSheet, AccessibilityInfo } from 'react-native';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import DismissibleDialog, { DialogSize } from '../../../components/DismissibleDialog';
|
||||
import DismissibleDialog, { DialogVariant } from '../../../components/DismissibleDialog';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
|
||||
interface Props {
|
||||
@@ -120,7 +120,7 @@ const PluginPanelViewer: React.FC<Props> = props => {
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.webViewContainer}>
|
||||
<View style={styles.webViewContainer} testID='plugin-tab-content'>
|
||||
<PluginUserWebView
|
||||
key={selectedTabId}
|
||||
themeId={props.themeId}
|
||||
@@ -164,7 +164,7 @@ const PluginPanelViewer: React.FC<Props> = props => {
|
||||
<DismissibleDialog
|
||||
themeId={props.themeId}
|
||||
visible={props.visible}
|
||||
size={DialogSize.Large}
|
||||
size={DialogVariant.Large}
|
||||
onDismiss={onClose}
|
||||
>
|
||||
{renderTabContent()}
|
||||
|
||||