mirror of
https://github.com/laurent22/joplin.git
synced 2026-11-02 15:00:57 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f53bb2d167 | ||
|
|
113b259b81 | ||
|
|
c7e31d1ac9 | ||
|
|
c51b13ca73 | ||
|
|
f5febb18b4 |
No files matched your search
+34
-51
@@ -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
@@ -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
@@ -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
@@ -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/"
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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:');
|
||||
@@ -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",
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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}
|
||||
|
||||
@@ -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,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) => ``,
|
||||
).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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
@@ -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', () => {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
+3
-3
@@ -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;
|
||||
+5
-5
@@ -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;
|
||||
+3
-3
@@ -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> => {
|
||||
+6
-7
@@ -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;
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { EditorSelection } from '@codemirror/state';
|
||||
import createTestEditor from '../../testing/createTestEditor';
|
||||
import createTestEditor from '../testUtil/createTestEditor';
|
||||
import computeSelectionFormatting from './computeSelectionFormatting';
|
||||
|
||||
|
||||
+1
-1
@@ -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';
|
||||
|
||||
+2
-2
@@ -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);
|
||||
|
||||
File renamed without changes.
+2
-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';
|
||||
|
||||
File renamed without changes.
+2
-2
@@ -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
-3
@@ -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', () => {
|
||||
|
||||
+5
-22
@@ -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);
|
||||
});
|
||||
});
|
||||
+4
-30
@@ -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
-1
@@ -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', () => {
|
||||
File renamed without changes.
File renamed without changes.
@@ -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 => {
|
||||
|
||||
File renamed without changes.
File renamed without changes.
+3
-3
@@ -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'),
|
||||
File renamed without changes.
File renamed without changes.
+1
-1
@@ -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).
|
||||
File renamed without changes.
Loaded 100 of 232 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user