Compare commits

..
5 Commits
Author SHA1 Message Date
Laurent Cozic f53bb2d167 Merge branch 'dev' into sharing_bug_2 2025-07-11 17:28:09 +01:00
Laurent Cozic 113b259b81 update 2025-07-05 11:52:27 +01:00
Laurent Cozic c7e31d1ac9 update 2025-07-04 16:37:17 +01:00
Laurent Cozic c51b13ca73 update 2025-07-04 15:37:35 +01:00
Laurent Cozic f5febb18b4 Tools: Setup runForTesting script to also create shares and send to recipient 2025-07-04 15:35:32 +01:00
232 changed files with 30490 additions and 53717 deletions

No files matched your search

+34 -51
View File
@@ -553,8 +553,6 @@ packages/app-desktop/integration-tests/util/setSettingValue.js
packages/app-desktop/integration-tests/util/test.js
packages/app-desktop/integration-tests/util/waitForNextOpenPath.js
packages/app-desktop/integration-tests/wcag.spec.js
packages/app-desktop/main-html.js
packages/app-desktop/main.js
packages/app-desktop/playwright.config.js
packages/app-desktop/plugins/GotoAnything.js
packages/app-desktop/services/autoUpdater/AutoUpdaterService.test.js
@@ -627,21 +625,16 @@ packages/app-mobile/components/CameraView/Camera/index.web.js
packages/app-mobile/components/CameraView/Camera/types.js
packages/app-mobile/components/CameraView/CameraView.test.js
packages/app-mobile/components/CameraView/CameraView.js
packages/app-mobile/components/CameraView/CameraView.web.js
packages/app-mobile/components/CameraView/CameraViewMultiPage.test.js
packages/app-mobile/components/CameraView/CameraViewMultiPage.js
packages/app-mobile/components/CameraView/PhotoPreview.js
packages/app-mobile/components/CameraView/ScannedBarcodes.js
packages/app-mobile/components/CameraView/types.js
packages/app-mobile/components/CameraView/utils/fitRectIntoBounds.js
packages/app-mobile/components/CameraView/utils/testing.js
packages/app-mobile/components/CameraView/utils/useBarcodeScanner.js
packages/app-mobile/components/Checkbox.js
packages/app-mobile/components/ComboBox.test.js
packages/app-mobile/components/ComboBox.js
packages/app-mobile/components/DialogManager/PromptButton.js
packages/app-mobile/components/DialogManager/PromptDialog.js
packages/app-mobile/components/DialogManager/TextInputDialog.js
packages/app-mobile/components/DialogManager/hooks/useDialogControl.js
packages/app-mobile/components/DialogManager/index.js
packages/app-mobile/components/DialogManager/types.js
@@ -668,7 +661,6 @@ packages/app-mobile/components/Icon.js
packages/app-mobile/components/IconButton.js
packages/app-mobile/components/Modal.js
packages/app-mobile/components/ModalDialog.js
packages/app-mobile/components/NestableFlatList.js
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.test.js
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js
packages/app-mobile/components/NoteBodyViewer/bundledJs/Renderer.test.js
@@ -713,19 +705,15 @@ packages/app-mobile/components/NoteList.js
packages/app-mobile/components/ProfileSwitcher/ProfileEditor.js
packages/app-mobile/components/ProfileSwitcher/ProfileSwitcher.js
packages/app-mobile/components/ProfileSwitcher/useProfileConfig.js
packages/app-mobile/components/SafeAreaView.js
packages/app-mobile/components/ScreenHeader/Menu.js
packages/app-mobile/components/ScreenHeader/WarningBanner.test.js
packages/app-mobile/components/ScreenHeader/WarningBanner.js
packages/app-mobile/components/ScreenHeader/WarningBox.js
packages/app-mobile/components/ScreenHeader/WebBetaButton.js
packages/app-mobile/components/ScreenHeader/index.js
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/TagEditor.test.js
packages/app-mobile/components/TagEditor.js
packages/app-mobile/components/TextInput.js
packages/app-mobile/components/accessibility/AccessibleView.test.js
packages/app-mobile/components/accessibility/AccessibleView.js
@@ -821,8 +809,6 @@ packages/app-mobile/components/screens/ConfigScreen/plugins/utils/usePluginItem.
packages/app-mobile/components/screens/ConfigScreen/plugins/utils/useRepoApi.js
packages/app-mobile/components/screens/ConfigScreen/plugins/utils/useUpdateState.js
packages/app-mobile/components/screens/ConfigScreen/types.js
packages/app-mobile/components/screens/DocumentScanner/DocumentScanner.js
packages/app-mobile/components/screens/DocumentScanner/NotePreview.js
packages/app-mobile/components/screens/JoplinCloudLoginScreen.js
packages/app-mobile/components/screens/LogScreen.js
packages/app-mobile/components/screens/Note/Note.test.js
@@ -892,7 +878,6 @@ packages/app-mobile/utils/ShareUtils.js
packages/app-mobile/utils/TlsUtils.js
packages/app-mobile/utils/appDefaultState.js
packages/app-mobile/utils/autodetectTheme.js
packages/app-mobile/utils/buildStartupTasks.js
packages/app-mobile/utils/checkPermissions.js
packages/app-mobile/utils/createRootStyle.js
packages/app-mobile/utils/database-driver-react-native.js
@@ -966,46 +951,47 @@ packages/editor/CodeMirror/editorCommands/duplicateLine.js
packages/editor/CodeMirror/editorCommands/editorCommands.js
packages/editor/CodeMirror/editorCommands/insertLineAfter.test.js
packages/editor/CodeMirror/editorCommands/insertLineAfter.js
packages/editor/CodeMirror/editorCommands/insertNewlineContinueMarkup.test.js
packages/editor/CodeMirror/editorCommands/insertNewlineContinueMarkup.js
packages/editor/CodeMirror/editorCommands/jumpToHash.test.js
packages/editor/CodeMirror/editorCommands/jumpToHash.js
packages/editor/CodeMirror/editorCommands/markdownCommands.bulletedVsChecklist.test.js
packages/editor/CodeMirror/editorCommands/markdownCommands.test.js
packages/editor/CodeMirror/editorCommands/markdownCommands.toggleList.test.js
packages/editor/CodeMirror/editorCommands/markdownCommands.js
packages/editor/CodeMirror/editorCommands/sortSelectedLines.test.js
packages/editor/CodeMirror/editorCommands/sortSelectedLines.js
packages/editor/CodeMirror/editorCommands/supportsCommand.js
packages/editor/CodeMirror/extensions/biDirectionalTextExtension.js
packages/editor/CodeMirror/extensions/keyUpHandlerExtension.js
packages/editor/CodeMirror/extensions/markdownDecorationExtension.test.js
packages/editor/CodeMirror/extensions/markdownDecorationExtension.js
packages/editor/CodeMirror/extensions/markdownHighlightExtension.test.js
packages/editor/CodeMirror/extensions/markdownHighlightExtension.js
packages/editor/CodeMirror/extensions/markdownMathExtension.test.js
packages/editor/CodeMirror/extensions/markdownMathExtension.js
packages/editor/CodeMirror/extensions/overwriteModeExtension.test.js
packages/editor/CodeMirror/extensions/overwriteModeExtension.js
packages/editor/CodeMirror/extensions/searchExtension.js
packages/editor/CodeMirror/extensions/selectedNoteIdExtension.js
packages/editor/CodeMirror/getScrollFraction.js
packages/editor/CodeMirror/markdown/MarkdownHighlightExtension.test.js
packages/editor/CodeMirror/markdown/MarkdownHighlightExtension.js
packages/editor/CodeMirror/markdown/MarkdownMathExtension.test.js
packages/editor/CodeMirror/markdown/MarkdownMathExtension.js
packages/editor/CodeMirror/markdown/codeBlockLanguages/allLanguages.js
packages/editor/CodeMirror/markdown/codeBlockLanguages/defaultLanguage.js
packages/editor/CodeMirror/markdown/codeBlockLanguages/lookUpLanguage.js
packages/editor/CodeMirror/markdown/computeSelectionFormatting.test.js
packages/editor/CodeMirror/markdown/computeSelectionFormatting.js
packages/editor/CodeMirror/markdown/decoratorExtension.test.js
packages/editor/CodeMirror/markdown/decoratorExtension.js
packages/editor/CodeMirror/markdown/insertNewlineContinueMarkup.test.js
packages/editor/CodeMirror/markdown/insertNewlineContinueMarkup.js
packages/editor/CodeMirror/markdown/markdownCommands.bulletedVsChecklist.test.js
packages/editor/CodeMirror/markdown/markdownCommands.test.js
packages/editor/CodeMirror/markdown/markdownCommands.toggleList.test.js
packages/editor/CodeMirror/markdown/markdownCommands.js
packages/editor/CodeMirror/markdown/utils/renumberSelectedLists.test.js
packages/editor/CodeMirror/markdown/utils/renumberSelectedLists.js
packages/editor/CodeMirror/markdown/utils/stripBlockquote.js
packages/editor/CodeMirror/pluginApi/PluginLoader.js
packages/editor/CodeMirror/pluginApi/codeMirrorRequire.js
packages/editor/CodeMirror/pluginApi/customEditorCompletion.test.js
packages/editor/CodeMirror/pluginApi/customEditorCompletion.js
packages/editor/CodeMirror/testing/createEditorControl.js
packages/editor/CodeMirror/testing/createEditorSettings.js
packages/editor/CodeMirror/testing/createTestEditor.js
packages/editor/CodeMirror/testing/findNodesWithName.js
packages/editor/CodeMirror/testing/forceFullParse.js
packages/editor/CodeMirror/testing/loadLanguages.js
packages/editor/CodeMirror/testing/pressReleaseKey.js
packages/editor/CodeMirror/testing/typeText.js
packages/editor/CodeMirror/testUtil/createEditorControl.js
packages/editor/CodeMirror/testUtil/createEditorSettings.js
packages/editor/CodeMirror/testUtil/createTestEditor.js
packages/editor/CodeMirror/testUtil/findNodesWithName.js
packages/editor/CodeMirror/testUtil/forceFullParse.js
packages/editor/CodeMirror/testUtil/loadLanguages.js
packages/editor/CodeMirror/testUtil/pressReleaseKey.js
packages/editor/CodeMirror/testUtil/typeText.js
packages/editor/CodeMirror/theme.js
packages/editor/CodeMirror/utils/biDirectionalTextExtension.js
packages/editor/CodeMirror/utils/formatting/RegionSpec.js
packages/editor/CodeMirror/utils/formatting/computeSelectionFormatting.test.js
packages/editor/CodeMirror/utils/formatting/computeSelectionFormatting.js
packages/editor/CodeMirror/utils/formatting/findInlineMatch.test.js
packages/editor/CodeMirror/utils/formatting/findInlineMatch.js
packages/editor/CodeMirror/utils/formatting/isIndentationEquivalent.js
@@ -1024,12 +1010,11 @@ packages/editor/CodeMirror/utils/handleLinkEditRequests.js
packages/editor/CodeMirror/utils/handlePasteEvent.js
packages/editor/CodeMirror/utils/isCursorAtBeginning.js
packages/editor/CodeMirror/utils/isInSyntaxNode.js
packages/editor/CodeMirror/utils/markdown/codeBlockLanguages/allLanguages.js
packages/editor/CodeMirror/utils/markdown/codeBlockLanguages/defaultLanguage.js
packages/editor/CodeMirror/utils/markdown/codeBlockLanguages/lookUpLanguage.js
packages/editor/CodeMirror/utils/markdown/renumberSelectedLists.test.js
packages/editor/CodeMirror/utils/markdown/renumberSelectedLists.js
packages/editor/CodeMirror/utils/markdown/stripBlockquote.js
packages/editor/CodeMirror/utils/keyUpHandlerExtension.js
packages/editor/CodeMirror/utils/overwriteModeExtension.test.js
packages/editor/CodeMirror/utils/overwriteModeExtension.js
packages/editor/CodeMirror/utils/searchExtension.js
packages/editor/CodeMirror/utils/selectedNoteIdExtension.js
packages/editor/CodeMirror/utils/setupVim.js
packages/editor/SelectionFormatting.js
packages/editor/events.js
@@ -1078,8 +1063,6 @@ packages/lib/JoplinDatabase.js
packages/lib/JoplinError.js
packages/lib/JoplinServerApi.js
packages/lib/ObjectUtils.js
packages/lib/PerformanceLogger.test.js
packages/lib/PerformanceLogger.js
packages/lib/PoorManIntervals.js
packages/lib/RotatingLogs.test.js
packages/lib/RotatingLogs.js
+34 -51
View File
@@ -528,8 +528,6 @@ packages/app-desktop/integration-tests/util/setSettingValue.js
packages/app-desktop/integration-tests/util/test.js
packages/app-desktop/integration-tests/util/waitForNextOpenPath.js
packages/app-desktop/integration-tests/wcag.spec.js
packages/app-desktop/main-html.js
packages/app-desktop/main.js
packages/app-desktop/playwright.config.js
packages/app-desktop/plugins/GotoAnything.js
packages/app-desktop/services/autoUpdater/AutoUpdaterService.test.js
@@ -602,21 +600,16 @@ packages/app-mobile/components/CameraView/Camera/index.web.js
packages/app-mobile/components/CameraView/Camera/types.js
packages/app-mobile/components/CameraView/CameraView.test.js
packages/app-mobile/components/CameraView/CameraView.js
packages/app-mobile/components/CameraView/CameraView.web.js
packages/app-mobile/components/CameraView/CameraViewMultiPage.test.js
packages/app-mobile/components/CameraView/CameraViewMultiPage.js
packages/app-mobile/components/CameraView/PhotoPreview.js
packages/app-mobile/components/CameraView/ScannedBarcodes.js
packages/app-mobile/components/CameraView/types.js
packages/app-mobile/components/CameraView/utils/fitRectIntoBounds.js
packages/app-mobile/components/CameraView/utils/testing.js
packages/app-mobile/components/CameraView/utils/useBarcodeScanner.js
packages/app-mobile/components/Checkbox.js
packages/app-mobile/components/ComboBox.test.js
packages/app-mobile/components/ComboBox.js
packages/app-mobile/components/DialogManager/PromptButton.js
packages/app-mobile/components/DialogManager/PromptDialog.js
packages/app-mobile/components/DialogManager/TextInputDialog.js
packages/app-mobile/components/DialogManager/hooks/useDialogControl.js
packages/app-mobile/components/DialogManager/index.js
packages/app-mobile/components/DialogManager/types.js
@@ -643,7 +636,6 @@ packages/app-mobile/components/Icon.js
packages/app-mobile/components/IconButton.js
packages/app-mobile/components/Modal.js
packages/app-mobile/components/ModalDialog.js
packages/app-mobile/components/NestableFlatList.js
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.test.js
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js
packages/app-mobile/components/NoteBodyViewer/bundledJs/Renderer.test.js
@@ -688,19 +680,15 @@ packages/app-mobile/components/NoteList.js
packages/app-mobile/components/ProfileSwitcher/ProfileEditor.js
packages/app-mobile/components/ProfileSwitcher/ProfileSwitcher.js
packages/app-mobile/components/ProfileSwitcher/useProfileConfig.js
packages/app-mobile/components/SafeAreaView.js
packages/app-mobile/components/ScreenHeader/Menu.js
packages/app-mobile/components/ScreenHeader/WarningBanner.test.js
packages/app-mobile/components/ScreenHeader/WarningBanner.js
packages/app-mobile/components/ScreenHeader/WarningBox.js
packages/app-mobile/components/ScreenHeader/WebBetaButton.js
packages/app-mobile/components/ScreenHeader/index.js
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/TagEditor.test.js
packages/app-mobile/components/TagEditor.js
packages/app-mobile/components/TextInput.js
packages/app-mobile/components/accessibility/AccessibleView.test.js
packages/app-mobile/components/accessibility/AccessibleView.js
@@ -796,8 +784,6 @@ packages/app-mobile/components/screens/ConfigScreen/plugins/utils/usePluginItem.
packages/app-mobile/components/screens/ConfigScreen/plugins/utils/useRepoApi.js
packages/app-mobile/components/screens/ConfigScreen/plugins/utils/useUpdateState.js
packages/app-mobile/components/screens/ConfigScreen/types.js
packages/app-mobile/components/screens/DocumentScanner/DocumentScanner.js
packages/app-mobile/components/screens/DocumentScanner/NotePreview.js
packages/app-mobile/components/screens/JoplinCloudLoginScreen.js
packages/app-mobile/components/screens/LogScreen.js
packages/app-mobile/components/screens/Note/Note.test.js
@@ -867,7 +853,6 @@ packages/app-mobile/utils/ShareUtils.js
packages/app-mobile/utils/TlsUtils.js
packages/app-mobile/utils/appDefaultState.js
packages/app-mobile/utils/autodetectTheme.js
packages/app-mobile/utils/buildStartupTasks.js
packages/app-mobile/utils/checkPermissions.js
packages/app-mobile/utils/createRootStyle.js
packages/app-mobile/utils/database-driver-react-native.js
@@ -941,46 +926,47 @@ packages/editor/CodeMirror/editorCommands/duplicateLine.js
packages/editor/CodeMirror/editorCommands/editorCommands.js
packages/editor/CodeMirror/editorCommands/insertLineAfter.test.js
packages/editor/CodeMirror/editorCommands/insertLineAfter.js
packages/editor/CodeMirror/editorCommands/insertNewlineContinueMarkup.test.js
packages/editor/CodeMirror/editorCommands/insertNewlineContinueMarkup.js
packages/editor/CodeMirror/editorCommands/jumpToHash.test.js
packages/editor/CodeMirror/editorCommands/jumpToHash.js
packages/editor/CodeMirror/editorCommands/markdownCommands.bulletedVsChecklist.test.js
packages/editor/CodeMirror/editorCommands/markdownCommands.test.js
packages/editor/CodeMirror/editorCommands/markdownCommands.toggleList.test.js
packages/editor/CodeMirror/editorCommands/markdownCommands.js
packages/editor/CodeMirror/editorCommands/sortSelectedLines.test.js
packages/editor/CodeMirror/editorCommands/sortSelectedLines.js
packages/editor/CodeMirror/editorCommands/supportsCommand.js
packages/editor/CodeMirror/extensions/biDirectionalTextExtension.js
packages/editor/CodeMirror/extensions/keyUpHandlerExtension.js
packages/editor/CodeMirror/extensions/markdownDecorationExtension.test.js
packages/editor/CodeMirror/extensions/markdownDecorationExtension.js
packages/editor/CodeMirror/extensions/markdownHighlightExtension.test.js
packages/editor/CodeMirror/extensions/markdownHighlightExtension.js
packages/editor/CodeMirror/extensions/markdownMathExtension.test.js
packages/editor/CodeMirror/extensions/markdownMathExtension.js
packages/editor/CodeMirror/extensions/overwriteModeExtension.test.js
packages/editor/CodeMirror/extensions/overwriteModeExtension.js
packages/editor/CodeMirror/extensions/searchExtension.js
packages/editor/CodeMirror/extensions/selectedNoteIdExtension.js
packages/editor/CodeMirror/getScrollFraction.js
packages/editor/CodeMirror/markdown/MarkdownHighlightExtension.test.js
packages/editor/CodeMirror/markdown/MarkdownHighlightExtension.js
packages/editor/CodeMirror/markdown/MarkdownMathExtension.test.js
packages/editor/CodeMirror/markdown/MarkdownMathExtension.js
packages/editor/CodeMirror/markdown/codeBlockLanguages/allLanguages.js
packages/editor/CodeMirror/markdown/codeBlockLanguages/defaultLanguage.js
packages/editor/CodeMirror/markdown/codeBlockLanguages/lookUpLanguage.js
packages/editor/CodeMirror/markdown/computeSelectionFormatting.test.js
packages/editor/CodeMirror/markdown/computeSelectionFormatting.js
packages/editor/CodeMirror/markdown/decoratorExtension.test.js
packages/editor/CodeMirror/markdown/decoratorExtension.js
packages/editor/CodeMirror/markdown/insertNewlineContinueMarkup.test.js
packages/editor/CodeMirror/markdown/insertNewlineContinueMarkup.js
packages/editor/CodeMirror/markdown/markdownCommands.bulletedVsChecklist.test.js
packages/editor/CodeMirror/markdown/markdownCommands.test.js
packages/editor/CodeMirror/markdown/markdownCommands.toggleList.test.js
packages/editor/CodeMirror/markdown/markdownCommands.js
packages/editor/CodeMirror/markdown/utils/renumberSelectedLists.test.js
packages/editor/CodeMirror/markdown/utils/renumberSelectedLists.js
packages/editor/CodeMirror/markdown/utils/stripBlockquote.js
packages/editor/CodeMirror/pluginApi/PluginLoader.js
packages/editor/CodeMirror/pluginApi/codeMirrorRequire.js
packages/editor/CodeMirror/pluginApi/customEditorCompletion.test.js
packages/editor/CodeMirror/pluginApi/customEditorCompletion.js
packages/editor/CodeMirror/testing/createEditorControl.js
packages/editor/CodeMirror/testing/createEditorSettings.js
packages/editor/CodeMirror/testing/createTestEditor.js
packages/editor/CodeMirror/testing/findNodesWithName.js
packages/editor/CodeMirror/testing/forceFullParse.js
packages/editor/CodeMirror/testing/loadLanguages.js
packages/editor/CodeMirror/testing/pressReleaseKey.js
packages/editor/CodeMirror/testing/typeText.js
packages/editor/CodeMirror/testUtil/createEditorControl.js
packages/editor/CodeMirror/testUtil/createEditorSettings.js
packages/editor/CodeMirror/testUtil/createTestEditor.js
packages/editor/CodeMirror/testUtil/findNodesWithName.js
packages/editor/CodeMirror/testUtil/forceFullParse.js
packages/editor/CodeMirror/testUtil/loadLanguages.js
packages/editor/CodeMirror/testUtil/pressReleaseKey.js
packages/editor/CodeMirror/testUtil/typeText.js
packages/editor/CodeMirror/theme.js
packages/editor/CodeMirror/utils/biDirectionalTextExtension.js
packages/editor/CodeMirror/utils/formatting/RegionSpec.js
packages/editor/CodeMirror/utils/formatting/computeSelectionFormatting.test.js
packages/editor/CodeMirror/utils/formatting/computeSelectionFormatting.js
packages/editor/CodeMirror/utils/formatting/findInlineMatch.test.js
packages/editor/CodeMirror/utils/formatting/findInlineMatch.js
packages/editor/CodeMirror/utils/formatting/isIndentationEquivalent.js
@@ -999,12 +985,11 @@ packages/editor/CodeMirror/utils/handleLinkEditRequests.js
packages/editor/CodeMirror/utils/handlePasteEvent.js
packages/editor/CodeMirror/utils/isCursorAtBeginning.js
packages/editor/CodeMirror/utils/isInSyntaxNode.js
packages/editor/CodeMirror/utils/markdown/codeBlockLanguages/allLanguages.js
packages/editor/CodeMirror/utils/markdown/codeBlockLanguages/defaultLanguage.js
packages/editor/CodeMirror/utils/markdown/codeBlockLanguages/lookUpLanguage.js
packages/editor/CodeMirror/utils/markdown/renumberSelectedLists.test.js
packages/editor/CodeMirror/utils/markdown/renumberSelectedLists.js
packages/editor/CodeMirror/utils/markdown/stripBlockquote.js
packages/editor/CodeMirror/utils/keyUpHandlerExtension.js
packages/editor/CodeMirror/utils/overwriteModeExtension.test.js
packages/editor/CodeMirror/utils/overwriteModeExtension.js
packages/editor/CodeMirror/utils/searchExtension.js
packages/editor/CodeMirror/utils/selectedNoteIdExtension.js
packages/editor/CodeMirror/utils/setupVim.js
packages/editor/SelectionFormatting.js
packages/editor/events.js
@@ -1053,8 +1038,6 @@ packages/lib/JoplinDatabase.js
packages/lib/JoplinError.js
packages/lib/JoplinServerApi.js
packages/lib/ObjectUtils.js
packages/lib/PerformanceLogger.test.js
packages/lib/PerformanceLogger.js
packages/lib/PoorManIntervals.js
packages/lib/RotatingLogs.test.js
packages/lib/RotatingLogs.js
+2 -2
View File
@@ -9,14 +9,14 @@
"vips.dev": {
"platforms": ["aarch64-darwin"],
},
"nodejs": "23.10.0",
"nodejs": "23.8.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
"version": "",
"platforms": ["aarch64-darwin", "x86_64-darwin"],
},
"python": "3.13.2",
"python": "3.13.1",
"bat": "latest",
"electron": {
"version": "latest",
+5 -6
View File
@@ -80,11 +80,11 @@
"eslint-plugin-react": "7.34.3",
"execa": "5.1.1",
"fs-extra": "11.2.0",
"glob": "11.0.2",
"glob": "11.0.1",
"gulp": "4.0.2",
"husky": "9.1.7",
"lerna": "3.22.1",
"lint-staged": "15.5.1",
"lint-staged": "15.5.0",
"madge": "8.0.0",
"npm-package-json-lint": "8.0.0",
"typescript": "5.4.5"
@@ -93,8 +93,8 @@
"@types/fs-extra": "11.0.4",
"eslint-plugin-github": "4.10.2",
"http-server": "14.1.1",
"node-gyp": "11.2.0",
"nodemon": "3.1.10"
"node-gyp": "9.4.1",
"nodemon": "3.1.9"
},
"packageManager": "yarn@4.9.2",
"resolutions": {
@@ -115,7 +115,6 @@
"pdfjs-dist@2.16.105": "patch:pdfjs-dist@npm%3A3.11.174#./.yarn/patches/pdfjs-dist-npm-3.11.174-67f2fee6d6.patch",
"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"
"canvas@npm:^2.11.2": "link:./.yarn/joplin-empty-package/"
}
}
+1 -1
View File
@@ -73,7 +73,7 @@
"@joplin/tools": "~3.4",
"@types/fs-extra": "11.0.4",
"@types/jest": "29.5.12",
"@types/node": "18.19.87",
"@types/node": "18.19.86",
"@types/proper-lockfile": "^4.1.2",
"gulp": "4.0.2",
"jest": "29.7.0",
@@ -1,7 +0,0 @@
---
aliases:
- An Obsidian-style note
---
This is a note with no `title` field in the YAML Frontmatter.
Joplin should be smart enough to pull the title from the filename in such cases.
+1 -1
View File
@@ -8,7 +8,7 @@ import { FileLocker } from '@joplin/utils/fs';
import { IpcMessageHandler, IpcServer, Message, newHttpError, sendMessage, SendMessageOptions, startServer, stopServer } from '@joplin/utils/ipc';
import { BrowserWindow, Tray, WebContents, screen, App, nativeTheme } from 'electron';
import bridge from './bridge';
import * as url from 'url';
const url = require('url');
const path = require('path');
const { dirname } = require('@joplin/lib/path-utils');
const fs = require('fs-extra');
@@ -18,12 +18,11 @@ const logger = Logger.create('shouldPasteResources');
// instead the clipboard resources, which will contain the actual image.
//
// We have a lot of log statements so that if someone reports a bug we can ask
// them to check the console and give us the messages they have. However, to avoid
// including sensitive information in the logs, users will need to check the console,
// not the log file.
// them to check the console and give us the messages they have.
export default (pastedText: string, pastedHtml: string, resourceMds: string[]) => {
const debugInformation = JSON.stringify({ pastedText, pastedHtml, resourceMds }, undefined, '\t');
logger.debug('Input data:', debugInformation);
logger.info('Pasted text:', pastedText);
logger.info('Pasted HTML:', pastedHtml);
logger.info('Resources:', resourceMds);
if (pastedText) {
logger.info('Not pasting resources only because the clipboard contains plain text');
@@ -411,14 +411,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
const ll = this.latLongFromLocation(value);
url = Note.geoLocationUrlFromLatLong(ll.latitude, ll.longitude);
}
const urlStyle: React.CSSProperties = {
...theme.urlStyle,
maxWidth: '180px',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
display: 'inline-block',
};
const urlStyle: React.CSSProperties = { ...theme.urlStyle, maxWidth: '180px', overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' };
controlComp = (
<a href="#" onClick={() => bridge().openExternal(url)} style={urlStyle}>
{displayedValue}
@@ -2,35 +2,35 @@
// Disable React message in console "Download the React DevTools for a better development experience"
// https://stackoverflow.com/questions/42196819/disable-hide-download-the-react-devtools#42196820
// eslint-disable-next-line no-undef, @typescript-eslint/no-explicit-any
(window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
// eslint-disable-next-line no-undef
window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
supportsFiber: true,
inject: function() {},
onCommitFiberRoot: function() {},
onCommitFiberUnmount: function() {},
};
import './utils/sourceMapSetup';
import app from './app';
import Folder from '@joplin/lib/models/Folder';
import Resource from '@joplin/lib/models/Resource';
import BaseItem from '@joplin/lib/models/BaseItem';
import Note from '@joplin/lib/models/Note';
import Tag from '@joplin/lib/models/Tag';
import NoteTag from '@joplin/lib/models/NoteTag';
import MasterKey from '@joplin/lib/models/MasterKey';
import Setting, { AppType } from '@joplin/lib/models/Setting';
import Revision from '@joplin/lib/models/Revision';
import Logger from '@joplin/utils/Logger';
import FsDriverNode from '@joplin/lib/fs-driver-node';
import bridge from './services/bridge';
import shim from '@joplin/lib/shim';
require('./utils/sourceMapSetup');
const app = require('./app').default;
const Folder = require('@joplin/lib/models/Folder').default;
const Resource = require('@joplin/lib/models/Resource').default;
const BaseItem = require('@joplin/lib/models/BaseItem').default;
const Note = require('@joplin/lib/models/Note').default;
const Tag = require('@joplin/lib/models/Tag').default;
const NoteTag = require('@joplin/lib/models/NoteTag').default;
const MasterKey = require('@joplin/lib/models/MasterKey').default;
const Setting = require('@joplin/lib/models/Setting').default;
const Revision = require('@joplin/lib/models/Revision').default;
const Logger = require('@joplin/utils/Logger').default;
const FsDriverNode = require('@joplin/lib/fs-driver-node').default;
const bridge = require('./services/bridge').default;
const shim = require('@joplin/lib/shim').default;
const { shimInit } = require('@joplin/lib/shim-init-node.js');
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
import FileApiDriverLocal from '@joplin/lib/file-api-driver-local';
import * as React from 'react';
import nodeSqlite = require('sqlite3');
import initLib from '@joplin/lib/initLib';
const EncryptionService = require('@joplin/lib/services/e2ee/EncryptionService').default;
const FileApiDriverLocal = require('@joplin/lib/file-api-driver-local').default;
const React = require('react');
const nodeSqlite = require('sqlite3');
const initLib = require('@joplin/lib/initLib').default;
const pdfJs = require('pdfjs-dist');
const { isAppleSilicon } = require('is-apple-silicon');
require('@sentry/electron/renderer');
@@ -60,7 +60,7 @@ const main = async () => {
BaseItem.loadClass('Revision', Revision);
Setting.setConstant('appId', bridge().appId());
Setting.setConstant('appType', AppType.Desktop);
Setting.setConstant('appType', 'desktop');
Setting.setConstant('pluginAssetDir', `${__dirname}/pluginAssets`);
// eslint-disable-next-line no-console
@@ -127,6 +127,6 @@ main().catch((error) => {
// In dev, we give the option to leave the app open as debug statements in the
// console can be useful
const canIgnore = env === 'dev';
void bridge().electronApp().handleAppFailure(errorMessage, canIgnore);
bridge().electronApp().handleAppFailure(errorMessage, canIgnore);
});
@@ -1,18 +1,18 @@
// This is the basic initialization for the Electron MAIN process
import './utils/sourceMapSetup';
import { app as electronApp } from 'electron';
require('./utils/sourceMapSetup');
const electronApp = require('electron').app;
require('@electron/remote/main').initialize();
import ElectronAppWrapper from './ElectronAppWrapper';
import { pathExistsSync, readFileSync, mkdirpSync } from 'fs-extra';
import { initBridge } from './bridge';
import Logger from '@joplin/utils/Logger';
import FsDriverNode from '@joplin/lib/fs-driver-node';
const ElectronAppWrapper = require('./ElectronAppWrapper').default;
const { pathExistsSync, readFileSync, mkdirpSync } = require('fs-extra');
const { initBridge } = require('./bridge');
const Logger = require('@joplin/utils/Logger').default;
const FsDriverNode = require('@joplin/lib/fs-driver-node').default;
const envFromArgs = require('@joplin/lib/envFromArgs');
const packageInfo = require('./packageInfo.js');
import { isCallbackUrl } from '@joplin/lib/callbackUrlUtils';
import determineBaseAppDirs from '@joplin/lib/determineBaseAppDirs';
import registerCustomProtocols from './utils/customProtocols/registerCustomProtocols';
const { isCallbackUrl } = require('@joplin/lib/callbackUrlUtils');
const determineBaseAppDirs = require('@joplin/lib/determineBaseAppDirs').default;
const registerCustomProtocols = require('./utils/customProtocols/registerCustomProtocols').default;
// Electron takes the application name from package.json `name` and
// displays this in the tray icon toolip and message box titles, however in
@@ -26,7 +26,7 @@ process.on('unhandledRejection', (reason, p) => {
process.exit(1);
});
const getFlagValueFromArgs = (args: string[], flag: string, defaultValue: string|null) => {
const getFlagValueFromArgs = (args, flag, defaultValue) => {
if (!args) return null;
const index = args.indexOf(flag);
if (index <= 0 || index >= args.length - 1) return defaultValue;
@@ -75,13 +75,7 @@ const wrapper = new ElectronAppWrapper(electronApp, {
env, profilePath: rootProfileDir, isDebugMode, initialCallbackUrl, isEndToEndTesting,
});
type ExtendedGlobal = {
joplinBridge: unknown;
};
(globalThis as unknown as ExtendedGlobal).joplinBridge = (
initBridge(wrapper, appId, appName, rootProfileDir, autoUploadCrashDumps, altInstanceId)
);
globalThis.joplinBridge = initBridge(wrapper, appId, appName, rootProfileDir, autoUploadCrashDumps, altInstanceId);
wrapper.start().catch((error) => {
console.error('Electron App fatal error:');
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
"version": "3.4.3",
"version": "3.4.1",
"description": "Joplin for Desktop",
"main": "main.bundle.js",
"private": true,
@@ -147,7 +147,7 @@
"@testing-library/react-hooks": "8.0.1",
"@types/jest": "29.5.12",
"@types/mustache": "4.2.5",
"@types/node": "18.19.87",
"@types/node": "18.19.86",
"@types/react": "18.3.20",
"@types/react-dom": "18.3.6",
"@types/react-redux": "7.1.33",
@@ -166,7 +166,7 @@
"electron-window-state": "5.0.3",
"esbuild": "^0.25.3",
"formatcoords": "1.1.3",
"glob": "11.0.2",
"glob": "11.0.1",
"gulp": "4.0.2",
"highlight.js": "11.11.1",
"immer": "9.0.21",
+6
View File
@@ -182,6 +182,12 @@ do
fi
done
echo '----------------------------------------------------'
echo 'Running commands:'
echo '';
cat "$CMD_FILE"
echo '----------------------------------------------------'
cd "$ROOT_DIR/packages/app-cli"
yarn start --profile "$PROFILE_DIR" batch "$CMD_FILE"
@@ -51,8 +51,10 @@
const modulePath = args && args.length ? args[0] : null;
if (!modulePath) throw new Error('No module path specified on `require` call');
// The sqlite3 is actually part of the lib package so we need to do
// something convoluted to get it working.
if (modulePath === 'sqlite3') {
return require('sqlite3');
return require('../../node_modules/@joplin/lib/node_modules/sqlite3/lib/sqlite3.js');
}
if (modulePath === 'fs-extra') {
+2 -2
View File
@@ -120,8 +120,8 @@ const makeBuildContext = (entryPoint: string, renderer: boolean, computeFileSize
const bundleJs = async (writeStats: boolean) => {
const entryPoints = [
{ fileName: 'main.ts', renderer: false },
{ fileName: 'main-html.ts', renderer: true },
{ fileName: 'main.js', renderer: false },
{ fileName: 'main-html.js', renderer: true },
];
for (const { fileName, renderer } of entryPoints) {
const compiler = await makeBuildContext(fileName, renderer, writeStats);
+2 -2
View File
@@ -89,8 +89,8 @@ android {
applicationId "net.cozic.joplin"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 2097775
versionName "3.4.2"
versionCode 2097773
versionName "3.4.0"
ndk {
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
}
+6
View File
@@ -40,6 +40,12 @@ allprojects {
// https://search.maven.org/artifact/com.alphacephei/vosk-android/0.3.46/aar
maven { url "https://maven.apache.org" }
maven {
// Required by react-native-fingerprint-scanner
// https://github.com/hieuvp/react-native-fingerprint-scanner/issues/192
url "https://maven.aliyun.com/repository/jcenter"
}
// Also required for react-native-vosk?
maven { url "https://maven.google.com" }
@@ -15,7 +15,6 @@ pluginManagement {
"../android/expo-gradle-plugin"
).absolutePath
includeBuild(expoPluginsPath)
includeBuild("../node_modules/@react-native/gradle-plugin")
}
plugins {
@@ -15,7 +15,6 @@ const Camera = (props: Props, ref: ForwardedRef<CameraRef>) => {
await shim.fsDriver().writeFile(
path,
`<svg viewBox="0 -70 232 78" width="232" height="78" version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg">
<rect width="232" height="78" y="-70" rx="32" style="fill: #ccc;"/>
<text style="font-family: serif; font-size: 104px; fill: rgb(128, 51, 128);">Test!</text>
</svg>`,
'utf8',
@@ -16,12 +16,24 @@ import useBarcodeScanner from './utils/useBarcodeScanner';
import ScannedBarcodes from './ScannedBarcodes';
import { CameraRef } from './Camera/types';
import Camera from './Camera/index';
import { CameraViewProps } from './types';
import { CameraResult, OnInsertBarcode } from './types';
import Logger from '@joplin/utils/Logger';
import useBackHandler from '../../utils/hooks/useBackHandler';
const logger = Logger.create('CameraView');
interface Props {
themeId: number;
style: ViewStyle;
cameraType: CameraDirection;
cameraRatio: string;
onPhoto: (data: CameraResult)=> void;
// If null, cancelling should be handled by the parent
// component
onCancel: (()=> void)|null;
onInsertBarcode: OnInsertBarcode;
}
interface UseStyleProps {
themeId: number;
style: ViewStyle;
@@ -92,7 +104,7 @@ const useAvailableRatios = (): string[] => {
};
const CameraViewComponent: React.FC<CameraViewProps> = props => {
const CameraViewComponent: React.FC<Props> = props => {
const styles = useStyles(props);
const cameraRef = useRef<CameraRef|null>(null);
const [cameraReady, setCameraReady] = useState(false);
@@ -1,58 +0,0 @@
import * as React from 'react';
import { useCallback, useMemo } from 'react';
import { connect } from 'react-redux';
import { StyleSheet, View } from 'react-native';
import { _ } from '@joplin/lib/locale';
import { AppState } from '../../utils/types';
import { PrimaryButton } from '../buttons';
import { themeStyle } from '../global-style';
import { CameraViewProps } from './types';
import pickDocument from '../../utils/pickDocument';
const useStyles = (themeId: number) => {
return useMemo(() => {
const theme = themeStyle(themeId);
return StyleSheet.create({
container: {
backgroundColor: theme.backgroundColor,
flexGrow: 1,
justifyContent: 'center',
alignItems: 'center',
},
});
}, [themeId]);
};
const CameraViewComponent: React.FC<CameraViewProps> = props => {
const styles = useStyles(props.themeId);
const onUploadPress = useCallback(async () => {
const response = await pickDocument({ preferCamera: true });
for (const asset of response) {
props.onPhoto({
uri: asset.uri,
type: asset.type,
});
}
}, [props.onPhoto]);
return (
<View style={styles.container}>
<PrimaryButton
icon='file-upload'
onPress={onUploadPress}
>{_('Upload photo')}</PrimaryButton>
</View>
);
};
const mapStateToProps = (state: AppState) => {
return {
themeId: state.settings.theme,
cameraRatio: state.settings['camera.ratio'],
cameraType: state.settings['camera.type'],
};
};
export default connect(mapStateToProps)(CameraViewComponent);
@@ -1,6 +1,6 @@
import * as React from 'react';
import Setting from '@joplin/lib/models/Setting';
import CameraViewMultiPage from './CameraViewMultiPage';
import CameraViewMultiPage, { OnComplete } from './CameraViewMultiPage';
import { CameraResult, OnInsertBarcode } from './types';
import { Store } from 'redux';
import { AppState } from '../../utils/types';
@@ -8,29 +8,24 @@ import TestProviderStack from '../testing/TestProviderStack';
import createMockReduxStore from '../../utils/testing/createMockReduxStore';
import { fireEvent, render, screen, waitFor } from '@testing-library/react-native';
import { startCamera, takePhoto } from './utils/testing';
import { useState } from 'react';
interface WrapperProps {
onComplete?: (finalPhotos: CameraResult[])=> void;
onCancel?: ()=> void;
onInsertBarcode?: OnInsertBarcode;
onComplete?: OnComplete;
}
let store: Store<AppState>;
const WrappedCamera: React.FC<WrapperProps> = ({
onCancel = jest.fn(),
onComplete = jest.fn(),
onInsertBarcode = jest.fn(),
onCancel = jest.fn(),
}) => {
const [photos, setPhotos] = useState<CameraResult[]>([]);
return <TestProviderStack store={store}>
<CameraViewMultiPage
themeId={Setting.THEME_LIGHT}
photos={photos}
onSetPhotos={setPhotos}
onCancel={onCancel}
onComplete={() => onComplete(photos)}
onComplete={onComplete}
onInsertBarcode={onInsertBarcode}
/>
</TestProviderStack>;
@@ -1,22 +1,20 @@
import * as React from 'react';
import { CameraResult } from './types';
import { View, StyleSheet } from 'react-native';
import { View, StyleSheet, Platform, ImageBackground, ViewStyle, TextStyle } from 'react-native';
import CameraView from './CameraView';
import { useCallback, useMemo } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { themeStyle } from '../global-style';
import { Button } from 'react-native-paper';
import { Button, Text } from 'react-native-paper';
import { _ } from '@joplin/lib/locale';
import PhotoPreview from './PhotoPreview';
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
import shim from '@joplin/lib/shim';
export type OnPhotosChange = (photos: CameraResult[])=> void;
export type OnComplete = ()=> void;
export type OnComplete = (photos: CameraResult[])=> void;
interface Props {
themeId: number;
onCancel: ()=> void;
onComplete: OnComplete;
photos: CameraResult[];
onSetPhotos: OnPhotosChange;
onInsertBarcode: (barcodeText: string)=> void;
}
@@ -44,8 +42,17 @@ const useStyle = (themeId: number) => {
imagePreview: {
maxWidth: 70,
flexShrink: 1,
flexGrow: 1,
alignContent: 'center',
justifyContent: 'center',
},
imageCountText: {
marginLeft: 'auto',
marginRight: 'auto',
marginTop: 'auto',
padding: 2,
borderRadius: 4,
backgroundColor: theme.backgroundColor2,
color: theme.color2,
},
@@ -53,13 +60,55 @@ const useStyle = (themeId: number) => {
}, [themeId]);
};
interface PhotoProps {
source: CameraResult;
backgroundStyle: ViewStyle;
textStyle: TextStyle;
label: number;
}
const PhotoPreview: React.FC<PhotoProps> = ({ source, label, backgroundStyle, textStyle }) => {
const [uri, setUri] = useState('');
useAsyncEffect(async (event) => {
if (Platform.OS === 'web') {
const file = await shim.fsDriver().fileAtPath(source.uri);
if (event.cancelled) return;
const uri = URL.createObjectURL(file);
setUri(uri);
event.onCleanup(() => {
URL.revokeObjectURL(uri);
});
} else {
setUri(source.uri);
}
}, [source]);
return <ImageBackground
style={backgroundStyle}
resizeMode='contain'
source={{ uri }}
accessibilityLabel={_('%d photo(s) taken', label)}
>
<Text
style={textStyle}
testID='photo-count'
>{label}</Text>
</ImageBackground>;
};
const CameraViewMultiPage: React.FC<Props> = ({
onInsertBarcode, onCancel, onComplete, themeId, photos, onSetPhotos,
onInsertBarcode, onCancel, onComplete, themeId,
}) => {
const [photos, setPhotos] = useState<CameraResult[]>([]);
const onPhoto = useCallback((data: CameraResult) => {
onSetPhotos([...photos, data]);
}, [photos, onSetPhotos]);
setPhotos(photos => [...photos, data]);
}, []);
const onDonePressed = useCallback(() => {
onComplete(photos);
}, [photos, onComplete]);
const styles = useStyle(themeId);
const renderLastPhoto = () => {
@@ -88,7 +137,7 @@ const CameraViewMultiPage: React.FC<Props> = ({
<Button
icon='arrow-right'
disabled={photos.length === 0}
onPress={onComplete}
onPress={onDonePressed}
>{_('Next')}</Button>
</View>
</View>;
@@ -1,69 +0,0 @@
import * as React from 'react';
import { ViewStyle, TextStyle, Platform, ImageBackground, Text, StyleSheet } from 'react-native';
import { useState } from 'react';
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
import { CameraResult } from './types';
import shim from '@joplin/lib/shim';
import { _ } from '@joplin/lib/locale';
interface PhotoProps {
source: CameraResult;
backgroundStyle: ViewStyle;
textStyle: TextStyle;
label: number;
}
const styles = StyleSheet.create({
background: {
maxWidth: 70,
flexShrink: 1,
flexGrow: 1,
alignContent: 'center',
justifyContent: 'center',
},
text: {
marginLeft: 'auto',
marginRight: 'auto',
marginTop: 'auto',
padding: 4,
borderRadius: 32,
color: 'white',
backgroundColor: '#11c',
},
});
const PhotoPreview: React.FC<PhotoProps> = ({ source, label, backgroundStyle, textStyle }) => {
const [uri, setUri] = useState('');
useAsyncEffect(async (event) => {
if (!source) {
setUri('');
} else if (Platform.OS === 'web') {
const file = await shim.fsDriver().fileAtPath(source.uri);
if (event.cancelled) return;
const uri = URL.createObjectURL(file);
setUri(uri);
event.onCleanup(() => {
URL.revokeObjectURL(uri);
});
} else {
setUri(source.uri);
}
}, [source]);
return <ImageBackground
style={[styles.background, backgroundStyle]}
resizeMode='contain'
source={{ uri }}
accessibilityLabel={_('%d photo(s) taken', label)}
>
<Text
style={[styles.text, textStyle]}
testID='photo-count'
>{label}</Text>
</ImageBackground>;
};
export default PhotoPreview;
@@ -1,5 +1,3 @@
import type { CameraDirection } from '@joplin/lib/models/settings/builtInMetadata';
import type { ViewStyle } from 'react-native';
export type OnInsertBarcode = (barcodeText: string)=> void;
@@ -7,15 +5,3 @@ export interface CameraResult {
uri: string;
type: string;
}
export interface CameraViewProps {
themeId: number;
style: ViewStyle;
cameraType: CameraDirection;
cameraRatio: string;
onPhoto: (data: CameraResult)=> void;
// If null, cancelling should be handled by the parent
// component
onCancel: (()=> void)|null;
onInsertBarcode: OnInsertBarcode;
}
@@ -1,90 +0,0 @@
import * as React from 'react';
import { fireEvent, render, screen } from '@testing-library/react-native';
import createMockReduxStore from '../utils/testing/createMockReduxStore';
import TestProviderStack from './testing/TestProviderStack';
import ComboBox, { OnItemSelected, Option } from './ComboBox';
import { useMemo } from 'react';
interface Item {
title: string;
}
interface WrapperProps {
items: Item[];
onItemSelected?: OnItemSelected;
}
const store = createMockReduxStore();
const WrappedComboBox: React.FC<WrapperProps> = ({
items,
onItemSelected = jest.fn(),
}: WrapperProps) => {
const mappedItems = useMemo(() => {
return items.map((item): Option => ({
title: item.title,
icon: undefined,
accessibilityHint: undefined,
willRemoveOnPress: false,
}));
}, [items]);
return <TestProviderStack store={store}>
<ComboBox
items={mappedItems}
alwaysExpand={true}
style={{}}
onItemSelected={onItemSelected}
placeholder={'Test combobox'}
/>
</TestProviderStack>;
};
const getSearchInput = () => {
return screen.getByPlaceholderText('Test combobox');
};
const getSearchResults = () => {
return screen.getAllByTestId(/^search-result-/);
};
describe('ComboBox', () => {
test('should list all items when the search query is empty', () => {
const testItems = [
{ title: 'test 1' },
{ title: 'test 2' },
{ title: 'test 3' },
];
const { unmount } = render(
<WrappedComboBox
items={testItems}
/>,
);
expect(getSearchInput()).toHaveTextContent('');
expect(getSearchResults()).toHaveLength(3);
// Manually unmounting prevents a warning
unmount();
});
test('changing the search query should limit which items are visible', () => {
const testItems = [
{ title: 'a' },
{ title: 'b' },
{ title: 'c' },
{ title: 'aa' },
];
const { unmount } = render(
<WrappedComboBox items={testItems}/>,
);
expect(getSearchResults()).toHaveLength(4);
fireEvent.changeText(getSearchInput(), 'a');
const updatedResults = getSearchResults();
expect(updatedResults[0]).toHaveTextContent('a');
expect(updatedResults[1]).toHaveTextContent('aa');
expect(updatedResults).toHaveLength(2);
unmount();
});
});
-601
View File
@@ -1,601 +0,0 @@
import * as React from 'react';
import { AccessibilityInfo, NativeSyntheticEvent, Platform, Role, ScrollViewProps, StyleSheet, TextInput, TextInputProps, useWindowDimensions, View, ViewProps, ViewStyle } from 'react-native';
import { TouchableRipple, Text } from 'react-native-paper';
import { connect } from 'react-redux';
import { AppState } from '../utils/types';
import { themeStyle } from './global-style';
import Icon from './Icon';
import { RefObject, useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
import { _ } from '@joplin/lib/locale';
import SearchInput from './SearchInput';
import focusView from '../utils/focusView';
import AsyncActionQueue from '@joplin/lib/AsyncActionQueue';
import NestableFlatList, { NestableFlatListControl } from './NestableFlatList';
const naturalCompare = require('string-natural-compare');
export interface Option {
title: string;
icon: string|undefined;
accessibilityHint: string|undefined;
onPress?: ()=> void;
// True if pressing this option removes it. Used for working around
// focus issues.
willRemoveOnPress: boolean;
}
export type OnItemSelected = (item: Option, index: number)=> void;
interface BaseProps {
themeId: number;
items: Option[];
alwaysExpand: boolean;
placeholder: string;
onItemSelected: OnItemSelected;
style: ViewStyle;
searchInputProps?: TextInputProps;
searchResultProps?: ScrollViewProps;
}
type OnAddItem = (content: string)=> void;
type OnCanAddItem = (item: string)=> boolean;
type Props = BaseProps & ({
onAddItem: OnAddItem|null;
canAddItem: OnCanAddItem;
}|{
onAddItem?: undefined;
canAddItem?: undefined;
});
const optionKeyExtractor = (option: Option) => option.title;
interface UseSearchResultsOptions {
search: string;
setSearch: (search: string)=> void;
options: Option[];
onAddItem: null|OnAddItem;
canAddItem: OnCanAddItem;
}
const useSearchResults = ({
search, setSearch, options, onAddItem, canAddItem,
}: UseSearchResultsOptions) => {
const results = useMemo(() => {
return options
.filter(option => option.title.startsWith(search))
.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);
});
}, [search, options]);
const canAdd = (
!!onAddItem
&& search.trim()
&& results[0]?.title !== search
&& canAddItem(search)
);
// Use a ref to prevent unnecessary rerenders if onAddItem changes
const addCurrentSearch = useRef(()=>{});
addCurrentSearch.current = () => {
onAddItem(search);
AccessibilityInfo.announceForAccessibility(_('Added new: %s', search));
setSearch('');
};
return useMemo(() => {
if (!canAdd) return results;
return [
...results,
{
title: _('Add new'),
icon: 'fas fa-plus',
accessibilityHint: undefined,
willRemoveOnPress: true,
onPress: () => {
addCurrentSearch.current?.();
},
},
];
}, [canAdd, results]);
};
interface SelectedIndexControl {
onNextResult: ()=> void;
onPreviousResult: ()=> void;
onFirstResult: ()=> void;
onLastResult: ()=> void;
}
const useSelectedIndex = (search: string, searchResults: Option[]) => {
const [selectedIndex, setSelectedIndex] = useState(0);
useEffect(() => {
if (search) {
setSelectedIndex(0);
} else {
const hasResults = !!searchResults.length;
setSelectedIndex(hasResults ? 0 : -1);
}
}, [searchResults, search]);
const resultCount = searchResults.length;
const selectedIndexControl: SelectedIndexControl = useMemo(() => ({
onNextResult: () => {
setSelectedIndex(index => {
return Math.min(index + 1, resultCount - 1);
});
},
onPreviousResult: () => {
setSelectedIndex(index => {
return Math.max(index - 1, 0);
});
},
onFirstResult: () => {
setSelectedIndex(0);
},
onLastResult: () => {
setSelectedIndex(resultCount - 1);
},
}), [resultCount]);
return { selectedIndex, selectedIndexControl };
};
const useStyles = (themeId: number, showSearchResults: boolean) => {
const { fontScale } = useWindowDimensions();
const menuItemHeight = 40 * fontScale;
const theme = themeStyle(themeId);
const styles = useMemo(() => {
const borderRadius = 4;
const itemMarginVertical = 8;
return StyleSheet.create({
root: {
flexDirection: 'column',
overflow: 'hidden',
borderRadius,
backgroundColor: theme.backgroundColor,
borderColor: theme.dividerColor,
borderWidth: showSearchResults ? 1 : 0,
},
searchInputContainer: {
borderRadius,
backgroundColor: theme.backgroundColor,
borderColor: theme.dividerColor,
borderWidth: 1,
...(showSearchResults ? {
borderTopWidth: 0,
borderLeftWidth: 0,
borderRightWidth: 0,
} : {}),
},
tagSearchHelp: {
color: theme.colorFaded,
marginTop: 6,
},
searchInput: {
minHeight: 32,
},
searchResults: {
height: 200,
flexGrow: 1,
flexShrink: 1,
...(showSearchResults ? {} : {
display: 'none',
}),
},
optionIcon: {
color: theme.color,
fontSize: theme.fontSizeSmaller,
textAlign: 'center',
paddingLeft: 4,
paddingRight: 4,
},
optionLabel: {
fontSize: theme.fontSize,
color: theme.color,
paddingInlineStart: 3,
},
optionContent: {
flexDirection: 'row',
alignItems: 'center',
borderRadius,
height: menuItemHeight - itemMarginVertical,
marginTop: itemMarginVertical / 2,
marginBottom: itemMarginVertical / 2,
paddingHorizontal: 3,
},
optionContentSelected: {
backgroundColor: theme.selectedColor,
},
});
}, [theme, menuItemHeight, showSearchResults]);
return { menuItemHeight, styles };
};
type Styles = ReturnType<typeof useStyles>['styles'];
interface SearchResultProps {
text: string;
icon: string;
selected: boolean;
styles: Styles;
}
const SearchResult: React.FC<SearchResultProps> = ({
text, styles, selected, icon: iconName,
}) => {
const icon = iconName ? <Icon
style={styles.optionIcon}
name={iconName}
// Description is provided by adjacent text
accessibilityLabel={null}
/> : null;
return (
<View style={[styles.optionContent, selected && styles.optionContentSelected]}>
{icon}
<Text
style={styles.optionLabel}
>{text}</Text>
</View>
);
};
interface ResultWrapperProps extends ViewProps {
index: number;
item: Option;
}
interface SearchResultContainerProps {
onItemSelected: OnItemSelected;
selectedIndex: number;
baseId: string;
resultCount: number;
searchInputRef: RefObject<TextInput>;
// Used to determine focus
resultsHideOnPress: boolean;
}
const useSearchResultContainerComponent = ({
onItemSelected, selectedIndex, baseId, resultCount, searchInputRef, resultsHideOnPress,
}: SearchResultContainerProps): React.FC<ResultWrapperProps> => {
const listItemsRef = useRef<Record<number, View>>({});
const eventQueue = useMemo(() => {
const queue = new AsyncActionQueue(100);
// Don't allow skipping any onItemSelected calls:
queue.setCanSkipTaskHandler(() => false);
return queue;
}, []);
const onItemPressRef = useRef(onItemSelected);
onItemPressRef.current = (item, index) => {
let focusTarget = null;
if (resultsHideOnPress) {
focusTarget = searchInputRef.current;
} else if (Platform.OS === 'android' && item.willRemoveOnPress) {
// Workaround for an accessibility bug on Android: By default, when an item is removed
// from the list of results, focus can occasionally jump to the start of the document.
// To prevent this, manually move focus to the next item before the results list changes:
const adjacentView = listItemsRef.current[index + 1] ?? listItemsRef.current[index - 1];
focusTarget = adjacentView ?? searchInputRef.current;
}
if (focusTarget) {
focusView('ComboBox::focusAfterPress', focusTarget);
eventQueue.push(() => {
onItemSelected(item, index);
});
} else {
onItemSelected(item, index);
}
};
// For the correct accessibility structure, the `TouchableRipple`s need to be siblings.
return useMemo(() => ({ index, item, children, ...rest }) => (
<TouchableRipple
{...rest}
ref={(item) => {
listItemsRef.current[index] = item;
}}
onPress={() => { onItemPressRef.current(item, index); }}
// On web, focus is controlled using the arrow keys. On other
// platforms, arrow key navigation is not available and each item
// needs to be focusable
tabIndex={Platform.OS === 'web' ? -1 : undefined}
role={Platform.OS === 'web' ? 'option' : 'button'}
accessibilityHint={item.accessibilityHint}
aria-selected={index === selectedIndex}
nativeID={`${baseId}-${index}`}
testID={`search-result-${index}`}
aria-setsize={resultCount}
aria-posinset={index + 1}
><View>{children}</View></TouchableRipple>
), [selectedIndex, baseId, resultCount]);
};
const useShowSearchResults = (alwaysExpand: boolean, search: string) => {
const [showSearchResults, setShowSearchResults] = useState(alwaysExpand);
const showResultsRef = useRef(showSearchResults);
showResultsRef.current = showSearchResults;
useEffect(() => {
if (alwaysExpand) {
setShowSearchResults(true);
}
}, [alwaysExpand]);
useEffect(() => {
if (search.length > 0 && !showResultsRef.current) {
setShowSearchResults(true);
}
}, [search]);
return { showSearchResults, setShowSearchResults };
};
interface AnnounceSelectionOptions {
enabled: boolean;
selectedResultTitle: string|undefined;
resultCount: number;
searchQuery: string;
}
const useAnnounceSelection = ({ selectedResultTitle, resultCount, enabled, searchQuery }: AnnounceSelectionOptions) => {
const enabledRef = useRef(enabled);
enabledRef.current = enabled;
const announcement = (() => {
if (!searchQuery) return '';
if (resultCount === 0) return _('No results');
if (selectedResultTitle) return _('Selected: %s', selectedResultTitle);
return '';
})();
useEffect(() => {
if (enabledRef.current && announcement) {
AccessibilityInfo.announceForAccessibility(announcement);
}
}, [announcement]);
};
const useSelectionAutoScroll = (
listRef: RefObject<NestableFlatListControl|null>, results: Option[], selectedIndex: number,
) => {
const resultsRef = useRef(results);
resultsRef.current = results;
useEffect(() => {
if (resultsRef.current?.length && selectedIndex >= 0) {
listRef.current?.scrollToIndex({ index: selectedIndex, animated: false, viewPosition: 0.4 });
}
}, [selectedIndex, listRef]);
};
interface UseInputEventHandlersProps {
selectedIndexControl: SelectedIndexControl;
onItemSelected: OnItemSelected;
selectedIndex: number;
selectedResult: Option|null;
alwaysExpand: boolean;
showSearchResults: boolean;
setShowSearchResults: (show: boolean)=> void;
setSearch: (search: string)=> void;
}
const useInputEventHandlers = ({
selectedIndexControl,
onItemSelected: propsOnItemSelected, setShowSearchResults, alwaysExpand,
setSearch, selectedResult, selectedIndex, showSearchResults,
}: UseInputEventHandlersProps) => {
const propsOnItemSelectedRef = useRef(propsOnItemSelected);
propsOnItemSelectedRef.current = propsOnItemSelected;
const onItemSelected = useCallback((item: Option, index: number) => {
let result;
if (item.onPress) {
result = item.onPress();
} else {
result = propsOnItemSelectedRef.current(item, index);
}
if (!alwaysExpand) {
setSearch('');
setShowSearchResults(false);
}
return result;
}, [setShowSearchResults, alwaysExpand, setSearch]);
const onSubmit = useCallback(() => {
if (selectedResult) {
onItemSelected(selectedResult, selectedIndex);
setSearch('');
}
}, [onItemSelected, selectedResult, selectedIndex, setSearch]);
// For now, onKeyPress only works on web.
// See https://github.com/react-native-community/discussions-and-proposals/issues/249
type KeyPressEvent = { key: string };
const onKeyPress = useCallback((event: NativeSyntheticEvent<KeyPressEvent>) => {
const key = event.nativeEvent.key;
const isDownArrow = key === 'ArrowDown';
const isUpArrow = key === 'ArrowUp';
if (!showSearchResults && (isDownArrow || isUpArrow)) {
setShowSearchResults(true);
if (isUpArrow) {
selectedIndexControl.onLastResult();
} else {
selectedIndexControl.onFirstResult();
}
event.preventDefault();
} else if (key === 'ArrowDown') {
selectedIndexControl.onNextResult();
event.preventDefault();
} else if (key === 'ArrowUp') {
selectedIndexControl.onPreviousResult();
event.preventDefault();
} else if (key === 'Enter') {
// This case is necessary on web to prevent the
// search input from becoming defocused after
// pressing "enter".
event.preventDefault();
onSubmit();
setSearch('');
} else if (key === 'Escape' && !alwaysExpand) {
setShowSearchResults(false);
event.preventDefault();
}
}, [onSubmit, setSearch, selectedIndexControl, setShowSearchResults, showSearchResults, alwaysExpand]);
return { onKeyPress, onItemSelected, onSubmit };
};
const ComboBox: React.FC<Props> = ({
themeId,
items,
onItemSelected: propsOnItemSelected,
placeholder,
onAddItem,
canAddItem,
style: rootStyle,
alwaysExpand,
searchInputProps,
searchResultProps,
}) => {
const [search, setSearch] = useState('');
const { showSearchResults, setShowSearchResults } = useShowSearchResults(alwaysExpand, search);
const { styles, menuItemHeight } = useStyles(themeId, showSearchResults);
const results = useSearchResults({
search,
setSearch,
options: items,
onAddItem,
canAddItem,
});
const { selectedIndex, selectedIndexControl } = useSelectedIndex(search, results);
const searchInputRef = useRef<TextInput|null>(null);
const listRef = useRef<NestableFlatListControl|null>(null);
useSelectionAutoScroll(listRef, results, selectedIndex);
useAnnounceSelection({
// On web, announcements are handled natively based on accessibility roles.
// Manual announcements are only needed on iOS and Android:
enabled: Platform.OS !== 'web',
selectedResultTitle: results[selectedIndex]?.title,
searchQuery: search,
resultCount: results.length,
});
const { onItemSelected, onKeyPress, onSubmit } = useInputEventHandlers({
selectedIndexControl,
onItemSelected: propsOnItemSelected,
selectedIndex,
selectedResult: results[selectedIndex],
alwaysExpand,
showSearchResults,
setShowSearchResults,
setSearch,
});
const baseId = useId();
const SearchResultWrapper = useSearchResultContainerComponent({
onItemSelected, selectedIndex, baseId, searchInputRef, resultCount: results.length,
resultsHideOnPress: !alwaysExpand,
});
type RenderEvent = { item: Option; index: number };
const renderItem = useCallback(({ item, index }: RenderEvent) => {
return <SearchResult
text={item.title}
styles={styles}
selected={index === selectedIndex}
icon={item.icon ?? ''}
/>;
}, [selectedIndex, styles]);
const webProps = {
onKeyDown: onKeyPress,
};
const activeId = `${baseId}-${selectedIndex}`;
const searchResults = <NestableFlatList
ref={listRef}
data={results}
{...searchResultProps}
CellRendererComponent={SearchResultWrapper}
itemHeight={menuItemHeight}
contentWrapperProps={{
// A better role would be 'listbox', but that isn't supported by RN.
role: Platform.OS === 'web' ? 'listbox' as Role : undefined,
'aria-activedescendant': activeId,
nativeID: `menuBox-${baseId}`,
onKeyPress,
// Allow focusing the results list directly on web. It has been observed
// that certain screen readers on web sometimes fail to read changes to the results list.
// Being able to navigate directly to the results list may help users in this case.
tabIndex: Platform.OS === 'web' ? 0 : undefined,
} as ViewProps}
style={styles.searchResults}
keyExtractor={optionKeyExtractor}
extraData={renderItem}
renderItem={renderItem}
/>;
const helpComponent = <Text style={styles.tagSearchHelp}>{_('To create a new tag, type the name and press enter.')}</Text>;
return <View style={[styles.root, rootStyle]} {...webProps}>
<SearchInput
inputRef={searchInputRef}
themeId={themeId}
containerStyle={styles.searchInputContainer}
style={styles.searchInput}
value={search}
onChangeText={setSearch}
onKeyPress={onKeyPress}
onSubmitEditing={onSubmit}
placeholder={placeholder}
aria-activedescendant={showSearchResults ? activeId : undefined}
aria-controls={`menuBox-${baseId}`}
// Certain accessibility properties only work well on web:
{...(Platform.OS === 'web' ? {
role: 'combobox',
'aria-autocomplete': 'list',
'aria-expanded': showSearchResults,
'aria-label': placeholder,
} : {})}
{...searchInputProps}
/>
{searchResults}
{!showSearchResults && helpComponent}
</View>;
};
export default connect((state: AppState) => ({
themeId: state.settings.theme,
}))(ComboBox);
@@ -1,19 +1,28 @@
import * as React from 'react';
import { Dialog, Divider, Surface, Text } from 'react-native-paper';
import { DialogType, ButtonDialogData } from './types';
import { StyleSheet, ViewStyle } from 'react-native';
import { DialogType, PromptDialogData } from './types';
import { StyleSheet } from 'react-native';
import { useMemo } from 'react';
import { themeStyle } from '../global-style';
import PromptButton from './PromptButton';
interface Props {
dialog: ButtonDialogData;
containerStyle: ViewStyle;
dialog: PromptDialogData;
themeId: number;
}
const useStyles = (isMenu: boolean) => {
const useStyles = (themeId: number, isMenu: boolean) => {
return useMemo(() => {
const theme = themeStyle(themeId);
return StyleSheet.create({
dialogContainer: {
backgroundColor: theme.backgroundColor,
borderRadius: theme.borderRadius,
paddingTop: theme.borderRadius,
marginLeft: 4,
marginRight: 4,
},
dialogContent: {
paddingBottom: 14,
@@ -31,12 +40,12 @@ const useStyles = (isMenu: boolean) => {
textAlign: isMenu ? 'center' : undefined,
},
});
}, [isMenu]);
}, [themeId, isMenu]);
};
const PromptDialog: React.FC<Props> = ({ dialog, containerStyle, themeId }) => {
const PromptDialog: React.FC<Props> = ({ dialog, themeId }) => {
const isMenu = dialog.type === DialogType.Menu;
const styles = useStyles(isMenu);
const styles = useStyles(themeId, isMenu);
const buttons = dialog.buttons.map((button, index) => {
return <PromptButton
@@ -54,7 +63,7 @@ const PromptDialog: React.FC<Props> = ({ dialog, containerStyle, themeId }) => {
return (
<Surface
testID={'prompt-dialog'}
style={containerStyle}
style={styles.dialogContainer}
key={dialog.key}
elevation={1}
>
@@ -1,76 +0,0 @@
import * as React from 'react';
import { Dialog, Surface, Text } from 'react-native-paper';
import { TextInputDialogData } from './types';
import { StyleSheet, ViewStyle } from 'react-native';
import { useId, useMemo, useState } from 'react';
import PromptButton from './PromptButton';
import { _ } from '@joplin/lib/locale';
import TextInput from '../TextInput';
interface Props {
dialog: TextInputDialogData;
containerStyle: ViewStyle;
themeId: number;
}
const useStyles = () => {
return useMemo(() => {
return StyleSheet.create({
dialogContent: {
paddingBottom: 14,
},
dialogActions: {
paddingBottom: 14,
paddingTop: 4,
},
});
}, []);
};
const TextInputDialog: React.FC<Props> = ({ dialog, containerStyle, themeId }) => {
const styles = useStyles();
const [text, setText] = useState('');
const labelId = useId();
return (
<Surface
testID={'prompt-dialog'}
style={containerStyle}
key={dialog.key}
elevation={1}
>
<Dialog.Content style={styles.dialogContent}>
<Text
variant='bodyMedium'
nativeID={labelId}
>{dialog.message}</Text>
<TextInput
aria-labelledby={labelId}
themeId={themeId}
value={text}
onChangeText={setText}
/>
</Dialog.Content>
<Dialog.Actions
style={styles.dialogActions}
>
<PromptButton
buttonSpec={{
text: _('Cancel'),
onPress: dialog.onDismiss,
}}
themeId={themeId}
/>
<PromptButton
buttonSpec={{
text: _('Okay'),
onPress: () => dialog.onSubmit(text),
}}
themeId={themeId}
/>
</Dialog.Actions>
</Surface>
);
};
export default TextInputDialog;
@@ -1,16 +1,16 @@
import * as React from 'react';
import { Alert, Platform } from 'react-native';
import { DialogControl, DialogType, MenuChoice, PromptButtonSpec, DialogData, PromptOptions } from '../types';
import { DialogControl, DialogType, MenuChoice, PromptButtonSpec, PromptDialogData, PromptOptions } from '../types';
import { _ } from '@joplin/lib/locale';
import { useMemo, useRef } from 'react';
type SetPromptDialogs = React.Dispatch<React.SetStateAction<DialogData[]>>;
type SetPromptDialogs = React.Dispatch<React.SetStateAction<PromptDialogData[]>>;
const useDialogControl = (setPromptDialogs: SetPromptDialogs) => {
const nextDialogIdRef = useRef(0);
const dialogControl: DialogControl = useMemo(() => {
const onDismiss = (dialog: DialogData) => {
const onDismiss = (dialog: PromptDialogData) => {
setPromptDialogs(dialogs => dialogs.filter(d => d !== dialog));
};
@@ -39,8 +39,8 @@ const useDialogControl = (setPromptDialogs: SetPromptDialogs) => {
Alert.alert(title, message, buttons, options);
} else {
const cancelable = options?.cancelable ?? true;
const dialog: DialogData = {
type: DialogType.ButtonPrompt,
const dialog: PromptDialogData = {
type: DialogType.Prompt,
key: `dialog-${nextDialogIdRef.current++}`,
title,
message,
@@ -69,7 +69,7 @@ const useDialogControl = (setPromptDialogs: SetPromptDialogs) => {
return new Promise<T>((resolve) => {
const dismiss = () => onDismiss(dialog);
const dialog: DialogData = {
const dialog: PromptDialogData = {
type: DialogType.Menu,
key: `menu-dialog-${nextDialogIdRef.current++}`,
title: '',
@@ -91,33 +91,6 @@ const useDialogControl = (setPromptDialogs: SetPromptDialogs) => {
});
});
},
promptForText: (message: string) => {
return new Promise<string|null>((resolve) => {
const dismiss = () => {
onDismiss(dialog);
};
const dialog: DialogData = {
type: DialogType.TextInput,
key: `prompt-dialog-${nextDialogIdRef.current++}`,
message,
onSubmit: (text) => {
resolve(text);
dismiss();
},
onDismiss: () => {
resolve(null);
dismiss();
},
};
setPromptDialogs(dialogs => {
return [
...dialogs,
dialog,
];
});
});
},
};
return control;
@@ -5,11 +5,10 @@ import { Portal } from 'react-native-paper';
import Modal from '../Modal';
import shim from '@joplin/lib/shim';
import makeShowMessageBox from '../../utils/makeShowMessageBox';
import { DialogControl, DialogData, DialogType } from './types';
import { DialogControl, PromptDialogData } from './types';
import useDialogControl from './hooks/useDialogControl';
import PromptDialog from './PromptDialog';
import { themeStyle } from '../global-style';
import TextInputDialog from './TextInputDialog';
export type { DialogControl } from './types';
export const DialogContext = createContext<DialogControl>(null);
@@ -19,11 +18,10 @@ interface Props {
children: React.ReactNode;
}
const useStyles = (themeId: number) => {
const useStyles = () => {
const windowSize = useWindowDimensions();
return useMemo(() => {
const theme = themeStyle(themeId);
return StyleSheet.create({
modalContainer: {
marginLeft: 'auto',
@@ -33,20 +31,12 @@ const useStyles = (themeId: number) => {
width: Math.max(windowSize.width / 2, 400),
maxWidth: '100%',
},
dialogContainer: {
backgroundColor: theme.backgroundColor,
borderRadius: theme.borderRadius,
paddingTop: theme.borderRadius,
marginLeft: 4,
marginRight: 4,
},
});
}, [windowSize.width, themeId]);
}, [windowSize.width]);
};
const DialogManager: React.FC<Props> = props => {
const [dialogModels, setPromptDialogs] = useState<DialogData[]>([]);
const [dialogModels, setPromptDialogs] = useState<PromptDialogData[]>([]);
const dialogControl = useDialogControl(setPromptDialogs);
const dialogControlRef = useRef(dialogControl);
@@ -61,34 +51,17 @@ const DialogManager: React.FC<Props> = props => {
}, []);
const theme = themeStyle(props.themeId);
const styles = useStyles(props.themeId);
const styles = useStyles();
const dialogComponents: React.ReactNode[] = [];
for (const dialog of dialogModels) {
const dialogProps = {
containerStyle: styles.dialogContainer,
themeId: props.themeId,
};
if (dialog.type === DialogType.Menu || dialog.type === DialogType.ButtonPrompt) {
dialogComponents.push(
<PromptDialog
dialog={dialog}
{...dialogProps}
key={dialog.key}
/>,
);
} else if (dialog.type === DialogType.TextInput) {
dialogComponents.push(
<TextInputDialog
dialog={dialog}
{...dialogProps}
key={dialog.key}
/>,
);
} else {
const exhaustivenessCheck: never = dialog.type;
throw new Error(`Unexpected dialog type ${exhaustivenessCheck}`);
}
dialogComponents.push(
<PromptDialog
key={dialog.key}
dialog={dialog}
themeId={props.themeId}
/>,
);
}
// Web: Use a <Modal> wrapper for better keyboard focus handling.
@@ -24,18 +24,16 @@ export interface DialogControl {
info(message: string): Promise<void>;
error(message: string): Promise<void>;
prompt(title: string, message: string, buttons?: PromptButtonSpec[], options?: PromptOptions): void;
promptForText(message: string): Promise<string>;
showMenu<IdType>(title: string, choices: MenuChoice<IdType>[]): Promise<IdType>;
}
export enum DialogType {
ButtonPrompt,
Prompt,
Menu,
TextInput,
}
export interface ButtonDialogData {
type: DialogType.ButtonPrompt|DialogType.Menu;
export interface PromptDialogData {
type: DialogType;
key: string;
title: string;
message: string;
@@ -43,13 +41,3 @@ export interface ButtonDialogData {
onDismiss: (()=> void)|null;
}
export interface TextInputDialogData {
type: DialogType.TextInput;
key: string;
message: string;
onSubmit: (text: string)=> void;
onDismiss: ()=> void;
}
export type DialogData = ButtonDialogData | TextInputDialogData;
@@ -1,13 +1,10 @@
import * as React from 'react';
import { FunctionComponent, ReactElement, useCallback, useContext } from 'react';
import { FunctionComponent, ReactElement } from 'react';
import { _ } from '@joplin/lib/locale';
import Folder, { FolderEntityWithChildren } from '@joplin/lib/models/Folder';
import { themeStyle } from './global-style';
import Dropdown, { DropdownListItem, OnValueChangedListener } from './Dropdown';
import { FolderEntity } from '@joplin/lib/services/database/types';
import { View } from 'react-native';
import { Button } from 'react-native-paper';
import { DialogContext } from './DialogManager';
interface FolderPickerProps {
disabled?: boolean;
@@ -19,7 +16,6 @@ interface FolderPickerProps {
darkText?: boolean;
themeId?: number;
coverableChildrenRight?: ReactElement|ReactElement[];
onNewFolder?: (title: string)=> void;
}
@@ -32,7 +28,6 @@ const FolderPicker: FunctionComponent<FolderPickerProps> = ({
placeholder,
darkText,
coverableChildrenRight,
onNewFolder,
themeId,
}) => {
const theme = themeStyle(themeId);
@@ -66,15 +61,7 @@ const FolderPicker: FunctionComponent<FolderPickerProps> = ({
return output;
};
const dialogs = useContext(DialogContext);
const onNewFolderPress = useCallback(async () => {
const title = await dialogs.promptForText(_('New notebook title'));
if (title !== null) {
onNewFolder(title);
}
}, [dialogs, onNewFolder]);
const dropdown = (
return (
<Dropdown
items={titlePickerItems(!!mustSelect)}
accessibilityHint={_('Selects a notebook')}
@@ -101,19 +88,6 @@ const FolderPicker: FunctionComponent<FolderPickerProps> = ({
}}
/>
);
if (onNewFolder) {
return <View style={{ flexDirection: 'column', flex: 1 }}>
{dropdown}
<Button
style={{ alignSelf: 'flex-end' }}
icon='plus'
onPress={onNewFolderPress}
>{_('Create new notebook')}</Button>
</View>;
} else {
return dropdown;
}
};
export default FolderPicker;
+2 -2
View File
@@ -1,6 +1,6 @@
import * as React from 'react';
import { TextStyle, Text, StyleProp } from 'react-native';
import { TextStyle, Text } from 'react-native';
const FontAwesomeIcon = require('react-native-vector-icons/FontAwesome5').default;
const AntIcon = require('react-native-vector-icons/AntDesign').default;
@@ -9,7 +9,7 @@ const Ionicon = require('react-native-vector-icons/Ionicons').default;
interface Props {
name: string;
style: StyleProp<TextStyle>;
style: TextStyle;
// If `null` is given, the content must be labeled elsewhere.
accessibilityLabel: string|null;
@@ -5,8 +5,8 @@
import * as React from 'react';
import { themeStyle } from '@joplin/lib/theme';
import { Theme } from '@joplin/lib/themes/type';
import { useState, useMemo, useCallback, useRef, Ref } from 'react';
import { Text, Pressable, ViewStyle, StyleSheet, LayoutChangeEvent, LayoutRectangle, Animated, AccessibilityState, AccessibilityRole, TextStyle, GestureResponderEvent, Platform, Role, StyleProp, View } from 'react-native';
import { useState, useMemo, useCallback, useRef } from 'react';
import { Text, Pressable, ViewStyle, StyleSheet, LayoutChangeEvent, LayoutRectangle, Animated, AccessibilityState, AccessibilityRole, TextStyle, GestureResponderEvent, Platform, Role } from 'react-native';
import { Menu, MenuOptions, MenuTrigger, renderers } from 'react-native-popup-menu';
import Icon from './Icon';
import AccessibleView from './accessibility/AccessibleView';
@@ -15,13 +15,11 @@ type ButtonClickListener = ()=> void;
interface ButtonProps {
onPress: ButtonClickListener;
pressableRef?: Ref<View>;
// Accessibility label and text shown in a tooltip
description: string;
iconName: string;
iconStyle: StyleProp<TextStyle>;
iconStyle: TextStyle;
themeId: number;
@@ -89,7 +87,6 @@ const IconButton = (props: ButtonProps) => {
const button = (
<Pressable
ref={props.pressableRef}
onPress={props.onPress}
onLongPress={onLongPress}
onPressIn={onPressIn}
+67 -64
View File
@@ -1,88 +1,91 @@
import * as React from 'react';
import { useMemo } from 'react';
import { View, StyleSheet } from 'react-native';
import { ReactNode } from 'react';
import { Text, View, StyleSheet, Button, TextStyle, ViewStyle } from 'react-native';
import { themeStyle } from './global-style';
import { _ } from '@joplin/lib/locale';
import Modal from './Modal';
import { PrimaryButton } from './buttons';
import { _ } from '@joplin/lib/locale';
import { Button } from 'react-native-paper';
interface Props {
themeId: number;
children: React.ReactNode;
ContentComponent: ReactNode;
buttonBarEnabled: boolean;
okTitle: string;
cancelTitle: string;
title: string;
onOkPress: ()=> void;
onCancelPress: ()=> void;
}
const useStyles = (themeId: number) => {
return useMemo(() => {
interface State {
}
class ModalDialog extends React.Component<Props, State> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
private styles_: any;
public constructor(props: Props) {
super(props);
this.styles_ = {};
}
private styles() {
const themeId = this.props.themeId;
const theme = themeStyle(themeId);
return StyleSheet.create({
container: {
borderRadius: 4,
if (this.styles_[themeId]) return this.styles_[themeId];
this.styles_ = {};
const styles: Record<string, ViewStyle|TextStyle> = {
modalContentWrapper: {
flex: 1,
flexDirection: 'column',
backgroundColor: theme.backgroundColor,
maxWidth: 600,
maxHeight: 500,
width: '100%',
height: '100%',
alignSelf: 'center',
marginVertical: 'auto',
flexGrow: 1,
flexShrink: 1,
padding: theme.margin,
borderWidth: 1,
borderColor: theme.dividerColor,
margin: 20,
padding: 10,
borderRadius: 5,
elevation: 10,
},
title: theme.headerStyle,
contentWrapper: {
flexGrow: 1,
flexShrink: 1,
modalContentWrapper2: {
flex: 1,
},
title: { ...theme.normalText, borderBottomWidth: 1,
borderBottomColor: theme.dividerColor,
paddingBottom: 10,
fontWeight: 'bold' },
buttonRow: {
flexDirection: 'row',
justifyContent: 'flex-end',
gap: theme.margin,
marginTop: theme.marginTop,
borderTopWidth: 1,
borderTopColor: theme.dividerColor,
paddingTop: 10,
},
// Ensures that screen-reader-only headings have size (necessary for focusing/reading them).
invisibleHeading: {
flexGrow: 1,
},
});
}, [themeId]);
};
};
const ModalDialog: React.FC<Props> = props => {
const styles = useStyles(props.themeId);
const theme = themeStyle(props.themeId);
this.styles_[themeId] = StyleSheet.create(styles);
return this.styles_[themeId];
}
return (
<Modal
transparent={true}
visible={true}
onRequestClose={null}
containerStyle={styles.container}
backgroundColor={theme.backgroundColorTransparent2}
>
<View style={styles.contentWrapper}>{props.children}</View>
<View style={styles.buttonRow}>
<View
// This heading makes it easier for screen readers to jump to the
// actions list. Without a heading here, it can be difficult to locate the "ok" and "cancel"
// buttons.
role='heading'
aria-label={_('Actions')}
accessible={true}
style={styles.invisibleHeading}
/>
<Button disabled={!props.buttonBarEnabled} onPress={props.onCancelPress}>{props.cancelTitle}</Button>
<PrimaryButton disabled={!props.buttonBarEnabled} onPress={props.onOkPress}>{props.okTitle}</PrimaryButton>
</View>
</Modal>
);
};
public override render() {
const ContentComponent = this.props.ContentComponent;
const buttonBarEnabled = this.props.buttonBarEnabled !== false;
return (
<Modal transparent={true} visible={true} onRequestClose={() => {}} containerStyle={this.styles().modalContentWrapper}>
<Text style={this.styles().title}>{this.props.title}</Text>
<View style={this.styles().modalContentWrapper2}>{ContentComponent}</View>
<View style={this.styles().buttonRow}>
<View style={{ flex: 1 }}>
<Button disabled={!buttonBarEnabled} title={_('OK')} onPress={this.props.onOkPress}></Button>
</View>
<View style={{ flex: 1, marginLeft: 5 }}>
<Button disabled={!buttonBarEnabled} title={_('Cancel')} onPress={this.props.onCancelPress}></Button>
</View>
</View>
</Modal>
);
}
}
export default ModalDialog;
@@ -1,127 +0,0 @@
import * as React from 'react';
import { Ref, useRef, useImperativeHandle, useState, useCallback } from 'react';
import { LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent, ScrollView, ScrollViewProps, View, ViewProps } from 'react-native';
interface RenderEvent<T> {
item: T;
index: number;
}
interface ScrollToOptions {
index: number;
viewPosition: number;
animated: boolean;
}
export interface NestableFlatListControl {
scrollToIndex(options: ScrollToOptions): void;
}
interface CellRendererProps<T> {
index: number;
item: T;
children: React.ReactNode;
}
// For compatibility, these props should be mostly compatible with the
// native FlatList props:
interface Props<T> extends ScrollViewProps {
ref: Ref<NestableFlatListControl>;
data: T[];
itemHeight: number;
CellRendererComponent?: React.ComponentType<CellRendererProps<T>>;
renderItem: (event: RenderEvent<T>)=> React.ReactNode;
keyExtractor: (item: T)=> string;
extraData: unknown;
// Additional props.
// The contentWrapperProps can be used to improve accessibility by
// applying certain content roles to the <View> that directly contains
// the list's content. At least on web, applying these props directly to the ScrollView may
// not work due to additional <View>s added by React Native.
contentWrapperProps?: ViewProps;
}
// This component allows working around restrictions on nesting React Native's built-in
// <FlatList> within <ScrollView>s. For the most part, this component's interface should
// be compatible with the <FlatList> API.
//
// See https://github.com/facebook/react-native/issues/31697.
const NestableFlatList = function<T>({
ref,
itemHeight,
renderItem,
keyExtractor,
data,
CellRendererComponent = React.Fragment,
contentWrapperProps,
...rest
}: Props<T>) {
const scrollViewRef = useRef<ScrollView|null>(null);
const [scroll, setScroll] = useState(0);
const [listHeight, setListHeight] = useState(0);
useImperativeHandle(ref, () => {
return {
scrollToIndex: ({ index, animated, viewPosition }) => {
const offset = Math.max(0, index * itemHeight - viewPosition * listHeight);
scrollViewRef.current.scrollTo({
y: offset,
animated,
});
// onScroll events don't seem to be sent when scrolling with .scrollTo.
// The scroll offset needs to be updated manually:
setScroll(offset);
},
};
}, [itemHeight, listHeight]);
const onScroll = useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => {
setScroll(event.nativeEvent.contentOffset.y);
}, []);
const onLayout = useCallback((event: LayoutChangeEvent) => {
setListHeight(event.nativeEvent.layout.height);
}, []);
const bufferSize = 10;
const visibleStartIndex = Math.floor(scroll / itemHeight);
const visibleEndIndex = Math.ceil((scroll + listHeight) / itemHeight);
const startIndex = Math.max(0, visibleStartIndex - bufferSize);
const maximumIndex = data.length - 1;
const endIndex = Math.min(visibleEndIndex + bufferSize, maximumIndex);
const paddingTop = startIndex * itemHeight;
const paddingBottom = (maximumIndex - endIndex) * itemHeight;
const renderVisibleItems = () => {
const result: React.ReactNode[] = [];
for (let i = startIndex; i <= endIndex; i++) {
result.push(
<CellRendererComponent
index={i}
item={data[i]}
key={keyExtractor(data[i])}
>
{renderItem({ item: data[i], index: i })}
</CellRendererComponent>,
);
}
return result;
};
return <ScrollView
ref={scrollViewRef}
onScroll={onScroll}
onLayout={onLayout}
{...rest}
>
<View {...contentWrapperProps}>
<View style={{ height: paddingTop }}/>
{renderVisibleItems()}
<View style={{ height: paddingBottom }}/>
</View>
</ScrollView>;
};
export default NestableFlatList;
@@ -3,7 +3,7 @@
import CommandService from '@joplin/lib/services/CommandService';
import useEditorCommandHandler from './useEditorCommandHandler';
import commandDeclarations from '../commandDeclarations';
import createTestEditorControl from '@joplin/editor/CodeMirror/testing/createEditorControl';
import createTestEditorControl from '@joplin/editor/CodeMirror/testUtil/createEditorControl';
import { renderHook } from '../../../utils/testing/testingLibrary';
import { defaultState } from '@joplin/lib/reducer';
@@ -0,0 +1,38 @@
import { View, Platform, SafeAreaView, StyleSheet /* , StatusBar */ } from 'react-native';
const React = require('react');
// import DeviceInfo from 'react-native-device-info';
// Untested! This should check if the device has a notch and, if it does, apply
// an extra padding on top of the screen.
const styles = StyleSheet.create({
AndroidSafeArea: {
// Disabled for now because it seems that even when there's a notch the system status bar
// covers it, and thus we should add our own gap.
// Can only test on emulator though
// Fixes: https://github.com/laurent22/joplin/issues/2694
// paddingTop: Platform.OS === 'android' && DeviceInfo.hasNotch() ? StatusBar.currentHeight : 0,
paddingTop: 0,
},
});
function JoplinSafeAreaView(props) {
if (Platform.OS === 'ios') {
return <SafeAreaView {...props}>{props.children}</SafeAreaView>;
} else {
const viewProps = { ...props };
const style = [];
if (viewProps.style) {
style.push(viewProps.style);
delete viewProps.style;
}
style.push(styles.AndroidSafeArea);
return <View style={style} {...viewProps}>{props.children}</View>;
}
}
module.exports = JoplinSafeAreaView;
@@ -1,46 +0,0 @@
import * as React from 'react';
import { View, Platform, StyleSheet } from 'react-native';
import { SafeAreaViewProps } from 'react-native-safe-area-context';
import useSafeAreaPadding from '../utils/hooks/useSafeAreaPadding';
import { useMemo } from 'react';
interface Props extends SafeAreaViewProps {
titleBarUnderlayColor?: string;
}
const useStyles = (titleBarUnderlayColor: string) => {
const padding = useSafeAreaPadding();
return useMemo(() => {
return StyleSheet.create({
titleBarUnderlay: {
height: padding.paddingTop,
position: 'absolute',
left: padding.paddingLeft,
right: padding.paddingRight,
top: 0,
backgroundColor: titleBarUnderlayColor,
},
safeArea: {
...padding,
},
});
}, [titleBarUnderlayColor, padding]);
};
const JoplinSafeAreaView: React.FC<Props> = ({ titleBarUnderlayColor, ...forwardedProps }) => {
const styles = useStyles(titleBarUnderlayColor);
if (Platform.OS !== 'web') {
return <>
{titleBarUnderlayColor ? <View style={styles.titleBarUnderlay}/> : null}
<View
{...forwardedProps}
style={[forwardedProps.style, styles.safeArea]}
>{forwardedProps.children}</View>
</>;
} else {
return <View {...forwardedProps}>{forwardedProps.children}</View>;
}
};
export default JoplinSafeAreaView;
@@ -6,7 +6,6 @@ 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';
interface Props {
wrapperStyle: ViewStyle;
@@ -52,7 +51,6 @@ const WebBetaButton: React.FC<Props> = props => {
<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>
</View>
</DismissibleDialog>
</>
@@ -64,7 +64,6 @@ interface ScreenHeaderProps {
onSaveButtonPress: OnPressCallback;
sortButton_press?: OnPressCallback;
onSearchButtonPress?: OnPressCallback;
onDeleteButtonPress?: OnPressCallback;
showSideMenuButton?: boolean;
showSearchButton?: boolean;
@@ -389,22 +388,6 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const customDeleteButton = (styles: any, onPress: OnPressCallback) => {
return (
<IconButton
onPress={onPress}
description={_('Delete')}
themeId={themeId}
contentWrapperStyle={styles.iconButton}
iconName='fas trash'
iconStyle={styles.topIcon}
/>
);
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const pluginPanelToggleButton = (styles: any, onPress: OnPressCallback) => {
const allPluginViews = Object.values(this.props.plugins).map(plugin => Object.values(plugin.views)).flat();
@@ -627,7 +610,6 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
const showSearchButton = !!this.props.showSearchButton && !this.props.noteSelectionEnabled;
const showContextMenuButton = this.props.showContextMenuButton !== false;
const showBackButton = !!this.props.noteSelectionEnabled || this.props.showBackButton !== false;
const showStandardDeleteButton = !this.props.onDeleteButtonPress && !selectedFolderInTrash && this.props.noteSelectionEnabled;
let backButtonDisabled = !this.props.historyCanGoBack;
if (this.props.noteSelectionEnabled) backButtonDisabled = false;
@@ -639,8 +621,7 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
const betaIconComp = betaIconButton();
const selectAllButtonComp = !showSelectAllButton ? null : selectAllButton(this.styles(), () => this.selectAllButton_press());
const searchButtonComp = !showSearchButton ? null : searchButton(this.styles(), () => this.searchButton_press());
const customDeleteButtonComp = this.props.onDeleteButtonPress ? customDeleteButton(this.styles(), this.props.onDeleteButtonPress) : null;
const deleteButtonComp = showStandardDeleteButton ? deleteButton(this.styles(), () => this.deleteButton_press(), headerItemDisabled) : null;
const deleteButtonComp = !selectedFolderInTrash && this.props.noteSelectionEnabled ? deleteButton(this.styles(), () => this.deleteButton_press(), headerItemDisabled) : null;
const restoreButtonComp = selectedFolderInTrash && this.props.noteSelectionEnabled ? restoreButton(this.styles(), () => this.restoreButton_press(), headerItemDisabled) : null;
const duplicateButtonComp = !selectedFolderInTrash && this.props.noteSelectionEnabled ? duplicateButton(this.styles(), () => this.duplicateButton_press(), headerItemDisabled) : null;
const sortButtonComp = !this.props.noteSelectionEnabled && this.props.sortButton_press ? sortButton(this.styles(), () => this.props.sortButton_press()) : null;
@@ -692,7 +673,6 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
{selectAllButtonComp}
{searchButtonComp}
{deleteButtonComp}
{customDeleteButtonComp}
{restoreButtonComp}
{duplicateButtonComp}
{sortButtonComp}
@@ -1,87 +0,0 @@
import * as React from 'react';
import TextInput from './TextInput';
import { View, StyleSheet, TextInputProps, ViewStyle, TextInput as ReactNativeTextInput } from 'react-native';
import { _ } from '@joplin/lib/locale';
import { Ref, useCallback, useMemo } from 'react';
import { themeStyle } from './global-style';
import IconButton from './IconButton';
import Icon from './Icon';
interface SearchInputProps extends TextInputProps {
inputRef?: Ref<ReactNativeTextInput>;
value: string;
onChangeText: (text: string)=> void;
themeId: number;
containerStyle: ViewStyle;
}
const useStyles = (themeId: number, hasContent: boolean) => {
return useMemo(() => {
const theme = themeStyle(themeId);
return StyleSheet.create({
root: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
},
inputStyle: {
fontSize: theme.fontSize,
flexGrow: 1,
borderWidth: 0,
borderBlockColor: 'transparent',
paddingLeft: 0,
paddingRight: 0,
paddingTop: 0,
paddingBottom: 0,
},
closeButton: hasContent ? {} : {
opacity: 0,
},
icon: {
color: theme.colorFaded,
fontSize: theme.fontSizeLarger,
width: 32,
verticalAlign: 'middle',
textAlign: 'center',
alignContent: 'center',
},
});
}, [themeId, hasContent]);
};
const SearchInput: React.FC<SearchInputProps> = ({ inputRef, themeId, value, containerStyle, style, onChangeText, ...rest }) => {
const styles = useStyles(themeId, !!value);
const onClear = useCallback(() => {
onChangeText('');
}, [onChangeText]);
return <View style={[styles.root, containerStyle]}>
<Icon
aria-hidden={true}
name='material magnify'
accessibilityLabel={null}
style={styles.icon}
/>
<TextInput
ref={inputRef}
style={[styles.inputStyle, style]}
themeId={themeId}
value={value}
onChangeText={onChangeText}
underlineColorAndroid='transparent'
{...rest}
/>
<IconButton
iconName='material close'
onPress={onClear}
description={_('Clear search')}
disabled={value.length === 0}
iconStyle={[styles.icon, styles.closeButton]}
themeId={themeId}
/>
</View>;
};
export default SearchInput;
@@ -1,123 +0,0 @@
import * as React from 'react';
import { act, fireEvent, render, screen } from '@testing-library/react-native';
import createMockReduxStore from '../utils/testing/createMockReduxStore';
import TestProviderStack from './testing/TestProviderStack';
import { TagEntity } from '@joplin/lib/services/database/types';
import { useEffect, useState } from 'react';
import TagEditor, { TagEditorMode } from './TagEditor';
import Setting from '@joplin/lib/models/Setting';
interface WrapperProps {
allTags: TagEntity[];
initialTags: string[];
onTagsChanged: (tags: string[])=> void;
}
const emptyStyle = {};
const store = createMockReduxStore();
const WrappedTagEditor: React.FC<WrapperProps> = props => {
const [tags, setTags] = useState(props.initialTags);
useEffect(() => {
props.onTagsChanged(tags);
}, [tags, props.onTagsChanged]);
return <TestProviderStack store={store}>
<TagEditor
themeId={Setting.THEME_LIGHT}
style={emptyStyle}
onTagsChange={setTags}
mode={TagEditorMode.Large}
allTags={props.allTags}
tags={tags}
/>
</TestProviderStack>;
};
describe('TagEditor', () => {
beforeEach(() => {
jest.useFakeTimers({ advanceTimers: true });
});
test('clicking "remove" should remove a tag', async () => {
const initialTags = ['test', 'testing'];
let currentTags = initialTags;
const onTagsChanged = (tags: string[]) => {
currentTags = tags;
};
const { unmount } = render(
<WrappedTagEditor
allTags={initialTags.map(t => ({ title: t }))}
initialTags={initialTags}
onTagsChanged={onTagsChanged}
/>,
);
const removeButton = screen.getByRole('button', { name: 'Remove testing' });
fireEvent.press(removeButton);
await act(async () => {
jest.advanceTimersByTime(200);
await jest.runAllTimersAsync();
});
expect(currentTags).toEqual(['test']);
// Manually unmount to prevent warnings
unmount();
});
test('clicking on search result should add it as a tag', () => {
const initialTags = ['test'];
let currentTags = initialTags;
const onTagsChanged = (tags: string[]) => {
currentTags = tags;
};
const { unmount } = render(
<WrappedTagEditor
allTags={[...initialTags, 'new tag 1'].map(t => ({ title: t }))}
initialTags={initialTags}
onTagsChanged={onTagsChanged}
/>,
);
const searchInput = screen.getByPlaceholderText('Search tags');
fireEvent.changeText(searchInput, 'new');
const searchResult = screen.getByRole('button', { name: 'new tag 1' });
fireEvent.press(searchResult);
expect(currentTags).toEqual(['test', 'new tag 1']);
// Manually unmount to prevent warnings
unmount();
});
test('searching for a tag and pressing "add new" should add a new tag', () => {
const initialTags = ['test'];
let currentTags = initialTags;
const onTagsChanged = (tags: string[]) => {
currentTags = tags;
};
const { unmount } = render(
<WrappedTagEditor
allTags={initialTags.map(t => ({ title: t }))}
initialTags={initialTags}
onTagsChanged={onTagsChanged}
/>,
);
const searchInput = screen.getByPlaceholderText('Search tags');
fireEvent.changeText(searchInput, 'create');
const addNewButton = screen.getByRole('button', { name: 'Add new' });
fireEvent.press(addNewButton);
expect(currentTags).toEqual(['test', 'create']);
unmount();
});
});
@@ -1,281 +0,0 @@
import * as React from 'react';
import { StyleSheet, View, Text, ScrollView, ViewStyle, Platform, AccessibilityInfo, ScrollViewProps, TextStyle } from 'react-native';
import { _ } from '@joplin/lib/locale';
import { themeStyle } from './global-style';
import ComboBox, { Option } from './ComboBox';
import IconButton from './IconButton';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
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';
export enum TagEditorMode {
Large,
Compact,
}
interface Props {
themeId: number;
tags: string[];
allTags: TagEntity[];
mode: TagEditorMode;
style: ViewStyle;
onTagsChange: (newTags: string[])=> void;
headerStyle?: TextStyle;
searchResultProps?: ScrollViewProps;
}
const useStyles = (themeId: number, headerStyle: TextStyle|undefined) => {
return useMemo(() => {
const theme = themeStyle(themeId);
return StyleSheet.create({
tag: {
borderRadius: 16,
paddingStart: 8,
backgroundColor: theme.backgroundColor3,
color: theme.color3,
flexDirection: 'row',
alignItems: 'center',
gap: 4,
},
tagText: {
color: theme.color3,
fontSize: theme.fontSize,
},
removeTagButton: {
color: theme.color3,
fontSize: theme.fontSize,
padding: 3,
},
tagBoxRoot: {
flexDirection: 'column',
flexGrow: 1,
flexShrink: 1,
},
tagBoxScrollView: {
borderColor: theme.dividerColor,
borderWidth: 1,
borderRadius: 4,
height: 80,
flexShrink: 1,
},
tagBoxContent: {
flexDirection: 'row',
gap: 4,
paddingTop: theme.itemMarginTop,
paddingBottom: theme.itemMarginBottom,
paddingLeft: 4,
paddingRight: 4,
flexWrap: 'wrap',
maxWidth: '100%',
},
header: {
...theme.headerStyle,
fontSize: theme.fontSize,
marginBottom: theme.itemMarginBottom,
...headerStyle,
},
divider: {
marginTop: theme.margin * 1.4,
marginBottom: theme.margin,
backgroundColor: theme.dividerColor,
},
tagSearch: {
flexShrink: 1,
},
noTagsLabel: {
fontSize: theme.fontSize,
color: theme.colorFaded,
},
});
}, [themeId, headerStyle]);
};
type Styles = ReturnType<typeof useStyles>;
interface TagChipProps {
title: string;
themeId: number;
styles: Styles;
onRemove: (title: string)=> void;
autofocus: boolean;
onAutoFocusComplete: ()=> void;
}
const TagCard: React.FC<TagChipProps> = props => {
const onRemove = useCallback(() => {
props.onRemove(props.title);
}, [props.title, props.onRemove]);
const removeButtonRef = useRef<View>(null);
useEffect(() => {
if (props.autofocus) {
focusView('TagEditor::TagCard', removeButtonRef.current);
props.onAutoFocusComplete();
}
}, [props.autofocus, props.onAutoFocusComplete]);
return <View
style={props.styles.tag}
role='listitem'
>
<Text style={props.styles.tagText}>{props.title}</Text>
<IconButton
pressableRef={removeButtonRef}
themeId={props.themeId}
description={_('Remove %s', props.title)}
iconName='fas fa-times-circle'
iconStyle={props.styles.removeTagButton}
onPress={onRemove}
/>
</View>;
};
interface TagsBoxProps {
tags: string[];
autofocusTag: string;
onAutoFocusComplete: ()=> void;
styles: Styles;
themeId: number;
onRemoveTag: (tag: string)=> void;
}
const TagsBox: React.FC<TagsBoxProps> = props => {
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}
/>
));
} else {
return <Text
style={props.styles.noTagsLabel}
>{_('No tags')}</Text>;
}
};
return <View style={props.styles.tagBoxRoot}>
<Text style={props.styles.header} role='heading'>{_('Associated tags:')}</Text>
<ScrollView
style={props.styles.tagBoxScrollView}
// On web, specifying aria-live here announces changes to the associated tags.
// However, on Android (and possibly iOS), this breaks focus behavior:
aria-live={Platform.OS === 'web' ? 'polite' : undefined}
>
<View
// Accessibility: Marking the list of tags as a list seems to prevent focus from jumping
// to the top of the modal after removing a tag.
role='list'
style={props.styles.tagBoxContent}
>
{renderContent()}
</View>
</ScrollView>
</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))
.map((tag): Option => {
const title = tag.title ?? 'Untitled';
return {
title,
icon: null,
accessibilityHint: _('Adds tag'),
willRemoveOnPress: true,
};
});
}, [props.tags, props.allTags]);
const [autofocusTag, setAutofocusTag] = useState('');
const onAutoFocusComplete = useCallback(() => {
// Clear the auto-focus state so that a different view can be auto-focused in the future
setAutofocusTag('');
}, []);
const onAddTag = useCallback((title: string) => {
AccessibilityInfo.announceForAccessibility(_('Added tag: %s', title));
props.onTagsChange([...props.tags, normalizeTag(title)]);
}, [props.tags, props.onTagsChange]);
const onRemoveTag = useCallback(async (title: string) => {
const previousTagIndex = props.tags.indexOf(title);
const targetTag = props.tags[previousTagIndex + 1] ?? props.tags[previousTagIndex - 1];
setAutofocusTag(targetTag);
// Workaround: Delay auto-focusing the next tag. On iOS, a brief delay is required to
// 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.tags, props.onTagsChange]);
const onComboBoxSelect = useCallback((item: { title: string }) => {
onAddTag(item.title);
return { willRemove: true };
}, [onAddTag]);
const allTagsSet = useMemo(() => {
return new Set([
...props.allTags.map(tag => tag.title),
...props.tags,
]);
}, [props.allTags, props.tags]);
const onCanAddTag = useCallback((tag: string) => {
return !allTagsSet.has(normalizeTag(tag));
}, [allTagsSet]);
const showAssociatedTags = props.mode === TagEditorMode.Large || props.tags.length > 0;
return <View style={props.style}>
{showAssociatedTags && <>
<TagsBox
themeId={props.themeId}
styles={styles}
tags={props.tags}
onRemoveTag={onRemoveTag}
autofocusTag={autofocusTag}
onAutoFocusComplete={onAutoFocusComplete}
/>
<Divider style={styles.divider}/>
</>}
<Text style={styles.header} role='heading'>{_('Add tags:')}</Text>
<ComboBox
items={comboBoxItems}
onItemSelected={onComboBoxSelect}
onAddItem={onAddTag}
canAddItem={onCanAddTag}
alwaysExpand={props.mode === TagEditorMode.Large}
style={styles.tagSearch}
placeholder={_('Search tags')}
searchInputProps={{
autoCapitalize: 'none',
}}
searchResultProps={props.searchResultProps}
/>
</View>;
};
export default TagEditor;
+1 -2
View File
@@ -1,10 +1,9 @@
import * as React from 'react';
import { Ref, useMemo } from 'react';
import { useMemo } from 'react';
import { themeStyle } from './global-style';
import { TextInput, TextInputProps, StyleSheet } from 'react-native';
interface Props extends TextInputProps {
ref?: Ref<TextInput>;
themeId: number;
}
@@ -1,16 +1,19 @@
import Logger from '@joplin/utils/Logger';
import FingerprintScanner, { Errors } from 'react-native-fingerprint-scanner';
import { _ } from '@joplin/lib/locale';
import { authenticateAsync } from 'expo-local-authentication';
const logger = Logger.create('biometricAuthenticate');
export default async () => {
logger.info('Authenticate...');
const result = await authenticateAsync({ promptMessage: _('Verify your identity') });
try {
logger.info('Authenticate...');
await FingerprintScanner.authenticate({ description: _('Verify your identity') });
logger.info('Authenticate done');
} catch (error) {
const errorName = (error as Errors).name;
if (result.success === false) {
const errorName = result.error;
if (errorName === 'not_enrolled' || errorName === 'not_available') {
const errorMessage = error.message;
if (errorName === 'FingerprintScannerNotEnrolled' || errorName === 'FingerprintScannerNotAvailable') {
// In that case we skip the check because the device biometric unlock has been disabled
// by the user. It should be safe to skip the check since in order to disable it, they
// must have full access to the phone, and should have to enter their pin. Not skipping
@@ -23,8 +26,13 @@ export default async () => {
// errorMessage = _('Biometric unlock is not setup on the device. Please set it up in order to unlock Joplin. If the device is on lockout, consider switching it off and on to reset biometrics scanning.');
}
throw new Error(_('Could not verify your identity: %s', errorName));
}
logger.info('Authenticate done');
error.message = _('Could not verify your identity: %s', errorMessage);
logger.warn(error);
throw error;
} finally {
FingerprintScanner.release();
}
};
@@ -1,6 +1,6 @@
import Logger from '@joplin/utils/Logger';
import Setting from '@joplin/lib/models/Setting';
import { AuthenticationType, hasHardwareAsync, supportedAuthenticationTypesAsync } from 'expo-local-authentication';
import FingerprintScanner from 'react-native-fingerprint-scanner';
const logger = Logger.create('sensorInfo');
export interface SensorInfo {
@@ -42,30 +42,14 @@ export default async (): Promise<SensorInfo> => {
// `isSensorAvailable()` is pretty much useless. Instead we ask for
// fingerprint when the user turns on the feature and at that point we
// know if the device supports biometrics or not.
//
// 2025-07-10: isSensorAvailable has been replaced with hasHardwareAsync,
// which may be more reliable. However, hasHardwareAsync may return false
// if the user has locked the app with a PIN (rather than biometrics). Test
// carefully when changing this behavior:
const hasSensor = await hasHardwareAsync();
supportedSensors = (await supportedAuthenticationTypesAsync()).map(sensor => {
if (sensor === AuthenticationType.FINGERPRINT) {
return 'Touch ID';
} else if (sensor === AuthenticationType.FACIAL_RECOGNITION) {
return 'Face ID';
} else if (sensor === AuthenticationType.IRIS) {
return 'Iris';
} else {
return 'Other';
}
}).join(',');
const result = await FingerprintScanner.isSensorAvailable();
logger.info('isSensorAvailable result', result);
supportedSensors = result;
logger.info('isSensorAvailable result', hasSensor, supportedSensors);
if (hasSensor) {
if (supportedSensors !== Setting.value('security.biometricsSupportedSensors')) {
if (result) {
if (result !== Setting.value('security.biometricsSupportedSensors')) {
hasChanged = true;
Setting.setValue('security.biometricsSupportedSensors', supportedSensors);
Setting.setValue('security.biometricsSupportedSensors', result);
}
}
} catch (error) {
@@ -42,9 +42,6 @@ const useStyles = (themeId: number) => {
borderRadius: 8,
padding: 8,
},
buttonText: {
textAlign: 'center',
},
});
}, [themeId]);
};
@@ -63,8 +60,7 @@ const LabelledIconButton: React.FC<Props> = ({ title, icon, style, themeId, ...o
<Text
variant='labelMedium'
ellipsizeMode='tail'
numberOfLines={2}
style={styles.buttonText}
numberOfLines={1}
>{title}</Text>
</View>
</TouchableRipple>;
@@ -1,164 +0,0 @@
import * as React from 'react';
import { StyleSheet, View } from 'react-native';
import { connect } from 'react-redux';
import { AppState } from '../../../utils/types';
import { useCallback, useMemo, useState } from 'react';
import { themeStyle } from '../../global-style';
import CameraViewMultiPage from '../../CameraView/CameraViewMultiPage';
import { Dispatch } from 'redux';
import ScreenHeader from '../../ScreenHeader';
import { _ } from '@joplin/lib/locale';
import { CameraResult } from '../../CameraView/types';
import NotePreview, { CreateNoteEvent } from './NotePreview';
import shim from '@joplin/lib/shim';
import Note from '@joplin/lib/models/Note';
import Tag from '@joplin/lib/models/Tag';
import { Portal, ProgressBar, Snackbar } from 'react-native-paper';
import useBackHandler from '../../../utils/hooks/useBackHandler';
import Logger from '@joplin/utils/Logger';
import NavService from '@joplin/lib/services/NavService';
const logger = Logger.create('DocumentScanner');
interface Props {
dispatch: Dispatch;
themeId: number;
}
const useStyles = (themeId: number) => {
return useMemo(() => {
const theme = themeStyle(themeId);
return StyleSheet.create({
root: theme.rootStyle,
noRemainingPhotosContainer: {
margin: theme.margin,
gap: theme.margin,
},
progressBarContainer: {
flexGrow: 0,
},
});
}, [themeId]);
};
const DocumentScanner: React.FC<Props> = ({ themeId, dispatch }) => {
const styles = useStyles(themeId);
const [cameraVisible, setCameraVisible] = useState(true);
const [photos, setPhotos] = useState<CameraResult[]>([]);
const [snackbarMessage, setSnackbarMessage] = useState('');
const [creatingNote, setCreatingNote] = useState(false);
useBackHandler(() => {
if (photos.length && !cameraVisible) {
setCameraVisible(true);
return true;
}
return false;
});
const onDeleteLastPhoto = useCallback(() => {
if (photos.length <= 1) {
setCameraVisible(true);
}
setSnackbarMessage('');
setPhotos(photos => {
const result = [...photos];
result.pop();
return result;
});
}, [photos]);
const onCloseScreen = useCallback(() => {
setPhotos([]);
dispatch({ type: 'NAV_BACK' });
}, [dispatch]);
const onCreateNote = useCallback(async (event: CreateNoteEvent) => {
setSnackbarMessage(_('Creating note "%s"...', event.title));
setCreatingNote(true);
try {
const resources = [];
for (const image of photos) {
resources.push(await shim.createResourceFromPath(
image.uri,
{ title: event.title, mime: image.type },
));
}
const note = await Note.save({
title: event.title,
body: resources.map(
(image, index) => `![${_('Photo %d', index + 1)}](:/${image.id})`,
).join('\n\n'),
parent_id: event.parentId,
});
await Tag.setNoteTagsByTitles(note.id, event.tags);
await NavService.go('Note', { noteId: note.id });
} catch (error) {
logger.error('Error creating note', error);
await shim.showErrorDialog(`Failed to create note: ${error}`);
} finally {
setCreatingNote(false);
}
}, [photos]);
const onDismissSnackbar = useCallback(() => {
setSnackbarMessage('');
}, []);
const onHideCamera = useCallback(() => {
setCameraVisible(false);
}, []);
const renderContent = () => {
if (cameraVisible) {
return <CameraViewMultiPage
themeId={themeId}
onCancel={onCloseScreen}
onComplete={onHideCamera}
photos={photos}
onSetPhotos={setPhotos}
onInsertBarcode={()=>{}}
/>;
} else if (photos.length > 0) {
return <>
<ScreenHeader title={_('Note preview')} onDeleteButtonPress={onDeleteLastPhoto}/>
{creatingNote && <View style={styles.progressBarContainer}>
<ProgressBar visible indeterminate aria-label={_('Creating note.')}/>
</View>}
<NotePreview
imageCount={photos.length}
lastImage={photos[photos.length - 1]}
onCreateNote={creatingNote ? null : onCreateNote}
/>
</>;
} else {
// Error/loading state
return <>
<ScreenHeader title={'Document scanner'}/>
</>;
}
};
return <View style={styles.root}>
{renderContent()}
<Portal>
<Snackbar
key={`snackbar--${snackbarMessage}`}
visible={!!snackbarMessage}
onDismiss={onDismissSnackbar}
action={{
label: _('Dismiss'),
onPress: onDismissSnackbar,
}}
>{snackbarMessage}</Snackbar>
</Portal>
</View>;
};
export default connect((state: AppState) => ({
themeId: state.settings.theme,
}))(DocumentScanner);
@@ -1,178 +0,0 @@
import * as React from 'react';
import { useMemo, useState, useCallback, useEffect } from 'react';
import { themeStyle } from '../../global-style';
import { ScrollView, StyleSheet, View } from 'react-native';
import { CameraResult } from '../../CameraView/types';
import TextInput from '../../TextInput';
import PhotoPreview from '../../CameraView/PhotoPreview';
import TagEditor, { TagEditorMode } from '../../TagEditor';
import { AppState } from '../../../utils/types';
import { connect } from 'react-redux';
import { FolderEntity, TagEntity } from '@joplin/lib/services/database/types';
import { _ } from '@joplin/lib/locale';
import FolderPicker from '../../FolderPicker';
import Folder from '@joplin/lib/models/Folder';
import Setting from '@joplin/lib/models/Setting';
import { formatMsToLocal } from '@joplin/utils/time';
import { PrimaryButton } from '../../buttons';
export interface CreateNoteEvent {
title: string;
tags: string[];
parentId: string;
}
type OnCreateNote = (event: CreateNoteEvent)=> void;
interface Props {
themeId: number;
imageCount: number;
lastImage: CameraResult;
allTags: TagEntity[];
allFolders: FolderEntity[];
selectedFolderId: string;
onCreateNote: null|OnCreateNote;
}
const useStyles = (themeId: number) => {
return useMemo(() => {
const theme = themeStyle(themeId);
return StyleSheet.create({
titleInput: {
color: theme.color,
fontSize: theme.fontSize,
},
rootScrollView: {
flex: 1,
width: '100%',
maxWidth: 700,
alignSelf: 'center',
},
photoBackground: {
},
photoLabel: {
},
tagEditor: {
marginHorizontal: theme.margin,
},
tagEditorHeader: {
fontWeight: 'normal',
},
folderPickerLine: {
flexDirection: 'row',
justifyContent: 'space-between',
gap: theme.margin * 2,
margin: theme.margin,
marginBottom: theme.margin * 2,
},
folderPicker: {
flexGrow: 1,
},
actionButton: {
alignSelf: 'flex-end',
margin: theme.margin,
},
});
}, [themeId]);
};
const tagSearchResultsProps = {
// Required on Android when including one <ScrollView> inside another:
nestedScrollEnabled: true,
};
const NotePreview: React.FC<Props> = ({
themeId, lastImage, imageCount, allTags, onCreateNote, allFolders, selectedFolderId: propsSelectedFolderId,
}) => {
const styles = useStyles(themeId);
const [title, setTitle] = useState('');
const [tags, setTags] = useState([]);
const [selectedFolderId, setSelectedFolderId] = useState(propsSelectedFolderId);
const realFolders = useMemo(() => {
return Folder.getRealFolders(allFolders);
}, [allFolders]);
useEffect(() => {
// Don't allow selecting a virtual folder
if (selectedFolderId && realFolders.every(folder => folder.id !== selectedFolderId)) {
setSelectedFolderId('');
}
}, [realFolders, selectedFolderId]);
useEffect(() => {
const template = Setting.value('scanner.titleTemplate');
const date = formatMsToLocal(Date.now(), Setting.value('dateFormat'));
setTitle(
template.replace(/{date}/g, date)
.replace(/{count}/g, `${imageCount}`),
);
}, [imageCount]);
const onNewNote = useCallback(() => {
if (!onCreateNote) return;
onCreateNote({
tags,
title,
parentId: selectedFolderId ?? '',
});
}, [onCreateNote, tags, title, selectedFolderId]);
const onNewFolder = useCallback(async (title: string) => {
const folder = await Folder.save({ title });
setSelectedFolderId(folder.id);
}, []);
return <ScrollView style={styles.rootScrollView}>
<TextInput
style={styles.titleInput}
themeId={themeId}
value={title}
onChangeText={setTitle}
/>
<View style={styles.folderPickerLine}>
<PhotoPreview
source={lastImage}
backgroundStyle={styles.photoBackground}
textStyle={styles.photoLabel}
label={imageCount}
/>
<FolderPicker
themeId={themeId}
darkText
placeholder={_('Select notebook')}
folders={realFolders}
onValueChange={setSelectedFolderId}
selectedFolderId={selectedFolderId}
mustSelect={true}
onNewFolder={onNewFolder}
/>
</View>
<TagEditor
themeId={themeId}
tags={tags}
mode={TagEditorMode.Compact}
allTags={allTags}
style={styles.tagEditor}
onTagsChange={setTags}
headerStyle={styles.tagEditorHeader}
searchResultProps={tagSearchResultsProps}
/>
<PrimaryButton
onPress={onNewNote}
style={styles.actionButton}
disabled={!onCreateNote}
>{_('Create note')}</PrimaryButton>
</ScrollView>;
};
export default connect((state: AppState) => ({
allTags: state.tags,
allFolders: state.folders,
selectedFolderId: state.selectedFolderId,
themeId: state.settings.theme,
}))(NotePreview);
@@ -1,14 +1,15 @@
import * as React from 'react';
import { StyleSheet, View, Text, FlatList, TouchableOpacity, TextInput } from 'react-native';
import { connect } from 'react-redux';
import Tag from '@joplin/lib/models/Tag';
import { _ } from '@joplin/lib/locale';
import { themeStyle } from '../global-style';
import ModalDialog from '../ModalDialog';
import { AppState } from '../../utils/types';
import { TagEntity } from '@joplin/lib/services/database/types';
import TagEditor, { TagEditorMode } from '../TagEditor';
import { _ } from '@joplin/lib/locale';
import { useCallback, useEffect, useState } from 'react';
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
import Icon from '../Icon';
const naturalCompare = require('string-natural-compare');
interface Props {
themeId: number;
@@ -17,57 +18,223 @@ interface Props {
tags: TagEntity[];
}
const NoteTagsDialogComponent: React.FC<Props> = props => {
const [noteId, setNoteId] = useState(props.noteId);
const [savingTags, setSavingTags] = useState(false);
const [noteTags, setNoteTags] = useState<string[]>([]);
interface TagListRecord {
id: string;
title: string;
selected: boolean;
}
useEffect(() => {
if (props.noteId) setNoteId(props.noteId);
}, [props.noteId]);
interface State {
noteTagIds: string[];
tagListData: TagListRecord[];
noteId: string|null;
newTags: string;
savingTags: boolean;
tagFilter: string;
}
const onOkayPress = useCallback(async () => {
setSavingTags(true);
let idCounter = 0;
class NoteTagsDialogComponent extends React.Component<Props, State> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
private styles_: any;
private labelId_ = `tag-input-${idCounter++}`;
public constructor(props: Props) {
super(props);
this.styles_ = {};
this.state = {
noteTagIds: [],
noteId: null,
tagListData: [],
newTags: '',
savingTags: false,
tagFilter: '',
};
}
private noteHasTag(tagId: string) {
for (let i = 0; i < this.state.tagListData.length; i++) {
if (this.state.tagListData[i].id === tagId) return this.state.tagListData[i].selected;
}
return false;
}
private newTagTitles() {
return this.state.newTags
.split(',')
.map(t => t.trim().toLowerCase())
.filter(t => !!t);
}
private tag_press = (tagId: string) => {
const newData = this.state.tagListData.slice();
for (let i = 0; i < newData.length; i++) {
const t = newData[i];
if (t.id === tagId) {
const newTag = { ...t };
newTag.selected = !newTag.selected;
newData[i] = newTag;
break;
}
}
this.setState({ tagListData: newData });
};
private renderTag = (data: { item: TagListRecord }) => {
const tag = data.item;
const hasTag = this.noteHasTag(tag.id);
const iconName = hasTag ? 'ionicon checkbox-outline' : 'ionicon square-outline';
return (
<TouchableOpacity
key={tag.id} onPress={() => this.tag_press(tag.id)}
style={this.styles().tag}
accessibilityRole='checkbox'
accessibilityHint={_('Add tag %s to note', tag.title)}
aria-checked={hasTag}
accessibilityState={{ checked: hasTag }}
>
<View style={this.styles().tagIconText}>
<Icon name={iconName} style={this.styles().tagCheckbox} accessibilityLabel={null} />
<Text style={this.styles().tagText}>{tag.title}</Text>
</View>
</TouchableOpacity>
);
};
private tagKeyExtractor = (tag: TagListRecord) => tag.id;
private okButton_press = async () => {
this.setState({ savingTags: true });
try {
await Tag.setNoteTagsByTitles(noteId, noteTags);
const tagIds = this.state.tagListData.filter(t => t.selected).map(t => t.id);
await Tag.setNoteTagsByIds(this.state.noteId, tagIds);
const extraTitles = this.newTagTitles();
for (let i = 0; i < extraTitles.length; i++) {
await Tag.addNoteTagByTitle(this.state.noteId, extraTitles[i]);
}
} finally {
setSavingTags(false);
this.setState({ savingTags: false });
}
props.onCloseRequested?.();
}, [props.onCloseRequested, noteId, noteTags]);
if (this.props.onCloseRequested) this.props.onCloseRequested();
};
const onCancelPress = useCallback(() => {
props.onCloseRequested?.();
}, [props.onCloseRequested]);
private cancelButton_press = () => {
if (this.props.onCloseRequested) this.props.onCloseRequested();
};
useAsyncEffect(async (event) => {
private filterTags(allTags: TagListRecord[]) {
return allTags.filter((tag) => tag.title.toLowerCase().includes(this.state.tagFilter.toLowerCase()), allTags);
}
public override UNSAFE_componentWillMount() {
const noteId = this.props.noteId;
this.setState({ noteId: noteId });
void this.loadNoteTags(noteId);
}
private async loadNoteTags(noteId: string) {
const tags = await Tag.tagsByNoteId(noteId);
const noteTags = tags.map(t => t.title);
if (!event.cancelled) {
setNoteTags(noteTags);
}
}, [noteId]);
const tagIds = tags.map(t => t.id);
return <ModalDialog
themeId={props.themeId}
onOkPress={onOkayPress}
onCancelPress={onCancelPress}
buttonBarEnabled={!savingTags}
okTitle={_('Apply')}
cancelTitle={_('Cancel')}
>
<TagEditor
themeId={props.themeId}
tags={noteTags}
allTags={props.tags}
onTagsChange={setNoteTags}
mode={TagEditorMode.Large}
style={{ flex: 1 }}
/>
</ModalDialog>;
};
const tagListData = this.props.tags.map(tag => {
return {
id: tag.id,
title: tag.title,
selected: tagIds.indexOf(tag.id) >= 0,
};
});
tagListData.sort((a, b) => {
if (a.selected === b.selected) return naturalCompare(a.title, b.title, { caseInsensitive: true });
else if (b.selected === true) return 1;
else return -1;
});
this.setState({ tagListData: tagListData });
}
private styles() {
const themeId = this.props.themeId;
const theme = themeStyle(themeId);
if (this.styles_[themeId]) return this.styles_[themeId];
this.styles_ = {};
const styles = StyleSheet.create({
tag: {
padding: 10,
borderBottomWidth: 1,
borderBottomColor: theme.dividerColor,
},
tagIconText: {
flexDirection: 'row',
alignItems: 'center',
},
tagText: { ...theme.normalText },
tagCheckbox: {
marginRight: 8,
fontSize: 20,
color: theme.color,
},
tagBox: {
flexDirection: 'row',
alignItems: 'center',
paddingLeft: 10,
paddingRight: 10,
borderBottomWidth: 1,
borderBottomColor: theme.dividerColor,
},
newTagBoxLabel: { ...theme.normalText, marginRight: 8 },
tagBoxInput: { ...theme.lineInput, flex: 1 },
});
this.styles_[themeId] = styles;
return this.styles_[themeId];
}
public override render() {
const theme = themeStyle(this.props.themeId);
const dialogContent = (
<View style={{ flex: 1 }}>
<View style={this.styles().tagBox}>
<Text style={this.styles().newTagBoxLabel} nativeID={this.labelId_}>{_('New tags:')}</Text>
<TextInput
selectionColor={theme.textSelectionColor}
keyboardAppearance={theme.keyboardAppearance}
value={this.state.newTags}
onChangeText={value => {
this.setState({ newTags: value });
}}
style={this.styles().tagBoxInput}
placeholder={_('tag1, tag2, ...')}
accessibilityLabelledBy={this.labelId_}
/>
</View>
<View style={this.styles().tagBox}>
<TextInput
selectionColor={theme.textSelectionColor}
keyboardAppearance={theme.keyboardAppearance}
value={this.state.tagFilter}
onChangeText={value => {
this.setState({ tagFilter: value });
}}
placeholder={_('Filter tags')}
style={this.styles().tagBoxInput}
/>
</View>
<FlatList data={this.filterTags(this.state.tagListData)} renderItem={this.renderTag} keyExtractor={this.tagKeyExtractor} />
</View>
);
return <ModalDialog themeId={this.props.themeId} ContentComponent={dialogContent} title={_('Type new tags or select from list')} onOkPress={this.okButton_press} onCancelPress={this.cancelButton_press} buttonBarEnabled={!this.state.savingTags} />;
}
}
const NoteTagsDialog = connect((state: AppState) => {
return {
@@ -10,7 +10,6 @@ import TextButton, { ButtonSize, ButtonType } from '../../buttons/TextButton';
import { useCallback, useMemo, useRef } from 'react';
import Logger from '@joplin/utils/Logger';
import focusView from '../../../utils/focusView';
import NavService from '@joplin/lib/services/NavService';
const logger = Logger.create('NewNoteButton');
@@ -27,8 +26,7 @@ const makeNewNote = (isTodo: boolean, action?: AttachFileAction) => {
const styles = StyleSheet.create({
buttonRow: {
flexDirection: 'row',
flexWrap: 'wrap-reverse',
justifyContent: 'space-between',
flexWrap: 'wrap',
gap: 2,
},
mainButtonRow: {
@@ -36,9 +34,9 @@ const styles = StyleSheet.create({
gap: 12,
},
shortcutButton: {
flexGrow: 0,
flexGrow: 1,
flexShrink: 1,
width: 88,
flexBasis: 0,
},
mainButton: {
flexShrink: 1,
@@ -59,12 +57,9 @@ const styles = StyleSheet.create({
const NewNoteButton: React.FC<Props> = _props => {
const newNoteRef = useRef<View|null>(null);
type ActionType = AttachFileAction|(()=> void);
const renderShortcutButton = (action: ActionType, icon: string, title: string) => {
const actionSource = typeof action === 'function' ? null : action;
action = typeof action === 'function' ? action : () => makeNewNote(false, actionSource);
const renderShortcutButton = (action: AttachFileAction, icon: string, title: string) => {
return <LabelledIconButton
onPress={action}
onPress={() => makeNewNote(false, action)}
style={styles.shortcutButton}
title={title}
accessibilityHint={_('Creates a new note with an attachment of type %s', title)}
@@ -78,7 +73,6 @@ const NewNoteButton: React.FC<Props> = _props => {
{renderShortcutButton(AttachFileAction.RecordAudio, 'material microphone', _('Recording'))}
{renderShortcutButton(AttachFileAction.TakePhoto, 'material camera', _('Camera'))}
{renderShortcutButton(AttachFileAction.AttachDrawing, 'material draw', _('Drawing'))}
{renderShortcutButton(() => NavService.go('DocumentScanner'), 'material data-matrix-scan', _('Scan notebook'))}
</View>
<Divider/>
<View style={[styles.buttonRow, styles.mainButtonRow]}>
@@ -364,7 +364,6 @@
"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/react-native-image-picker/RNImagePickerPrivacyInfo.bundle",
);
name = "[CP] Copy Pods Resources";
outputPaths = (
@@ -396,7 +395,6 @@
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNImagePickerPrivacyInfo.bundle",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
@@ -646,7 +644,9 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
HEADER_SEARCH_PATHS = "$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS\n\n$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS\n\n$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS\n\n$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS\n\n";
HEADER_SEARCH_PATHS = (
"$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS\n\n$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS\n\n$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS\n\n$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS\n\n",
);
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD = "";
LDPLUSPLUS = "";
@@ -670,10 +670,7 @@
"-DFOLLY_CFG_NO_COROUTINES=1",
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
);
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
OTHER_LDFLAGS = "$(inherited) ";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
@@ -724,7 +721,9 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
HEADER_SEARCH_PATHS = "$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS\n\n$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS\n\n$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS\n\n$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS\n\n";
HEADER_SEARCH_PATHS = (
"$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS\n\n$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS\n\n$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS\n\n$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS\n\n",
);
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD = "";
LDPLUSPLUS = "";
@@ -747,10 +746,7 @@
"-DFOLLY_CFG_NO_COROUTINES=1",
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
);
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
OTHER_LDFLAGS = "$(inherited) ";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
USE_HERMES = true;
+39 -61
View File
@@ -1,12 +1,12 @@
PODS:
- boost (1.84.0)
- DoubleConversion (1.1.6)
- EXAV (15.1.7):
- EXAV (15.1.4):
- ExpoModulesCore
- ReactCommon/turbomodule/core
- EXConstants (17.1.7):
- EXConstants (17.1.6):
- ExpoModulesCore
- Expo (53.0.19):
- Expo (53.0.9):
- DoubleConversion
- ExpoModulesCore
- glog
@@ -33,19 +33,17 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- ExpoAsset (11.1.7):
- ExpoAsset (11.1.5):
- ExpoModulesCore
- ExpoCamera (16.1.10):
- ExpoCamera (16.1.6):
- ExpoModulesCore
- ZXingObjC/OneD
- ZXingObjC/PDF417
- ExpoFileSystem (18.1.11):
- ExpoFileSystem (18.1.10):
- ExpoModulesCore
- ExpoFont (13.3.2):
- ExpoFont (13.3.1):
- ExpoModulesCore
- ExpoLocalAuthentication (16.0.5):
- ExpoModulesCore
- ExpoModulesCore (2.4.2):
- ExpoModulesCore (2.3.13):
- DoubleConversion
- glog
- hermes-engine
@@ -1432,7 +1430,9 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- react-native-geolocation (3.4.0):
- react-native-fingerprint-scanner (6.0.0):
- React
- react-native-geolocation (3.3.0):
- DoubleConversion
- glog
- hermes-engine
@@ -1458,7 +1458,7 @@ PODS:
- Yoga
- react-native-get-random-values (1.11.0):
- React-Core
- react-native-image-picker (7.2.3):
- react-native-image-picker (7.1.1):
- DoubleConversion
- glog
- hermes-engine
@@ -1486,7 +1486,7 @@ PODS:
- React-Core
- react-native-netinfo (11.4.1):
- React-Core
- react-native-quick-crypto (0.7.13):
- react-native-quick-crypto (0.7.12):
- DoubleConversion
- glog
- hermes-engine
@@ -1870,13 +1870,13 @@ PODS:
- React-utils (= 0.79.2)
- rn-fetch-blob (0.12.0):
- React-Core
- RNCClipboard (1.16.2):
- RNCClipboard (1.14.3):
- React-Core
- RNCPushNotificationIOS (1.11.0):
- React-Core
- RNDateTimePicker (8.3.0):
- React-Core
- RNDeviceInfo (14.0.4):
- RNDeviceInfo (10.14.0):
- React-Core
- RNExitApp (2.0.0):
- React-Core
@@ -1884,37 +1884,15 @@ PODS:
- React-Core
- RNFS (2.20.0):
- React-Core
- RNLocalize (3.4.1):
- RNLocalize (3.2.1):
- React-Core
- RNQuickAction (0.3.13):
- React
- RNSecureRandom (1.0.1):
- React
- RNShare (12.0.9):
- DoubleConversion
- glog
- hermes-engine
- RCT-Folly (= 2024.11.18.00)
- RCTRequired
- RCTTypeSafety
- RNShare (10.2.1):
- React-Core
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-hermes
- React-ImageManager
- React-jsi
- React-NativeModulesApple
- React-RCTFabric
- React-renderercss
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- RNVectorIcons (10.2.0):
- RNVectorIcons (10.1.0):
- DoubleConversion
- glog
- hermes-engine
@@ -1956,7 +1934,6 @@ DEPENDENCIES:
- ExpoCamera (from `../node_modules/expo-camera/ios`)
- ExpoFileSystem (from `../node_modules/expo-file-system/ios`)
- ExpoFont (from `../node_modules/expo-font/ios`)
- ExpoLocalAuthentication (from `../node_modules/expo-local-authentication/ios`)
- ExpoModulesCore (from `../node_modules/expo-modules-core`)
- fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`)
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
@@ -2000,6 +1977,7 @@ DEPENDENCIES:
- React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`)
- "react-native-alarm-notification (from `../node_modules/@joplin/react-native-alarm-notification`)"
- "react-native-document-picker (from `../node_modules/@react-native-documents/picker`)"
- react-native-fingerprint-scanner (from `../node_modules/react-native-fingerprint-scanner`)
- "react-native-geolocation (from `../node_modules/@react-native-community/geolocation`)"
- react-native-get-random-values (from `../node_modules/react-native-get-random-values`)
- react-native-image-picker (from `../node_modules/react-native-image-picker`)
@@ -2083,8 +2061,6 @@ EXTERNAL SOURCES:
:path: "../node_modules/expo-file-system/ios"
ExpoFont:
:path: "../node_modules/expo-font/ios"
ExpoLocalAuthentication:
:path: "../node_modules/expo-local-authentication/ios"
ExpoModulesCore:
:path: "../node_modules/expo-modules-core"
fast_float:
@@ -2168,6 +2144,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/@joplin/react-native-alarm-notification"
react-native-document-picker:
:path: "../node_modules/@react-native-documents/picker"
react-native-fingerprint-scanner:
:path: "../node_modules/react-native-fingerprint-scanner"
react-native-geolocation:
:path: "../node_modules/@react-native-community/geolocation"
react-native-get-random-values:
@@ -2286,19 +2264,18 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90
DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb
EXAV: ae28256069c4cdde93d185c007d8f68d92902c2e
EXConstants: 98bcf0f22b820f9b28f9fee55ff2daededadd2f8
Expo: 4b1c6de7c441e1caa1918671ae0aa34d51f019a5
ExpoAsset: ef06e880126c375f580d4923fdd1cdf4ee6ee7d6
ExpoCamera: 7edf99216d92e40b991d4e7ed69eba9527c94cda
ExpoFileSystem: 7f92f7be2f5c5ed40a7c9efc8fa30821181d9d63
ExpoFont: cf508bc2e6b70871e05386d71cab927c8524cc8e
ExpoLocalAuthentication: c35f18692dcb35775a1be0f37b2131096951a6bd
ExpoModulesCore: e2c98670a94932b744f5bc4e394520e1c63b5462
EXAV: 6ef85347ee2dba74841f79f8ce3f65a69268bfba
EXConstants: 9f310f44bfedba09087042756802040e464323c0
Expo: 0b5c6afd3f78a88a0f539369d5098a57afff58a7
ExpoAsset: 3bc9adb7dbbf27ae82c18ca97eb988a3ae7e73b1
ExpoCamera: 316809537daad6a5bd2c04b98e0589304beb5551
ExpoFileSystem: c36eb8155eb2381c83dda7dc210e3eec332368b6
ExpoFont: abbb91a911eb961652c2b0a22eef801860425ed6
ExpoModulesCore: 202223696d7cb0c605a01dce4739956f3059db7e
fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6
FBLazyVector: 84b955f7b4da8b895faf5946f73748267347c975
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
glog: eb93e2f488219332457c3c4eafd2738ddc7e80b8
hermes-engine: 314be5250afa5692b57b4dd1705959e1973a8ebe
JoplinCommonShareExtension: a8b60b02704d85a7305627912c0240e94af78db7
JoplinRNShareExtension: e158a4b53ee0aa9cd3037a16221dc8adbd6f7860
@@ -2336,12 +2313,13 @@ SPEC CHECKSUMS:
React-microtasksnativemodule: 054f34e9b82f02bd40f09cebd4083828b5b2beb6
react-native-alarm-notification: fd7c73a3dc15ce2d5bd9b28dfaa5aa2e25850c7b
react-native-document-picker: 3491100f1048571593b284b74b93ce99b14e20a2
react-native-geolocation: ec15ffebc53790314885eb9e5f2132132fbc2600
react-native-fingerprint-scanner: 91bf6825709dd7bd3abc4588a4772eb097a2b2d8
react-native-geolocation: 4636238083f80179c3475e43e0324a7c2a8499ad
react-native-get-random-values: d16467cf726c618e9c7a8c3c39c31faa2244bbba
react-native-image-picker: 99fbcec11cf4679170a7cfba4e4d9f598297448c
react-native-image-picker: f93cd040b48aab8c46b504a848bc994d706d52b0
react-native-image-resizer: 24c5d06fae2176dc0caed4b6396e02befb44064a
react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187
react-native-quick-crypto: 988d8d57cd720dbe218272b60775a8e0210d0b80
react-native-quick-crypto: b8e25bc5d0c241f631f4ddae9cd06107edfc26e8
react-native-rsa-native: a7931cdda1f73a8576a46d7f431378c5550f0c38
react-native-saf-x: 24ebe9aa153f82ec6726de459ae77508d68d5599
react-native-safe-area-context: 9d72abf6d8473da73033b597090a80b709c0b2f1
@@ -2380,18 +2358,18 @@ SPEC CHECKSUMS:
ReactCodegen: c63eda03ba1d94353fb97b031fc84f75a0d125ba
ReactCommon: 76d2dc87136d0a667678668b86f0fca0c16fdeb0
rn-fetch-blob: 25612b6d6f6e980c6f17ed98ba2f58f5696a51ca
RNCClipboard: e1d17c9d093d8129ef50b39b63a17a0e8ccd0ade
RNCClipboard: 7c3e3b5f71d84ef61690ad377b6c50cf27864ff5
RNCPushNotificationIOS: 6c4ca3388c7434e4a662b92e4dfeeee858e6f440
RNDateTimePicker: 29264364ea7b8cc0fb355b3843cf276a4ff78966
RNDeviceInfo: d863506092aef7e7af3a1c350c913d867d795047
RNDeviceInfo: 98bb51ba1519cd3f19f14e7236b5bb1c312c780f
RNExitApp: 4432b9b7cc5ccec9f91c94e507849891282befd4
RNFileViewer: 4b5d83358214347e4ab2d4ca8d5c1c90d869e251
RNFS: 89de7d7f4c0f6bafa05343c578f61118c8282ed8
RNLocalize: 15463c4d79c7da45230064b4adcf5e9bb984667e
RNLocalize: e7378161f0b6a6365407eb2377aab46cc38047d8
RNQuickAction: c2c8f379e614428be0babe4d53a575739667744d
RNSecureRandom: b64d263529492a6897e236a22a2c4249aa1b53dc
RNShare: ef61d9be34bf9881d515851d0710a023cdc4f0f4
RNVectorIcons: d53917643fddb261b22bd6d889776f336893622b
RNShare: 694e19d7f74ac4c04de3a8af0649e9ccc03bd8b1
RNVectorIcons: 59d3cf9496d9358698a7eb14d2fc7038c686764f
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
Yoga: c758bfb934100bb4bf9cbaccb52557cee35e8bdf
ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5
+13 -12
View File
@@ -41,10 +41,9 @@
"crypto-browserify": "3.12.1",
"deprecated-react-native-prop-types": "5.0.0",
"events": "3.3.0",
"expo": "53.0.19",
"expo-av": "15.1.7",
"expo-camera": "16.1.10",
"expo-local-authentication": "16.0.5",
"expo": "53.0.9",
"expo-av": "15.1.4",
"expo-camera": "16.1.6",
"lodash": "4.17.21",
"md5": "2.3.0",
"path-browserify": "1.0.1",
@@ -56,15 +55,16 @@
"react-native-dropdownalert": "5.1.0",
"react-native-exit-app": "2.0.0",
"react-native-file-viewer": "2.1.5",
"react-native-fingerprint-scanner": "6.0.0",
"react-native-fs": "2.20.0",
"react-native-get-random-values": "1.11.0",
"react-native-image-picker": "7.2.3",
"react-native-localize": "3.4.1",
"react-native-modal-datetime-picker": "18.0.0",
"react-native-paper": "5.13.5",
"react-native-paper": "5.13.4",
"react-native-popup-menu": "0.17.0",
"react-native-quick-actions": "0.3.13",
"react-native-quick-crypto": "0.7.13",
"react-native-quick-crypto": "0.7.12",
"react-native-rsa-native": "2.0.5",
"react-native-safe-area-context": "5.4.0",
"react-native-securerandom": "1.0.1",
@@ -94,9 +94,9 @@
"@joplin/tools": "~3.4",
"@js-draw/material-icons": "1.30.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
"@react-native-community/cli": "16.0.3",
"@react-native-community/cli-platform-android": "16.0.3",
"@react-native-community/cli-platform-ios": "16.0.3",
"@react-native-community/cli": "16.0.2",
"@react-native-community/cli-platform-android": "16.0.2",
"@react-native-community/cli-platform-ios": "16.0.2",
"@react-native/babel-preset": "0.79.2",
"@react-native/metro-config": "0.79.2",
"@react-native/typescript-config": "0.79.2",
@@ -104,10 +104,10 @@
"@testing-library/react-native": "13.2.0",
"@types/fs-extra": "11.0.4",
"@types/jest": "29.5.12",
"@types/node": "18.19.87",
"@types/node": "18.19.86",
"@types/react": "19.0.14",
"@types/react-redux": "7.1.33",
"@types/serviceworker": "0.0.132",
"@types/serviceworker": "0.0.127",
"@types/tar-stream": "3.1.3",
"babel-jest": "29.7.0",
"babel-loader": "9.1.3",
@@ -121,7 +121,7 @@
"jetifier": "2.0.0",
"js-draw": "1.30.0",
"jsdom": "25.0.1",
"nodemon": "3.1.10",
"nodemon": "3.1.9",
"punycode": "2.3.1",
"react-dom": "19.0.0",
"react-native-web": "0.20.0",
@@ -145,6 +145,7 @@
"expo": {
"autolinking": {
"exclude": [
"expo-application",
"expo-keep-awake"
]
},
+1 -1
View File
@@ -1,5 +1,5 @@
module.exports = {
hash:"987efee92a78bb62fe4f889c5b38aceb", files: {
hash:"39ce682c4ff5dd85d571d0e99718648f", files: {
'highlight.js/atom-one-dark-reasonable.css': { data: require('./highlight.js/atom-one-dark-reasonable.css.base64.js'), mime: 'text/css', encoding: 'base64' },
'highlight.js/atom-one-light.css': { data: require('./highlight.js/atom-one-light.css.base64.js'), mime: 'text/css', encoding: 'base64' },
'katex/fonts/KaTeX_AMS-Regular.woff2': { data: require('./katex/fonts/KaTeX_AMS-Regular.woff2.base64.js'), mime: 'application/octet-stream', encoding: 'base64' },
@@ -1 +1 @@
module.exports = {"hash":"987efee92a78bb62fe4f889c5b38aceb","files":["highlight.js/atom-one-dark-reasonable.css","highlight.js/atom-one-light.css","katex/fonts/KaTeX_AMS-Regular.woff2","katex/fonts/KaTeX_Caligraphic-Bold.woff2","katex/fonts/KaTeX_Caligraphic-Regular.woff2","katex/fonts/KaTeX_Fraktur-Bold.woff2","katex/fonts/KaTeX_Fraktur-Regular.woff2","katex/fonts/KaTeX_Main-Bold.woff2","katex/fonts/KaTeX_Main-BoldItalic.woff2","katex/fonts/KaTeX_Main-Italic.woff2","katex/fonts/KaTeX_Main-Regular.woff2","katex/fonts/KaTeX_Math-BoldItalic.woff2","katex/fonts/KaTeX_Math-Italic.woff2","katex/fonts/KaTeX_SansSerif-Bold.woff2","katex/fonts/KaTeX_SansSerif-Italic.woff2","katex/fonts/KaTeX_SansSerif-Regular.woff2","katex/fonts/KaTeX_Script-Regular.woff2","katex/fonts/KaTeX_Size1-Regular.woff2","katex/fonts/KaTeX_Size2-Regular.woff2","katex/fonts/KaTeX_Size3-Regular.woff2","katex/fonts/KaTeX_Size4-Regular.woff2","katex/fonts/KaTeX_Typewriter-Regular.woff2","katex/katex.css","mermaid/mermaid.min.js","mermaid/mermaid_render.js"]}
module.exports = {"hash":"39ce682c4ff5dd85d571d0e99718648f","files":["highlight.js/atom-one-dark-reasonable.css","highlight.js/atom-one-light.css","katex/fonts/KaTeX_AMS-Regular.woff2","katex/fonts/KaTeX_Caligraphic-Bold.woff2","katex/fonts/KaTeX_Caligraphic-Regular.woff2","katex/fonts/KaTeX_Fraktur-Bold.woff2","katex/fonts/KaTeX_Fraktur-Regular.woff2","katex/fonts/KaTeX_Main-Bold.woff2","katex/fonts/KaTeX_Main-BoldItalic.woff2","katex/fonts/KaTeX_Main-Italic.woff2","katex/fonts/KaTeX_Main-Regular.woff2","katex/fonts/KaTeX_Math-BoldItalic.woff2","katex/fonts/KaTeX_Math-Italic.woff2","katex/fonts/KaTeX_SansSerif-Bold.woff2","katex/fonts/KaTeX_SansSerif-Italic.woff2","katex/fonts/KaTeX_SansSerif-Regular.woff2","katex/fonts/KaTeX_Script-Regular.woff2","katex/fonts/KaTeX_Size1-Regular.woff2","katex/fonts/KaTeX_Size2-Regular.woff2","katex/fonts/KaTeX_Size3-Regular.woff2","katex/fonts/KaTeX_Size4-Regular.woff2","katex/fonts/KaTeX_Typewriter-Regular.woff2","katex/katex.css","mermaid/mermaid.min.js","mermaid/mermaid_render.js"]}
@@ -1 +1 @@
module.exports = `InVzZSBzdHJpY3QiO3ZhciBfX2VzYnVpbGRfZXNtX21lcm1haWQ9KCgpPT57dmFyIEIyZT1PYmplY3QuY3JlYXRlO3ZhciBieT1PYmplY3QuZGVmaW5lUHJvcGVydHk7dmFyIEYyZT1PYmplY3QuZ2V0T3duUHJvcGVydHlEZXNjcmlwdG9yO3ZhciAkMmU9T2JqZWN0LmdldE93blByb3BlcnR5TmFtZXM7dmFyIHoyZT1PYmplY3QuZ2V0UHJvdG90eXBlT2YsRzJlPU9iamVjdC5wcm90b3R5cGUuaGFzT3duUHJvcGVydHk7dmFyIG89KHQsZSk9PmJ5KHQsIm5hbWUiLHt2YWx1ZTplLGNvbmZpZ3VyYWJsZTohMH0pO3ZhciBOPSh0LGUpPT4oKT0+KHQmJihlPXQodD0wKSksZSk7dmFyIE1pPSh0LGUpPT4oKT0+KGV8fHQoKGU9e2V4cG9ydHM6e319KS5leHBvcnRzLGUpLGUuZXhwb3J0cyksaHI9KHQsZSk9Pntmb3IodmFyIHIgaW4gZSlieSh0LHIse2dldDplW3JdLGVudW1lcmFibGU6ITB9KX0sTDQ9KHQsZSxyLG4pPT57aWYoZSYmdHlwZW9mIGU9PSJvYmplY3QifHx0eXBlb2YgZT09ImZ1bmN0aW9uIilmb3IobGV0IGkgb2YgJDJlKGUpKSFHMmUuY2FsbCh0LGkpJiZpIT09ciYmYnkodCxpLHtnZXQ6KCk9PmVbaV0sZW51bWVyYWJsZTohKG49RjJlKGUsaSkpfHxuLmVudW1lcmFibGV9KTtyZXR1cm4gdH0sU3I9KHQsZSxyKT0+KEw0KHQsZSwiZGVmYXVsdCIpLHImJkw0KHIsZSwiZGVmYXVsdCIpKSxTYT0odCxlLHIpPT4ocj10IT1udWxsP0IyZSh6MmUodCkpOnt9LEw0KGV8fCF0fHwhdC5fX2VzTW9kdWxlP2J5KHIsImRlZmF1bHQiLHt2YWx1ZTp0LGVudW1lcmFibGU6ITB9KTpyLHQpKSxWMmU9dD0+TDQoYnkoe30sIl9fZXNNb2R1bGUiLHt2YWx1ZTohMH0pLHQpO3ZhciBSND1NaSgoRUMsU0MpPT57InVzZSBzdHJpY3QiOyhmdW5jdGlvbih0LGUpe3R5cGVvZiBFQz09Im9iamVjdCImJnR5cGVvZiBTQzwidSI/U0MuZXhwb3J0cz1lKCk6dHlwZW9mIGRlZmluZT09ImZ1bmN0aW9uIiYmZGVmaW5lLmFtZD9kZWZpbmUoZSk6KHQ9dHlwZW9mIGdsb2JhbFRoaXM8InUiP2dsb2JhbFRoaXM6dHx8c2VsZikuZGF5anM9ZSgpfSkoRUMsZnVuY3Rpb24oKXsidXNlIHN0cmljdCI7dmFyIHQ9MWUzLGU9NmU0LHI9MzZlNSxuPSJtaWxsaXNlY29uZCIsaT0ic2Vjb25kIixhPSJtaW51dGUiLHM9ImhvdXIiLGw9ImRheSIsdT0id2VlayIsaD0ibW9udGgiLGY9InF1YXJ0ZXIiLGQ9InllYXIiLHA9ImRhdGUiLG09IkludmFsaWQgRGF0ZSIsZz0vXihcZHs0fSlbLS9dPyhcZHsxLDJ9KT9bLS9dPyhcZHswLDJ9KVtUdFxzXSooXGR7MSwyfSk/Oj8oXGR7MSwyfSk/Oj8oXGR7MSwyfSk/Wy46XT8oXGQrKT8kLyx5PS9cWyhbXlxdXSspXXxZezEsNH18TXsxLDR9fER7MSwyfXxkezEsNH18SHsxLDJ9fGh7MSwyfXxhfEF8bXsxLDJ9fHN7MSwyfXxaezEsMn18U1NTL2csdj17bmFtZToiZW4iLHdlZWtkYXlzOiJTdW5kYXlfTW9uZGF5X1R1ZXNkYXlfV2VkbmVzZGF5X1RodXJzZGF5X0ZyaWRheV9TYXR1cmRheSIuc3BsaXQoIl8iKSxtb250aHM6IkphbnVhcnlfRmVicnVhcnlfTWFyY2hfQXByaWxfTWF5X0p1bmVfSnVseV9BdWd1c3RfU2VwdGVtYmVyX09jdG9iZXJfTm92ZW1iZXJfRGVjZW1iZXIiLnNwbGl0KCJfIiksb3JkaW5hbDpvKGZ1bmN0aW9uKGspe3ZhciBMPVsidGgiLCJzdCIsIm5kIiwicmQiXSxSPWslMTAwO3JldHVybiJbIitrKyhMWyhSLTIwKSUxMF18fExbUl18fExbMF0pKyJdIn0sIm9yZGluYWwiKX0seD1vKGZ1bmN0aW9uKGssTCxSKXt2YXIgTz1TdHJpbmcoayk7cmV0dXJuIU98fE8ubGVuZ3RoPj1MP2s6IiIrQXJyYXkoTCsxLU8ubGVuZ3RoKS5qb2luKFIpK2t9LCJtIiksYj17czp4LHo6byhmdW5jdGlvbihrKXt2YXIgTD0tay51dGNPZmZzZXQoKSxSPU1hdGguYWJzKEwpLE89TWF0aC5mbG9vcihSLzYwKSxNPVIlNjA7cmV0dXJuKEw8PTA/IisiOiItIikreChPLDIsIjAiKSsiOiIreChNLDIsIjAiKX0sInoiKSxtOm8oZnVuY3Rpb24gayhMLFIpe2lmKEwuZGF0ZSgpPFIuZGF0ZSgpKXJldHVybi1rKFIsTCk7dmFyIE89MTIqKFIueWVhcigpLUwueWVhcigpKSsoUi5tb250aCgpLUwubW9udGgoKSksTT1MLmNsb25lKCkuYWRkKE8saCksQj1SLU08MCxGPUwuY2xvbmUoKS5hZGQoTysoQj8tMToxKSxoKTtyZXR1cm4rKC0oTysoUi1NKS8oQj9NLUY6Ri1NKSl8fDApfSwidCIpLGE6byhmdW5jdGlvbihrKXtyZXR1cm4gazwwP01hdGguY2VpbChrKXx8MDpNYXRoLmZsb29yKGspfSwiYSIpLHA6byhmdW5jdGlvbihrKXtyZXR1cm57TTpoLHk6ZCx3OnUsZDpsLEQ6cCxoOnMsbTphLHM6aSxtczpuLFE6Zn1ba118fFN0cmluZyhrfHwiIikudG9Mb3dlckNhc2UoKS5yZXBsYWNlKC9zJC8sIiIpfSwicCIpLHU6byhmdW5jdGlvbihrKXtyZXR1cm4gaz09PXZvaWQgMH0sInUiKX0sdz0iZW4iLEM9e307Q1t3XT12O3ZhciBUPSIkaXNEYXlqc09iamVjdCIsRT1vKGZ1bmN0aW9uKGspe3JldHVybiBrIGluc3RhbmNlb2YgSXx8ISgha3x8IWtbVF0pfSwiUyIpLEE9byhmdW5jdGlvbiBrKEwsUixPKXt2YXIgTTtpZighTClyZXR1cm4gdztpZih0eXBlb2YgTD09InN0cmluZyIpe3ZhciBCPUwudG9Mb3dlckNhc2UoKTtDW0JdJiYoTT1CKSxSJiYoQ1tCXT1SLE09Qik7dmFyIEY9TC5zcGxpdCgiLSIpO2lmKCFNJiZGLmxlbmd0aD4xKXJldHVybiBrKEZbMF0pfWVsc2V7dmFyIFA9TC5uYW1lO0NbUF09TCxNPVB9cmV0dXJuIU8mJk0mJih3PU0pLE18fCFPJiZ3fSwidCIpLFM9byhmdW5jdGlvbihrLEwpe2lmKEUoaykpcmV0dXJuIGsuY2xvbmUoKTt2YXIgUj10eXBlb2YgTD09Im9iamVjdCI/TDp7fTtyZXR1cm4gUi5kYXRlPWssUi5hcmdzPWFyZ3VtZW50cyxuZXcgSShSKX0sIk8iKSxfPWI7Xy5sPUEsXy5pPUUsXy53PWZ1bmN0aW9uKGssTCl7cmV0dXJuIFMoayx7bG9jYWxlOkwuJEwsdXRjOkwuJHUseDpMLiR4LCRvZmZzZXQ6TC4kb2Zmc2V0fSl9O3ZhciBJPWZ1bmN0aW9uKCl7ZnVuY3Rpb24gayhSKXt0aGlzLiRMPUEoUi5sb2NhbGUsbnVsbCwhMCksdGhpcy5wYXJzZShSKSx0aGlzLiR4PXRoaXMuJHh8fFIueHx8e30sdGhpc1tUXT0hMH1vKGssIk0iKTt2YXIgTD1rLnByb3RvdHlwZTtyZXR1cm4gTC5wYXJzZT1mdW5jdGlvbihSKXt0aGlzLiRkPWZ1bmN0aW9uKE8pe3ZhciBNPU8uZGF0ZSxCPU8udXRjO2lmKE09PT1udWxsKXJldHVybiBuZXcgRGF0ZShOYU4pO2lmKF8udShNKSlyZXR1cm4gbmV3IERhdGU7aWYoTSBpbnN0YW5jZW9mIERhdGUpcmV0dXJuIG5ldyBEYXRlKE0pO2lmKHR5cGVvZiBNPT0ic3RyaW5nIiYmIS9aJC9pLnRlc3QoTSkpe3ZhciBGPU0ubWF0Y2goZyk7aWYoRil7dmFyIFA9RlsyXS0xfHwwLHo9KEZbN118fCIwIikuc3Vic3RyaW5nKDAsMyk7cmV0dXJuIEI/bmV3IERhdGUoRGF0ZS5VVEMoRlsxXSxQLEZbM118fDEsRls0XXx8MCxGWzVdfHwwLEZbNl18fDAseikpOm5ldyBEYXRlKEZbMV0sUCxGWzNdfHwxLEZbNF18fDAsRls1XXx8MCxGWzZdfHwwLHopfX1yZXR1cm4gbmV3IERhdGUoTSl9KFIpLHRoaXMuaW5pdCgpfSxMLmluaXQ9ZnVuY3Rpb24oKXt2YXIgUj10aGlzLiRkO3RoaXMuJHk9Ui5nZXRGdWxsWWVhcigpLHRoaXMuJE09Ui5nZXRNb250aCgpLHRoaXMuJEQ9Ui5nZXREYXRlKCksdGhpcy4kVz1SLmdldERheSgpLHRoaXMuJEg9Ui5nZXRIb3VycygpLHRoaXMuJG09Ui5nZXRNaW51dGVzKCksdGhpcy4kcz1SLmdldFNlY29uZHMoKSx0aGlzLiRtcz1SLmdldE1pbGxpc2Vjb25kcygpfSxMLiR1dGlscz1mdW5jdGlvbigpe3JldHVybiBffSxMLmlzVmFsaWQ9ZnVuY Line truncated
module.exports = `InVzZSBzdHJpY3QiO3ZhciBfX2VzYnVpbGRfZXNtX21lcm1haWQ9KCgpPT57dmFyIGN4ZT1PYmplY3QuY3JlYXRlO3ZhciBSMT1PYmplY3QuZGVmaW5lUHJvcGVydHk7dmFyIHV4ZT1PYmplY3QuZ2V0T3duUHJvcGVydHlEZXNjcmlwdG9yO3ZhciBoeGU9T2JqZWN0LmdldE93blByb3BlcnR5TmFtZXM7dmFyIGZ4ZT1PYmplY3QuZ2V0UHJvdG90eXBlT2YsZHhlPU9iamVjdC5wcm90b3R5cGUuaGFzT3duUHJvcGVydHk7dmFyIG89KHQsZSk9PlIxKHQsIm5hbWUiLHt2YWx1ZTplLGNvbmZpZ3VyYWJsZTohMH0pO3ZhciBNPSh0LGUpPT4oKT0+KHQmJihlPXQodD0wKSksZSk7dmFyIE5pPSh0LGUpPT4oKT0+KGV8fHQoKGU9e2V4cG9ydHM6e319KS5leHBvcnRzLGUpLGUuZXhwb3J0cyksdnI9KHQsZSk9Pntmb3IodmFyIHIgaW4gZSlSMSh0LHIse2dldDplW3JdLGVudW1lcmFibGU6ITB9KX0sQ2I9KHQsZSxyLG4pPT57aWYoZSYmdHlwZW9mIGU9PSJvYmplY3QifHx0eXBlb2YgZT09ImZ1bmN0aW9uIilmb3IobGV0IGkgb2YgaHhlKGUpKSFkeGUuY2FsbCh0LGkpJiZpIT09ciYmUjEodCxpLHtnZXQ6KCk9PmVbaV0sZW51bWVyYWJsZTohKG49dXhlKGUsaSkpfHxuLmVudW1lcmFibGV9KTtyZXR1cm4gdH0sRXI9KHQsZSxyKT0+KENiKHQsZSwiZGVmYXVsdCIpLHImJkNiKHIsZSwiZGVmYXVsdCIpKSxrYT0odCxlLHIpPT4ocj10IT1udWxsP2N4ZShmeGUodCkpOnt9LENiKGV8fCF0fHwhdC5fX2VzTW9kdWxlP1IxKHIsImRlZmF1bHQiLHt2YWx1ZTp0LGVudW1lcmFibGU6ITB9KTpyLHQpKSxweGU9dD0+Q2IoUjEoe30sIl9fZXNNb2R1bGUiLHt2YWx1ZTohMH0pLHQpO3ZhciBBYj1OaSgod1MsVFMpPT57InVzZSBzdHJpY3QiOyhmdW5jdGlvbih0LGUpe3R5cGVvZiB3Uz09Im9iamVjdCImJnR5cGVvZiBUUzwidSI/VFMuZXhwb3J0cz1lKCk6dHlwZW9mIGRlZmluZT09ImZ1bmN0aW9uIiYmZGVmaW5lLmFtZD9kZWZpbmUoZSk6KHQ9dHlwZW9mIGdsb2JhbFRoaXM8InUiP2dsb2JhbFRoaXM6dHx8c2VsZikuZGF5anM9ZSgpfSkod1MsZnVuY3Rpb24oKXsidXNlIHN0cmljdCI7dmFyIHQ9MWUzLGU9NmU0LHI9MzZlNSxuPSJtaWxsaXNlY29uZCIsaT0ic2Vjb25kIixhPSJtaW51dGUiLHM9ImhvdXIiLGw9ImRheSIsdT0id2VlayIsaD0ibW9udGgiLGY9InF1YXJ0ZXIiLGQ9InllYXIiLHA9ImRhdGUiLG09IkludmFsaWQgRGF0ZSIsZz0vXihcZHs0fSlbLS9dPyhcZHsxLDJ9KT9bLS9dPyhcZHswLDJ9KVtUdFxzXSooXGR7MSwyfSk/Oj8oXGR7MSwyfSk/Oj8oXGR7MSwyfSk/Wy46XT8oXGQrKT8kLyx5PS9cWyhbXlxdXSspXXxZezEsNH18TXsxLDR9fER7MSwyfXxkezEsNH18SHsxLDJ9fGh7MSwyfXxhfEF8bXsxLDJ9fHN7MSwyfXxaezEsMn18U1NTL2csdj17bmFtZToiZW4iLHdlZWtkYXlzOiJTdW5kYXlfTW9uZGF5X1R1ZXNkYXlfV2VkbmVzZGF5X1RodXJzZGF5X0ZyaWRheV9TYXR1cmRheSIuc3BsaXQoIl8iKSxtb250aHM6IkphbnVhcnlfRmVicnVhcnlfTWFyY2hfQXByaWxfTWF5X0p1bmVfSnVseV9BdWd1c3RfU2VwdGVtYmVyX09jdG9iZXJfTm92ZW1iZXJfRGVjZW1iZXIiLnNwbGl0KCJfIiksb3JkaW5hbDpvKGZ1bmN0aW9uKGspe3ZhciBSPVsidGgiLCJzdCIsIm5kIiwicmQiXSxTPWslMTAwO3JldHVybiJbIitrKyhSWyhTLTIwKSUxMF18fFJbU118fFJbMF0pKyJdIn0sIm9yZGluYWwiKX0seD1vKGZ1bmN0aW9uKGssUixTKXt2YXIgTz1TdHJpbmcoayk7cmV0dXJuIU98fE8ubGVuZ3RoPj1SP2s6IiIrQXJyYXkoUisxLU8ubGVuZ3RoKS5qb2luKFMpK2t9LCJtIiksYj17czp4LHo6byhmdW5jdGlvbihrKXt2YXIgUj0tay51dGNPZmZzZXQoKSxTPU1hdGguYWJzKFIpLE89TWF0aC5mbG9vcihTLzYwKSxOPVMlNjA7cmV0dXJuKFI8PTA/IisiOiItIikreChPLDIsIjAiKSsiOiIreChOLDIsIjAiKX0sInoiKSxtOm8oZnVuY3Rpb24gayhSLFMpe2lmKFIuZGF0ZSgpPFMuZGF0ZSgpKXJldHVybi1rKFMsUik7dmFyIE89MTIqKFMueWVhcigpLVIueWVhcigpKSsoUy5tb250aCgpLVIubW9udGgoKSksTj1SLmNsb25lKCkuYWRkKE8saCksUD1TLU48MCxGPVIuY2xvbmUoKS5hZGQoTysoUD8tMToxKSxoKTtyZXR1cm4rKC0oTysoUy1OKS8oUD9OLUY6Ri1OKSl8fDApfSwidCIpLGE6byhmdW5jdGlvbihrKXtyZXR1cm4gazwwP01hdGguY2VpbChrKXx8MDpNYXRoLmZsb29yKGspfSwiYSIpLHA6byhmdW5jdGlvbihrKXtyZXR1cm57TTpoLHk6ZCx3OnUsZDpsLEQ6cCxoOnMsbTphLHM6aSxtczpuLFE6Zn1ba118fFN0cmluZyhrfHwiIikudG9Mb3dlckNhc2UoKS5yZXBsYWNlKC9zJC8sIiIpfSwicCIpLHU6byhmdW5jdGlvbihrKXtyZXR1cm4gaz09PXZvaWQgMH0sInUiKX0sdz0iZW4iLF89e307X1t3XT12O3ZhciBUPSIkaXNEYXlqc09iamVjdCIsRT1vKGZ1bmN0aW9uKGspe3JldHVybiBrIGluc3RhbmNlb2YgSXx8ISgha3x8IWtbVF0pfSwiUyIpLEw9byhmdW5jdGlvbiBrKFIsUyxPKXt2YXIgTjtpZighUilyZXR1cm4gdztpZih0eXBlb2YgUj09InN0cmluZyIpe3ZhciBQPVIudG9Mb3dlckNhc2UoKTtfW1BdJiYoTj1QKSxTJiYoX1tQXT1TLE49UCk7dmFyIEY9Ui5zcGxpdCgiLSIpO2lmKCFOJiZGLmxlbmd0aD4xKXJldHVybiBrKEZbMF0pfWVsc2V7dmFyIEI9Ui5uYW1lO19bQl09UixOPUJ9cmV0dXJuIU8mJk4mJih3PU4pLE58fCFPJiZ3fSwidCIpLEM9byhmdW5jdGlvbihrLFIpe2lmKEUoaykpcmV0dXJuIGsuY2xvbmUoKTt2YXIgUz10eXBlb2YgUj09Im9iamVjdCI/Ujp7fTtyZXR1cm4gUy5kYXRlPWssUy5hcmdzPWFyZ3VtZW50cyxuZXcgSShTKX0sIk8iKSxBPWI7QS5sPUwsQS5pPUUsQS53PWZ1bmN0aW9uKGssUil7cmV0dXJuIEMoayx7bG9jYWxlOlIuJEwsdXRjOlIuJHUseDpSLiR4LCRvZmZzZXQ6Ui4kb2Zmc2V0fSl9O3ZhciBJPWZ1bmN0aW9uKCl7ZnVuY3Rpb24gayhTKXt0aGlzLiRMPUwoUy5sb2NhbGUsbnVsbCwhMCksdGhpcy5wYXJzZShTKSx0aGlzLiR4PXRoaXMuJHh8fFMueHx8e30sdGhpc1tUXT0hMH1vKGssIk0iKTt2YXIgUj1rLnByb3RvdHlwZTtyZXR1cm4gUi5wYXJzZT1mdW5jdGlvbihTKXt0aGlzLiRkPWZ1bmN0aW9uKE8pe3ZhciBOPU8uZGF0ZSxQPU8udXRjO2lmKE49PT1udWxsKXJldHVybiBuZXcgRGF0ZShOYU4pO2lmKEEudShOKSlyZXR1cm4gbmV3IERhdGU7aWYoTiBpbnN0YW5jZW9mIERhdGUpcmV0dXJuIG5ldyBEYXRlKE4pO2lmKHR5cGVvZiBOPT0ic3RyaW5nIiYmIS9aJC9pLnRlc3QoTikpe3ZhciBGPU4ubWF0Y2goZyk7aWYoRil7dmFyIEI9RlsyXS0xfHwwLCQ9KEZbN118fCIwIikuc3Vic3RyaW5nKDAsMyk7cmV0dXJuIFA/bmV3IERhdGUoRGF0ZS5VVEMoRlsxXSxCLEZbM118fDEsRls0XXx8MCxGWzVdfHwwLEZbNl18fDAsJCkpOm5ldyBEYXRlKEZbMV0sQixGWzNdfHwxLEZbNF18fDAsRls1XXx8MCxGWzZdfHwwLCQpfX1yZXR1cm4gbmV3IERhdGUoTil9KFMpLHRoaXMuaW5pdCgpfSxSLmluaXQ9ZnVuY3Rpb24oKXt2YXIgUz10aGlzLiRkO3RoaXMuJHk9Uy5nZXRGdWxsWWVhcigpLHRoaXMuJE09Uy5nZXRNb250aCgpLHRoaXMuJEQ9Uy5nZXREYXRlKCksdGhpcy4kVz1TLmdldERheSgpLHRoaXMuJEg9Uy5nZXRIb3VycygpLHRoaXMuJG09Uy5nZXRNaW51dGVzKCksdGhpcy4kcz1TLmdldFNlY29uZHMoKSx0aGlzLiRtcz1TLmdldE1pbGxpc2Vjb25kcygpfSxSLiR1dGlscz1mdW5jdGlvbigpe3JldHVybiBBfSxSLmlzVmFsaWQ9ZnVuY Line truncated
+455 -53
View File
@@ -1,32 +1,39 @@
import * as React from 'react';
import shim from '@joplin/lib/shim';
import PerformanceLogger from '@joplin/lib/PerformanceLogger';
shim.setReact(React);
PerformanceLogger.onAppStartBegin();
import setupQuickActions from './setupQuickActions';
import PluginAssetsLoader from './PluginAssetsLoader';
import AlarmService from '@joplin/lib/services/AlarmService';
import Alarm from '@joplin/lib/models/Alarm';
import time from '@joplin/lib/time';
import Logger from '@joplin/utils/Logger';
import Logger, { TargetType } from '@joplin/utils/Logger';
import BaseModel from '@joplin/lib/BaseModel';
import BaseService from '@joplin/lib/services/BaseService';
import ResourceService from '@joplin/lib/services/ResourceService';
import KvStore from '@joplin/lib/services/KvStore';
import NoteScreen from './components/screens/Note/Note';
import UpgradeSyncTargetScreen from './components/screens/UpgradeSyncTargetScreen';
import Setting, { } from '@joplin/lib/models/Setting';
import Setting, { AppType, Env } from '@joplin/lib/models/Setting';
import PoorManIntervals from '@joplin/lib/PoorManIntervals';
import reducer, { NotesParent, serializeNotesParent } from '@joplin/lib/reducer';
import reducer, { NotesParent, parseNotesParent, serializeNotesParent } from '@joplin/lib/reducer';
import ShareExtension, { UnsubscribeShareListener } from './utils/ShareExtension';
import handleShared from './utils/shareHandler';
import uuid from '@joplin/lib/uuid';
import { loadKeychainServiceAndSettings } from '@joplin/lib/services/SettingUtils';
import { _, setLocale } from '@joplin/lib/locale';
import SyncTargetJoplinServer from '@joplin/lib/SyncTargetJoplinServer';
import SyncTargetJoplinCloud from '@joplin/lib/SyncTargetJoplinCloud';
import SyncTargetOneDrive from '@joplin/lib/SyncTargetOneDrive';
import initProfile from '@joplin/lib/services/profileConfig/initProfile';
const VersionInfo = require('react-native-version-info').default;
import { Keyboard, BackHandler, Animated, StatusBar, Platform, Dimensions } from 'react-native';
import { AppState as RNAppState, EmitterSubscription, View, Text, Linking, NativeEventSubscription, Appearance, ActivityIndicator } from 'react-native';
import getResponsiveValue from './components/getResponsiveValue';
import NetInfo, { NetInfoSubscription } from '@react-native-community/netinfo';
const DropdownAlert = require('react-native-dropdownalert').default;
import SafeAreaView from './components/SafeAreaView';
const AlarmServiceDriver = require('./services/AlarmServiceDriver').default;
const SafeAreaView = require('./components/SafeAreaView');
const { connect, Provider } = require('react-redux');
import fastDeepEqual = require('fast-deep-equal');
import { Provider as PaperProvider, MD3DarkTheme, MD3LightTheme } from 'react-native-paper';
@@ -34,8 +41,20 @@ import BackButtonService, { BackButtonHandler } from './services/BackButtonServi
import NavService from '@joplin/lib/services/NavService';
import { createStore, applyMiddleware, Dispatch } from 'redux';
import reduxSharedMiddleware from '@joplin/lib/components/shared/reduxSharedMiddleware';
import shimInit from './utils/shim-init-react';
const { AppNav } = require('./components/app-nav.js');
import Note from '@joplin/lib/models/Note';
import Folder from '@joplin/lib/models/Folder';
import BaseSyncTarget from '@joplin/lib/BaseSyncTarget';
import Resource from '@joplin/lib/models/Resource';
import Tag from '@joplin/lib/models/Tag';
import NoteTag from '@joplin/lib/models/NoteTag';
import BaseItem from '@joplin/lib/models/BaseItem';
import MasterKey from '@joplin/lib/models/MasterKey';
import Revision from '@joplin/lib/models/Revision';
import RevisionService from '@joplin/lib/services/RevisionService';
import JoplinDatabase from '@joplin/lib/JoplinDatabase';
import Database from '@joplin/lib/database';
import NotesScreen from './components/screens/Notes/Notes';
import TagsScreen from './components/screens/tags';
import ConfigScreen from './components/screens/ConfigScreen/ConfigScreen';
@@ -52,8 +71,10 @@ import SideMenuContent from './components/side-menu-content';
import SideMenuContentNote, { SideMenuContentOptions } from './components/SideMenuContentNote';
import { reg } from '@joplin/lib/registry';
import { defaultState } from '@joplin/lib/reducer';
import FileApiDriverLocal from '@joplin/lib/file-api-driver-local';
import ResourceFetcher from '@joplin/lib/services/ResourceFetcher';
import SearchEngine from '@joplin/lib/services/search/SearchEngine';
import WelcomeUtils from '@joplin/lib/WelcomeUtils';
import { themeStyle } from './components/global-style';
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
import SyncTargetFilesystem from '@joplin/lib/SyncTargetFilesystem';
@@ -63,6 +84,7 @@ const SyncTargetDropbox = require('@joplin/lib/SyncTargetDropbox.js');
const SyncTargetAmazonS3 = require('@joplin/lib/SyncTargetAmazonS3.js');
import SyncTargetJoplinServerSAML from '@joplin/lib/SyncTargetJoplinServerSAML';
import BiometricPopup from './components/biometrics/BiometricPopup';
import initLib from '@joplin/lib/initLib';
import { isCallbackUrl, parseCallbackUrl, CallbackUrlCommand } from '@joplin/lib/callbackUrlUtils';
import JoplinCloudLoginScreen from './components/screens/JoplinCloudLoginScreen';
@@ -81,23 +103,42 @@ SyncTargetRegistry.addClass(SyncTargetJoplinServer);
SyncTargetRegistry.addClass(SyncTargetJoplinServerSAML);
SyncTargetRegistry.addClass(SyncTargetJoplinCloud);
import FsDriverRN from './utils/fs-driver/fs-driver-rn';
import DecryptionWorker from '@joplin/lib/services/DecryptionWorker';
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
import MigrationService from '@joplin/lib/services/MigrationService';
import { clearSharedFilesCache } from './utils/ShareUtils';
import setIgnoreTlsErrors from './utils/TlsUtils';
import ShareService from '@joplin/lib/services/share/ShareService';
import setupNotifications from './utils/setupNotifications';
import { loadMasterKeysFromSettings } from '@joplin/lib/services/e2ee/utils';
import { loadMasterKeysFromSettings, migrateMasterPassword } from '@joplin/lib/services/e2ee/utils';
import { setRSA } from '@joplin/lib/services/e2ee/ppk';
import RSA from './services/e2ee/RSA.react-native';
import { runIntegrationTests as runRsaIntegrationTests } from '@joplin/lib/services/e2ee/ppkTestUtils';
import { runIntegrationTests as runCryptoIntegrationTests } from '@joplin/lib/services/e2ee/cryptoTestUtils';
import { Theme, ThemeAppearance } from '@joplin/lib/themes/type';
import ProfileSwitcher from './components/ProfileSwitcher/ProfileSwitcher';
import ProfileEditor from './components/ProfileSwitcher/ProfileEditor';
import sensorInfo, { SensorInfo } from './components/biometrics/sensorInfo';
import { setDispatch } from './services/profiles';
import { getCurrentProfile } from '@joplin/lib/services/profileConfig';
import { getDatabaseName, getPluginDataDir, getProfilesRootDir, getResourceDir, setDispatch } from './services/profiles';
import userFetcher, { initializeUserFetcher } from '@joplin/lib/utils/userFetcher';
import { ReactNode } from 'react';
import { parseShareCache } from '@joplin/lib/services/share/reducer';
import autodetectTheme, { onSystemColorSchemeChange } from './utils/autodetectTheme';
import runOnDeviceFsDriverTests from './utils/fs-driver/runOnDeviceTests';
import PluginRunnerWebView from './components/plugins/PluginRunnerWebView';
import { refreshFolders, scheduleRefreshFolders } from '@joplin/lib/folders-screen-utils';
import KeymapService from '@joplin/lib/services/KeymapService';
import PluginService from '@joplin/lib/services/plugins/PluginService';
import initializeCommandService from './utils/initializeCommandService';
import PlatformImplementation from './services/plugins/PlatformImplementation';
import ShareManager from './components/screens/ShareManager';
import appDefaultState from './utils/appDefaultState';
import appDefaultState, { DEFAULT_ROUTE } from './utils/appDefaultState';
import { setDateFormat, setTimeFormat, setTimeLocale } from '@joplin/utils/time';
import DatabaseDriverReactNative from './utils/database-driver-react-native';
import DialogManager from './components/DialogManager';
import lockToSingleInstance from './utils/lockToSingleInstance';
import { AppState } from './utils/types';
import { getDisplayParentId } from '@joplin/lib/services/trash';
import PluginNotification from './components/plugins/PluginNotification';
@@ -105,12 +146,8 @@ import FocusControl from './components/accessibility/FocusControl/FocusControl';
import SsoLoginScreen from './components/screens/SsoLoginScreen';
import SamlShared from '@joplin/lib/components/shared/SamlShared';
import NoteRevisionViewer from './components/screens/NoteRevisionViewer';
import DocumentScanner from './components/screens/DocumentScanner/DocumentScanner';
import buildStartupTasks from './utils/buildStartupTasks';
import { SafeAreaProvider } from 'react-native-safe-area-context';
const logger = Logger.create('root');
const perfLogger = PerformanceLogger.create('root');
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
let storeDispatch: any = function(_action: any) {};
@@ -242,10 +279,6 @@ const navHistory: any[] = [];
function historyCanGoBackTo(route: any) {
if (route.routeName === 'Folder') return false;
// This is an intermediate screen that acts more like a modal -- it should be skipped in the
// navigation history.
if (route.routeName === 'DocumentScanner') return false;
// There's no point going back to these screens in general and, at least in OneDrive case,
// it can be buggy to do so, due to incorrectly relying on global state (reg.syncTarget...)
if (route.routeName === 'OneDriveLogin') return false;
@@ -432,23 +465,398 @@ const appReducer = (state = appDefaultState, action: any) => {
const store = createStore(appReducer, applyMiddleware(generalMiddleware));
storeDispatch = store.dispatch;
async function initialize(dispatch: Dispatch) {
setDispatch(dispatch);
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
function resourceFetcher_downloadComplete(event: any) {
if (event.encrypted) {
void DecryptionWorker.instance().scheduleStart();
}
}
const startupTasks = buildStartupTasks(dispatch, store);
function decryptionWorker_resourceMetadataButNotBlobDecrypted() {
ResourceFetcher.instance().scheduleAutoAddResources();
}
for (const [name, task] of startupTasks) {
await perfLogger.track(name, async () => {
try {
await task();
} catch (error) {
logger.error(`Startup failure during task: ${name}`);
throw error;
}
});
const initializeTempDir = async () => {
const tempDir = `${getProfilesRootDir()}/tmp`;
// Re-create the temporary directory.
try {
await shim.fsDriver().remove(tempDir);
} catch (_error) {
// The logger may not exist yet. Do nothing.
}
logger.info('Application initialized');
await shim.fsDriver().mkdir(tempDir);
return tempDir;
};
const getInitialActiveFolder = async () => {
let folderId = Setting.value('activeFolderId');
// In some cases (e.g. new profile/install), activeFolderId hasn't been set yet.
// Because activeFolderId is used to determine the parent for new notes, initialize
// it here:
if (!folderId) {
folderId = (await Folder.defaultFolder())?.id;
if (folderId) {
Setting.setValue('activeFolderId', folderId);
}
}
return await Folder.load(folderId);
};
const singleInstanceLock = lockToSingleInstance();
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
async function initialize(dispatch: Dispatch) {
shimInit();
setDispatch(dispatch);
const { profileConfig, isSubProfile } = await initProfile(getProfilesRootDir());
const currentProfile = getCurrentProfile(profileConfig);
dispatch({
type: 'PROFILE_CONFIG_SET',
value: profileConfig,
});
Setting.setConstant('env', __DEV__ ? Env.Dev : Env.Prod);
Setting.setConstant('appId', 'net.cozic.joplin-mobile');
Setting.setConstant('appType', AppType.Mobile);
Setting.setConstant('tempDir', await initializeTempDir());
Setting.setConstant('cacheDir', `${getProfilesRootDir()}/cache`);
const resourceDir = getResourceDir(currentProfile, isSubProfile);
Setting.setConstant('resourceDir', resourceDir);
Setting.setConstant('pluginAssetDir', `${Setting.value('resourceDir')}/pluginAssets`);
Setting.setConstant('pluginDir', `${getProfilesRootDir()}/plugins`);
Setting.setConstant('pluginDataDir', getPluginDataDir(currentProfile, isSubProfile));
await shim.fsDriver().mkdir(resourceDir);
// Do as much setup as possible before checking the lock -- the lock intentionally waits for
// messages from other clients for several hundred ms.
await singleInstanceLock;
const logDatabase = new Database(new DatabaseDriverReactNative());
await logDatabase.open({ name: 'log.sqlite' });
await logDatabase.exec(Logger.databaseCreateTableSql());
const mainLogger = new Logger();
mainLogger.addTarget(TargetType.Database, { database: logDatabase, source: 'm' });
mainLogger.setLevel(Logger.LEVEL_INFO);
if (Setting.value('env') === 'dev') {
mainLogger.addTarget(TargetType.Console);
mainLogger.setLevel(Logger.LEVEL_DEBUG);
}
Logger.initializeGlobalLogger(mainLogger);
initLib(mainLogger);
reg.setLogger(mainLogger);
reg.setShowErrorMessageBoxHandler((message: string) => { alert(message); });
reg.setDispatch(dispatch);
BaseService.logger_ = mainLogger;
// require('@joplin/lib/ntpDate').setLogger(reg.logger());
reg.logger().info('====================================');
reg.logger().info(`Starting application ${Setting.value('appId')} v${VersionInfo.appVersion} (${Setting.value('env')})`);
const dbLogger = new Logger();
dbLogger.addTarget(TargetType.Database, { database: logDatabase, source: 'm' });
if (Setting.value('env') === 'dev') {
dbLogger.addTarget(TargetType.Console);
dbLogger.setLevel(Logger.LEVEL_INFO); // Set to LEVEL_DEBUG for full SQL queries
} else {
dbLogger.setLevel(Logger.LEVEL_INFO);
}
const db = new JoplinDatabase(new DatabaseDriverReactNative());
db.setLogger(dbLogger);
reg.setDb(db);
// reg.dispatch = dispatch;
BaseModel.dispatch = dispatch;
BaseSyncTarget.dispatch = dispatch;
NavService.dispatch = dispatch;
BaseModel.setDb(db);
KvStore.instance().setDb(reg.db());
BaseItem.loadClass('Note', Note);
BaseItem.loadClass('Folder', Folder);
BaseItem.loadClass('Resource', Resource);
BaseItem.loadClass('Tag', Tag);
BaseItem.loadClass('NoteTag', NoteTag);
BaseItem.loadClass('MasterKey', MasterKey);
BaseItem.loadClass('Revision', Revision);
const fsDriver = new FsDriverRN();
Resource.fsDriver_ = fsDriver;
FileApiDriverLocal.fsDriver_ = fsDriver;
AlarmService.setDriver(new AlarmServiceDriver(mainLogger));
AlarmService.setLogger(mainLogger);
// Currently CommandService is just used for plugins.
initializeCommandService(store);
// KeymapService is also present for plugin compatibility
KeymapService.instance().initialize();
// Even if there are no plugins, we need to initialize the PluginService so that
// plugin search can work.
const platformImplementation = PlatformImplementation.instance();
PluginService.instance().initialize(
platformImplementation.versionInfo.version, platformImplementation, null, store,
);
setRSA(RSA);
try {
if (Setting.value('env') === 'prod') {
await db.open({ name: getDatabaseName(currentProfile, isSubProfile) });
} else {
await db.open({ name: getDatabaseName(currentProfile, isSubProfile, '-20240127-1') });
// await db.clearForTesting();
}
reg.logger().info('Database is ready.');
reg.logger().info('Loading settings...');
await loadKeychainServiceAndSettings([]);
await migrateMasterPassword();
if (!Setting.value('clientId')) Setting.setValue('clientId', uuid.create());
reg.logger().info(`Client ID: ${Setting.value('clientId')}`);
BaseItem.syncShareCache = parseShareCache(Setting.value('sync.shareCache'));
if (Setting.value('firstStart')) {
const detectedLocale = shim.detectAndSetLocale(Setting);
reg.logger().info(`First start: detected locale as ${detectedLocale}`);
if (shim.mobilePlatform() === 'web') {
// Web browsers generally have more limited storage than desktop and mobile apps:
Setting.setValue('sync.resourceDownloadMode', 'auto');
// For now, geolocation is disabled by default on web until the web permissions workflow
// is improved. At present, trackLocation=true causes the "allow location access" prompt
// to appear without a clear indicator for why Joplin wants this information.
Setting.setValue('trackLocation', false);
logger.info('First start on web: Set resource download mode to auto and disabled location tracking.');
}
Setting.skipDefaultMigrations();
Setting.setValue('firstStart', false);
} else {
Setting.applyDefaultMigrations();
}
if (Setting.value('env') === Env.Dev) {
// Setting.setValue('sync.10.path', 'https://api.joplincloud.com');
// Setting.setValue('sync.10.userContentPath', 'https://joplinusercontent.com');
Setting.setValue('sync.10.path', 'http://api.joplincloud.local:22300');
Setting.setValue('sync.10.userContentPath', 'http://joplinusercontent.local:22300');
Setting.setValue('sync.10.website', 'http://joplincloud.local:22300');
// Setting.setValue('sync.target', 10);
// Setting.setValue('sync.10.username', 'user1@example.com');
// Setting.setValue('sync.10.password', '111111');
}
if (Setting.value('db.ftsEnabled') === -1) {
const ftsEnabled = await db.ftsEnabled();
Setting.setValue('db.ftsEnabled', ftsEnabled ? 1 : 0);
reg.logger().info('db.ftsEnabled = ', Setting.value('db.ftsEnabled'));
}
if (Setting.value('env') === 'dev') {
Setting.setValue('welcome.enabled', false);
}
await PluginAssetsLoader.instance().importAssets();
// eslint-disable-next-line require-atomic-updates
BaseItem.revisionService_ = RevisionService.instance();
// Note: for now we hard-code the folder sort order as we need to
// create a UI to allow customisation (started in branch mobile_add_sidebar_buttons)
Setting.setValue('folders.sortOrder.field', 'title');
Setting.setValue('folders.sortOrder.reverse', false);
reg.logger().info(`Sync target: ${Setting.value('sync.target')}`);
setLocale(Setting.value('locale'));
if (Platform.OS === 'android') {
const ignoreTlsErrors = Setting.value('net.ignoreTlsErrors');
if (ignoreTlsErrors) {
await setIgnoreTlsErrors(ignoreTlsErrors);
}
}
// ----------------------------------------------------------------
// E2EE SETUP
// ----------------------------------------------------------------
EncryptionService.fsDriver_ = fsDriver;
// eslint-disable-next-line require-atomic-updates
BaseItem.encryptionService_ = EncryptionService.instance();
BaseItem.shareService_ = ShareService.instance();
Resource.shareService_ = ShareService.instance();
DecryptionWorker.instance().dispatch = dispatch;
DecryptionWorker.instance().setLogger(mainLogger);
DecryptionWorker.instance().setKvStore(KvStore.instance());
DecryptionWorker.instance().setEncryptionService(EncryptionService.instance());
await loadMasterKeysFromSettings(EncryptionService.instance());
DecryptionWorker.instance().on('resourceMetadataButNotBlobDecrypted', decryptionWorker_resourceMetadataButNotBlobDecrypted);
// ----------------------------------------------------------------
// / E2EE SETUP
// ----------------------------------------------------------------
await ShareService.instance().initialize(store, EncryptionService.instance());
reg.logger().info('Loading folders...');
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
await refreshFolders((action: any) => dispatch(action), '');
const tags = await Tag.allWithNotes();
dispatch({
type: 'TAG_UPDATE_ALL',
items: tags,
});
// const masterKeys = await MasterKey.all();
// dispatch({
// type: 'MASTERKEY_UPDATE_ALL',
// items: masterKeys,
// });
const folder = await getInitialActiveFolder();
dispatch({
type: 'FOLDER_SET_COLLAPSED_ALL',
ids: Setting.value('collapsedFolderIds'),
});
const notesParent = parseNotesParent(Setting.value('notesParent'), Setting.value('activeFolderId'));
if (notesParent && notesParent.type === 'SmartFilter') {
dispatch(DEFAULT_ROUTE);
} else if (!folder) {
dispatch(DEFAULT_ROUTE);
} else {
dispatch({
type: 'NAV_GO',
routeName: 'Notes',
folderId: folder.id,
});
}
await clearSharedFilesCache();
} catch (error) {
alert(`Initialization error: ${error.message}`);
reg.logger().error('Initialization error:', error);
}
reg.setupRecurrentSync();
initializeUserFetcher();
PoorManIntervals.setInterval(() => { void userFetcher(); }, 1000 * 60 * 60);
PoorManIntervals.setTimeout(() => {
void AlarmService.garbageCollect();
}, 1000 * 60 * 60);
ResourceService.runInBackground();
ResourceFetcher.instance().setFileApi(() => { return reg.syncTarget().fileApi(); });
ResourceFetcher.instance().setLogger(reg.logger());
ResourceFetcher.instance().dispatch = dispatch;
ResourceFetcher.instance().on('downloadComplete', resourceFetcher_downloadComplete);
void ResourceFetcher.instance().start();
SearchEngine.instance().setDb(reg.db());
SearchEngine.instance().setLogger(reg.logger());
SearchEngine.instance().scheduleSyncTables();
await MigrationService.instance().run();
// When the app starts we want the full sync to
// start almost immediately to get the latest data.
// doWifiConnectionCheck set to true so initial sync
// doesn't happen on mobile data
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
void reg.scheduleSync(100, null, true).then(() => {
// Wait for the first sync before updating the notifications, since synchronisation
// might change the notifications.
void AlarmService.updateAllNotifications();
void DecryptionWorker.instance().scheduleStart();
});
await WelcomeUtils.install(Setting.value('locale'), dispatch);
// Collect revisions more frequently on mobile because it doesn't auto-save
// and it cannot collect anything when the app is not active.
RevisionService.instance().runInBackground(1000 * 30);
// ----------------------------------------------------------------------------
// Plugin service setup
// ----------------------------------------------------------------------------
// On startup, we can clear plugin update state -- plugins that were updated when the
// user last ran the app have been updated and will be reloaded.
const pluginService = PluginService.instance();
const pluginSettings = pluginService.unserializePluginSettings(Setting.value('plugins.states'));
const updatedSettings = pluginService.clearUpdateState(pluginSettings);
Setting.setValue('plugins.states', updatedSettings);
// ----------------------------------------------------------------------------
// Keep this below to test react-native-rsa-native
// ----------------------------------------------------------------------------
// const testData = await createTestData();
// await checkTestData(testData);
// const testData = {
// "publicKey": "-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEAoMx9NBioka8DUjO3bKrWMn8uJ23LH1xySogQFR6yh6qbl6i5LKTw\nPgqvv55FUuQtYTMtUTVLggYQhdCBvwbBrD1OqO4xU6Ew7x5/TQKPV3MSgYaps3FF\nOdipC4FyA00jBe6Z1CIpL+ZaSnvjDbMUf5lW8bmfRuXfdBGAcdSBjqm9ttajOws+\n7BBSQ9nI5dnBnWRIVEUb7e9bulgANzM1LMUOE+gaef7T3uKzc+Cx3BhHgw1JdFbL\nZAndYtP52KI5N3oiFM4II26DxmDrO1tQokNM88l5xT0BXPhYiEl1CeBpo5VHZBY2\nRHr4MM/OyAXSUdulsDzbntpE+Y85zv7gpQIDAQAB\n-----END RSA PUBLIC KEY-----",
// "privateKey": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAoMx9NBioka8DUjO3bKrWMn8uJ23LH1xySogQFR6yh6qbl6i5\nLKTwPgqvv55FUuQtYTMtUTVLggYQhdCBvwbBrD1OqO4xU6Ew7x5/TQKPV3MSgYap\ns3FFOdipC4FyA00jBe6Z1CIpL+ZaSnvjDbMUf5lW8bmfRuXfdBGAcdSBjqm9ttaj\nOws+7BBSQ9nI5dnBnWRIVEUb7e9bulgANzM1LMUOE+gaef7T3uKzc+Cx3BhHgw1J\ndFbLZAndYtP52KI5N3oiFM4II26DxmDrO1tQokNM88l5xT0BXPhYiEl1CeBpo5VH\nZBY2RHr4MM/OyAXSUdulsDzbntpE+Y85zv7gpQIDAQABAoIBAEA0Zmm+ztAcyX6x\nF7RUImLXVV55AHntN9V6rrFAKJjzDl1oCUhCM4sSSUqBr7yBT31YKegbF6M7OK21\nq5jS4dIcSKQ7N4bk/dz8mGfvdby9Pc5qLqhvuex3DkiBzzxyOGHN+64wVbHCkJrd\nDLQTpUOtvoGWVHrCno6Bzn+lEnYbvdr0hqI5H4D0ubk6TYed1/4ZlJf0R/o/4jnl\nou0UG2hpJN4ur506cttkZJSTxLjzdO38JuQIAkCEglrMYVY61lBNPxC11Kr3ZN7o\ncm7gWZVyP26KoU27t/g+2FoiBGsWLqGYiuTaqT6dKZ2vHyJGjJIZZStv5ye2Ez8V\nKQwpjQECgYEA3xtwYu4n/G5UjEMumkXHNd/bDamelo1aQvvjkVvxKeASNBqV8cM0\n6Jb2FCuT9Y3mWbFTM0jpqXehpHUOCCnrPKGKnJ0ZS4/SRIrtw0iM6q17fTAqmuOt\nhX0pJ77Il8lVCtx4ItsW+LUGbm6CwotlYLVUuyluhKe0pGw2yafi2N0CgYEAuIFk\ng4p7x0i1LFAlIP0YQ07bJQ0E6FEWbCfMgrV3VjtbnT99EaqPOHhMasITCuoEFlh8\ncgyZ6oH7GEy4IRWrM+Mlm47S+NTrr6KgnTGf570ZAFuqnJac97oFB7BvlQsQot6F\n0L2JKM7dQKIMlvwA9DoXZdKX/9ykiqqIpawNxmkCgYEAuyJOwAw2ads4+3UWT7wb\nfarIF8ugA3OItAqHNFNEEvWpDx8FigVMCZMl0IFE14AwKCc+PBP6OXTolgLAxEQ0\n1WRB2V9D6kc1/Nvy1guydt0QaU7PTZ+O2hrDPF0f74Cl3jhSZBoUSIO+Yz46W2eE\nnvs5mMsFsirgr9E8myRAd9kCgYAGMCDE4KIiHugkolN8dcCYkU58QaGGgSG1YuhT\nAe8Mr1T1QynYq9W92RsHAZdN6GdWsIUL9iw7VzyqpfgO9AEX7mhWfUXKHqoA6/1j\nCEUKqqbqAikIs2x0SoLcrSgw4XwfWkM2qwSsn7N/9W9iqPUHO+OJALUkWawTEoEe\nvVSA8QKBgQCEYCPnxgeQSZkrv7x5soXzgF1YN5EZRa1mTUqPBubs564ZjIIY66mI\nCTaHl7U1cPAhx7mHkSzP/i5NjjLqPZZNOyawWDEEmOzxX69OIzKImb6mEQNyS3do\nI8jnpN5q9pw5TvuEIYSrGqQVnHeaEjSvcT48W9GuzjNVscGfw76fPg==\n-----END RSA PRIVATE KEY-----",
// "plaintext": "just testing",
// "ciphertext": "BfkKLdrmd2UX4sPf0bzhfqrg3rKwH5DS7dPAqdmoQuHlrvEBrYKqheekwpnWQgGggGcm/orlrsQRwlexLv7jfRbb0bMnElkySMu4w6wTxILB66RX9H3vXCz02SwHKFRcuGJxlzTPUC23ki6f/McYJ2n/2L8qYxBO8fncTKutIWV54jY19RS1wQ4IdVDBqzji8D0QsRxUhVlpRk4qxsVnyuoyg9AyDe91LOYKfRc6NdapFij996nKzjxFcKOdBqpis34fN3Cg7avcs2Dm5vi7zlRhyGqJJhORXTU3x6hVwOBkVAisgaB7xS3lHiYp6Fs5tP3hBd0kFwVVx8gALbHsgg=="
// };
// await checkTestData(testData);
// await printTestData();
// ----------------------------------------------------------------------------
// On desktop and CLI we run various tests to check that node-rsa is working
// as expected. On mobile however we cannot run test units directly on
// device, and that's what would be needed to automatically verify
// react-native-rsa-native. So instead we run the tests every time the
// mobile app is started in dev mode. If there's any regression the below
// call will throw an error, alerting us of the issue. Otherwise it will
// just print some messages in the console.
// ----------------------------------------------------------------------------
if (Setting.value('env') === 'dev') {
if (Platform.OS !== 'web') {
await runRsaIntegrationTests();
} else {
logger.info('Skipping encryption tests -- not supported on web.');
}
await runCryptoIntegrationTests();
await runOnDeviceFsDriverTests();
}
reg.logger().info('Application initialized');
}
interface AppComponentProps {
@@ -593,7 +1001,7 @@ class AppComponent extends React.Component<AppComponentProps, AppComponentState>
}
try {
await perfLogger.track('initialize', () => initialize(this.props.dispatch));
await initialize(this.props.dispatch);
} catch (error) {
alert(`Something went wrong while starting the application: ${error}`);
this.props.dispatch({
@@ -669,13 +1077,9 @@ class AppComponent extends React.Component<AppComponentProps, AppComponentState>
);
onSystemColorSchemeChange(Appearance.getColorScheme());
this.quickActionShortcutListener_ = await perfLogger.track('setupQuickActions',
() => setupQuickActions(this.props.dispatch),
);
this.quickActionShortcutListener_ = await setupQuickActions(this.props.dispatch);
await perfLogger.track('setupNotifications',
() => setupNotifications(this.props.dispatch),
);
await setupNotifications(this.props.dispatch);
this.keyboardShowListener_ = Keyboard.addListener('keyboardDidShow', () => {
this.props.dispatch({
@@ -916,7 +1320,6 @@ class AppComponent extends React.Component<AppComponentProps, AppComponentState>
Status: { screen: StatusScreen },
Search: { screen: SearchScreen },
Config: { screen: ConfigScreen },
DocumentScanner: { screen: DocumentScanner },
};
@@ -951,7 +1354,8 @@ class AppComponent extends React.Component<AppComponentProps, AppComponentState>
disableGestures={disableSideMenuGestures}
>
<View style={{ flexGrow: 1, flexShrink: 1, flexBasis: '100%' }}>
<SafeAreaView style={{ flex: 1 }} titleBarUnderlayColor={theme.backgroundColor2}>
<SafeAreaView style={{ flex: 0, backgroundColor: theme.backgroundColor2 }}/>
<SafeAreaView style={{ flex: 1 }}>
<View style={{ flex: 1, backgroundColor: theme.backgroundColor }}>
{ shouldShowMainContent && <AppNav screens={appNavInit} dispatch={this.props.dispatch} /> }
</View>
@@ -1011,19 +1415,17 @@ class AppComponent extends React.Component<AppComponentProps, AppComponentState>
style={{ flex: 1 }}
closeButtonLabel={_('Dismiss')}
>
<SafeAreaProvider>
<FocusControl.MainAppContent style={{ flex: 1 }}>
{shouldShowMainContent ? mainContent : (
<SafeAreaView>
<BiometricPopup
dispatch={this.props.dispatch}
themeId={this.props.themeId}
sensorInfo={this.state.sensorInfo}
/>
</SafeAreaView>
)}
</FocusControl.MainAppContent>
</SafeAreaProvider>
<FocusControl.MainAppContent style={{ flex: 1 }}>
{shouldShowMainContent ? mainContent : (
<SafeAreaView>
<BiometricPopup
dispatch={this.props.dispatch}
themeId={this.props.themeId}
sensorInfo={this.state.sensorInfo}
/>
</SafeAreaView>
)}
</FocusControl.MainAppContent>
</MenuProvider>
</DialogManager>
</PaperProvider>
@@ -1,496 +0,0 @@
import PluginAssetsLoader from '../PluginAssetsLoader';
import AlarmService from '@joplin/lib/services/AlarmService';
import Logger, { TargetType } from '@joplin/utils/Logger';
import BaseModel from '@joplin/lib/BaseModel';
import BaseService from '@joplin/lib/services/BaseService';
import ResourceService from '@joplin/lib/services/ResourceService';
import KvStore from '@joplin/lib/services/KvStore';
import Setting, { AppType, Env } from '@joplin/lib/models/Setting';
import PoorManIntervals from '@joplin/lib/PoorManIntervals';
import { parseNotesParent } from '@joplin/lib/reducer';
import uuid from '@joplin/lib/uuid';
import { loadKeychainServiceAndSettings } from '@joplin/lib/services/SettingUtils';
import { setLocale } from '@joplin/lib/locale';
import SyncTargetJoplinServer from '@joplin/lib/SyncTargetJoplinServer';
import SyncTargetJoplinCloud from '@joplin/lib/SyncTargetJoplinCloud';
import SyncTargetOneDrive from '@joplin/lib/SyncTargetOneDrive';
import initProfile from '@joplin/lib/services/profileConfig/initProfile';
const VersionInfo = require('react-native-version-info').default;
const AlarmServiceDriver = require('../services/AlarmServiceDriver').default;
import NavService from '@joplin/lib/services/NavService';
import { Dispatch, Store } from 'redux';
import shimInit from '../utils/shim-init-react';
import Note from '@joplin/lib/models/Note';
import Folder from '@joplin/lib/models/Folder';
import BaseSyncTarget from '@joplin/lib/BaseSyncTarget';
import Resource from '@joplin/lib/models/Resource';
import Tag from '@joplin/lib/models/Tag';
import NoteTag from '@joplin/lib/models/NoteTag';
import BaseItem from '@joplin/lib/models/BaseItem';
import MasterKey from '@joplin/lib/models/MasterKey';
import Revision from '@joplin/lib/models/Revision';
import RevisionService from '@joplin/lib/services/RevisionService';
import JoplinDatabase from '@joplin/lib/JoplinDatabase';
import Database from '@joplin/lib/database';
import { reg } from '@joplin/lib/registry';
import FileApiDriverLocal from '@joplin/lib/file-api-driver-local';
import ResourceFetcher from '@joplin/lib/services/ResourceFetcher';
import SearchEngine from '@joplin/lib/services/search/SearchEngine';
import WelcomeUtils from '@joplin/lib/WelcomeUtils';
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
import SyncTargetFilesystem from '@joplin/lib/SyncTargetFilesystem';
const SyncTargetNextcloud = require('@joplin/lib/SyncTargetNextcloud.js');
const SyncTargetWebDAV = require('@joplin/lib/SyncTargetWebDAV.js');
const SyncTargetDropbox = require('@joplin/lib/SyncTargetDropbox.js');
const SyncTargetAmazonS3 = require('@joplin/lib/SyncTargetAmazonS3.js');
import SyncTargetJoplinServerSAML from '@joplin/lib/SyncTargetJoplinServerSAML';
import initLib from '@joplin/lib/initLib';
import SyncTargetNone from '@joplin/lib/SyncTargetNone';
const logger = Logger.create('buildStartupTasks');
SyncTargetRegistry.addClass(SyncTargetNone);
SyncTargetRegistry.addClass(SyncTargetOneDrive);
SyncTargetRegistry.addClass(SyncTargetNextcloud);
SyncTargetRegistry.addClass(SyncTargetWebDAV);
SyncTargetRegistry.addClass(SyncTargetDropbox);
SyncTargetRegistry.addClass(SyncTargetFilesystem);
SyncTargetRegistry.addClass(SyncTargetAmazonS3);
SyncTargetRegistry.addClass(SyncTargetJoplinServer);
SyncTargetRegistry.addClass(SyncTargetJoplinServerSAML);
SyncTargetRegistry.addClass(SyncTargetJoplinCloud);
import DecryptionWorker from '@joplin/lib/services/DecryptionWorker';
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
import MigrationService from '@joplin/lib/services/MigrationService';
import { clearSharedFilesCache } from '../utils/ShareUtils';
import setIgnoreTlsErrors from '../utils/TlsUtils';
import ShareService from '@joplin/lib/services/share/ShareService';
import { loadMasterKeysFromSettings, migrateMasterPassword } from '@joplin/lib/services/e2ee/utils';
import { setRSA } from '@joplin/lib/services/e2ee/ppk';
import RSA from '../services/e2ee/RSA.react-native';
import { runIntegrationTests as runRsaIntegrationTests } from '@joplin/lib/services/e2ee/ppkTestUtils';
import { runIntegrationTests as runCryptoIntegrationTests } from '@joplin/lib/services/e2ee/cryptoTestUtils';
import { getCurrentProfile } from '@joplin/lib/services/profileConfig';
import { getDatabaseName, getPluginDataDir, getProfilesRootDir, getResourceDir } from '../services/profiles';
import userFetcher, { initializeUserFetcher } from '@joplin/lib/utils/userFetcher';
import { parseShareCache } from '@joplin/lib/services/share/reducer';
import runOnDeviceFsDriverTests from '../utils/fs-driver/runOnDeviceTests';
import { refreshFolders } from '@joplin/lib/folders-screen-utils';
import KeymapService from '@joplin/lib/services/KeymapService';
import PluginService from '@joplin/lib/services/plugins/PluginService';
import initializeCommandService from '../utils/initializeCommandService';
import PlatformImplementation from '../services/plugins/PlatformImplementation';
import { DEFAULT_ROUTE } from '../utils/appDefaultState';
import DatabaseDriverReactNative from '../utils/database-driver-react-native';
import lockToSingleInstance from '../utils/lockToSingleInstance';
import { AppState } from '../utils/types';
import PerformanceLogger from '@joplin/lib/PerformanceLogger';
import { Profile } from '@joplin/lib/services/profileConfig/types';
import shim from '@joplin/lib/shim';
import { Platform } from 'react-native';
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
function resourceFetcher_downloadComplete(event: any) {
if (event.encrypted) {
void DecryptionWorker.instance().scheduleStart();
}
}
function decryptionWorker_resourceMetadataButNotBlobDecrypted() {
ResourceFetcher.instance().scheduleAutoAddResources();
}
const initializeTempDir = async () => {
const tempDir = `${getProfilesRootDir()}/tmp`;
// Re-create the temporary directory.
try {
await shim.fsDriver().remove(tempDir);
} catch (_error) {
// The logger may not exist yet. Do nothing.
}
await shim.fsDriver().mkdir(tempDir);
return tempDir;
};
const getInitialActiveFolder = async () => {
let folderId = Setting.value('activeFolderId');
// In some cases (e.g. new profile/install), activeFolderId hasn't been set yet.
// Because activeFolderId is used to determine the parent for new notes, initialize
// it here:
if (!folderId) {
folderId = (await Folder.defaultFolder())?.id;
if (folderId) {
Setting.setValue('activeFolderId', folderId);
}
}
return await Folder.load(folderId);
};
const buildStartupTasks = (
dispatch: Dispatch, store: Store<AppState>,
) => {
type StartupTask = [string, ()=> Promise<void>];
const startupTasks: StartupTask[] = [];
const addTask = (name: string, task: ()=> Promise<void>) => {
startupTasks.push([name, task]);
};
let logDatabase: Database;
let db: JoplinDatabase;
let isSubProfile: boolean;
let currentProfile: Profile;
let singleInstanceLock: Promise<void>;
addTask('prepare single instance lock', async () => {
// The single instance lock involves waiting for messages -- start checking
// the lock as early as possible.
singleInstanceLock = lockToSingleInstance();
});
addTask('shimInit', async () => {
shimInit();
});
addTask('initProfile', async () => {
const profile = await initProfile(getProfilesRootDir());
isSubProfile = profile.isSubProfile;
currentProfile = getCurrentProfile(profile.profileConfig);
dispatch({
type: 'PROFILE_CONFIG_SET',
value: profile.profileConfig,
});
});
addTask('set constants', async () => {
Setting.setConstant('env', __DEV__ ? Env.Dev : Env.Prod);
Setting.setConstant('appId', 'net.cozic.joplin-mobile');
Setting.setConstant('appType', AppType.Mobile);
Setting.setConstant('tempDir', await initializeTempDir());
Setting.setConstant('cacheDir', `${getProfilesRootDir()}/cache`);
const resourceDir = getResourceDir(currentProfile, isSubProfile);
Setting.setConstant('resourceDir', resourceDir);
Setting.setConstant('pluginAssetDir', `${Setting.value('resourceDir')}/pluginAssets`);
Setting.setConstant('pluginDir', `${getProfilesRootDir()}/plugins`);
Setting.setConstant('pluginDataDir', getPluginDataDir(currentProfile, isSubProfile));
});
addTask('make resource directory', async () => {
await shim.fsDriver().mkdir(Setting.value('resourceDir'));
});
addTask('singleInstanceLock', async () => {
// Do as much setup as possible before checking the lock -- the lock intentionally waits for
// messages from other clients for several hundred ms.
await singleInstanceLock;
});
addTask('set up logger', async () => {
logDatabase = new Database(new DatabaseDriverReactNative());
await logDatabase.open({ name: 'log.sqlite' });
await logDatabase.exec(Logger.databaseCreateTableSql());
const mainLogger = new Logger();
mainLogger.addTarget(TargetType.Database, { database: logDatabase, source: 'm' });
mainLogger.setLevel(Logger.LEVEL_INFO);
if (Setting.value('env') === 'dev') {
mainLogger.addTarget(TargetType.Console);
mainLogger.setLevel(Logger.LEVEL_DEBUG);
}
Logger.initializeGlobalLogger(mainLogger);
initLib(mainLogger);
reg.setLogger(mainLogger);
BaseService.logger_ = mainLogger;
PerformanceLogger.setLogger(mainLogger);
});
addTask('set up database', async () => {
reg.setShowErrorMessageBoxHandler((message: string) => { alert(message); });
reg.setDispatch(dispatch);
// require('@joplin/lib/ntpDate').setLogger(reg.logger());
reg.logger().info('====================================');
reg.logger().info(`Starting application ${Setting.value('appId')} v${VersionInfo.appVersion} (${Setting.value('env')})`);
const dbLogger = new Logger();
dbLogger.addTarget(TargetType.Database, { database: logDatabase, source: 'm' });
if (Setting.value('env') === 'dev') {
dbLogger.addTarget(TargetType.Console);
dbLogger.setLevel(Logger.LEVEL_INFO); // Set to LEVEL_DEBUG for full SQL queries
} else {
dbLogger.setLevel(Logger.LEVEL_INFO);
}
db = new JoplinDatabase(new DatabaseDriverReactNative());
db.setLogger(dbLogger);
reg.setDb(db);
});
addTask('initialize item classes', async () => {
// reg.dispatch = dispatch;
BaseModel.dispatch = dispatch;
BaseSyncTarget.dispatch = dispatch;
NavService.dispatch = dispatch;
BaseModel.setDb(reg.db());
KvStore.instance().setDb(reg.db());
BaseItem.loadClass('Note', Note);
BaseItem.loadClass('Folder', Folder);
BaseItem.loadClass('Resource', Resource);
BaseItem.loadClass('Tag', Tag);
BaseItem.loadClass('NoteTag', NoteTag);
BaseItem.loadClass('MasterKey', MasterKey);
BaseItem.loadClass('Revision', Revision);
BaseItem.revisionService_ = RevisionService.instance();
Resource.fsDriver_ = shim.fsDriver();
FileApiDriverLocal.fsDriver_ = shim.fsDriver();
});
addTask('initializeAlarmService', async () => {
AlarmService.setLogger(reg.logger());
AlarmService.setDriver(new AlarmServiceDriver(reg.logger()));
});
addTask('openDatabase', async () => {
if (Setting.value('env') === 'prod') {
await db.open({ name: getDatabaseName(currentProfile, isSubProfile) });
} else {
await db.open({ name: getDatabaseName(currentProfile, isSubProfile, '-20240127-1') });
// await db.clearForTesting();
}
});
addTask('setUpSettings', async () => {
await loadKeychainServiceAndSettings([]);
await migrateMasterPassword();
if (!Setting.value('clientId')) Setting.setValue('clientId', uuid.create());
reg.logger().info(`Client ID: ${Setting.value('clientId')}`);
BaseItem.syncShareCache = parseShareCache(Setting.value('sync.shareCache'));
if (Setting.value('firstStart')) {
const detectedLocale = shim.detectAndSetLocale(Setting);
reg.logger().info(`First start: detected locale as ${detectedLocale}`);
if (shim.mobilePlatform() === 'web') {
// Web browsers generally have more limited storage than desktop and mobile apps:
Setting.setValue('sync.resourceDownloadMode', 'auto');
// For now, geolocation is disabled by default on web until the web permissions workflow
// is improved. At present, trackLocation=true causes the "allow location access" prompt
// to appear without a clear indicator for why Joplin wants this information.
Setting.setValue('trackLocation', false);
logger.info('First start on web: Set resource download mode to auto and disabled location tracking.');
}
Setting.skipDefaultMigrations();
Setting.setValue('firstStart', false);
} else {
Setting.applyDefaultMigrations();
}
if (Setting.value('env') === Env.Dev) {
// Setting.setValue('sync.10.path', 'https://api.joplincloud.com');
// Setting.setValue('sync.10.userContentPath', 'https://joplinusercontent.com');
Setting.setValue('sync.10.path', 'http://api.joplincloud.local:22300');
Setting.setValue('sync.10.userContentPath', 'http://joplinusercontent.local:22300');
Setting.setValue('sync.10.website', 'http://joplincloud.local:22300');
// Setting.setValue('sync.target', 10);
// Setting.setValue('sync.10.username', 'user1@example.com');
// Setting.setValue('sync.10.password', '111111');
}
if (Setting.value('db.ftsEnabled') === -1) {
const ftsEnabled = await db.ftsEnabled();
Setting.setValue('db.ftsEnabled', ftsEnabled ? 1 : 0);
reg.logger().info('db.ftsEnabled = ', Setting.value('db.ftsEnabled'));
}
if (Setting.value('env') === 'dev') {
Setting.setValue('welcome.enabled', false);
}
// Note: for now we hard-code the folder sort order as we need to
// create a UI to allow customisation (started in branch mobile_add_sidebar_buttons)
Setting.setValue('folders.sortOrder.field', 'title');
Setting.setValue('folders.sortOrder.reverse', false);
reg.logger().info(`Sync target: ${Setting.value('sync.target')}`);
setLocale(Setting.value('locale'));
if (Platform.OS === 'android') {
const ignoreTlsErrors = Setting.value('net.ignoreTlsErrors');
if (ignoreTlsErrors) {
await setIgnoreTlsErrors(ignoreTlsErrors);
}
}
});
addTask('import plugin assets', async () => {
await PluginAssetsLoader.instance().importAssets();
});
addTask('set up command & keymap services', async () => {
initializeCommandService(store);
KeymapService.instance().initialize();
});
addTask('set up E2EE', async () => {
setRSA(RSA);
EncryptionService.fsDriver_ = shim.fsDriver();
// eslint-disable-next-line require-atomic-updates
BaseItem.encryptionService_ = EncryptionService.instance();
BaseItem.shareService_ = ShareService.instance();
Resource.shareService_ = ShareService.instance();
DecryptionWorker.instance().dispatch = dispatch;
DecryptionWorker.instance().setLogger(reg.logger());
DecryptionWorker.instance().setKvStore(KvStore.instance());
DecryptionWorker.instance().setEncryptionService(EncryptionService.instance());
await loadMasterKeysFromSettings(EncryptionService.instance());
DecryptionWorker.instance().on('resourceMetadataButNotBlobDecrypted', decryptionWorker_resourceMetadataButNotBlobDecrypted);
});
addTask('set up sharing', async () => {
await ShareService.instance().initialize(store, EncryptionService.instance());
});
addTask('load folders', async () => {
await refreshFolders(dispatch, '');
dispatch({
type: 'FOLDER_SET_COLLAPSED_ALL',
ids: Setting.value('collapsedFolderIds'),
});
});
addTask('load tags', async () => {
const tags = await Tag.allWithNotes();
dispatch({
type: 'TAG_UPDATE_ALL',
items: tags,
});
});
addTask('clear shared files cache', clearSharedFilesCache);
addTask('go: initial route', async () => {
const folder = await getInitialActiveFolder();
const notesParent = parseNotesParent(Setting.value('notesParent'), Setting.value('activeFolderId'));
if (notesParent && notesParent.type === 'SmartFilter') {
dispatch(DEFAULT_ROUTE);
} else if (!folder) {
dispatch(DEFAULT_ROUTE);
} else {
dispatch({
type: 'NAV_GO',
routeName: 'Notes',
folderId: folder.id,
});
}
});
addTask('set up search', async () => {
SearchEngine.instance().setDb(reg.db());
SearchEngine.instance().setLogger(reg.logger());
SearchEngine.instance().scheduleSyncTables();
});
addTask('run migrations', async () => {
await MigrationService.instance().run();
});
addTask('set up background tasks', async () => {
initializeUserFetcher();
PoorManIntervals.setInterval(() => { void userFetcher(); }, 1000 * 60 * 60);
PoorManIntervals.setTimeout(() => {
void AlarmService.garbageCollect();
}, 1000 * 60 * 60);
ResourceService.runInBackground();
ResourceFetcher.instance().setFileApi(() => { return reg.syncTarget().fileApi(); });
ResourceFetcher.instance().setLogger(reg.logger());
ResourceFetcher.instance().dispatch = dispatch;
ResourceFetcher.instance().on('downloadComplete', resourceFetcher_downloadComplete);
void ResourceFetcher.instance().start();
// Collect revisions more frequently on mobile because it doesn't auto-save
// and it cannot collect anything when the app is not active.
RevisionService.instance().runInBackground(1000 * 30);
reg.setupRecurrentSync();
// When the app starts we want the full sync to
// start almost immediately to get the latest data.
// doWifiConnectionCheck set to true so initial sync
// doesn't happen on mobile data
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
void reg.scheduleSync(100, null, true).then(() => {
// Wait for the first sync before updating the notifications, since synchronisation
// might change the notifications.
void AlarmService.updateAllNotifications();
void DecryptionWorker.instance().scheduleStart();
});
});
addTask('set up welcome utils', async () => {
await WelcomeUtils.install(Setting.value('locale'), dispatch);
});
addTask('set up plugin service', async () => {
// Even if there are no plugins, we need to initialize the PluginService so that
// plugin search can work.
const platformImplementation = PlatformImplementation.instance();
PluginService.instance().initialize(
platformImplementation.versionInfo.version, platformImplementation, null, store,
);
// On startup, we can clear plugin update state -- plugins that were updated when the
// user last ran the app have been updated and will be reloaded.
const pluginService = PluginService.instance();
const pluginSettings = pluginService.unserializePluginSettings(Setting.value('plugins.states'));
const updatedSettings = pluginService.clearUpdateState(pluginSettings);
Setting.setValue('plugins.states', updatedSettings);
});
addTask('run startup tests', async () => {
// ----------------------------------------------------------------------------
// On desktop and CLI we run various tests to check that node-rsa is working
// as expected. On mobile however we cannot run test units directly on
// device, and that's what would be needed to automatically verify
// react-native-rsa-native. So instead we run the tests every time the
// mobile app is started in dev mode. If there's any regression the below
// call will throw an error, alerting us of the issue. Otherwise it will
// just print some messages in the console.
// ----------------------------------------------------------------------------
if (Setting.value('env') === 'dev') {
if (Platform.OS !== 'web') {
await runRsaIntegrationTests();
} else {
logger.info('Skipping encryption tests -- not supported on web.');
}
await runCryptoIntegrationTests();
await runOnDeviceFsDriverTests();
}
// ----------------------------------------------------------------------------
// Keep this below to test react-native-rsa-native
// ----------------------------------------------------------------------------
// const testData = await createTestData();
// await checkTestData(testData);
// const testData = {
// "publicKey": "-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEAoMx9NBioka8DUjO3bKrWMn8uJ23LH1xySogQFR6yh6qbl6i5LKTw\nPgqvv55FUuQtYTMtUTVLggYQhdCBvwbBrD1OqO4xU6Ew7x5/TQKPV3MSgYaps3FF\nOdipC4FyA00jBe6Z1CIpL+ZaSnvjDbMUf5lW8bmfRuXfdBGAcdSBjqm9ttajOws+\n7BBSQ9nI5dnBnWRIVEUb7e9bulgANzM1LMUOE+gaef7T3uKzc+Cx3BhHgw1JdFbL\nZAndYtP52KI5N3oiFM4II26DxmDrO1tQokNM88l5xT0BXPhYiEl1CeBpo5VHZBY2\nRHr4MM/OyAXSUdulsDzbntpE+Y85zv7gpQIDAQAB\n-----END RSA PUBLIC KEY-----",
// "privateKey": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAoMx9NBioka8DUjO3bKrWMn8uJ23LH1xySogQFR6yh6qbl6i5\nLKTwPgqvv55FUuQtYTMtUTVLggYQhdCBvwbBrD1OqO4xU6Ew7x5/TQKPV3MSgYap\ns3FFOdipC4FyA00jBe6Z1CIpL+ZaSnvjDbMUf5lW8bmfRuXfdBGAcdSBjqm9ttaj\nOws+7BBSQ9nI5dnBnWRIVEUb7e9bulgANzM1LMUOE+gaef7T3uKzc+Cx3BhHgw1J\ndFbLZAndYtP52KI5N3oiFM4II26DxmDrO1tQokNM88l5xT0BXPhYiEl1CeBpo5VH\nZBY2RHr4MM/OyAXSUdulsDzbntpE+Y85zv7gpQIDAQABAoIBAEA0Zmm+ztAcyX6x\nF7RUImLXVV55AHntN9V6rrFAKJjzDl1oCUhCM4sSSUqBr7yBT31YKegbF6M7OK21\nq5jS4dIcSKQ7N4bk/dz8mGfvdby9Pc5qLqhvuex3DkiBzzxyOGHN+64wVbHCkJrd\nDLQTpUOtvoGWVHrCno6Bzn+lEnYbvdr0hqI5H4D0ubk6TYed1/4ZlJf0R/o/4jnl\nou0UG2hpJN4ur506cttkZJSTxLjzdO38JuQIAkCEglrMYVY61lBNPxC11Kr3ZN7o\ncm7gWZVyP26KoU27t/g+2FoiBGsWLqGYiuTaqT6dKZ2vHyJGjJIZZStv5ye2Ez8V\nKQwpjQECgYEA3xtwYu4n/G5UjEMumkXHNd/bDamelo1aQvvjkVvxKeASNBqV8cM0\n6Jb2FCuT9Y3mWbFTM0jpqXehpHUOCCnrPKGKnJ0ZS4/SRIrtw0iM6q17fTAqmuOt\nhX0pJ77Il8lVCtx4ItsW+LUGbm6CwotlYLVUuyluhKe0pGw2yafi2N0CgYEAuIFk\ng4p7x0i1LFAlIP0YQ07bJQ0E6FEWbCfMgrV3VjtbnT99EaqPOHhMasITCuoEFlh8\ncgyZ6oH7GEy4IRWrM+Mlm47S+NTrr6KgnTGf570ZAFuqnJac97oFB7BvlQsQot6F\n0L2JKM7dQKIMlvwA9DoXZdKX/9ykiqqIpawNxmkCgYEAuyJOwAw2ads4+3UWT7wb\nfarIF8ugA3OItAqHNFNEEvWpDx8FigVMCZMl0IFE14AwKCc+PBP6OXTolgLAxEQ0\n1WRB2V9D6kc1/Nvy1guydt0QaU7PTZ+O2hrDPF0f74Cl3jhSZBoUSIO+Yz46W2eE\nnvs5mMsFsirgr9E8myRAd9kCgYAGMCDE4KIiHugkolN8dcCYkU58QaGGgSG1YuhT\nAe8Mr1T1QynYq9W92RsHAZdN6GdWsIUL9iw7VzyqpfgO9AEX7mhWfUXKHqoA6/1j\nCEUKqqbqAikIs2x0SoLcrSgw4XwfWkM2qwSsn7N/9W9iqPUHO+OJALUkWawTEoEe\nvVSA8QKBgQCEYCPnxgeQSZkrv7x5soXzgF1YN5EZRa1mTUqPBubs564ZjIIY66mI\nCTaHl7U1cPAhx7mHkSzP/i5NjjLqPZZNOyawWDEEmOzxX69OIzKImb6mEQNyS3do\nI8jnpN5q9pw5TvuEIYSrGqQVnHeaEjSvcT48W9GuzjNVscGfw76fPg==\n-----END RSA PRIVATE KEY-----",
// "plaintext": "just testing",
// "ciphertext": "BfkKLdrmd2UX4sPf0bzhfqrg3rKwH5DS7dPAqdmoQuHlrvEBrYKqheekwpnWQgGggGcm/orlrsQRwlexLv7jfRbb0bMnElkySMu4w6wTxILB66RX9H3vXCz02SwHKFRcuGJxlzTPUC23ki6f/McYJ2n/2L8qYxBO8fncTKutIWV54jY19RS1wQ4IdVDBqzji8D0QsRxUhVlpRk4qxsVnyuoyg9AyDe91LOYKfRc6NdapFij996nKzjxFcKOdBqpis34fN3Cg7avcs2Dm5vi7zlRhyGqJJhORXTU3x6hVwOBkVAisgaB7xS3lHiYp6Fs5tP3hBd0kFwVVx8gALbHsgg=="
// };
// await checkTestData(testData);
// await printTestData();
});
return startupTasks;
};
export default buildStartupTasks;
@@ -1,24 +1,20 @@
import { useEffect, useRef } from 'react';
import { useEffect } from 'react';
import BackButtonService from '../../services/BackButtonService';
type OnBackPress = ()=>(void|boolean);
const useBackHandler = (onBackPress: OnBackPress|null) => {
const onBackPressRef = useRef(onBackPress);
onBackPressRef.current = onBackPress ?? (() => {});
const hasHandler = !!onBackPress;
useEffect(() => {
if (!hasHandler) return () => {};
if (!onBackPress) return () => {};
const handler = () => {
return !!(onBackPressRef.current() ?? true);
return !!(onBackPress() ?? true);
};
BackButtonService.addHandler(handler);
return () => {
BackButtonService.removeHandler(handler);
};
}, [hasHandler]);
}, [onBackPress]);
};
export default useBackHandler;
@@ -11,7 +11,6 @@ const makeMockDialogControl = (onPrompt: OnPrompt): DialogControl => {
prompt: jest.fn((_title, _message, buttons, options) => {
onPrompt(buttons, options.onDismiss);
}),
promptForText: jest.fn(),
showMenu: jest.fn(),
};
};
@@ -1,7 +1,7 @@
import { ViewPlugin } from '@codemirror/view';
import createEditorControl from './testing/createEditorControl';
import createEditorControl from './testUtil/createEditorControl';
import { EditorCommandType } from '../types';
import pressReleaseKey from './testing/pressReleaseKey';
import pressReleaseKey from './testUtil/pressReleaseKey';
import { EditorSelection, EditorState } from '@codemirror/state';
describe('CodeMirrorControl', () => {
@@ -3,7 +3,7 @@ import { EditorCommandType, EditorControl, EditorSettings, LogMessageCallback, C
import CodeMirror5Emulation from './CodeMirror5Emulation/CodeMirror5Emulation';
import editorCommands from './editorCommands/editorCommands';
import { Compartment, EditorSelection, Extension, StateEffect } from '@codemirror/state';
import { updateLink } from './editorCommands/markdownCommands';
import { updateLink } from './markdown/markdownCommands';
import { searchPanelOpen, SearchQuery, setSearchQuery } from '@codemirror/search';
import PluginLoader from './pluginApi/PluginLoader';
import customEditorCompletion, { editorCompletionSource, enableLanguageDataAutocomplete } from './pluginApi/customEditorCompletion';
@@ -11,7 +11,7 @@ import { CompletionSource } from '@codemirror/autocomplete';
import { RegionSpec } from './utils/formatting/RegionSpec';
import toggleInlineSelectionFormat from './utils/formatting/toggleInlineSelectionFormat';
import getSearchState from './utils/getSearchState';
import { noteIdFacet, setNoteIdEffect } from './extensions/selectedNoteIdExtension';
import { noteIdFacet, setNoteIdEffect } from './utils/selectedNoteIdExtension';
import jumpToHash from './editorCommands/jumpToHash';
interface Callbacks {
@@ -5,15 +5,15 @@ import createTheme from './theme';
import { EditorState } from '@codemirror/state';
import { deleteMarkupBackward, markdown, markdownLanguage } from '@codemirror/lang-markdown';
import { GFM as GitHubFlavoredMarkdownExtension } from '@lezer/markdown';
import markdownMathExtension from './extensions/markdownMathExtension';
import markdownHighlightExtension from './extensions/markdownHighlightExtension';
import lookUpLanguage from './utils/markdown/codeBlockLanguages/lookUpLanguage';
import MarkdownMathExtension from './markdown/MarkdownMathExtension';
import MarkdownHighlightExtension from './markdown/MarkdownHighlightExtension';
import lookUpLanguage from './markdown/codeBlockLanguages/lookUpLanguage';
import { html } from '@codemirror/lang-html';
import { defaultKeymap, emacsStyleKeymap } from '@codemirror/commands';
import { vim } from '@replit/codemirror-vim';
import { indentUnit } from '@codemirror/language';
import { Prec } from '@codemirror/state';
import insertNewlineContinueMarkup from './editorCommands/insertNewlineContinueMarkup';
import insertNewlineContinueMarkup from './markdown/insertNewlineContinueMarkup';
const configFromSettings = (settings: EditorSettings) => {
const languageExtension = (() => {
@@ -26,10 +26,10 @@ const configFromSettings = (settings: EditorSettings) => {
extensions: [
GitHubFlavoredMarkdownExtension,
settings.markdownMarkEnabled ? markdownHighlightExtension : [],
settings.markdownMarkEnabled ? MarkdownHighlightExtension : [],
// Don't highlight KaTeX if the user disabled it
settings.katexEnabled ? markdownMathExtension : [],
settings.katexEnabled ? MarkdownMathExtension : [],
],
codeLanguages: lookUpLanguage,
@@ -5,10 +5,10 @@
import createEditor from './createEditor';
import Setting from '@joplin/lib/models/Setting';
import { forceParsing } from '@codemirror/language';
import loadLanguages from './testing/loadLanguages';
import loadLanguages from './testUtil/loadLanguages';
import { expect, describe, it } from '@jest/globals';
import createEditorSettings from './testing/createEditorSettings';
import createEditorSettings from './testUtil/createEditorSettings';
describe('createEditor', () => {
+7 -7
View File
@@ -21,21 +21,21 @@ import {
insertOrIncreaseIndent,
toggleBolded, toggleCode,
toggleItalicized, toggleMath,
} from './editorCommands/markdownCommands';
import decoratorExtension from './extensions/markdownDecorationExtension';
import computeSelectionFormatting from './utils/formatting/computeSelectionFormatting';
} from './markdown/markdownCommands';
import decoratorExtension from './markdown/decoratorExtension';
import computeSelectionFormatting from './markdown/computeSelectionFormatting';
import { selectionFormattingEqual } from '../SelectionFormatting';
import configFromSettings from './configFromSettings';
import getScrollFraction from './getScrollFraction';
import CodeMirrorControl from './CodeMirrorControl';
import insertLineAfter from './editorCommands/insertLineAfter';
import handlePasteEvent from './utils/handlePasteEvent';
import biDirectionalTextExtension from './extensions/biDirectionalTextExtension';
import searchExtension from './extensions/searchExtension';
import biDirectionalTextExtension from './utils/biDirectionalTextExtension';
import searchExtension from './utils/searchExtension';
import isCursorAtBeginning from './utils/isCursorAtBeginning';
import overwriteModeExtension from './extensions/overwriteModeExtension';
import overwriteModeExtension from './utils/overwriteModeExtension';
import handleLinkEditRequests, { showLinkEditor } from './utils/handleLinkEditRequests';
import selectedNoteIdExtension, { setNoteIdEffect } from './extensions/selectedNoteIdExtension';
import selectedNoteIdExtension, { setNoteIdEffect } from './utils/selectedNoteIdExtension';
// Newer versions of CodeMirror by default use Chrome's EditContext API.
// While this might be stable enough for desktop use, it causes significant
@@ -7,7 +7,7 @@ import {
toggleBolded, toggleCode,
toggleHeaderLevel, toggleItalicized,
toggleList, toggleMath,
} from './markdownCommands';
} from '../markdown/markdownCommands';
import duplicateLine from './duplicateLine';
import sortSelectedLines from './sortSelectedLines';
import { closeSearchPanel, findNext, findPrevious, openSearchPanel, replaceAll, replaceNext, searchPanelOpen } from '@codemirror/search';
@@ -1,5 +1,5 @@
import { EditorSelection } from '@codemirror/state';
import createTestEditor from '../testing/createTestEditor';
import createTestEditor from '../testUtil/createTestEditor';
import insertLineAfter from './insertLineAfter';
describe('insertLineAfter', () => {
@@ -1,7 +1,7 @@
import { insertNewlineAndIndent } from '@codemirror/commands';
import { EditorSelection, SelectionRange } from '@codemirror/state';
import { EditorView } from '@codemirror/view';
import insertNewlineContinueMarkup from './insertNewlineContinueMarkup';
import insertNewlineContinueMarkup from '../markdown/insertNewlineContinueMarkup';
const insertLineAfter = (view: EditorView) => {
const state = view.state;
@@ -1,5 +1,5 @@
import { EditorSelection } from '@codemirror/state';
import createTestEditor from '../testing/createTestEditor';
import createTestEditor from '../testUtil/createTestEditor';
import jumpToHash from './jumpToHash';
describe('jumpToHash', () => {
@@ -1,8 +1,8 @@
import { EditorSelection, EditorState } from '@codemirror/state';
import createTestEditor from '../testing/createTestEditor';
import findNodesWithName from '../testing/findNodesWithName';
import { highlightMarkerTagName, highlightTagName } from './markdownHighlightExtension';
import createTestEditor from '../testUtil/createTestEditor';
import findNodesWithName from '../testUtil/findNodesWithName';
import { highlightMarkerTagName, highlightTagName } from './MarkdownHighlightExtension';
const createEditorState = async (initialText: string, expectedTags: string[]): Promise<EditorState> => {
return (await createTestEditor(initialText, EditorSelection.cursor(0), expectedTags)).state;
@@ -1,5 +1,5 @@
import { tags, Tag } from '@lezer/highlight';
import { MarkdownConfig, InlineContext, MarkdownExtension } from '@lezer/markdown';
import { MarkdownConfig, InlineContext } from '@lezer/markdown';
const equalsSignCharcode = 61;
@@ -16,7 +16,7 @@ const isSpaceOrEmpty = (text: string) => text.match(/^\s*$/);
// Markdown extension for recognizing highlighting. This is similar to the upstream
// extension for strikethrough:
// https://github.com/lezer-parser/markdown/blob/d6f0aa095722329a0188b9c7afe207dab4835e55/src/extension.ts#L10
const highlightConfig: MarkdownConfig = {
const HighlightConfig: MarkdownConfig = {
defineNodes: [
{
name: highlightTagName,
@@ -59,7 +59,7 @@ const highlightConfig: MarkdownConfig = {
}],
};
const markdownHighlightExtension: MarkdownExtension = [
highlightConfig,
const MarkdownHighlightExtension: MarkdownConfig[] = [
HighlightConfig,
];
export default markdownHighlightExtension;
export default MarkdownHighlightExtension;
@@ -1,8 +1,8 @@
import { EditorSelection, EditorState } from '@codemirror/state';
import { blockMathTagName, inlineMathContentTagName, inlineMathTagName } from './markdownMathExtension';
import { blockMathTagName, inlineMathContentTagName, inlineMathTagName } from './MarkdownMathExtension';
import createTestEditor from '../testing/createTestEditor';
import findNodesWithName from '../testing/findNodesWithName';
import createTestEditor from '../testUtil/createTestEditor';
import findNodesWithName from '../testUtil/findNodesWithName';
// Creates an EditorState with math and markdown extensions
const createEditorState = async (initialText: string, expectedTags: string[]): Promise<EditorState> => {
@@ -10,7 +10,6 @@ import { parseMixed, SyntaxNodeRef, Input, NestedParse, ParseWrapper } from '@le
import {
MarkdownConfig, InlineContext,
BlockContext, Line, LeafBlock,
MarkdownExtension,
} from '@lezer/markdown';
// The existing stexMath parser is used to parse the text between the $s
@@ -51,7 +50,7 @@ const wrappedTeXParser = (nodeTag: string): ParseWrapper => {
};
// Markdown extension for recognizing inline code
const inlineMathConfig: MarkdownConfig = {
const InlineMathConfig: MarkdownConfig = {
defineNodes: [
{
name: inlineMathTagName,
@@ -121,7 +120,7 @@ const inlineMathConfig: MarkdownConfig = {
};
// Extension for recognising block code
const blockMathConfig: MarkdownConfig = {
const BlockMathConfig: MarkdownConfig = {
defineNodes: [
{
name: blockMathTagName,
@@ -207,9 +206,9 @@ const blockMathConfig: MarkdownConfig = {
};
// Markdown configuration for block and inline math support.
const markdownMathExtension: MarkdownExtension = [
inlineMathConfig,
blockMathConfig,
const MarkdownMathExtension: MarkdownConfig[] = [
InlineMathConfig,
BlockMathConfig,
];
export default markdownMathExtension;
export default MarkdownMathExtension;
@@ -1,5 +1,5 @@
import { EditorSelection } from '@codemirror/state';
import createTestEditor from '../../testing/createTestEditor';
import createTestEditor from '../testUtil/createTestEditor';
import computeSelectionFormatting from './computeSelectionFormatting';
@@ -1,4 +1,4 @@
import SelectionFormatting, { MutableSelectionFormatting, defaultSelectionFormatting } from '../../../SelectionFormatting';
import SelectionFormatting, { MutableSelectionFormatting, defaultSelectionFormatting } from '../../SelectionFormatting';
import { syntaxTree } from '@codemirror/language';
import { EditorState } from '@codemirror/state';
@@ -1,6 +1,6 @@
import { EditorSelection } from '@codemirror/state';
import createTestEditor from '../testing/createTestEditor';
import decoratorExtension from './markdownDecorationExtension';
import createTestEditor from '../testUtil/createTestEditor';
import decoratorExtension from './decoratorExtension';
jest.retryTimes(2);
@@ -1,6 +1,6 @@
import { EditorSelection } from '@codemirror/state';
import createTestEditor from '../testing/createTestEditor';
import pressReleaseKey from '../testing/pressReleaseKey';
import createTestEditor from '../testUtil/createTestEditor';
import pressReleaseKey from '../testUtil/pressReleaseKey';
import { keymap } from '@codemirror/view';
import insertNewlineContinueMarkup from './insertNewlineContinueMarkup';
@@ -1,7 +1,7 @@
import { EditorSelection } from '@codemirror/state';
import { ListType } from '../../types';
import createTestEditor from '../testing/createTestEditor';
import { toggleList } from '../editorCommands/markdownCommands';
import createTestEditor from '../testUtil/createTestEditor';
import { toggleList } from './markdownCommands';
describe('markdownCommands.bulletedVsChecklist', () => {
@@ -3,9 +3,9 @@ import {
insertHorizontalRule,
insertOrIncreaseIndent,
toggleBolded, toggleCode, toggleHeaderLevel, toggleItalicized, toggleMath, updateLink,
} from '../editorCommands/markdownCommands';
import createTestEditor from '../testing/createTestEditor';
import { blockMathTagName } from '../extensions/markdownMathExtension';
} from './markdownCommands';
import createTestEditor from '../testUtil/createTestEditor';
import { blockMathTagName } from './MarkdownMathExtension';
describe('markdownCommands', () => {
@@ -1,7 +1,9 @@
import { EditorSelection, EditorState } from '@codemirror/state';
import { increaseIndent, toggleList } from '../editorCommands/markdownCommands';
import {
increaseIndent, toggleList,
} from './markdownCommands';
import { ListType } from '../../types';
import createTestEditor from '../testing/createTestEditor';
import createTestEditor from '../testUtil/createTestEditor';
describe('markdownCommands.toggleList', () => {
@@ -66,7 +68,7 @@ describe('markdownCommands.toggleList', () => {
const checklistEndText = ['- [ ] a', '- [ ] test'].join('\n');
const input = `${checklistStartText}\n\n${checklistEndText}`;
const expected = `${checklistStartText}\n- [ ] \n${checklistEndText}`; // new item
const expected = `${checklistStartText}\n\n${checklistEndText}`; // no change
const editor = await createTestEditor(
input,
@@ -464,23 +466,4 @@ A block quote:
expect(editor.state.doc.toString()).toBe(expectedDocText);
});
it.each([
[ListType.CheckList, '', '- [ ] '],
[ListType.OrderedList, '', '1. '],
[ListType.UnorderedList, '', '- '],
[ListType.UnorderedList, '> ', '> - '],
[ListType.UnorderedList, '# Test\n\n', '# Test\n\n- '],
])('should add lists when activated on an empty line or empty block quote (list type: %d, initial doc: %j)', async (
listType, originalDocument, expected,
) => {
const editor = await createTestEditor(
originalDocument,
EditorSelection.cursor(originalDocument.length),
[],
);
toggleList(listType)(editor);
expect(editor.state.doc.toString()).toBe(expected);
});
});
@@ -11,8 +11,8 @@ import intersectsSyntaxNode from '../utils/isInSyntaxNode';
import toggleRegionFormatGlobally from '../utils/formatting/toggleRegionFormatGlobally';
import { RegionSpec } from '../utils/formatting/RegionSpec';
import toggleInlineFormatGlobally from '../utils/formatting/toggleInlineFormatGlobally';
import stripBlockquote from '../utils/markdown/stripBlockquote';
import renumberSelectedLists from '../utils/markdown/renumberSelectedLists';
import stripBlockquote from './utils/stripBlockquote';
import renumberSelectedLists from './utils/renumberSelectedLists';
import toggleSelectedLinesStartWith from '../utils/formatting/toggleSelectedLinesStartWith';
@@ -213,28 +213,12 @@ export const toggleList = (listType: ListType): Command => {
return ListAction.SwitchFormatting;
};
const areAllLinesBlankInRange = (fromLine: Line, toLine: Line) => {
for (let lineNumber = fromLine.number; lineNumber <= toLine.number; lineNumber++) {
const line = state.doc.line(lineNumber);
// Consider lines within block quotes with no other content to be blank (this
// command should behave similarly regardless of whether in or out of a block
// quote).
if (stripBlockquote(line).trim() !== '') {
return false;
}
}
return true;
};
const changes: TransactionSpec = state.changeByRange((sel: SelectionRange) => {
const lineRange = getNextLineRange(sel);
if (!lineRange) return { range: sel };
const { fromLine, toLine } = lineRange;
const baselineIndent = getBaselineIndent(fromLine, toLine);
const action = getAction(fromLine, toLine);
const allLinesBlank = areAllLinesBlankInRange(fromLine, toLine);
// Outermost list item number
let outerCounter = 1;
@@ -246,18 +230,8 @@ export const toggleList = (listType: ListType): Command => {
for (let lineNum = fromLine.number; lineNum <= toLine.number; lineNum++) {
const line = doc.line(lineNum);
const origLineContent = stripBlockquote(line);
const currentLineBlank = origLineContent.trim() === '';
// To support changing the formatting of non-tight lists, skip blank lines within
// larger content. This allows changing the list format without adding a potentially
// large number of empty list items.
//
// However, if all lines are blank (e.g. if the cursor at the beginning of an empty)
// document, we're not changing the formatting of an existing list. In this case,
// skipping blank lines would cause the command to do nothing.
// See https://github.com/laurent22/joplin/pull/12745.
if (currentLineBlank && !allLinesBlank) {
continue;
if (origLineContent.trim() === '') {
continue; // skip blank lines
}
// Content excluding the block quote start
@@ -1,5 +1,5 @@
import { EditorSelection } from '@codemirror/state';
import createTestEditor from '../../testing/createTestEditor';
import createTestEditor from '../../testUtil/createTestEditor';
import renumberSelectedLists from './renumberSelectedLists';
describe('renumberSelectedLists', () => {
@@ -1,8 +1,8 @@
import { EditorState, StateEffect } from '@codemirror/state';
import createEditorControl from '../testing/createEditorControl';
import createEditorControl from '../testUtil/createEditorControl';
import { EditorView } from '@codemirror/view';
import { completeFromList, completionStatus, currentCompletions } from '@codemirror/autocomplete';
import typeText from '../testing/typeText';
import typeText from '../testUtil/typeText';
const waitForShownCompletionsToContain = (editor: EditorView, completionLabels: string[]) => {
return new Promise<string[]>(resolve => {
@@ -3,10 +3,10 @@ import { GFM as GithubFlavoredMarkdownExt } from '@lezer/markdown';
import { indentUnit, syntaxTree } from '@codemirror/language';
import { SelectionRange, EditorSelection, EditorState, Extension } from '@codemirror/state';
import { EditorView } from '@codemirror/view';
import MarkdownMathExtension from '../markdown/MarkdownMathExtension';
import forceFullParse from './forceFullParse';
import loadLanguages from './loadLanguages';
import markdownMathExtension from '../extensions/markdownMathExtension';
import markdownHighlightExtension from '../extensions/markdownHighlightExtension';
import MarkdownHighlightExtension from '../markdown/MarkdownHighlightExtension';
// Creates and returns a minimal editor with markdown extensions. Waits to return the editor
// until all syntax tree tags in `expectedSyntaxTreeTags` exist.
@@ -26,7 +26,7 @@ const createTestEditor = async (
selection: EditorSelection.create(initialSelection),
extensions: [
markdown({
extensions: [markdownMathExtension, markdownHighlightExtension, GithubFlavoredMarkdownExt],
extensions: [MarkdownMathExtension, MarkdownHighlightExtension, GithubFlavoredMarkdownExt],
addKeymap: addMarkdownKeymap,
}),
indentUnit.of('\t'),
@@ -1,4 +1,4 @@
import allLanguages from '../utils/markdown/codeBlockLanguages/allLanguages';
import allLanguages from '../markdown/codeBlockLanguages/allLanguages';
// Ensure languages we use are loaded. Without this, tests may randomly fail (LanguageDescriptions
// are loaded asynchronously, in the background).
Loaded 100 of 232 files, more files were not shown because too many files have changed in this diff. Show more