1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

Merge remote-tracking branch 'origin/dev' into note-history-and-trash-ttl-text-input

This commit is contained in:
mrjo118 2024-12-19 05:16:12 +00:00
commit c7e97f5d51
198 changed files with 2880 additions and 1997 deletions

View File

@ -31,6 +31,7 @@ packages/app-cli/tests/tmp
packages/app-clipper/content_scripts/JSDOMParser.js packages/app-clipper/content_scripts/JSDOMParser.js
packages/app-clipper/content_scripts/Readability-readerable.js packages/app-clipper/content_scripts/Readability-readerable.js
packages/app-clipper/content_scripts/Readability.js packages/app-clipper/content_scripts/Readability.js
packages/app-clipper/content_scripts/clipperUtils.js
packages/app-clipper/dist packages/app-clipper/dist
packages/app-clipper/icons packages/app-clipper/icons
packages/app-clipper/popup/build packages/app-clipper/popup/build
@ -159,8 +160,6 @@ packages/app-desktop/commands/focusElement.js
packages/app-desktop/commands/index.js packages/app-desktop/commands/index.js
packages/app-desktop/commands/openNoteInNewWindow.js packages/app-desktop/commands/openNoteInNewWindow.js
packages/app-desktop/commands/openProfileDirectory.js packages/app-desktop/commands/openProfileDirectory.js
packages/app-desktop/commands/renderMarkup.test.js
packages/app-desktop/commands/renderMarkup.js
packages/app-desktop/commands/replaceMisspelling.js packages/app-desktop/commands/replaceMisspelling.js
packages/app-desktop/commands/restoreNoteRevision.js packages/app-desktop/commands/restoreNoteRevision.js
packages/app-desktop/commands/startExternalEditing.js packages/app-desktop/commands/startExternalEditing.js
@ -596,6 +595,16 @@ packages/app-mobile/components/DialogManager/types.js
packages/app-mobile/components/DismissibleDialog.js packages/app-mobile/components/DismissibleDialog.js
packages/app-mobile/components/Dropdown.test.js packages/app-mobile/components/Dropdown.test.js
packages/app-mobile/components/Dropdown.js packages/app-mobile/components/Dropdown.js
packages/app-mobile/components/EditorToolbar/EditorToolbar.test.js
packages/app-mobile/components/EditorToolbar/EditorToolbar.js
packages/app-mobile/components/EditorToolbar/ToolbarButton.js
packages/app-mobile/components/EditorToolbar/ToolbarEditorDialog.js
packages/app-mobile/components/EditorToolbar/testing/mockCommandRuntimes.js
packages/app-mobile/components/EditorToolbar/types.js
packages/app-mobile/components/EditorToolbar/utils/allToolbarCommandNamesFromState.js
packages/app-mobile/components/EditorToolbar/utils/isSelected.js
packages/app-mobile/components/EditorToolbar/utils/selectedCommandNamesFromState.js
packages/app-mobile/components/EditorToolbar/utils/toolbarButtonsFromState.js
packages/app-mobile/components/ExtendedWebView/index.jest.js packages/app-mobile/components/ExtendedWebView/index.jest.js
packages/app-mobile/components/ExtendedWebView/index.js packages/app-mobile/components/ExtendedWebView/index.js
packages/app-mobile/components/ExtendedWebView/index.web.js packages/app-mobile/components/ExtendedWebView/index.web.js
@ -634,18 +643,6 @@ packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/startAutosaveLoop.
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/types.js packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/types.js
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/watchEditorForTemplateChanges.js packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/watchEditorForTemplateChanges.js
packages/app-mobile/components/NoteEditor/ImageEditor/promptRestoreAutosave.js packages/app-mobile/components/NoteEditor/ImageEditor/promptRestoreAutosave.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/MarkdownToolbar.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleSpaceButton.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarButton.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarOverflowRows.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useActionButtons.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useHeaderButtons.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useInlineFormattingButtons.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useListButtons.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/usePluginButtons.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/types.js
packages/app-mobile/components/NoteEditor/NoteEditor.test.js packages/app-mobile/components/NoteEditor/NoteEditor.test.js
packages/app-mobile/components/NoteEditor/NoteEditor.js packages/app-mobile/components/NoteEditor/NoteEditor.js
packages/app-mobile/components/NoteEditor/SearchPanel.js packages/app-mobile/components/NoteEditor/SearchPanel.js
@ -653,7 +650,6 @@ packages/app-mobile/components/NoteEditor/commandDeclarations.js
packages/app-mobile/components/NoteEditor/hooks/useCodeMirrorPlugins.js packages/app-mobile/components/NoteEditor/hooks/useCodeMirrorPlugins.js
packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.test.js packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.test.js
packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.js packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.js
packages/app-mobile/components/NoteEditor/hooks/useKeyboardVisible.js
packages/app-mobile/components/NoteEditor/types.js packages/app-mobile/components/NoteEditor/types.js
packages/app-mobile/components/NoteItem.js packages/app-mobile/components/NoteItem.js
packages/app-mobile/components/NoteList.js packages/app-mobile/components/NoteList.js
@ -670,6 +666,7 @@ packages/app-mobile/components/SelectDateTimeDialog.js
packages/app-mobile/components/SideMenu.js packages/app-mobile/components/SideMenu.js
packages/app-mobile/components/SideMenuContentNote.js packages/app-mobile/components/SideMenuContentNote.js
packages/app-mobile/components/TextInput.js packages/app-mobile/components/TextInput.js
packages/app-mobile/components/ToggleSpaceButton.js
packages/app-mobile/components/accessibility/AccessibleModalMenu.js packages/app-mobile/components/accessibility/AccessibleModalMenu.js
packages/app-mobile/components/accessibility/AccessibleView.js packages/app-mobile/components/accessibility/AccessibleView.js
packages/app-mobile/components/app-nav.js packages/app-mobile/components/app-nav.js
@ -753,8 +750,14 @@ packages/app-mobile/components/screens/ConfigScreen/plugins/utils/useUpdateState
packages/app-mobile/components/screens/ConfigScreen/types.js packages/app-mobile/components/screens/ConfigScreen/types.js
packages/app-mobile/components/screens/JoplinCloudLoginScreen.js packages/app-mobile/components/screens/JoplinCloudLoginScreen.js
packages/app-mobile/components/screens/LogScreen.js packages/app-mobile/components/screens/LogScreen.js
packages/app-mobile/components/screens/Note.test.js packages/app-mobile/components/screens/Note/Note.test.js
packages/app-mobile/components/screens/Note.js packages/app-mobile/components/screens/Note/Note.js
packages/app-mobile/components/screens/Note/commands/attachFile.js
packages/app-mobile/components/screens/Note/commands/hideKeyboard.js
packages/app-mobile/components/screens/Note/commands/index.js
packages/app-mobile/components/screens/Note/commands/setTags.js
packages/app-mobile/components/screens/Note/commands/toggleVisiblePanes.js
packages/app-mobile/components/screens/Note/types.js
packages/app-mobile/components/screens/NoteTagsDialog.js packages/app-mobile/components/screens/NoteTagsDialog.js
packages/app-mobile/components/screens/Notes.js packages/app-mobile/components/screens/Notes.js
packages/app-mobile/components/screens/SearchScreen/SearchResults.js packages/app-mobile/components/screens/SearchScreen/SearchResults.js
@ -778,6 +781,7 @@ packages/app-mobile/services/AlarmServiceDriver.android.js
packages/app-mobile/services/AlarmServiceDriver.ios.js packages/app-mobile/services/AlarmServiceDriver.ios.js
packages/app-mobile/services/AlarmServiceDriver.web.js packages/app-mobile/services/AlarmServiceDriver.web.js
packages/app-mobile/services/BackButtonService.js packages/app-mobile/services/BackButtonService.js
packages/app-mobile/services/commands/stateToWhenClauseContext.js
packages/app-mobile/services/e2ee/RSA.react-native.js packages/app-mobile/services/e2ee/RSA.react-native.js
packages/app-mobile/services/e2ee/crypto.js packages/app-mobile/services/e2ee/crypto.js
packages/app-mobile/services/plugins/PlatformImplementation.js packages/app-mobile/services/plugins/PlatformImplementation.js
@ -819,6 +823,7 @@ packages/app-mobile/utils/fs-driver/testUtil/createFilesFromPathRecord.js
packages/app-mobile/utils/fs-driver/testUtil/verifyDirectoryMatches.js packages/app-mobile/utils/fs-driver/testUtil/verifyDirectoryMatches.js
packages/app-mobile/utils/getPackageInfo.js packages/app-mobile/utils/getPackageInfo.js
packages/app-mobile/utils/getVersionInfoText.js packages/app-mobile/utils/getVersionInfoText.js
packages/app-mobile/utils/hooks/useKeyboardVisible.js
packages/app-mobile/utils/hooks/useOnLongPressProps.js packages/app-mobile/utils/hooks/useOnLongPressProps.js
packages/app-mobile/utils/hooks/useReduceMotionEnabled.js packages/app-mobile/utils/hooks/useReduceMotionEnabled.js
packages/app-mobile/utils/image/fileToImage.web.js packages/app-mobile/utils/image/fileToImage.web.js
@ -843,6 +848,7 @@ packages/app-mobile/utils/shim-init-react/shimInitShared.js
packages/app-mobile/utils/testing/createMockReduxStore.js packages/app-mobile/utils/testing/createMockReduxStore.js
packages/app-mobile/utils/testing/getWebViewDomById.js packages/app-mobile/utils/testing/getWebViewDomById.js
packages/app-mobile/utils/testing/getWebViewWindowById.js packages/app-mobile/utils/testing/getWebViewWindowById.js
packages/app-mobile/utils/testing/setupGlobalStore.js
packages/app-mobile/utils/types.js packages/app-mobile/utils/types.js
packages/app-mobile/web/serviceWorker.js packages/app-mobile/web/serviceWorker.js
packages/default-plugins/build.js packages/default-plugins/build.js
@ -915,6 +921,7 @@ packages/editor/CodeMirror/utils/formatting/toggleSelectedLinesStartWith.js
packages/editor/CodeMirror/utils/formatting/types.js packages/editor/CodeMirror/utils/formatting/types.js
packages/editor/CodeMirror/utils/getSearchState.js packages/editor/CodeMirror/utils/getSearchState.js
packages/editor/CodeMirror/utils/growSelectionToNode.js packages/editor/CodeMirror/utils/growSelectionToNode.js
packages/editor/CodeMirror/utils/handleLinkEditRequests.js
packages/editor/CodeMirror/utils/handlePasteEvent.js packages/editor/CodeMirror/utils/handlePasteEvent.js
packages/editor/CodeMirror/utils/isCursorAtBeginning.js packages/editor/CodeMirror/utils/isCursorAtBeginning.js
packages/editor/CodeMirror/utils/isInSyntaxNode.js packages/editor/CodeMirror/utils/isInSyntaxNode.js
@ -987,6 +994,8 @@ packages/lib/commands/historyForward.js
packages/lib/commands/index.js packages/lib/commands/index.js
packages/lib/commands/openMasterPasswordDialog.js packages/lib/commands/openMasterPasswordDialog.js
packages/lib/commands/permanentlyDeleteNote.js packages/lib/commands/permanentlyDeleteNote.js
packages/lib/commands/renderMarkup.test.js
packages/lib/commands/renderMarkup.js
packages/lib/commands/synchronize.js packages/lib/commands/synchronize.js
packages/lib/components/EncryptionConfigScreen/utils.test.js packages/lib/components/EncryptionConfigScreen/utils.test.js
packages/lib/components/EncryptionConfigScreen/utils.js packages/lib/components/EncryptionConfigScreen/utils.js
@ -1195,6 +1204,8 @@ packages/lib/services/interop/InteropService_Importer_Raw.js
packages/lib/services/interop/Module.test.js packages/lib/services/interop/Module.test.js
packages/lib/services/interop/Module.js packages/lib/services/interop/Module.js
packages/lib/services/interop/types.js packages/lib/services/interop/types.js
packages/lib/services/interop/utils.test.js
packages/lib/services/interop/utils.js
packages/lib/services/joplinCloudUtils.js packages/lib/services/joplinCloudUtils.js
packages/lib/services/joplinServer/personalizedUserContentBaseUrl.js packages/lib/services/joplinServer/personalizedUserContentBaseUrl.js
packages/lib/services/keychain/KeychainService.test.js packages/lib/services/keychain/KeychainService.test.js

View File

@ -87,6 +87,7 @@ module.exports = {
allowEmptyReject: true, allowEmptyReject: true,
}], }],
'no-throw-literal': ['error'], 'no-throw-literal': ['error'],
'no-unused-expressions': ['error'],
// This rule should not be enabled since it matters in what order // This rule should not be enabled since it matters in what order
// imports are done, in particular in relation to the shim.setReact // imports are done, in particular in relation to the shim.setReact

44
.gitignore vendored
View File

@ -135,8 +135,6 @@ packages/app-desktop/commands/focusElement.js
packages/app-desktop/commands/index.js packages/app-desktop/commands/index.js
packages/app-desktop/commands/openNoteInNewWindow.js packages/app-desktop/commands/openNoteInNewWindow.js
packages/app-desktop/commands/openProfileDirectory.js packages/app-desktop/commands/openProfileDirectory.js
packages/app-desktop/commands/renderMarkup.test.js
packages/app-desktop/commands/renderMarkup.js
packages/app-desktop/commands/replaceMisspelling.js packages/app-desktop/commands/replaceMisspelling.js
packages/app-desktop/commands/restoreNoteRevision.js packages/app-desktop/commands/restoreNoteRevision.js
packages/app-desktop/commands/startExternalEditing.js packages/app-desktop/commands/startExternalEditing.js
@ -572,6 +570,16 @@ packages/app-mobile/components/DialogManager/types.js
packages/app-mobile/components/DismissibleDialog.js packages/app-mobile/components/DismissibleDialog.js
packages/app-mobile/components/Dropdown.test.js packages/app-mobile/components/Dropdown.test.js
packages/app-mobile/components/Dropdown.js packages/app-mobile/components/Dropdown.js
packages/app-mobile/components/EditorToolbar/EditorToolbar.test.js
packages/app-mobile/components/EditorToolbar/EditorToolbar.js
packages/app-mobile/components/EditorToolbar/ToolbarButton.js
packages/app-mobile/components/EditorToolbar/ToolbarEditorDialog.js
packages/app-mobile/components/EditorToolbar/testing/mockCommandRuntimes.js
packages/app-mobile/components/EditorToolbar/types.js
packages/app-mobile/components/EditorToolbar/utils/allToolbarCommandNamesFromState.js
packages/app-mobile/components/EditorToolbar/utils/isSelected.js
packages/app-mobile/components/EditorToolbar/utils/selectedCommandNamesFromState.js
packages/app-mobile/components/EditorToolbar/utils/toolbarButtonsFromState.js
packages/app-mobile/components/ExtendedWebView/index.jest.js packages/app-mobile/components/ExtendedWebView/index.jest.js
packages/app-mobile/components/ExtendedWebView/index.js packages/app-mobile/components/ExtendedWebView/index.js
packages/app-mobile/components/ExtendedWebView/index.web.js packages/app-mobile/components/ExtendedWebView/index.web.js
@ -610,18 +618,6 @@ packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/startAutosaveLoop.
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/types.js packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/types.js
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/watchEditorForTemplateChanges.js packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/watchEditorForTemplateChanges.js
packages/app-mobile/components/NoteEditor/ImageEditor/promptRestoreAutosave.js packages/app-mobile/components/NoteEditor/ImageEditor/promptRestoreAutosave.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/MarkdownToolbar.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleSpaceButton.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarButton.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarOverflowRows.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useActionButtons.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useHeaderButtons.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useInlineFormattingButtons.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useListButtons.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/usePluginButtons.js
packages/app-mobile/components/NoteEditor/MarkdownToolbar/types.js
packages/app-mobile/components/NoteEditor/NoteEditor.test.js packages/app-mobile/components/NoteEditor/NoteEditor.test.js
packages/app-mobile/components/NoteEditor/NoteEditor.js packages/app-mobile/components/NoteEditor/NoteEditor.js
packages/app-mobile/components/NoteEditor/SearchPanel.js packages/app-mobile/components/NoteEditor/SearchPanel.js
@ -629,7 +625,6 @@ packages/app-mobile/components/NoteEditor/commandDeclarations.js
packages/app-mobile/components/NoteEditor/hooks/useCodeMirrorPlugins.js packages/app-mobile/components/NoteEditor/hooks/useCodeMirrorPlugins.js
packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.test.js packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.test.js
packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.js packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.js
packages/app-mobile/components/NoteEditor/hooks/useKeyboardVisible.js
packages/app-mobile/components/NoteEditor/types.js packages/app-mobile/components/NoteEditor/types.js
packages/app-mobile/components/NoteItem.js packages/app-mobile/components/NoteItem.js
packages/app-mobile/components/NoteList.js packages/app-mobile/components/NoteList.js
@ -646,6 +641,7 @@ packages/app-mobile/components/SelectDateTimeDialog.js
packages/app-mobile/components/SideMenu.js packages/app-mobile/components/SideMenu.js
packages/app-mobile/components/SideMenuContentNote.js packages/app-mobile/components/SideMenuContentNote.js
packages/app-mobile/components/TextInput.js packages/app-mobile/components/TextInput.js
packages/app-mobile/components/ToggleSpaceButton.js
packages/app-mobile/components/accessibility/AccessibleModalMenu.js packages/app-mobile/components/accessibility/AccessibleModalMenu.js
packages/app-mobile/components/accessibility/AccessibleView.js packages/app-mobile/components/accessibility/AccessibleView.js
packages/app-mobile/components/app-nav.js packages/app-mobile/components/app-nav.js
@ -729,8 +725,14 @@ packages/app-mobile/components/screens/ConfigScreen/plugins/utils/useUpdateState
packages/app-mobile/components/screens/ConfigScreen/types.js packages/app-mobile/components/screens/ConfigScreen/types.js
packages/app-mobile/components/screens/JoplinCloudLoginScreen.js packages/app-mobile/components/screens/JoplinCloudLoginScreen.js
packages/app-mobile/components/screens/LogScreen.js packages/app-mobile/components/screens/LogScreen.js
packages/app-mobile/components/screens/Note.test.js packages/app-mobile/components/screens/Note/Note.test.js
packages/app-mobile/components/screens/Note.js packages/app-mobile/components/screens/Note/Note.js
packages/app-mobile/components/screens/Note/commands/attachFile.js
packages/app-mobile/components/screens/Note/commands/hideKeyboard.js
packages/app-mobile/components/screens/Note/commands/index.js
packages/app-mobile/components/screens/Note/commands/setTags.js
packages/app-mobile/components/screens/Note/commands/toggleVisiblePanes.js
packages/app-mobile/components/screens/Note/types.js
packages/app-mobile/components/screens/NoteTagsDialog.js packages/app-mobile/components/screens/NoteTagsDialog.js
packages/app-mobile/components/screens/Notes.js packages/app-mobile/components/screens/Notes.js
packages/app-mobile/components/screens/SearchScreen/SearchResults.js packages/app-mobile/components/screens/SearchScreen/SearchResults.js
@ -754,6 +756,7 @@ packages/app-mobile/services/AlarmServiceDriver.android.js
packages/app-mobile/services/AlarmServiceDriver.ios.js packages/app-mobile/services/AlarmServiceDriver.ios.js
packages/app-mobile/services/AlarmServiceDriver.web.js packages/app-mobile/services/AlarmServiceDriver.web.js
packages/app-mobile/services/BackButtonService.js packages/app-mobile/services/BackButtonService.js
packages/app-mobile/services/commands/stateToWhenClauseContext.js
packages/app-mobile/services/e2ee/RSA.react-native.js packages/app-mobile/services/e2ee/RSA.react-native.js
packages/app-mobile/services/e2ee/crypto.js packages/app-mobile/services/e2ee/crypto.js
packages/app-mobile/services/plugins/PlatformImplementation.js packages/app-mobile/services/plugins/PlatformImplementation.js
@ -795,6 +798,7 @@ packages/app-mobile/utils/fs-driver/testUtil/createFilesFromPathRecord.js
packages/app-mobile/utils/fs-driver/testUtil/verifyDirectoryMatches.js packages/app-mobile/utils/fs-driver/testUtil/verifyDirectoryMatches.js
packages/app-mobile/utils/getPackageInfo.js packages/app-mobile/utils/getPackageInfo.js
packages/app-mobile/utils/getVersionInfoText.js packages/app-mobile/utils/getVersionInfoText.js
packages/app-mobile/utils/hooks/useKeyboardVisible.js
packages/app-mobile/utils/hooks/useOnLongPressProps.js packages/app-mobile/utils/hooks/useOnLongPressProps.js
packages/app-mobile/utils/hooks/useReduceMotionEnabled.js packages/app-mobile/utils/hooks/useReduceMotionEnabled.js
packages/app-mobile/utils/image/fileToImage.web.js packages/app-mobile/utils/image/fileToImage.web.js
@ -819,6 +823,7 @@ packages/app-mobile/utils/shim-init-react/shimInitShared.js
packages/app-mobile/utils/testing/createMockReduxStore.js packages/app-mobile/utils/testing/createMockReduxStore.js
packages/app-mobile/utils/testing/getWebViewDomById.js packages/app-mobile/utils/testing/getWebViewDomById.js
packages/app-mobile/utils/testing/getWebViewWindowById.js packages/app-mobile/utils/testing/getWebViewWindowById.js
packages/app-mobile/utils/testing/setupGlobalStore.js
packages/app-mobile/utils/types.js packages/app-mobile/utils/types.js
packages/app-mobile/web/serviceWorker.js packages/app-mobile/web/serviceWorker.js
packages/default-plugins/build.js packages/default-plugins/build.js
@ -891,6 +896,7 @@ packages/editor/CodeMirror/utils/formatting/toggleSelectedLinesStartWith.js
packages/editor/CodeMirror/utils/formatting/types.js packages/editor/CodeMirror/utils/formatting/types.js
packages/editor/CodeMirror/utils/getSearchState.js packages/editor/CodeMirror/utils/getSearchState.js
packages/editor/CodeMirror/utils/growSelectionToNode.js packages/editor/CodeMirror/utils/growSelectionToNode.js
packages/editor/CodeMirror/utils/handleLinkEditRequests.js
packages/editor/CodeMirror/utils/handlePasteEvent.js packages/editor/CodeMirror/utils/handlePasteEvent.js
packages/editor/CodeMirror/utils/isCursorAtBeginning.js packages/editor/CodeMirror/utils/isCursorAtBeginning.js
packages/editor/CodeMirror/utils/isInSyntaxNode.js packages/editor/CodeMirror/utils/isInSyntaxNode.js
@ -963,6 +969,8 @@ packages/lib/commands/historyForward.js
packages/lib/commands/index.js packages/lib/commands/index.js
packages/lib/commands/openMasterPasswordDialog.js packages/lib/commands/openMasterPasswordDialog.js
packages/lib/commands/permanentlyDeleteNote.js packages/lib/commands/permanentlyDeleteNote.js
packages/lib/commands/renderMarkup.test.js
packages/lib/commands/renderMarkup.js
packages/lib/commands/synchronize.js packages/lib/commands/synchronize.js
packages/lib/components/EncryptionConfigScreen/utils.test.js packages/lib/components/EncryptionConfigScreen/utils.test.js
packages/lib/components/EncryptionConfigScreen/utils.js packages/lib/components/EncryptionConfigScreen/utils.js
@ -1171,6 +1179,8 @@ packages/lib/services/interop/InteropService_Importer_Raw.js
packages/lib/services/interop/Module.test.js packages/lib/services/interop/Module.test.js
packages/lib/services/interop/Module.js packages/lib/services/interop/Module.js
packages/lib/services/interop/types.js packages/lib/services/interop/types.js
packages/lib/services/interop/utils.test.js
packages/lib/services/interop/utils.js
packages/lib/services/joplinCloudUtils.js packages/lib/services/joplinCloudUtils.js
packages/lib/services/joplinServer/personalizedUserContentBaseUrl.js packages/lib/services/joplinServer/personalizedUserContentBaseUrl.js
packages/lib/services/keychain/KeychainService.test.js packages/lib/services/keychain/KeychainService.test.js

View File

@ -3,23 +3,23 @@
"prefer-absolute-version-dependencies": ["error", "prefer-absolute-version-dependencies": ["error",
{ {
"exceptions": [ "exceptions": [
"@joplin/lib",
"@joplin/renderer",
"@joplin/editor", "@joplin/editor",
"@joplin/pdf-viewer",
"@joplin/fork-htmlparser2", "@joplin/fork-htmlparser2",
"@joplin/fork-sax", "@joplin/fork-sax",
"@joplin/fork-uslug", "@joplin/fork-uslug",
"@joplin/htmlpack", "@joplin/htmlpack",
"@joplin/turndown", "@joplin/lib",
"@joplin/turndown-plugin-gfm", "@joplin/onenote-converter",
"@joplin/tools", "@joplin/pdf-viewer",
"@joplin/react-native-saf-x",
"@joplin/react-native-alarm-notification", "@joplin/react-native-alarm-notification",
"@joplin/react-native-saf-x",
"@joplin/renderer",
"@joplin/tools",
"@joplin/turndown-plugin-gfm",
"@joplin/turndown",
"@joplin/utils" "@joplin/utils"
] ]
} }
] ]
} }
} }

View File

@ -1,13 +0,0 @@
diff --git a/templates/nsis/include/allowOnlyOneInstallerInstance.nsh b/templates/nsis/include/allowOnlyOneInstallerInstance.nsh
index a1fd1875d852ff69c087a3103eff827c20d37ca5..5222614ddad3276876857e7a9dde4017a6b9fc85 100644
--- a/templates/nsis/include/allowOnlyOneInstallerInstance.nsh
+++ b/templates/nsis/include/allowOnlyOneInstallerInstance.nsh
@@ -42,7 +42,7 @@
${nsProcess::FindProcess} "${_FILE}" ${_ERR}
!else
# find process owned by current user
- nsExec::Exec `cmd /c tasklist /FI "USERNAME eq %USERNAME%" /FI "IMAGENAME eq ${_FILE}" | %SYSTEMROOT%\System32\find.exe "${_FILE}"`
+ nsExec::Exec `cmd /c tasklist /FI "USERNAME eq %USERNAME%" /FI "PID ne $pid" /FI "IMAGENAME eq ${_FILE}" | %SYSTEMROOT%\System32\find.exe "${_FILE}"`
Pop ${_ERR}
!endif
!macroend

View File

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 682.66669 682.66669"
height="682.66669"
width="682.66669"
xml:space="preserve"
id="svg2"
version="1.1"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
sodipodi:docname="JoplinLetterBlue.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview13"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
showgrid="false"
inkscape:zoom="0.77490232"
inkscape:cx="366.49781"
inkscape:cy="360.69062"
inkscape:window-width="1366"
inkscape:window-height="708"
inkscape:window-x="0"
inkscape:window-y="30"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<defs
id="defs6">
<linearGradient
id="linearGradient26"
spreadMethod="pad"
gradientTransform="matrix(-4387.91,4387.91,4387.91,4387.91,4753.95,366.05)"
gradientUnits="userSpaceOnUse"
y2="0"
x2="1"
y1="0"
x1="0">
<stop
id="stop22"
offset="0"
style="stop-opacity:1;stop-color:#004caf" />
<stop
id="stop24"
offset="1"
style="stop-opacity:1;stop-color:#1f95f8" />
</linearGradient>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath829"><path
id="path831"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.999997"
d="M 3961.59,4435.23 H 2570.18 c -13.15,0 -23.78,-10.64 -23.78,-23.77 v -441.84 c 0,-14.87 12.04,-26.92 26.92,-26.92 h 190.77 c 77.16,0 139.73,-59.35 146.43,-134.77 V 3505 3336.23 1728.75 1717.36 h -0.052 c 0.48,-16.84 -0.1898,-33.4 -1.83,-49.71 -0.18,-2.38 -0.5003,-4.73 -0.7902,-7.09 -1.0998,-9.53 -2.3199,-19.01 -4.17,-28.29 -1.0098,-5.29 -2.4399,-10.44 -3.7098,-15.65 -1.71,-6.93 -3.09,-13.97 -5.22,-20.75 -12.5802,-40.27 -32.4702,-77.62 -59.9802,-110.5 -1.0098,-1.17 -2.2599,-2.25 -3.2598,-3.41 -8.3901,-9.72 -17.2002,-19.19 -26.9502,-28.06 -9.84,-8.95 -20.2599,-17.27 -31.2099,-25 -77.8401,-55.14 -182.61,-79.4 -299.67,-68.2 -149.2599,14.03 -297.3399,81.72 -417.03,190.62 -119.6701,108.89 -194.08,243.62 -209.4799,379.41 -13.8501,121.48 22.5498,228.38 102.42,301.05 0.21,0.1598 0.3997,0.3098 0.5602,0.48 3.09,2.77 6.4901,5.2 9.6701,7.87 57.16,47.89 131.6701,76.91 216.7,84.91 0.96,0.09 1.8801,0.24 2.79,0.3203 8.9499,0.79 18.0699,1.15 27.27,1.49 4.8099,0.1598 9.5601,0.5003 14.4399,0.54 1.62,0.023 3.1602,0.1898 4.7802,0.1898 2.8998,0 5.91,-0.3803 8.8098,-0.42 13.4001,-0.21 26.9001,-0.7601 40.6701,-1.9401 1.74,-0.1402 3.3999,-0.08 5.19,-0.24 1.2699,-0.1297 2.5299,-0.4102 3.8001,-0.54 78,-7.82 155.2299,-31.11 228.5199,-66.3999 1.53,-0.068 3.3,-0.54 5.5099,-1.7601 22.34,-12.3399 26.6201,0.9 27.2801,9.6501 v 382.2399 282.8201 c 0,19.05 -13.2501,35.8999 -31.83,39.99 -394.7601,86.88 -782.08,-3.5501 -1055.38,-252.3401 -238.7499,-217.1799 -354.24,-530.5799 -316.8201,-859.7899 33.39,-293.23 183.9102,-574.94 423.88,-793.33 233.8901,-212.79003 531.69,-345.86006 838.8801,-374.80106 42.33,-3.918 84.8601,-5.93797 126.36,-5.93797 293.3799,0 565.6099,100.59802 766.54,283.37903 190.3401,173.3 304.35,411.27 321.0799,670.16 l 1.55,1697.91 h 0.1703 v 453.97 h 0.06 v 7.92 c 1.72,80.1199 67.05,144.58 147.61,144.58 h 190.77 c 14.8599,0 26.9199,12.05 26.9199,26.9199 v 441.84 c 0,13.13 -10.6299,23.77 -23.7799,23.77" /></clipPath></defs>
<g
id="g14"
transform="matrix(0.13333333,0,0,-0.13333333,0,682.66667)"
mask="none"
clip-path="url(#clipPath829)">
<g
clip-path="url(#clipPath20)"
id="g16">
<path
id="path28"
style="fill:url(#linearGradient26);fill-opacity:1;fill-rule:nonzero;stroke:none"
d="M 3873.89,0 H 1246.11 C 560.754,0 0,560.75 0,1246.11 V 3873.88 C 0,4559.25 560.754,5120 1246.11,5120 H 3873.89 C 4559.25,5120 5120,4559.25 5120,3873.88 V 1246.11 C 5120,560.75 4559.25,0 3873.89,0" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -1,4 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Joplin]]></title><description><![CDATA[Joplin, the open source note-taking application]]></description><link>https://joplinapp.org</link><generator>RSS for Node</generator><lastBuildDate>Sat, 07 Dec 2024 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Sat, 07 Dec 2024 00:00:00 GMT</pubDate><item><title><![CDATA[Project 2: Making Joplin more accessible with WCAG-2 compliance]]></title><description><![CDATA[<p>We're always looking for ways to make Joplin better for everyone, and one of the key steps in improving accessibility is implementing the <a href="https://www.w3.org/TR/WCAG20/">Web Content Accessibility Guidelines</a> (WCAG 2). These guidelines help ensure that our app is usable for all users, including those with disabilities, while also enhancing the overall user experience for everyone.</p> <?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Joplin]]></title><description><![CDATA[Joplin, the open source note-taking application]]></description><link>https://joplinapp.org</link><generator>RSS for Node</generator><lastBuildDate>Tue, 17 Dec 2024 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Tue, 17 Dec 2024 00:00:00 GMT</pubDate><item><title><![CDATA[Project 4: Handwritten Text Recognition (HTR) for Joplin]]></title><description><![CDATA[<p>Joplin is partnering with a French government institution to bring you innovative new features! We will work on accessibility, voice typing, HTR and add Rocketbook integration. Today we'll present the planned HTR integration:</p>
<p>Currently, Joplin’s OCR (Optical Character Recognition) feature is designed to recognise printed text, which works great for scanning documents with standard fonts. However, we’re looking to expand this functionality to support handwritten text recognition (HTR), which would be beneficial to handle scanned, handwritten documents, as well as for the upcoming Rocketbook integration.</p>
<p>Handwritten text recognition is complex task, requiring significant processing power and large machine learning models. Because of this, we plan to implement HTR via a server, possibly integrated with Joplin Cloud or Joplin Server. The beauty of this approach is that handwritten text recognition is a rapidly evolving field, so we can continuously improve the server-side model. This means that every Joplin app can benefit from these updates without needing to redeploy or update the app itself.</p>
<p>For the Rocketbook integration, this integration will make a significant difference. Right now, your handwritten documents would be scanned as images, but with HTR, Joplin will be able to recognise the actual text you’ve written. Not only will your handwritten notes become searchable, but you’ll also be able to copy and paste the text into other documents.</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20241217-htr.jpg" alt="HTR illustration"></p>
]]></description><link>https://joplinapp.org/news/20241217-project-4-htr</link><guid isPermaLink="false">20241217-project-4-htr</guid><pubDate>Tue, 17 Dec 2024 00:00:00 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[Project 3: Rocketbook integration with Joplin]]></title><description><![CDATA[<p>Joplin is partnering with a French government institution to bring you innovative new features! We will work on accessibility, voice typing, HTR and add Rocketbook integration. Today we'll present the planned Rocketbook integration:</p>
<p><a href="https://getrocketbook.com/pages/how-rocketbook-works">Rocketbook</a> is a popular brand of smart notebooks designed to make note-taking more efficient and sustainable. These notebooks feature special erasable pages that you can write on with specific pens and then wipe clean with a damp cloth. What makes Rocketbook even more appealing is its ability to scan and upload your notes to cloud services for easy storage and sharing.</p>
<p>We plan to create an integration between Rocketbook and Joplin, designed to make the note-taking process even smoother. While Rocketbook typically works through its own official app, we plan to integrate it directly into Joplin without relying on third-party apps or APIs. This means that you can enjoy the benefits of Rocketbook’s smart features while using Joplin as your primary note-taking platform.</p>
<p>The great thing about this integration is that it’s not limited to just Rocketbook. As long as the smart notebook uses QR codes to identify pages, the same functionality will work. This means you can use Joplin with a wide variety of smart notebooks, making it a versatile tool for all your note-taking needs.</p>
<p>With this integration, we’re making it easier than ever to combine the power of smart notebooks with the flexibility of Joplin’s open-source platform. Whether you're using Rocketbook or another smart notebook, Joplin will be ready to help you organise and access your notes in the cloud!</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20241216-rocketbook.jpg" alt="Rocketbook notebook"></p>
]]></description><link>https://joplinapp.org/news/20241216-project-3-rocketbook</link><guid isPermaLink="false">20241216-project-3-rocketbook</guid><pubDate>Mon, 16 Dec 2024 00:00:00 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[Project 2: Making Joplin more accessible with WCAG-2 compliance]]></title><description><![CDATA[<p>We're always looking for ways to make Joplin better for everyone, and one of the key steps in improving accessibility is implementing the <a href="https://www.w3.org/TR/WCAG20/">Web Content Accessibility Guidelines</a> (WCAG 2). These guidelines help ensure that our app is usable for all users, including those with disabilities, while also enhancing the overall user experience for everyone.</p>
<p>To get started with WCAG 2 support, we’ll run an accessibility audit with a tool such as <a href="https://wave.webaim.org">Web Accessibility Evaluation Tools</a> (WAVE). This will help us spot any accessibility issues in the app that we need to address.</p> <p>To get started with WCAG 2 support, we’ll run an accessibility audit with a tool such as <a href="https://wave.webaim.org">Web Accessibility Evaluation Tools</a> (WAVE). This will help us spot any accessibility issues in the app that we need to address.</p>
<p>Once the audit is complete, we'll take action on the issues we find. Here's a look at some of the things we'll focus on:</p> <p>Once the audit is complete, we'll take action on the issues we find. Here's a look at some of the things we'll focus on:</p>
<ul> <ul>
@ -393,10 +404,4 @@ sys 0m38.013s</p>
<p>Longer term I would like to create a non-profit organisation to handle the open source applications and to make decisions about the project, as well as to decide how to allocate any funding we receive (for example from GSoC).</p> <p>Longer term I would like to create a non-profit organisation to handle the open source applications and to make decisions about the project, as well as to decide how to allocate any funding we receive (for example from GSoC).</p>
<h2>Looking forward<a name="looking-forward" href="#looking-forward" class="heading-anchor">🔗</a></h2> <h2>Looking forward<a name="looking-forward" href="#looking-forward" class="heading-anchor">🔗</a></h2>
<p>Those past 6 years of developing Joplin have been an exciting and rewarding experience, thank you to all of you of the friendly and vibrant Joplin community for your contribution toward making Joplin the software it is today, and looking forward to continuing the journey together!</p> <p>Those past 6 years of developing Joplin have been an exciting and rewarding experience, thank you to all of you of the friendly and vibrant Joplin community for your contribution toward making Joplin the software it is today, and looking forward to continuing the journey together!</p>
]]></description><link>https://joplinapp.org/news/20221012-Joplin-Company</link><guid isPermaLink="false">20221012-Joplin-Company</guid><pubDate>Wed, 12 Oct 2022 00:00:00 GMT</pubDate><twitter-text>Joplin Cloud is now operated by the Joplin company! More info on the announcement post.</twitter-text></item><item><title><![CDATA[Joplin interview on Website Planet]]></title><description><![CDATA[<p>Website Planet has recently conducted an interview about Joplin - it may give you some insight on the current status of the project, our priorities, and future plans! More on the article page - <a href="https://www.websiteplanet.com/blog/interview-joplin/">Organise Your Thoughts with Open Source Note-Taking App, Joplin</a></p> ]]></description><link>https://joplinapp.org/news/20221012-Joplin-Company</link><guid isPermaLink="false">20221012-Joplin-Company</guid><pubDate>Wed, 12 Oct 2022 00:00:00 GMT</pubDate><twitter-text>Joplin Cloud is now operated by the Joplin company! More info on the announcement post.</twitter-text></item></channel></rss>
]]></description><link>https://joplinapp.org/news/20220906-interview-websiteplanet</link><guid isPermaLink="false">20220906-interview-websiteplanet</guid><pubDate>Tue, 06 Sep 2022 00:00:00 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[Joplin first meetup on 30 August!]]></title><description><![CDATA[<p>We are glad to announce <a href="https://www.meetup.com/joplin/events/287611873/">the first Joplin Meetup</a> that will take place on 30 August 2022 in London!</p>
<p>This is an opportunity to meet other Joplin users as well as some of the main contributors, to discuss the apps, or to ask questions and exchange tips and tricks on how to use the app, develop plugins or contribute to the application. Everybody, technical or not, is welcome!</p>
<p>We will meet at the Old Thameside Inn next to London Bridge. If the weather allows we will be on the terrace outside, if not inside.</p>
<p>More information on the official Meetup page:</p>
<p><a href="https://www.meetup.com/joplin/events/287611873/">https://www.meetup.com/joplin/events/287611873/</a></p>
]]></description><link>https://joplinapp.org/news/20220808-first-meetup</link><guid isPermaLink="false">20220808-first-meetup</guid><pubDate>Mon, 08 Aug 2022 00:00:00 GMT</pubDate><twitter-text>Joplin will have its first Meetup on 30 August! Come and join us at the Old Thameside Inn next to London Bridge! https://www.meetup.com/joplin/events/287611873/</twitter-text></item></channel></rss>

View File

@ -31,7 +31,7 @@ Please see the [donation page](https://github.com/laurent22/joplin/blob/dev/read
# Sponsors # Sponsors
<!-- SPONSORS-ORG --> <!-- SPONSORS-ORG -->
<a href="https://seirei.ne.jp"><img title="Serei Network" width="256" src="https://joplinapp.org/images/sponsors/SeireiNetwork.png"/></a> <a href="https://www.hosting.de/nextcloud/?mtm_campaign=managed-nextcloud&amp;mtm_kwd=joplinapp&amp;mtm_source=joplinapp-webseite&amp;mtm_medium=banner"><img title="Hosting.de" width="256" src="https://joplinapp.org/images/sponsors/HostingDe.png"/></a> <a href="https://citricsheep.com"><img title="Citric Sheep" width="256" src="https://joplinapp.org/images/sponsors/CitricSheep.png"/></a> <a href="https://sorted.travel/?utm_source=joplinapp"><img title="Sorted Travel" width="256" src="https://joplinapp.org/images/sponsors/SortedTravel.png"/></a> <a href="https://celebian.com"><img title="Celebian" width="256" src="https://joplinapp.org/images/sponsors/Celebian.png"/></a> <a href="https://bestkru.com"><img title="BestKru" width="256" src="https://joplinapp.org/images/sponsors/BestKru.png"/></a> <a href="https://www.socialfollowers.uk/buy-tiktok-followers/"><img title="Social Followers" width="256" src="https://joplinapp.org/images/sponsors/SocialFollowers.png"/></a> <a href="https://stormlikes.com/"><img title="Stormlikes" width="256" src="https://joplinapp.org/images/sponsors/Stormlikes.png"/></a> <a href="https://route4me.com"><img title="Route4Me" width="256" src="https://joplinapp.org/images/sponsors/Route4Me.png"/></a> <a href="https://buyyoutubviews.com"><img title="BYTV" width="256" src="https://joplinapp.org/images/sponsors/BYTV.png"/></a> <a href="https://casinoreviews.net"><img title="Casino Reviews" width="256" src="https://joplinapp.org/images/sponsors/CasinoReviews.png"/></a> <a href="https://useviral.com.br/"><img title="Comprar seguidores Instagram" width="256" src="https://joplinapp.org/images/sponsors/Useviral.png"/></a> <a href="https://seirei.ne.jp"><img title="Serei Network" width="256" src="https://joplinapp.org/images/sponsors/SeireiNetwork.png"/></a> <a href="https://www.hosting.de/nextcloud/?mtm_campaign=managed-nextcloud&amp;mtm_kwd=joplinapp&amp;mtm_source=joplinapp-webseite&amp;mtm_medium=banner"><img title="Hosting.de" width="256" src="https://joplinapp.org/images/sponsors/HostingDe.png"/></a> <a href="https://citricsheep.com"><img title="Citric Sheep" width="256" src="https://joplinapp.org/images/sponsors/CitricSheep.png"/></a> <a href="https://sorted.travel/?utm_source=joplinapp"><img title="Sorted Travel" width="256" src="https://joplinapp.org/images/sponsors/SortedTravel.png"/></a> <a href="https://celebian.com"><img title="Celebian" width="256" src="https://joplinapp.org/images/sponsors/Celebian.png"/></a> <a href="https://bestkru.com"><img title="BestKru" width="256" src="https://joplinapp.org/images/sponsors/BestKru.png"/></a> <a href="https://www.socialfollowers.uk/buy-tiktok-followers/"><img title="Social Followers" width="256" src="https://joplinapp.org/images/sponsors/SocialFollowers.png"/></a> <a href="https://stormlikes.com/"><img title="Stormlikes" width="256" src="https://joplinapp.org/images/sponsors/Stormlikes.png"/></a> <a href="https://route4me.com"><img title="Route4Me" width="256" src="https://joplinapp.org/images/sponsors/Route4Me.png"/></a> <a href="https://buyyoutubviews.com"><img title="BYTV" width="256" src="https://joplinapp.org/images/sponsors/BYTV.png"/></a> <a href="https://casinoreviews.net"><img title="Casino Reviews" width="256" src="https://joplinapp.org/images/sponsors/CasinoReviews.png"/></a> <a href="https://useviral.com.br/"><img title="Comprar seguidores Instagram" width="256" src="https://joplinapp.org/images/sponsors/Useviral.png"/></a> <a href="https://ca.edubirdie.com/"><img title="Achieve academic success with Edubirdie — your trusted partner for expert writing assistance and resources!" width="256" src="https://joplinapp.org/images/sponsors/Edubirdie.png"/></a>
<!-- SPONSORS-ORG --> <!-- SPONSORS-ORG -->
* * * * * *

View File

@ -105,7 +105,6 @@
"react-native-camera@4.2.1": "patch:react-native-camera@npm%3A4.2.1#./.yarn/patches/react-native-camera-npm-4.2.1-24b2600a7e.patch", "react-native-camera@4.2.1": "patch:react-native-camera@npm%3A4.2.1#./.yarn/patches/react-native-camera-npm-4.2.1-24b2600a7e.patch",
"react-native-vosk@0.1.12": "patch:react-native-vosk@npm%3A0.1.12#./.yarn/patches/react-native-vosk-npm-0.1.12-76b1caaae8.patch", "react-native-vosk@0.1.12": "patch:react-native-vosk@npm%3A0.1.12#./.yarn/patches/react-native-vosk-npm-0.1.12-76b1caaae8.patch",
"eslint": "patch:eslint@8.57.0#./.yarn/patches/eslint-npm-8.39.0-d92bace04d.patch", "eslint": "patch:eslint@8.57.0#./.yarn/patches/eslint-npm-8.39.0-d92bace04d.patch",
"app-builder-lib@24.4.0": "patch:app-builder-lib@npm%3A24.4.0#./.yarn/patches/app-builder-lib-npm-24.4.0-05322ff057.patch",
"nanoid": "patch:nanoid@npm%3A3.3.7#./.yarn/patches/nanoid-npm-3.3.7-98824ba130.patch", "nanoid": "patch:nanoid@npm%3A3.3.7#./.yarn/patches/nanoid-npm-3.3.7-98824ba130.patch",
"pdfjs-dist": "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",
"husky": "patch:husky@npm%3A3.1.0#./.yarn/patches/husky-npm-3.1.0-5cc13e4e34.patch", "husky": "patch:husky@npm%3A3.1.0#./.yarn/patches/husky-npm-3.1.0-5cc13e4e34.patch",

View File

@ -408,6 +408,8 @@ class Application extends BaseApplication {
this.initRedux(); this.initRedux();
if (!shim.sharpEnabled()) this.logger().warn('Sharp is disabled - certain image-related features will not be available');
// If we have some arguments left at this point, it's a command // If we have some arguments left at this point, it's a command
// so execute it. // so execute it.
if (argv.length) { if (argv.length) {

View File

@ -165,18 +165,14 @@ export default class FolderListWidget extends ListWidget {
const wasSelectedItemId = this.selectedJoplinItemId; const wasSelectedItemId = this.selectedJoplinItemId;
const previousParentType = this.notesParentType; const previousParentType = this.notesParentType;
this.logger().info('FFFFFFFFFFFFF', JSON.stringify(this.folders, null, 4));
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
let newItems: any[] = []; let newItems: any[] = [];
const orderFolders = (parentId: string) => { const orderFolders = (parentId: string) => {
this.logger().info('PARENT', parentId);
for (let i = 0; i < this.folders.length; i++) { for (let i = 0; i < this.folders.length; i++) {
const f = this.folders[i]; const f = this.folders[i];
const originalParent = this.folders_.find(f => f.id === f.parent_id); const originalParent = this.folders_.find(f => f.id === f.parent_id);
const folderParentId = getDisplayParentId(f, originalParent); // f.parent_id ? f.parent_id : ''; const folderParentId = getDisplayParentId(f, originalParent); // f.parent_id ? f.parent_id : '';
this.logger().info('FFF', f.title, folderParentId);
if (folderParentId === parentId) { if (folderParentId === parentId) {
newItems.push(f); newItems.push(f);
if (this.folderHasChildren_(this.folders, f.id)) orderFolders(f.id); if (this.folderHasChildren_(this.folders, f.id)) orderFolders(f.id);

View File

@ -22,7 +22,6 @@ const Setting = require('@joplin/lib/models/Setting').default;
const Revision = require('@joplin/lib/models/Revision').default; const Revision = require('@joplin/lib/models/Revision').default;
const Logger = require('@joplin/utils/Logger').default; const Logger = require('@joplin/utils/Logger').default;
const FsDriverNode = require('@joplin/lib/fs-driver-node').default; const FsDriverNode = require('@joplin/lib/fs-driver-node').default;
const sharp = require('sharp');
const { shimInit } = require('@joplin/lib/shim-init-node.js'); const { shimInit } = require('@joplin/lib/shim-init-node.js');
const shim = require('@joplin/lib/shim').default; const shim = require('@joplin/lib/shim').default;
const { _ } = require('@joplin/lib/locale'); const { _ } = require('@joplin/lib/locale');
@ -32,6 +31,14 @@ const envFromArgs = require('@joplin/lib/envFromArgs');
const nodeSqlite = require('sqlite3'); const nodeSqlite = require('sqlite3');
const initLib = require('@joplin/lib/initLib').default; const initLib = require('@joplin/lib/initLib').default;
let sharp = null;
try {
sharp = require('sharp');
} catch (error) {
// Don't print an error or it will pollute stdout every time the app is started. A warning will
// be printed in app.ts
}
const env = envFromArgs(process.argv); const env = envFromArgs(process.argv);
const fsDriver = new FsDriverNode(); const fsDriver = new FsDriverNode();

View File

@ -35,7 +35,7 @@
], ],
"owner": "Laurent Cozic" "owner": "Laurent Cozic"
}, },
"version": "3.2.0", "version": "3.2.2",
"bin": "./main.js", "bin": "./main.js",
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"

View File

@ -0,0 +1,33 @@
<ul>
<li>
<img src=":/0123456789abcdef0123456789abcdef" width="10" height="11"/>
<ul>
<li>HTML images just before a sublist should have a closing &lt;img&gt; tag added.</li>
<li>Otherwise, when rendered to Markdown again, the subsequent list items will be rendered as HTML.</li>
</ul>
</li>
<li>
<img src=":/0123456789abcdef0123456789abcdef" width="10" height="11"/>
<br/>
<strong>A closing tag is also necessary if the image is followed by content on a new line.</strong>
</li>
<li>
<img src=":/0123456789abcdef0123456789abcdef"/>
<ul>
<li>Similar logic isn't necessary for images converted to Markdown.</li>
</ul>
</li>
<li>
<img src=":/0123456789abcdef0123456789abcdef" width="10" height="11"/>
</li>
<li>It also isn't necessary if the next item is at the same level as the image...</li>
<li>
...or if the image has text just before it.
<img src=":/0123456789abcdef0123456789abcdef" width="10" height="11"/>
<ul>
<li>
See <a href="https://github.com/laurent22/joplin/issues/11382">this issue</a> for details.
</li>
</ul>
</li>
</ul>

View File

@ -0,0 +1,11 @@
- <img src=":/0123456789abcdef0123456789abcdef" width="10" height="11"></img>
- HTML images just before a sublist should have a closing &lt;img&gt; tag added.
- Otherwise, when rendered to Markdown again, the subsequent list items will be rendered as HTML.
- <img src=":/0123456789abcdef0123456789abcdef" width="10" height="11"></img>
**A closing tag is also necessary if the image is followed by content on a new line.**
- ![](:/0123456789abcdef0123456789abcdef)
- Similar logic isn't necessary for images converted to Markdown.
- <img src=":/0123456789abcdef0123456789abcdef" width="10" height="11">
- It also isn't necessary if the next item is at the same level as the image...
- ...or if the image has text just before it. <img src=":/0123456789abcdef0123456789abcdef" width="10" height="11">
- See [this issue](https://github.com/laurent22/joplin/issues/11382) for details.

View File

@ -1 +1 @@
<table><tbody><tr><td>Left side of the main table</td><td><b>Nested Table</b><table><tbody><tr><td>nested table C1</td><td>nested table C2</td></tr><tr><td>nested table</td><td>nested table</td></tr></tbody></table></td></tr></tbody></table> <div class="joplin-table-wrapper"><table><tbody><tr><td>Left side of the main table</td><td><b>Nested Table</b><table><tbody><tr><td>nested table C1</td><td>nested table C2</td></tr><tr><td>nested table</td><td>nested table</td></tr></tbody></table></td></tr></tbody></table></div>

View File

@ -1 +1 @@
<table><thead><tr><th>A</th><th>B</th></tr></thead><tbody><tr><td><blockquote><p>Finally, from so little sleeping and so much reading, his brain dried up and he went completely out of his mind.</p><p>- Miguel de Cervantes</p></blockquote></td><td>d</td></tr></tbody></table> <div class="joplin-table-wrapper"><table><thead><tr><th>A</th><th>B</th></tr></thead><tbody><tr><td><blockquote><p>Finally, from so little sleeping and so much reading, his brain dried up and he went completely out of his mind.</p><p>- Miguel de Cervantes</p></blockquote></td><td>d</td></tr></tbody></table></div>

View File

@ -1 +1 @@
<table><thead><tr><th>Code</th><th>Description</th></tr><tr><td><pre><code>const test = "hello";</code></pre></td><td>abcd</td></tr><tr><td><pre><code>const test = "hello";</code></pre></td><td>abcd</td></tr></thead></table> <div class="joplin-table-wrapper"><table><thead><tr><th>Code</th><th>Description</th></tr><tr><td><pre><code>const test = "hello";</code></pre></td><td>abcd</td></tr><tr><td><pre><code>const test = "hello";</code></pre></td><td>abcd</td></tr></thead></table></div>

View File

@ -1 +1 @@
<table class="jop-noMdConv"><thead class="jop-noMdConv"><tr class="jop-noMdConv"><th class="jop-noMdConv">Code</th><th class="jop-noMdConv">Description</th></tr><tr class="jop-noMdConv"><td class="jop-noMdConv"><pre class="jop-noMdConv"><code class="">const test = "hello";</code></pre></td><td class="jop-noMdConv">abcda</td></tr><tr class="jop-noMdConv"><td class="jop-noMdConv"><pre class="jop-noMdConv"><code class="">const test = "hello";</code></pre></td><td class="jop-noMdConv">abcd</td></tr></thead></table> <div class="joplin-table-wrapper"><table class="jop-noMdConv"><thead class="jop-noMdConv"><tr class="jop-noMdConv"><th class="jop-noMdConv">Code</th><th class="jop-noMdConv">Description</th></tr><tr class="jop-noMdConv"><td class="jop-noMdConv"><pre class="jop-noMdConv"><code class="">const test = "hello";</code></pre></td><td class="jop-noMdConv">abcda</td></tr><tr class="jop-noMdConv"><td class="jop-noMdConv"><pre class="jop-noMdConv"><code class="">const test = "hello";</code></pre></td><td class="jop-noMdConv">abcd</td></tr></thead></table></div>

View File

@ -1,10 +1,10 @@
<table class="jop-noMdConv"><thead class="jop-noMdConv"><tr class="jop-noMdConv"><th class="jop-noMdConv">Code</th><th class="jop-noMdConv">Description</th></tr><tr class="jop-noMdConv"><td class="jop-noMdConv"><pre class="jop-noMdConv"><code class="">const test = "hello"; <div class="joplin-table-wrapper"><table class="jop-noMdConv"><thead class="jop-noMdConv"><tr class="jop-noMdConv"><th class="jop-noMdConv">Code</th><th class="jop-noMdConv">Description</th></tr><tr class="jop-noMdConv"><td class="jop-noMdConv"><pre class="jop-noMdConv"><code class="">const test = "hello";
<!-- -->
// Another line // Another line
console.log('Test...'); console.log('Test...');
<!-- -->
// Blank lines // Blank lines
<!-- -->
<!-- -->
<!-- -->
// Should not break things.</code></pre></td><td class="jop-noMdConv">abcda</td></tr><tr class="jop-noMdConv"><td class="jop-noMdConv"><pre class="jop-noMdConv"><code class="">const test = "hello";</code></pre></td><td class="jop-noMdConv">abcd</td></tr></thead></table> // Should not break things.</code></pre></td><td class="jop-noMdConv">abcda</td></tr><tr class="jop-noMdConv"><td class="jop-noMdConv"><pre class="jop-noMdConv"><code class="">const test = "hello";</code></pre></td><td class="jop-noMdConv">abcd</td></tr></thead></table></div>

View File

@ -1 +1 @@
<table><thead><tr><th>A</th><th>B</th></tr></thead><tbody><tr><td><h1>Testing</h1><p>hello</p></td><td>d</td></tr></tbody></table> <div class="joplin-table-wrapper"><table><thead><tr><th>A</th><th>B</th></tr></thead><tbody><tr><td><h1>Testing</h1><p>hello</p></td><td>d</td></tr></tbody></table></div>

View File

@ -1 +1 @@
<table><thead><tr><th>A</th><th>B</th></tr></thead><tbody><tr><td>One line<hr>Two line</td><td>d</td></tr></tbody></table> <div class="joplin-table-wrapper"><table><thead><tr><th>A</th><th>B</th></tr></thead><tbody><tr><td>One line<hr>Two line</td><td>d</td></tr></tbody></table></div>

View File

@ -1 +1 @@
<table border="1" style="border-collapse: collapse; width: 100%;" data-mce-selected="1"><tbody><tr><td style="width: 50.0518%;">Header 1</td><td style="width: 50.0518%;">Header 2</td></tr><tr><td style="width: 50.0518%;"><br></td><td style="width: 50.0518%;"><ul><li>Check 1</li><li>Check 2</li></ul></td></tr></tbody></table> <div class="joplin-table-wrapper"><table border="1" style="border-collapse: collapse; width: 100%;" data-mce-selected="1"><tbody><tr><td style="width: 50.0518%;">Header 1</td><td style="width: 50.0518%;">Header 2</td></tr><tr><td style="width: 50.0518%;"><br></td><td style="width: 50.0518%;"><ul><li>Check 1</li><li>Check 2</li></ul></td></tr></tbody></table></div>

View File

@ -0,0 +1 @@
Should keep this comment: <!-- keep this &amp; that -->

View File

@ -0,0 +1 @@
Should keep this comment: <!-- keep this &amp; that -->

View File

@ -8,7 +8,6 @@ import * as exportNotes from './exportNotes';
import * as focusElement from './focusElement'; import * as focusElement from './focusElement';
import * as openNoteInNewWindow from './openNoteInNewWindow'; import * as openNoteInNewWindow from './openNoteInNewWindow';
import * as openProfileDirectory from './openProfileDirectory'; import * as openProfileDirectory from './openProfileDirectory';
import * as renderMarkup from './renderMarkup';
import * as replaceMisspelling from './replaceMisspelling'; import * as replaceMisspelling from './replaceMisspelling';
import * as restoreNoteRevision from './restoreNoteRevision'; import * as restoreNoteRevision from './restoreNoteRevision';
import * as startExternalEditing from './startExternalEditing'; import * as startExternalEditing from './startExternalEditing';
@ -30,7 +29,6 @@ const index: any[] = [
focusElement, focusElement,
openNoteInNewWindow, openNoteInNewWindow,
openProfileDirectory, openProfileDirectory,
renderMarkup,
replaceMisspelling, replaceMisspelling,
restoreNoteRevision, restoreNoteRevision,
startExternalEditing, startExternalEditing,

View File

@ -38,7 +38,7 @@ function countElements(text: string, wordSetter: Function, characterSetter: Func
characterSetter(counter.all); characterSetter(counter.all);
characterNoSpaceSetter(counter.characters); characterNoSpaceSetter(counter.characters);
}); });
text === '' ? lineSetter(0) : lineSetter(text.split('\n').length); lineSetter(text === '' ? 0 : text.split('\n').length);
} }
function formatReadTime(readTimeMinutes: number) { function formatReadTime(readTimeMinutes: number) {

View File

@ -4,14 +4,14 @@ import ToolbarBase from '../../../ToolbarBase';
import { utils as pluginUtils } from '@joplin/lib/services/plugins/reducer'; import { utils as pluginUtils } from '@joplin/lib/services/plugins/reducer';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { AppState } from '../../../../app.reducer'; import { AppState } from '../../../../app.reducer';
import ToolbarButtonUtils, { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils'; import ToolbarButtonUtils, { ToolbarItem } from '@joplin/lib/services/commands/ToolbarButtonUtils';
import stateToWhenClauseContext from '../../../../services/commands/stateToWhenClauseContext'; import stateToWhenClauseContext from '../../../../services/commands/stateToWhenClauseContext';
import { _ } from '@joplin/lib/locale'; import { _ } from '@joplin/lib/locale';
const { buildStyle } = require('@joplin/lib/theme'); const { buildStyle } = require('@joplin/lib/theme');
interface ToolbarProps { interface ToolbarProps {
themeId: number; themeId: number;
toolbarButtonInfos: ToolbarButtonInfo[]; toolbarButtonInfos: ToolbarItem[];
disabled?: boolean; disabled?: boolean;
} }

View File

@ -6,7 +6,7 @@ import attachedResources from '@joplin/lib/utils/attachedResources';
import useScroll from './utils/useScroll'; import useScroll from './utils/useScroll';
import styles_ from './styles'; import styles_ from './styles';
import CommandService from '@joplin/lib/services/CommandService'; import CommandService from '@joplin/lib/services/CommandService';
import { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils'; import { ToolbarItem } from '@joplin/lib/services/commands/ToolbarButtonUtils';
import ToggleEditorsButton, { Value as ToggleEditorsButtonValue } from '../../../ToggleEditorsButton/ToggleEditorsButton'; import ToggleEditorsButton, { Value as ToggleEditorsButtonValue } from '../../../ToggleEditorsButton/ToggleEditorsButton';
import ToolbarButton from '../../../../gui/ToolbarButton/ToolbarButton'; import ToolbarButton from '../../../../gui/ToolbarButton/ToolbarButton';
import usePluginServiceRegistration from '../../utils/usePluginServiceRegistration'; import usePluginServiceRegistration from '../../utils/usePluginServiceRegistration';
@ -1383,7 +1383,9 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
}; };
}, []); }, []);
function renderExtraToolbarButton(key: string, info: ToolbarButtonInfo) { function renderExtraToolbarButton(key: string, info: ToolbarItem) {
if (info.type === 'separator') return null;
return <ToolbarButton return <ToolbarButton
key={key} key={key}
themeId={props.themeId} themeId={props.themeId}
@ -1412,7 +1414,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
for (const info of props.noteToolbarButtonInfos) { for (const info of props.noteToolbarButtonInfos) {
if (leftButtonCommandNames.includes(info.name)) continue; if (leftButtonCommandNames.includes(info.name)) continue;
if (info.name === 'toggleEditors') { if (info.type === 'button' && info.name === 'toggleEditors') {
buttons.push(<ToggleEditorsButton buttons.push(<ToggleEditorsButton
key={info.name} key={info.name}
value={ToggleEditorsButtonValue.RichText} value={ToggleEditorsButtonValue.RichText}

View File

@ -20,7 +20,7 @@ import ToolbarButton from '../ToolbarButton/ToolbarButton';
import Button, { ButtonLevel } from '../Button/Button'; import Button, { ButtonLevel } from '../Button/Button';
import eventManager, { EventName } from '@joplin/lib/eventManager'; import eventManager, { EventName } from '@joplin/lib/eventManager';
import { AppState } from '../../app.reducer'; import { AppState } from '../../app.reducer';
import ToolbarButtonUtils from '@joplin/lib/services/commands/ToolbarButtonUtils'; import ToolbarButtonUtils, { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
import { _, _n } from '@joplin/lib/locale'; import { _, _n } from '@joplin/lib/locale';
import TagList from '../TagList'; import TagList from '../TagList';
import NoteTitleBar from './NoteTitle/NoteTitleBar'; import NoteTitleBar from './NoteTitle/NoteTitleBar';
@ -742,7 +742,7 @@ const mapStateToProps = (state: AppState, ownProps: ConnectProps) => {
], whenClauseContext), ], whenClauseContext),
setTagsToolbarButtonInfo: toolbarButtonUtils.commandsToToolbarButtons([ setTagsToolbarButtonInfo: toolbarButtonUtils.commandsToToolbarButtons([
'setTags', 'setTags',
], whenClauseContext)[0], ], whenClauseContext)[0] as ToolbarButtonInfo,
contentMaxWidth: state.settings['style.editor.contentMaxWidth'], contentMaxWidth: state.settings['style.editor.contentMaxWidth'],
isSafeMode: state.settings.isSafeMode, isSafeMode: state.settings.isSafeMode,
useCustomPdfViewer: false, useCustomPdfViewer: false,

View File

@ -85,7 +85,12 @@ export default function NoteTitleBar(props: Props) {
const onTitleKeydown: React.KeyboardEventHandler<HTMLInputElement> = useCallback((event) => { const onTitleKeydown: React.KeyboardEventHandler<HTMLInputElement> = useCallback((event) => {
const titleElement = event.currentTarget; const titleElement = event.currentTarget;
const selectionAtEnd = titleElement.selectionEnd === titleElement.value.length; const selectionAtEnd = titleElement.selectionEnd === titleElement.value.length;
if ((event.key === 'ArrowDown' && selectionAtEnd) || (event.key === 'Enter' && !event.shiftKey)) { const isNavigationShortcut = (event.key === 'ArrowDown' && selectionAtEnd) || (event.key === 'Enter' && !event.shiftKey);
const composing = event.nativeEvent.isComposing;
// Don't change focus if the navigation shortcut is fired during composition. See
// https://github.com/laurent22/joplin/issues/11485.
if (!composing && isNavigationShortcut) {
event.preventDefault(); event.preventDefault();
const moveCursorToStart = event.key === 'ArrowDown'; const moveCursorToStart = event.key === 'ArrowDown';
void CommandService.instance().execute('focusElement', 'noteBody', { moveCursorToStart }); void CommandService.instance().execute('focusElement', 'noteBody', { moveCursorToStart });

View File

@ -1,5 +1,5 @@
import AsyncActionQueue from '@joplin/lib/AsyncActionQueue'; import AsyncActionQueue from '@joplin/lib/AsyncActionQueue';
import { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils'; import { ToolbarButtonInfo, ToolbarItem } from '@joplin/lib/services/commands/ToolbarButtonUtils';
import { PluginHtmlContents, PluginStates } from '@joplin/lib/services/plugins/reducer'; import { PluginHtmlContents, PluginStates } from '@joplin/lib/services/plugins/reducer';
import { MarkupLanguage } from '@joplin/renderer'; import { MarkupLanguage } from '@joplin/renderer';
import { RenderResult, RenderResultPluginAsset } from '@joplin/renderer/types'; import { RenderResult, RenderResultPluginAsset } from '@joplin/renderer/types';
@ -48,7 +48,7 @@ export interface NoteEditorProps {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
highlightedWords: any[]; highlightedWords: any[];
plugins: PluginStates; plugins: PluginStates;
toolbarButtonInfos: ToolbarButtonInfo[]; toolbarButtonInfos: ToolbarItem[];
setTagsToolbarButtonInfo: ToolbarButtonInfo; setTagsToolbarButtonInfo: ToolbarButtonInfo;
contentMaxWidth: number; contentMaxWidth: number;
isSafeMode: boolean; isSafeMode: boolean;
@ -136,7 +136,7 @@ export interface NoteBodyEditorProps {
locale: string; locale: string;
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied // eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
onDrop: DropHandler; onDrop: DropHandler;
noteToolbarButtonInfos: ToolbarButtonInfo[]; noteToolbarButtonInfos: ToolbarItem[];
plugins: PluginStates; plugins: PluginStates;
fontSize: number; fontSize: number;
contentMaxWidth: number; contentMaxWidth: number;

View File

@ -77,7 +77,7 @@ describe('useFormNote', () => {
encryption_applied: 0, encryption_applied: 0,
}); });
// A larger-than-default timeout is needed to prevent CI failures: // A larger-than-default timeout is needed to prevent CI failures:
}, { timeout: 5_000 }); }, { timeout: 15_000 });
formNote.unmount(); formNote.unmount();
}); });

View File

@ -2,7 +2,7 @@ import * as React from 'react';
import CommandService from '@joplin/lib/services/CommandService'; import CommandService from '@joplin/lib/services/CommandService';
import ToolbarBase from '../ToolbarBase'; import ToolbarBase from '../ToolbarBase';
import { utils as pluginUtils } from '@joplin/lib/services/plugins/reducer'; import { utils as pluginUtils } from '@joplin/lib/services/plugins/reducer';
import ToolbarButtonUtils, { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils'; import ToolbarButtonUtils, { ToolbarItem } from '@joplin/lib/services/commands/ToolbarButtonUtils';
import stateToWhenClauseContext from '../../services/commands/stateToWhenClauseContext'; import stateToWhenClauseContext from '../../services/commands/stateToWhenClauseContext';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { buildStyle } from '@joplin/lib/theme'; import { buildStyle } from '@joplin/lib/theme';
@ -14,7 +14,7 @@ interface NoteToolbarProps {
themeId: number; themeId: number;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
style: any; style: any;
toolbarButtonInfos: ToolbarButtonInfo[]; toolbarButtonInfos: ToolbarItem[];
disabled: boolean; disabled: boolean;
} }

View File

@ -2,29 +2,25 @@ import * as React from 'react';
import ToolbarButton from './ToolbarButton/ToolbarButton'; import ToolbarButton from './ToolbarButton/ToolbarButton';
import ToggleEditorsButton, { Value } from './ToggleEditorsButton/ToggleEditorsButton'; import ToggleEditorsButton, { Value } from './ToggleEditorsButton/ToggleEditorsButton';
import ToolbarSpace from './ToolbarSpace'; import ToolbarSpace from './ToolbarSpace';
import { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils'; import { ToolbarItem } from '@joplin/lib/services/commands/ToolbarButtonUtils';
import { AppState } from '../app.reducer'; import { AppState } from '../app.reducer';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { useCallback, useMemo, useRef, useState } from 'react'; import { useCallback, useMemo, useRef, useState } from 'react';
import { focus } from '@joplin/lib/utils/focusHandler'; import { focus } from '@joplin/lib/utils/focusHandler';
interface ToolbarItemInfo extends ToolbarButtonInfo {
type?: string;
}
interface Props { interface Props {
themeId: number; themeId: number;
style: React.CSSProperties; style: React.CSSProperties;
items: ToolbarItemInfo[]; items: ToolbarItem[];
disabled: boolean; disabled: boolean;
'aria-label': string; 'aria-label': string;
} }
const getItemType = (item: ToolbarItemInfo) => { const getItemType = (item: ToolbarItem) => {
return item.type ?? 'button'; return item.type ?? 'button';
}; };
const isFocusable = (item: ToolbarItemInfo) => { const isFocusable = (item: ToolbarItem) => {
if (!item.enabled) { if (!item.enabled) {
return false; return false;
} }
@ -32,11 +28,11 @@ const isFocusable = (item: ToolbarItemInfo) => {
return getItemType(item) === 'button'; return getItemType(item) === 'button';
}; };
const useCategorizedItems = (items: ToolbarItemInfo[]) => { const useCategorizedItems = (items: ToolbarItem[]) => {
return useMemo(() => { return useMemo(() => {
const itemsLeft: ToolbarItemInfo[] = []; const itemsLeft: ToolbarItem[] = [];
const itemsCenter: ToolbarItemInfo[] = []; const itemsCenter: ToolbarItem[] = [];
const itemsRight: ToolbarItemInfo[] = []; const itemsRight: ToolbarItem[] = [];
if (items) { if (items) {
for (const item of items) { for (const item of items) {
@ -63,7 +59,7 @@ const useCategorizedItems = (items: ToolbarItemInfo[]) => {
const useKeyboardHandler = ( const useKeyboardHandler = (
setSelectedIndex: React.Dispatch<React.SetStateAction<number>>, setSelectedIndex: React.Dispatch<React.SetStateAction<number>>,
focusableItems: ToolbarItemInfo[], focusableItems: ToolbarItem[],
) => { ) => {
const onKeyDown: React.KeyboardEventHandler<HTMLElement> = useCallback(event => { const onKeyDown: React.KeyboardEventHandler<HTMLElement> = useCallback(event => {
let direction = 0; let direction = 0;
@ -110,11 +106,10 @@ const ToolbarBaseComponent: React.FC<Props> = props => {
const containerHasFocus = !!containerRef.current?.contains(doc?.activeElement); const containerHasFocus = !!containerRef.current?.contains(doc?.activeElement);
let keyCounter = 0; let keyCounter = 0;
const renderItem = (o: ToolbarItemInfo, indexInFocusable: number) => { const renderItem = (o: ToolbarItem, indexInFocusable: number) => {
let key = o.iconName ? o.iconName : ''; let key = o.iconName ? o.iconName : '';
key += o.title ? o.title : ''; key += o.title ? o.title : '';
key += o.name ? o.name : ''; key += o.name ? o.name : '';
const itemType = !('type' in o) ? 'button' : o.type;
if (!key) key = `${o.type}_${keyCounter++}`; if (!key) key = `${o.type}_${keyCounter++}`;
@ -132,7 +127,7 @@ const ToolbarBaseComponent: React.FC<Props> = props => {
} }
}; };
if (o.name === 'toggleEditors') { if (o.type === 'button' && o.name === 'toggleEditors') {
return <ToggleEditorsButton return <ToggleEditorsButton
key={o.name} key={o.name}
buttonRef={setButtonRefCallback} buttonRef={setButtonRefCallback}
@ -141,7 +136,7 @@ const ToolbarBaseComponent: React.FC<Props> = props => {
toolbarButtonInfo={o} toolbarButtonInfo={o}
tabIndex={tabIndex} tabIndex={tabIndex}
/>; />;
} else if (itemType === 'button') { } else if (o.type === 'button') {
return ( return (
<ToolbarButton <ToolbarButton
tabIndex={tabIndex} tabIndex={tabIndex}
@ -149,7 +144,7 @@ const ToolbarBaseComponent: React.FC<Props> = props => {
{...buttonProps} {...buttonProps}
/> />
); );
} else if (itemType === 'separator') { } else if (o.type === 'separator') {
return <ToolbarSpace {...buttonProps} />; return <ToolbarSpace {...buttonProps} />;
} }
@ -157,7 +152,7 @@ const ToolbarBaseComponent: React.FC<Props> = props => {
}; };
let focusableIndex = 0; let focusableIndex = 0;
const renderList = (items: ToolbarItemInfo[]) => { const renderList = (items: ToolbarItem[]) => {
const result: React.ReactNode[] = []; const result: React.ReactNode[] = [];
for (const item of items) { for (const item of items) {

View File

@ -14,6 +14,10 @@ export default class SettingsScreen {
return this.page.getByRole('tab', { name: tabName }); return this.page.getByRole('tab', { name: tabName });
} }
public getLastTab() {
return this.page.getByRole('tablist').getByRole('tab').last();
}
public async waitFor() { public async waitFor() {
await this.okayButton.waitFor(); await this.okayButton.waitFor();
await this.appearanceTabButton.waitFor(); await this.appearanceTabButton.waitFor();

View File

@ -75,7 +75,7 @@ test.describe('settings', () => {
// Pressing Up when the first item is focused should focus the last item // Pressing Up when the first item is focused should focus the last item
await mainWindow.keyboard.press('ArrowUp'); await mainWindow.keyboard.press('ArrowUp');
await expect(focusedItem).toHaveText('Backup'); await expect(settingsScreen.getLastTab()).toBeFocused();
await mainWindow.keyboard.press('ArrowDown'); await mainWindow.keyboard.press('ArrowDown');
await mainWindow.keyboard.press('ArrowDown'); await mainWindow.keyboard.press('ArrowDown');

View File

@ -1,6 +1,6 @@
{ {
"name": "@joplin/app-desktop", "name": "@joplin/app-desktop",
"version": "3.2.3", "version": "3.2.5",
"description": "Joplin for Desktop", "description": "Joplin for Desktop",
"main": "main.js", "main": "main.js",
"private": true, "private": true,
@ -115,17 +115,25 @@
"icon": "../../Assets/LinuxIcons", "icon": "../../Assets/LinuxIcons",
"category": "Office", "category": "Office",
"desktop": { "desktop": {
"entry": {
"Icon": "joplin", "Icon": "joplin",
"MimeType": "x-scheme-handler/joplin;" "MimeType": "x-scheme-handler/joplin;"
}
}, },
"target": "AppImage" "target": [
"AppImage",
"deb"
],
"executableName": "joplin",
"maintainer": "Joplin Team <no-reply@joplinapp.org>",
"artifactName": "Joplin-${version}.${ext}"
} }
}, },
"homepage": "https://github.com/laurent22/joplin#readme", "homepage": "https://github.com/laurent22/joplin#readme",
"devDependencies": { "devDependencies": {
"7zip-bin": "5.2.0", "7zip-bin": "5.2.0",
"@axe-core/playwright": "4.10.0", "@axe-core/playwright": "4.10.0",
"@electron/rebuild": "3.6.0", "@electron/rebuild": "3.7.1",
"@joplin/default-plugins": "~3.2", "@joplin/default-plugins": "~3.2",
"@joplin/tools": "~3.2", "@joplin/tools": "~3.2",
"@playwright/test": "1.45.3", "@playwright/test": "1.45.3",
@ -139,7 +147,7 @@
"@types/tesseract.js": "2.0.0", "@types/tesseract.js": "2.0.0",
"axios": "^1.7.7", "axios": "^1.7.7",
"electron": "32.2.0", "electron": "32.2.0",
"electron-builder": "24.13.3", "electron-builder": "26.0.0-alpha.7",
"glob": "10.4.5", "glob": "10.4.5",
"gulp": "4.0.2", "gulp": "4.0.2",
"jest": "29.7.0", "jest": "29.7.0",

View File

@ -176,6 +176,29 @@ const isSafeToOpen = async (path: string) => {
} }
} }
// The check `extname(path) === ''` is present to prevent the following security issue:
//
// Suppose that an attacker can rapidly change the type of a file (e.g. on a network drive or shared folder).
//
// - **Example 1**: On a network drive, if:
// 1. In a loop,
// - A folder `test.exe\\` is created, replacing the file `test.exe` if it exists.
// - After a brief delay, the folder is replaced the **file** `test.exe`.
// 4. Joplin calls `stat('network-drive/path/here/test.exe')` and gets **isDirectory: true**.
// 5. The folder is replaced with a file.
// 6. Joplin calls `openPath('network-drive/path/here/test.exe')` and executes `test.exe`.
// - **Example 2**: An example that doesn't rely on timings (but whether it works depends on how network drives are implemented):
// 1. An attacker creates a custom network file server where:
// - It's logged when a client calls `stat` on a particular path.
// - The first time `stat` is called on the path, it returns `directory`.
// - Subsequent times, `stat` returns `file`.
// 2. Joplin calls `stat('network-drive/path/here/file.exe')`.
// 3. The network drive returns `isDirectory: true`.
// 4. The network drive replaces the directory `file.exe` with an executable with the same name.
// 5. Joplin marks the path as safe to open.
// 6. Joplin calls `openPath('network-drive/path/here/file.exe')`.
// 7. This executes the executable from step 4.
if (extname(path) === '' && (await stat(path)).isDirectory()) { if (extname(path) === '' && (await stat(path)).isDirectory()) {
return true; return true;
} }

View File

@ -79,8 +79,8 @@ android {
applicationId "net.cozic.joplin" applicationId "net.cozic.joplin"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 2097758 versionCode 2097759
versionName "3.2.2" versionName "3.2.3"
ndk { ndk {
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64" abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
} }

View File

@ -1,6 +1,6 @@
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService'; import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
import Logger from '@joplin/utils/Logger'; import Logger from '@joplin/utils/Logger';
import goToNote from './util/goToNote'; import goToNote, { GotoNoteOptions } from './util/goToNote';
import Note from '@joplin/lib/models/Note'; import Note from '@joplin/lib/models/Note';
import Setting from '@joplin/lib/models/Setting'; import Setting from '@joplin/lib/models/Setting';
@ -12,7 +12,7 @@ export const declaration: CommandDeclaration = {
export const runtime = (): CommandRuntime => { export const runtime = (): CommandRuntime => {
return { return {
execute: async (_context: CommandContext, body = '', todo = false) => { execute: async (_context: CommandContext, body = '', todo = false, options: GotoNoteOptions = null) => {
const folderId = Setting.value('activeFolderId'); const folderId = Setting.value('activeFolderId');
if (!folderId) { if (!folderId) {
logger.warn('Not creating new note -- no active folder ID.'); logger.warn('Not creating new note -- no active folder ID.');
@ -26,7 +26,7 @@ export const runtime = (): CommandRuntime => {
}, { provisional: true }); }, { provisional: true });
logger.info(`Navigating to note ${note.id}`); logger.info(`Navigating to note ${note.id}`);
await goToNote(note.id, ''); await goToNote(note.id, '', options);
}, },
}; };
}; };

View File

@ -1,7 +1,17 @@
import Note from '@joplin/lib/models/Note'; import Note from '@joplin/lib/models/Note';
import NavService from '@joplin/lib/services/NavService'; import NavService from '@joplin/lib/services/NavService';
import { AttachFileAction } from '../../components/screens/Note/commands/attachFile';
export interface GotoNoteOptions {
attachFileAction?: AttachFileAction | null;
}
const goToNote = async (id: string, hash?: string, options: GotoNoteOptions = null) => {
options = {
attachFileAction: null,
...options,
};
const goToNote = async (id: string, hash?: string) => {
if (!(await Note.load(id))) { if (!(await Note.load(id))) {
throw new Error(`No note with id ${id}`); throw new Error(`No note with id ${id}`);
} }
@ -9,6 +19,7 @@ const goToNote = async (id: string, hash?: string) => {
return NavService.go('Note', { return NavService.go('Note', {
noteId: id, noteId: id,
noteHash: hash, noteHash: hash,
newNoteAttachFileAction: options.attachFileAction,
}); });
}; };

View File

@ -1,7 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { StyleSheet, View, ViewStyle, useWindowDimensions } from 'react-native'; import { StyleSheet, View, ViewStyle, useWindowDimensions } from 'react-native';
import { IconButton, Surface } from 'react-native-paper'; import { IconButton, Surface, Text } from 'react-native-paper';
import { themeStyle } from './global-style'; import { themeStyle } from './global-style';
import Modal from './Modal'; import Modal from './Modal';
import { _ } from '@joplin/lib/locale'; import { _ } from '@joplin/lib/locale';
@ -19,6 +19,7 @@ interface Props {
onDismiss: ()=> void; onDismiss: ()=> void;
containerStyle?: ViewStyle; containerStyle?: ViewStyle;
children: React.ReactNode; children: React.ReactNode;
heading?: string;
size: DialogSize; size: DialogSize;
} }
@ -35,7 +36,11 @@ const useStyles = (themeId: number, containerStyle: ViewStyle, size: DialogSize)
return StyleSheet.create({ return StyleSheet.create({
closeButtonContainer: { closeButtonContainer: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'flex-end', justifyContent: 'space-between',
alignContent: 'center',
},
heading: {
alignSelf: 'center',
}, },
dialogContainer: { dialogContainer: {
maxHeight, maxHeight,
@ -66,8 +71,12 @@ const useStyles = (themeId: number, containerStyle: ViewStyle, size: DialogSize)
const DismissibleDialog: React.FC<Props> = props => { const DismissibleDialog: React.FC<Props> = props => {
const styles = useStyles(props.themeId, props.containerStyle, props.size); const styles = useStyles(props.themeId, props.containerStyle, props.size);
const closeButton = ( const heading = props.heading ? (
<Text variant='headlineSmall' role='heading' style={styles.heading}>{props.heading}</Text>
) : null;
const closeButtonRow = (
<View style={styles.closeButtonContainer}> <View style={styles.closeButtonContainer}>
{heading ?? <View/>}
<IconButton <IconButton
icon='close' icon='close'
accessibilityLabel={_('Close')} accessibilityLabel={_('Close')}
@ -87,7 +96,7 @@ const DismissibleDialog: React.FC<Props> = props => {
transparent={true} transparent={true}
> >
<Surface style={styles.dialogSurface} elevation={1}> <Surface style={styles.dialogSurface} elevation={1}>
{closeButton} {closeButtonRow}
{props.children} {props.children}
</Surface> </Surface>
</Modal> </Modal>

View File

@ -0,0 +1,139 @@
import * as React from 'react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react-native';
import '@testing-library/jest-native/extend-expect';
import { Store } from 'redux';
import { AppState } from '../../utils/types';
import TestProviderStack from '../testing/TestProviderStack';
import EditorToolbar from './EditorToolbar';
import { setupDatabase, switchClient } from '@joplin/lib/testing/test-utils';
import createMockReduxStore from '../../utils/testing/createMockReduxStore';
import setupGlobalStore from '../../utils/testing/setupGlobalStore';
import Setting from '@joplin/lib/models/Setting';
import { RegisteredRuntime } from '@joplin/lib/services/CommandService';
import mockCommandRuntimes from './testing/mockCommandRuntimes';
let store: Store<AppState>;
interface WrapperProps { }
const WrappedToolbar: React.FC<WrapperProps> = _props => {
return <TestProviderStack store={store}>
<EditorToolbar editorState={null} />
</TestProviderStack>;
};
const queryToolbarButton = (label: string) => {
return screen.queryByRole('button', { name: label });
};
const openSettings = async () => {
const settingButton = screen.getByRole('button', { name: 'Settings' });
fireEvent.press(settingButton);
// Settings should be open:
const settingsHeader = await screen.findByRole('heading', { name: 'Manage toolbar options' });
expect(settingsHeader).toBeVisible();
};
interface ToggleSettingItemProps {
name: string;
expectedInitialState: boolean;
}
const toggleSettingsItem = async (props: ToggleSettingItemProps) => {
const initialChecked = props.expectedInitialState;
const finalChecked = !props.expectedInitialState;
const itemCheckbox = await screen.findByRole('checkbox', { name: props.name });
expect(itemCheckbox).toBeVisible();
expect(itemCheckbox).toHaveAccessibilityState({ checked: initialChecked });
fireEvent.press(itemCheckbox);
await waitFor(() => {
expect(itemCheckbox).toHaveAccessibilityState({ checked: finalChecked });
});
};
let mockCommands: RegisteredRuntime|null = null;
describe('EditorToolbar', () => {
beforeEach(async () => {
await setupDatabase(0);
await switchClient(0);
store = createMockReduxStore();
setupGlobalStore(store);
mockCommands = mockCommandRuntimes(store);
// Start with the default set of buttons
Setting.setValue('editor.toolbarButtons', []);
});
afterEach(() => {
mockCommands?.deregister();
mockCommands = null;
});
it('unchecking items in settings should remove them from the toolbar', async () => {
const toolbar = render(<WrappedToolbar/>);
// The bold button should be visible by default (if this changes, switch this
// test to a button that is present by default).
const boldLabel = 'Bold';
const boldButton = queryToolbarButton(boldLabel);
expect(boldButton).toBeVisible();
await openSettings();
await toggleSettingsItem({ name: boldLabel, expectedInitialState: true });
// Bold button should be removed from the toolbar
await waitFor(() => {
expect(queryToolbarButton(boldLabel)).toBe(null);
});
toolbar.unmount();
});
it('checking items in settings should add them to the toolbar', async () => {
// Start with a mostly-empty toolbar for testing
Setting.setValue('editor.toolbarButtons', ['textBold', 'textItalic']);
const toolbar = render(<WrappedToolbar/>);
// Initially, the button shouldn't be present in the toolbar.
const commandLabel = 'Code';
expect(queryToolbarButton(commandLabel)).toBeNull();
await openSettings();
await toggleSettingsItem({ name: commandLabel, expectedInitialState: false });
// The button should now be added to the toolbar
await waitFor(() => {
expect(queryToolbarButton(commandLabel)).toBeVisible();
});
toolbar.unmount();
});
it('should only include the math toolbar button if math is enabled in global settings', async () => {
Setting.setValue('editor.toolbarButtons', ['editor.textMath']);
Setting.setValue('markdown.plugin.katex', true);
const toolbar = render(<WrappedToolbar/>);
// Should initially show in the toolbar
expect(queryToolbarButton('Math')).toBeVisible();
// After disabled: Should not show in the toolbar
await waitFor(() => {
Setting.setValue('markdown.plugin.katex', false);
expect(queryToolbarButton('Math')).toBeNull();
});
// Should not show in settings
await openSettings();
expect(screen.queryByRole('checkbox', { name: 'Math' })).toBeNull();
toolbar.unmount();
});
});

View File

@ -0,0 +1,117 @@
import * as React from 'react';
import { AppState } from '../../utils/types';
import { connect } from 'react-redux';
import { ScrollView, StyleSheet, View } from 'react-native';
import { ToolbarButtonInfo, ToolbarItem } from '@joplin/lib/services/commands/ToolbarButtonUtils';
import toolbarButtonsFromState from './utils/toolbarButtonsFromState';
import { useCallback, useMemo, useRef, useState } from 'react';
import { themeStyle } from '../global-style';
import ToggleSpaceButton from '../ToggleSpaceButton';
import ToolbarEditorDialog from './ToolbarEditorDialog';
import { EditorState } from './types';
import ToolbarButton from './ToolbarButton';
import isSelected from './utils/isSelected';
import { _ } from '@joplin/lib/locale';
interface Props {
themeId: number;
toolbarButtonInfos: ToolbarItem[];
editorState: EditorState;
}
const useStyles = (themeId: number) => {
return useMemo(() => {
const theme = themeStyle(themeId);
return StyleSheet.create({
content: {
flexGrow: 0,
backgroundColor: theme.backgroundColor3,
},
contentContainer: {
flexGrow: 1,
paddingVertical: 0,
flexDirection: 'row',
},
spacer: {
flexGrow: 1,
},
});
}, [themeId]);
};
type SetSettingsVisible = React.Dispatch<React.SetStateAction<boolean>>;
const useSettingButtonInfo = (setSettingsVisible: SetSettingsVisible) => {
return useMemo((): ToolbarButtonInfo => ({
type: 'button',
name: 'showToolbarSettings',
tooltip: _('Settings'),
iconName: 'material cogs',
enabled: true,
onClick: () => setSettingsVisible(true),
title: '',
}), [setSettingsVisible]);
};
const EditorToolbar: React.FC<Props> = props => {
const styles = useStyles(props.themeId);
const buttonInfos: ToolbarButtonInfo[] = [];
for (const info of props.toolbarButtonInfos) {
if (info.type !== 'separator') {
buttonInfos.push(info);
}
}
const renderButton = (info: ToolbarButtonInfo) => {
return <ToolbarButton
key={`command-${info.name}`}
buttonInfo={info}
themeId={props.themeId}
selected={isSelected(info.name, props.editorState)}
/>;
};
const [settingsVisible, setSettingsVisible] = useState(false);
const scrollViewRef = useRef<ScrollView|null>(null);
const onDismissSettingsDialog = useCallback(() => {
setSettingsVisible(false);
// On Android, if the ScrollView isn't manually scrolled to the end,
// all items can be invisible in some cases. This causes issues with
// TalkBack on Android.
// In particular, if 1) the toolbar initially has many items on a device
// with a small screen, and 2) the user removes most items, then most/all
// items are scrolled offscreen. Calling .scrollToEnd corrects this:
scrollViewRef.current?.scrollToEnd();
}, []);
const settingsButtonInfo = useSettingButtonInfo(setSettingsVisible);
const settingsButton = <ToolbarButton
buttonInfo={settingsButtonInfo}
themeId={props.themeId}
/>;
return <>
<ToggleSpaceButton themeId={props.themeId}>
<ScrollView
ref={scrollViewRef}
horizontal={true}
style={styles.content}
contentContainerStyle={styles.contentContainer}
>
{buttonInfos.map(renderButton)}
<View style={styles.spacer}/>
{settingsButton}
</ScrollView>
</ToggleSpaceButton>
<ToolbarEditorDialog visible={settingsVisible} onDismiss={onDismissSettingsDialog} />
</>;
};
export default connect((state: AppState) => {
return {
themeId: state.settings.theme,
toolbarButtonInfos: toolbarButtonsFromState(state),
};
})(EditorToolbar);

View File

@ -0,0 +1,58 @@
import * as React from 'react';
import { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
import IconButton from '../IconButton';
import { memo, useMemo } from 'react';
import { StyleSheet, useWindowDimensions } from 'react-native';
import { themeStyle } from '../global-style';
interface Props {
themeId: number;
buttonInfo: ToolbarButtonInfo;
selected?: boolean;
}
const useStyles = (themeId: number, selected: boolean, enabled: boolean) => {
const { fontScale } = useWindowDimensions();
return useMemo(() => {
const theme = themeStyle(themeId);
return StyleSheet.create({
icon: {
color: theme.color,
fontSize: 22 * fontScale,
},
button: {
// Scaling the button width/height by the device font scale causes the button to scale
// with the user's device font size.
width: 48 * fontScale,
height: 48 * fontScale,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: selected ? theme.backgroundColorHover3 : theme.backgroundColor3,
opacity: enabled ? 1 : theme.disabledOpacity,
},
});
}, [themeId, selected, enabled, fontScale]);
};
const ToolbarButton: React.FC<Props> = memo(({ themeId, buttonInfo, selected }) => {
const styles = useStyles(themeId, selected, buttonInfo.enabled);
const isToggleButton = selected !== undefined;
return <IconButton
iconName={buttonInfo.iconName}
description={buttonInfo.title || buttonInfo.tooltip}
onPress={buttonInfo.onClick}
disabled={!buttonInfo.enabled}
iconStyle={styles.icon}
containerStyle={styles.button}
accessibilityState={{ selected }}
accessibilityRole={isToggleButton ? 'togglebutton' : 'button'}
role={'button'}
aria-pressed={selected}
preventKeyboardDismiss={true}
themeId={themeId}
/>;
});
export default ToolbarButton;

View File

@ -0,0 +1,191 @@
import * as React from 'react';
import { useCallback, useMemo } from 'react';
import createRootStyle from '../../utils/createRootStyle';
import { View, StyleSheet, ScrollView } from 'react-native';
import { Divider, Text, TouchableRipple } from 'react-native-paper';
import { _ } from '@joplin/lib/locale';
import { themeStyle } from '../global-style';
import { connect } from 'react-redux';
import ToolbarButtonUtils, { ToolbarButtonInfo, ToolbarItem } from '@joplin/lib/services/commands/ToolbarButtonUtils';
import Icon from '../Icon';
import { AppState } from '../../utils/types';
import CommandService from '@joplin/lib/services/CommandService';
import allToolbarCommandNamesFromState from './utils/allToolbarCommandNamesFromState';
import Setting from '@joplin/lib/models/Setting';
import DismissibleDialog, { DialogSize } from '../DismissibleDialog';
import selectedCommandNamesFromState from './utils/selectedCommandNamesFromState';
import stateToWhenClauseContext from '../../services/commands/stateToWhenClauseContext';
import { DeleteButton } from '../buttons';
import shim from '@joplin/lib/shim';
const toolbarButtonUtils = new ToolbarButtonUtils(CommandService.instance());
interface EditorDialogProps {
themeId: number;
defaultToolbarButtonInfos: ToolbarItem[];
selectedCommandNames: string[];
allCommandNames: string[];
hasCustomizedLayout: boolean;
visible: boolean;
onDismiss: ()=> void;
}
const useStyle = (themeId: number) => {
return useMemo(() => {
const theme = themeStyle(themeId);
return StyleSheet.create({
...createRootStyle(themeId),
icon: {
color: theme.color,
fontSize: theme.fontSizeLarge,
},
labelText: {
fontSize: theme.fontSize,
},
listContainer: {
marginTop: theme.marginTop,
flex: 1,
},
resetButton: {
marginTop: theme.marginTop,
},
listItem: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'flex-start',
gap: theme.margin,
padding: 4,
paddingTop: theme.itemMarginTop,
paddingBottom: theme.itemMarginBottom,
},
});
}, [themeId]);
};
type Styles = ReturnType<typeof useStyle>;
const setCommandIncluded = (
commandName: string,
lastSelectedCommands: string[],
allCommandNames: string[],
include: boolean,
) => {
let newSelectedCommands;
if (include) {
newSelectedCommands = [];
for (const name of allCommandNames) {
const isDivider = name === '-';
if (isDivider || name === commandName || lastSelectedCommands.includes(name)) {
newSelectedCommands.push(name);
}
}
} else {
newSelectedCommands = lastSelectedCommands.filter(name => name !== commandName);
}
Setting.setValue('editor.toolbarButtons', newSelectedCommands);
};
interface ItemToggleProps {
item: ToolbarButtonInfo;
selectedCommandNames: string[];
allCommandNames: string[];
styles: Styles;
}
const ToolbarItemToggle: React.FC<ItemToggleProps> = ({
item, selectedCommandNames, styles, allCommandNames,
}) => {
const title = item.title || item.tooltip;
const checked = selectedCommandNames.includes(item.name);
const onToggle = useCallback(() => {
setCommandIncluded(item.name, selectedCommandNames, allCommandNames, !checked);
}, [item, selectedCommandNames, allCommandNames, checked]);
return (
<TouchableRipple
accessibilityRole='checkbox'
accessibilityState={{ checked }}
aria-checked={checked}
onPress={onToggle}
>
<View style={styles.listItem}>
<Icon name={checked ? 'ionicon checkbox-outline' : 'ionicon square-outline'} style={styles.icon} accessibilityLabel={null}/>
<Icon name={item.iconName} style={styles.icon} accessibilityLabel={null}/>
<Text style={styles.labelText}>
{title}
</Text>
</View>
</TouchableRipple>
);
};
const ToolbarEditorScreen: React.FC<EditorDialogProps> = props => {
const styles = useStyle(props.themeId);
const renderItem = (item: ToolbarItem, index: number) => {
if (item.type === 'separator') {
return <Divider key={`separator-${index}`} />;
}
return <ToolbarItemToggle
key={`command-${item.name}`}
item={item}
styles={styles}
allCommandNames={props.allCommandNames}
selectedCommandNames={props.selectedCommandNames}
/>;
};
const onRestoreDefaultLayout = useCallback(async () => {
// Dismiss before showing the confirm dialog to prevent modal conflicts.
// On some platforms (web and possibly iOS) showing multiple modals
// at the same time can cause issues.
props.onDismiss();
const message = _('Are you sure that you want to restore the default toolbar layout?\nThis cannot be undone.');
if (await shim.showConfirmationDialog(message)) {
Setting.setValue('editor.toolbarButtons', []);
}
}, [props.onDismiss]);
const restoreButton = <DeleteButton
style={styles.resetButton}
onPress={onRestoreDefaultLayout}
>
{_('Restore defaults')}
</DeleteButton>;
return (
<DismissibleDialog
size={DialogSize.Small}
themeId={props.themeId}
visible={props.visible}
onDismiss={props.onDismiss}
heading={_('Manage toolbar options')}
>
<View>
<Text variant='bodyMedium'>{_('Check elements to display in the toolbar')}</Text>
</View>
<ScrollView style={styles.listContainer}>
{props.defaultToolbarButtonInfos.map((item, index) => renderItem(item, index))}
{props.hasCustomizedLayout ? restoreButton : null}
</ScrollView>
</DismissibleDialog>
);
};
export default connect((state: AppState) => {
const whenClauseContext = stateToWhenClauseContext(state);
const allCommandNames = allToolbarCommandNamesFromState(state);
const selectedCommandNames = selectedCommandNamesFromState(state);
return {
themeId: state.settings.theme,
selectedCommandNames,
allCommandNames,
hasCustomizedLayout: state.settings['editor.toolbarButtons'].length > 0,
defaultToolbarButtonInfos: toolbarButtonUtils.commandsToToolbarButtons(allCommandNames, whenClauseContext),
};
})(ToolbarEditorScreen);

View File

@ -0,0 +1,28 @@
import { Store } from 'redux';
import { AppState } from '../../../utils/types';
import CommandService, { CommandRuntime } from '@joplin/lib/services/CommandService';
import allToolbarCommandNamesFromState from '../utils/allToolbarCommandNamesFromState';
// The toolbar expects all toolbar command runtimes to be registered before it can be
// rendered:
const mockCommandRuntimes = (store: Store<AppState>) => {
const makeMockRuntime = (commandName: string) => ({
declaration: { name: commandName },
runtime: (_props: null): CommandRuntime => ({
execute: jest.fn(),
}),
});
const isSeparator = (commandName: string) => commandName === '-';
const mockRuntimes = allToolbarCommandNamesFromState(
store.getState(),
).filter(
name => !isSeparator(name),
).map(makeMockRuntime);
return CommandService.instance().componentRegisterCommands(
null, mockRuntimes,
);
};
export default mockCommandRuntimes;

View File

@ -0,0 +1,6 @@
import SelectionFormatting from '@joplin/editor/SelectionFormatting';
export interface EditorState {
selectionState: SelectionFormatting;
searchVisible: boolean;
}

View File

@ -0,0 +1,54 @@
import { AppState } from '../../../utils/types';
import { utils as pluginUtils } from '@joplin/lib/services/plugins/reducer';
import { EditorCommandType } from '@joplin/editor/types';
const builtInCommandNames = [
'attachFile',
'-',
'editor.textHeading1',
'editor.textHeading2',
'editor.textHeading3',
'editor.textHeading4',
'editor.textHeading5',
EditorCommandType.ToggleBolded,
EditorCommandType.ToggleItalicized,
'-',
EditorCommandType.ToggleCode,
`editor.${EditorCommandType.ToggleMath}`,
'-',
EditorCommandType.ToggleNumberedList,
EditorCommandType.ToggleBulletedList,
EditorCommandType.ToggleCheckList,
'-',
EditorCommandType.IndentLess,
EditorCommandType.IndentMore,
'-',
EditorCommandType.EditLink,
'setTags',
EditorCommandType.ToggleSearch,
'hideKeyboard',
];
const allToolbarCommandNamesFromState = (state: AppState) => {
const pluginCommandNames = pluginUtils.commandNamesFromViews(state.pluginService.plugins, 'editorToolbar');
let allCommandNames = builtInCommandNames;
if (pluginCommandNames.length > 0) {
allCommandNames = allCommandNames.concat(['-'], pluginCommandNames);
}
// If the user disables math markup, the "toggle math" button won't be useful.
// Disabling the math markup button maintains compatibility with the previous
// toolbar.
const mathEnabled = state.settings['markdown.plugin.katex'];
if (!mathEnabled) {
allCommandNames = allCommandNames.filter(
name => name !== `editor.${EditorCommandType.ToggleMath}`,
);
}
return allCommandNames;
};
export default allToolbarCommandNamesFromState;

View File

@ -0,0 +1,40 @@
import SelectionFormatting from '@joplin/editor/SelectionFormatting';
import { EditorCommandType } from '@joplin/editor/types';
import { EditorState } from '../types';
type StateSelector = (selectionState: SelectionFormatting, searchVisible: boolean)=> boolean;
const commandNameToSelectionState: Record<string, StateSelector> = {
[EditorCommandType.ToggleBolded]: state => state.bolded,
[EditorCommandType.ToggleItalicized]: state => state.italicized,
[EditorCommandType.ToggleCode]: state => state.inCode,
[EditorCommandType.ToggleMath]: state => state.inMath,
[EditorCommandType.ToggleHeading1]: state => state.headerLevel === 1,
[EditorCommandType.ToggleHeading2]: state => state.headerLevel === 2,
[EditorCommandType.ToggleHeading3]: state => state.headerLevel === 3,
[EditorCommandType.ToggleHeading4]: state => state.headerLevel === 4,
[EditorCommandType.ToggleHeading5]: state => state.headerLevel === 5,
[EditorCommandType.ToggleBulletedList]: state => state.inUnorderedList,
[EditorCommandType.ToggleNumberedList]: state => state.inOrderedList,
[EditorCommandType.ToggleCheckList]: state => state.inChecklist,
[EditorCommandType.EditLink]: state => state.inLink,
[EditorCommandType.ToggleSearch]: (_selectionState, searchVisible) => searchVisible,
};
// Returns undefined if not a toggle button
const isSelected = (commandName: string, editorState: EditorState) => {
// Newer editor commands are registered with the "editor." prefix. Remove this
// prefix to simplify looking up the selection state:
commandName = commandName.replace(/^editor\./, '');
if (commandName in commandNameToSelectionState) {
if (!editorState) return false;
return commandNameToSelectionState[commandName as EditorCommandType](
editorState.selectionState, editorState.searchVisible,
);
}
return undefined;
};
export default isSelected;

View File

@ -0,0 +1,30 @@
import { AppState } from '../../../utils/types';
import allToolbarCommandNamesFromState from './allToolbarCommandNamesFromState';
import { Platform } from 'react-native';
const omitFromDefault: string[] = [
'editor.textHeading1',
'editor.textHeading3',
'editor.textHeading4',
'editor.textHeading5',
];
// The "hide keyboard" button is only needed on iOS, so only show it there by default.
// (There's no default "dismiss" button on iPhone software keyboards).
if (Platform.OS !== 'ios') {
omitFromDefault.push('hideKeyboard');
}
const selectedCommandNamesFromState = (state: AppState) => {
const allCommandNames = allToolbarCommandNamesFromState(state);
const defaultCommandNames = allCommandNames.filter(commandName => {
return !omitFromDefault.includes(commandName);
});
const commandNameSetting = state.settings['editor.toolbarButtons'] ?? [];
const selectedCommands = commandNameSetting.length > 0 ? commandNameSetting : defaultCommandNames;
return selectedCommands.filter(command => allCommandNames.includes(command));
};
export default selectedCommandNamesFromState;

View File

@ -0,0 +1,16 @@
import { AppState } from '../../../utils/types';
import ToolbarButtonUtils from '@joplin/lib/services/commands/ToolbarButtonUtils';
import CommandService from '@joplin/lib/services/CommandService';
import selectedCommandNamesFromState from './selectedCommandNamesFromState';
import stateToWhenClauseContext from '../../../services/commands/stateToWhenClauseContext';
const toolbarButtonUtils = new ToolbarButtonUtils(CommandService.instance());
const toolbarButtonsFromState = (state: AppState) => {
const whenClauseContext = stateToWhenClauseContext(state);
const commandNames = selectedCommandNamesFromState(state);
return toolbarButtonUtils.commandsToToolbarButtons(commandNames, whenClauseContext);
};
export default toolbarButtonsFromState;

View File

@ -6,7 +6,7 @@ import * as React from 'react';
import { themeStyle } from '@joplin/lib/theme'; import { themeStyle } from '@joplin/lib/theme';
import { Theme } from '@joplin/lib/themes/type'; import { Theme } from '@joplin/lib/themes/type';
import { useState, useMemo, useCallback, useRef } from 'react'; import { useState, useMemo, useCallback, useRef } from 'react';
import { Text, Pressable, ViewStyle, StyleSheet, LayoutChangeEvent, LayoutRectangle, Animated, AccessibilityState, AccessibilityRole, TextStyle, GestureResponderEvent, Platform } from 'react-native'; 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 { Menu, MenuOptions, MenuTrigger, renderers } from 'react-native-popup-menu';
import Icon from './Icon'; import Icon from './Icon';
import AccessibleView from './accessibility/AccessibleView'; import AccessibleView from './accessibility/AccessibleView';
@ -36,6 +36,8 @@ interface ButtonProps {
// Role of the button. Defaults to 'button'. // Role of the button. Defaults to 'button'.
accessibilityRole?: AccessibilityRole; accessibilityRole?: AccessibilityRole;
accessibilityState?: AccessibilityState; accessibilityState?: AccessibilityState;
'aria-pressed'?: boolean;
role?: Role;
disabled?: boolean; disabled?: boolean;
} }
@ -102,7 +104,9 @@ const IconButton = (props: ButtonProps) => {
accessibilityLabel={props.description} accessibilityLabel={props.description}
accessibilityHint={props.accessibilityHint} accessibilityHint={props.accessibilityHint}
accessibilityRole={props.accessibilityRole ?? 'button'} accessibilityRole={props.accessibilityRole ?? 'button'}
role={props.role}
accessibilityState={props.accessibilityState} accessibilityState={props.accessibilityState}
aria-pressed={props['aria-pressed']}
> >
<Animated.View style={{ <Animated.View style={{
opacity: fadeAnim, opacity: fadeAnim,

View File

@ -37,10 +37,6 @@ class ModalDialog extends React.Component<Props, State> {
this.styles_ = {}; this.styles_ = {};
const styles: Record<string, ViewStyle|TextStyle> = { const styles: Record<string, ViewStyle|TextStyle> = {
modalWrapper: {
flex: 1,
justifyContent: 'center',
},
modalContentWrapper: { modalContentWrapper: {
flex: 1, flex: 1,
flexDirection: 'column', flexDirection: 'column',
@ -76,7 +72,6 @@ class ModalDialog extends React.Component<Props, State> {
const buttonBarEnabled = this.props.buttonBarEnabled !== false; const buttonBarEnabled = this.props.buttonBarEnabled !== false;
return ( return (
<View style={this.styles().modalWrapper}>
<Modal transparent={true} visible={true} onRequestClose={() => {}} containerStyle={this.styles().modalContentWrapper}> <Modal transparent={true} visible={true} onRequestClose={() => {}} containerStyle={this.styles().modalContentWrapper}>
<Text style={this.styles().title}>{this.props.title}</Text> <Text style={this.styles().title}>{this.props.title}</Text>
<View style={this.styles().modalContentWrapper2}>{ContentComponent}</View> <View style={this.styles().modalContentWrapper2}>{ContentComponent}</View>
@ -89,7 +84,6 @@ class ModalDialog extends React.Component<Props, State> {
</View> </View>
</View> </View>
</Modal> </Modal>
</View>
); );
} }
} }

View File

@ -1,5 +1,5 @@
import { MarkupLanguage, MarkupToHtml } from '@joplin/renderer'; import { MarkupLanguage, MarkupToHtml } from '@joplin/renderer';
import type { MarkupToHtmlConverter, RenderResultPluginAsset, FsDriver as RendererFsDriver } from '@joplin/renderer/types'; import type { MarkupToHtmlConverter, RenderOptions, RenderResultPluginAsset, FsDriver as RendererFsDriver } from '@joplin/renderer/types';
import makeResourceModel from './utils/makeResourceModel'; import makeResourceModel from './utils/makeResourceModel';
import addPluginAssets from './utils/addPluginAssets'; import addPluginAssets from './utils/addPluginAssets';
import { ExtraContentScriptSource } from './types'; import { ExtraContentScriptSource } from './types';
@ -115,7 +115,7 @@ export default class Renderer {
this.lastSettings = settings; this.lastSettings = settings;
this.lastRenderMarkup = markup; this.lastRenderMarkup = markup;
const options = { const options: RenderOptions = {
onResourceLoaded: settings.onResourceLoaded, onResourceLoaded: settings.onResourceLoaded,
highlightedKeywords: settings.highlightedKeywords, highlightedKeywords: settings.highlightedKeywords,
resources: settings.resources, resources: settings.resources,
@ -140,6 +140,7 @@ export default class Renderer {
return settings.pluginSettings[settingKey]; return settings.pluginSettings[settingKey];
}, },
whiteBackgroundNoteRendering: markup.language === MarkupLanguage.Html,
}; };
this.markupToHtml.clearCache(markup.language); this.markupToHtml.clearCache(markup.language);

View File

@ -84,6 +84,7 @@ const EditLinkDialog = (props: LinkDialogProps) => {
const onSubmit = useCallback(() => { const onSubmit = useCallback(() => {
props.editorControl.updateLink(linkLabel, linkURL); props.editorControl.updateLink(linkLabel, linkURL);
props.editorControl.hideLinkDialog(); props.editorControl.hideLinkDialog();
focus('EditLinkDialog::onSubmit', props.editorControl);
}, [props.editorControl, linkLabel, linkURL]); }, [props.editorControl, linkLabel, linkURL]);
// See https://www.hingehealth.com/engineering-blog/accessible-react-native-textinput/ // See https://www.hingehealth.com/engineering-blog/accessible-react-native-textinput/

View File

@ -1,140 +0,0 @@
// A toolbar for the markdown editor.
import * as React from 'react';
import { Platform, StyleSheet } from 'react-native';
import { useMemo } from 'react';
import { _ } from '@joplin/lib/locale';
import { MarkdownToolbarProps, StyleSheetData } from './types';
import Toolbar from './Toolbar';
import { buttonSize } from './ToolbarButton';
import { Theme } from '@joplin/lib/themes/type';
import ToggleSpaceButton from './ToggleSpaceButton';
import useHeaderButtons from './buttons/useHeaderButtons';
import useInlineFormattingButtons from './buttons/useInlineFormattingButtons';
import useActionButtons from './buttons/useActionButtons';
import useListButtons from './buttons/useListButtons';
import useKeyboardVisible from '../hooks/useKeyboardVisible';
import usePluginButtons from './buttons/usePluginButtons';
const MarkdownToolbar: React.FC<MarkdownToolbarProps> = (props: MarkdownToolbarProps) => {
const themeData = props.editorSettings.themeData;
const styles = useStyles(props.style, themeData);
const { keyboardVisible, hasSoftwareKeyboard } = useKeyboardVisible();
const buttonProps = {
...props,
iconStyle: styles.text,
keyboardVisible,
hasSoftwareKeyboard,
};
const headerButtons = useHeaderButtons(buttonProps);
const inlineFormattingBtns = useInlineFormattingButtons(buttonProps);
const actionButtons = useActionButtons(buttonProps);
const listButtons = useListButtons(buttonProps);
const pluginButtons = usePluginButtons(buttonProps);
const styleData: StyleSheetData = useMemo(() => ({
styles: styles,
themeId: props.editorSettings.themeId,
}), [styles, props.editorSettings.themeId]);
const toolbarButtons = useMemo(() => {
const buttons = [
{
title: _('Formatting'),
items: inlineFormattingBtns,
},
{
title: _('Headers'),
items: headerButtons,
},
{
title: _('Lists'),
items: listButtons,
},
{
title: _('Actions'),
items: actionButtons,
},
];
if (pluginButtons.length > 0) {
buttons.push({
title: _('Plugins'),
items: pluginButtons,
});
}
return buttons;
}, [headerButtons, inlineFormattingBtns, listButtons, actionButtons, pluginButtons]);
return (
<ToggleSpaceButton
spaceApplicable={ Platform.OS === 'ios' && keyboardVisible }
themeId={props.editorSettings.themeId}
style={styles.container}
>
<Toolbar
styleSheet={styleData}
buttons={toolbarButtons}
/>
</ToggleSpaceButton>
);
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const useStyles = (styleProps: any, theme: Theme) => {
return useMemo(() => {
return StyleSheet.create({
container: {
...styleProps,
},
button: {
width: buttonSize,
height: buttonSize,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: theme.backgroundColor,
},
buttonDisabled: {
opacity: 0.5,
},
buttonDisabledContent: {
},
buttonActive: {
backgroundColor: theme.backgroundColor3,
color: theme.color3,
borderWidth: 1,
borderColor: theme.color3,
borderRadius: 6,
},
buttonActiveContent: {
color: theme.color3,
},
text: {
fontSize: 22,
color: theme.color,
},
toolbarRow: {
flex: 0,
flexDirection: 'row',
alignItems: 'baseline',
justifyContent: 'center',
// Add a small amount of additional padding for button borders
height: buttonSize + 6,
},
toolbarContainer: {
flexShrink: 1,
},
toolbarContent: {
flexGrow: 1,
justifyContent: 'center',
},
});
}, [styleProps, theme]);
};
export default MarkdownToolbar;

View File

@ -1,31 +0,0 @@
import * as React from 'react';
import { _ } from '@joplin/lib/locale';
import ToolbarButton from './ToolbarButton';
import { ButtonSpec, StyleSheetData } from './types';
type OnToggleOverflowCallback = ()=> void;
interface ToggleOverflowButtonProps {
overflowVisible: boolean;
onToggleOverflowVisible: OnToggleOverflowCallback;
styleSheet: StyleSheetData;
}
// Button that shows/hides the overflow menu.
const ToggleOverflowButton: React.FC<ToggleOverflowButtonProps> = (props: ToggleOverflowButtonProps) => {
const spec: ButtonSpec = {
icon: 'material dots-horizontal',
description:
props.overflowVisible ? _('Hide more actions') : _('Show more actions'),
active: props.overflowVisible,
onPress: props.onToggleOverflowVisible,
};
return (
<ToolbarButton
styleSheet={props.styleSheet}
spec={spec}
/>
);
};
export default ToggleOverflowButton;

View File

@ -1,124 +0,0 @@
import * as React from 'react';
import { ReactElement, useCallback, useMemo, useState } from 'react';
import { LayoutChangeEvent, ScrollView, View, ViewStyle } from 'react-native';
import ToggleOverflowButton from './ToggleOverflowButton';
import ToolbarButton, { buttonSize } from './ToolbarButton';
import ToolbarOverflowRows from './ToolbarOverflowRows';
import { ButtonGroup, ButtonSpec, StyleSheetData } from './types';
interface ToolbarProps {
buttons: ButtonGroup[];
styleSheet: StyleSheetData;
style?: ViewStyle;
}
// Displays a list of buttons with an overflow menu.
const Toolbar: React.FC<ToolbarProps> = (props: ToolbarProps) => {
const [overflowButtonsVisible, setOverflowPopupVisible] = useState(false);
const [maxButtonsEachSide, setMaxButtonsEachSide] = useState(0);
const allButtonSpecs = useMemo(() => {
const buttons = props.buttons.reduce((accumulator: ButtonSpec[], current: ButtonGroup) => {
const newItems: ButtonSpec[] = [];
for (const item of current.items) {
if (item.visible ?? true) {
newItems.push(item);
}
}
return accumulator.concat(...newItems);
}, []);
// Sort from highest priority to lowest
buttons.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
return buttons;
}, [props.buttons]);
const allButtonComponents: ReactElement[] = [];
let key = 0;
for (const spec of allButtonSpecs) {
key++;
allButtonComponents.push(
<ToolbarButton
key={key.toString()}
styleSheet={props.styleSheet}
spec={spec}
/>,
);
}
const onContainerLayout = useCallback((event: LayoutChangeEvent) => {
const containerWidth = event.nativeEvent.layout.width;
const maxButtonsTotal = Math.floor(containerWidth / buttonSize);
setMaxButtonsEachSide(Math.floor(
Math.min((maxButtonsTotal - 1) / 2, allButtonSpecs.length / 2),
));
}, [allButtonSpecs.length]);
const onToggleOverflowVisible = useCallback(() => {
setOverflowPopupVisible(!overflowButtonsVisible);
}, [overflowButtonsVisible]);
const toggleOverflowButton = (
<ToggleOverflowButton
key={(++key).toString()}
styleSheet={props.styleSheet}
overflowVisible={overflowButtonsVisible}
onToggleOverflowVisible={onToggleOverflowVisible}
/>
);
const mainButtons: ReactElement[] = [];
if (maxButtonsEachSide >= allButtonComponents.length) {
mainButtons.push(...allButtonComponents);
} else if (maxButtonsEachSide > 0) {
// We want the menu to look something like this:
// B I (…) 🔍 ⌨
// where (…) shows/hides overflow.
// Add from the left and right of [allButtonComponents] to ensure that
// the (…) button is in the center:
mainButtons.push(...allButtonComponents.slice(0, maxButtonsEachSide));
mainButtons.push(toggleOverflowButton);
mainButtons.push(...allButtonComponents.slice(-maxButtonsEachSide));
} else {
mainButtons.push(toggleOverflowButton);
}
const styles = props.styleSheet.styles;
const mainButtonRow = (
<View style={styles.toolbarRow}>
{ mainButtons }
</View>
);
const overflow = (
<ScrollView>
<ToolbarOverflowRows
buttonGroups={props.buttons}
styleSheet={props.styleSheet}
onToggleOverflow={onToggleOverflowVisible}
/>
</ScrollView>
);
return (
<View
style={{
...styles.toolbarContainer,
// The number of buttons displayed is based on the width of the
// container. As such, we can't base the container's width on the
// size of its content.
width: '100%',
...props.style,
}}
onLayout={onContainerLayout}
>
{ overflowButtonsVisible ? overflow : null }
{ !overflowButtonsVisible ? mainButtonRow : null }
</View>
);
};
export default Toolbar;

View File

@ -1,74 +0,0 @@
import * as React from 'react';
import { useCallback, useMemo } from 'react';
import { TextStyle, StyleSheet } from 'react-native';
import { ButtonSpec, StyleSheetData } from './types';
import IconButton from '../../IconButton';
export const buttonSize = 54;
interface ToolbarButtonProps {
styleSheet: StyleSheetData;
style?: TextStyle;
spec: ButtonSpec;
onActionComplete?: ()=> void;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const useStyles = (baseStyleSheet: any, baseButtonStyle: any, buttonSpec: ButtonSpec, visible: boolean, disabled: boolean) => {
return useMemo(() => {
const activatedStyle = buttonSpec.active ? baseStyleSheet.buttonActive : {};
const disabledStyle = disabled ? baseStyleSheet.buttonDisabled : {};
const activatedTextStyle = buttonSpec.active ? baseStyleSheet.buttonActiveContent : {};
const disabledTextStyle = disabled ? baseStyleSheet.buttonDisabledContent : {};
return StyleSheet.create({
iconStyle: {
...activatedTextStyle,
...disabledTextStyle,
...baseStyleSheet.text,
},
buttonStyle: {
...baseStyleSheet.button,
...activatedStyle,
...disabledStyle,
...baseButtonStyle,
...(!visible ? { opacity: 0 } : null),
},
});
}, [
baseStyleSheet.button, baseStyleSheet.text, baseButtonStyle, baseStyleSheet.buttonActive,
baseStyleSheet.buttonDisabled, baseStyleSheet.buttonActiveContent, baseStyleSheet.buttonDisabledContent,
buttonSpec.active, visible, disabled,
]);
};
const ToolbarButton = ({ styleSheet, spec, onActionComplete, style }: ToolbarButtonProps) => {
const visible = spec.visible ?? true;
const disabled = (spec.disabled ?? false) && visible;
const styles = useStyles(styleSheet.styles, style, spec, visible, disabled);
const sourceOnPress = spec.onPress;
const onPress = useCallback(() => {
if (!disabled) {
sourceOnPress();
onActionComplete?.();
}
}, [disabled, sourceOnPress, onActionComplete]);
return (
<IconButton
containerStyle={styles.buttonStyle}
themeId={styleSheet.themeId}
onPress={onPress}
description={ spec.description }
disabled={ disabled }
preventKeyboardDismiss={true}
iconName={spec.icon}
iconStyle={styles.iconStyle}
/>
);
};
export default ToolbarButton;

View File

@ -1,134 +0,0 @@
import * as React from 'react';
import { _ } from '@joplin/lib/locale';
import { ReactElement, useCallback, useState } from 'react';
import { LayoutChangeEvent, ScrollView, View } from 'react-native';
import ToggleOverflowButton from './ToggleOverflowButton';
import ToolbarButton, { buttonSize } from './ToolbarButton';
import { ButtonGroup, ButtonSpec, StyleSheetData } from './types';
type OnToggleOverflowCallback = ()=> void;
interface OverflowPopupProps {
buttonGroups: ButtonGroup[];
styleSheet: StyleSheetData;
// Should be created using useCallback
onToggleOverflow: OnToggleOverflowCallback;
}
// Specification for a button that acts as padding.
const paddingButtonSpec = { visible: false, icon: '', onPress: ()=>{}, description: '' };
// Contains buttons that overflow the available space.
// Displays all buttons in [props.buttonGroups] if [props.visible].
// Otherwise, displays nothing.
const ToolbarOverflowRows: React.FC<OverflowPopupProps> = (props: OverflowPopupProps) => {
const overflowRows: ReactElement[] = [];
let key = 0;
for (let i = 0; i < props.buttonGroups.length; i++) {
key++;
const row: ReactElement[] = [];
const group = props.buttonGroups[i];
for (let j = 0; j < group.items.length; j++) {
key++;
const buttonSpec = group.items[j];
row.push(
<ToolbarButton
key={key.toString()}
styleSheet={props.styleSheet}
spec={buttonSpec}
// After invoking this button's action, hide the overflow menu
onActionComplete={props.onToggleOverflow}
/>,
);
// Show the "hide overflow" button if in the center of the last row
const isLastRow = i === props.buttonGroups.length - 1;
const isCenterOfRow = j + 1 === Math.floor(group.items.length / 2);
if (isLastRow && (isCenterOfRow || group.items.length === 1)) {
row.push(
<ToggleOverflowButton
key={(++key).toString()}
styleSheet={props.styleSheet}
overflowVisible={true}
onToggleOverflowVisible={props.onToggleOverflow}
/>,
);
}
}
// Pad to an odd number of items to ensure that buttons are centered properly
if (row.length % 2 === 0) {
row.push(
<ToolbarButton
key={`padding-${i}`}
styleSheet={props.styleSheet}
spec={paddingButtonSpec}
/>,
);
}
overflowRows.push(
<View
key={key.toString()}
>
<ScrollView
horizontal={true}
contentContainerStyle={props.styleSheet.styles.toolbarContent}
>
{row}
</ScrollView>
</View>,
);
}
const [hasSpaceForCloseBtn, setHasSpaceForCloseBtn] = useState(true);
const onContainerLayout = useCallback((event: LayoutChangeEvent) => {
if (props.buttonGroups.length === 0) {
return;
}
// Add 1 to account for the close button
const totalButtonCount = props.buttonGroups[0].items.length + 1;
const newWidth = event.nativeEvent.layout.width;
setHasSpaceForCloseBtn(newWidth > totalButtonCount * buttonSize);
}, [setHasSpaceForCloseBtn, props.buttonGroups]);
const closeButtonSpec: ButtonSpec = {
icon: 'text ⨉',
description: _('Close'),
onPress: props.onToggleOverflow,
};
const closeButton = (
<ToolbarButton
styleSheet={props.styleSheet}
spec={closeButtonSpec}
style={{
position: 'absolute',
right: 0,
zIndex: 1,
}}
/>
);
return (
<View
style={{
height: props.buttonGroups.length * buttonSize,
flexDirection: 'column',
flexGrow: 1,
display: 'flex',
}}
onLayout={onContainerLayout}
>
{hasSpaceForCloseBtn ? closeButton : null}
{overflowRows}
</View>
);
};
export default ToolbarOverflowRows;

View File

@ -1,83 +0,0 @@
import { useCallback, useMemo } from 'react';
import { ButtonSpec } from '../types';
import { _ } from '@joplin/lib/locale';
import { ButtonRowProps } from '../types';
import time from '@joplin/lib/time';
import { Keyboard, Platform } from 'react-native';
export interface ActionButtonRowProps extends ButtonRowProps {
keyboardVisible: boolean;
hasSoftwareKeyboard: boolean;
}
const useActionButtons = (props: ActionButtonRowProps) => {
const onDismissKeyboard = useCallback(() => {
// Keyboard.dismiss() doesn't dismiss the keyboard if it's editing the WebView.
Keyboard.dismiss();
// As such, dismiss the keyboard by sending a message to the View.
props.editorControl.hideKeyboard();
}, [props.editorControl]);
const onSearch = useCallback(() => {
if (props.searchState.dialogVisible) {
props.editorControl.searchControl.hideSearch();
} else {
props.editorControl.searchControl.showSearch();
}
}, [props.editorControl, props.searchState.dialogVisible]);
const onAttach = useCallback(() => {
onDismissKeyboard();
props.onAttach();
}, [props.onAttach, onDismissKeyboard]);
return useMemo(() => {
const actionButtons: ButtonSpec[] = [];
actionButtons.push({
icon: 'fa calendar-plus',
description: _('Insert time'),
onPress: () => {
props.editorControl.insertText(time.formatDateToLocal(new Date()));
},
disabled: props.readOnly,
});
actionButtons.push({
icon: 'material attachment',
description: _('Attach'),
onPress: onAttach,
disabled: props.readOnly,
});
actionButtons.push({
icon: 'material magnify',
description: (
props.searchState.dialogVisible ? _('Close') : _('Find and replace')
),
active: props.searchState.dialogVisible,
onPress: onSearch,
priority: -3,
disabled: props.readOnly,
});
actionButtons.push({
icon: 'material keyboard-close',
description: _('Hide keyboard'),
disabled: !props.keyboardVisible,
visible: props.hasSoftwareKeyboard && Platform.OS === 'ios',
onPress: onDismissKeyboard,
priority: -3,
});
return actionButtons;
}, [
props.editorControl, props.keyboardVisible, props.hasSoftwareKeyboard,
props.readOnly, props.searchState.dialogVisible,
onAttach, onDismissKeyboard, onSearch,
]);
};
export default useActionButtons;

View File

@ -1,34 +0,0 @@
import { useMemo } from 'react';
import { ButtonSpec } from '../types';
import { _ } from '@joplin/lib/locale';
import { ButtonRowProps } from '../types';
const useHeaderButtons = ({ selectionState, editorControl, readOnly }: ButtonRowProps) => {
return useMemo(() => {
const headerButtons: ButtonSpec[] = [];
for (let level = 1; level <= 5; level++) {
const active = selectionState.headerLevel === level;
headerButtons.push({
icon: `text H${level}`,
description: _('Header %d', level),
active,
// We only call addHeaderButton 5 times and in the same order, so
// the linter error is safe to ignore.
// eslint-disable-next-line @seiyab/react-hooks/rules-of-hooks
onPress: () => {
editorControl.toggleHeaderLevel(level);
},
// Make it likely for the first three header buttons to show, less likely for
// the others.
priority: level < 3 ? 2 : 0,
disabled: readOnly,
});
}
return headerButtons;
}, [selectionState, editorControl, readOnly]);
};
export default useHeaderButtons;

View File

@ -1,67 +0,0 @@
import { useMemo } from 'react';
import { ButtonSpec } from '../types';
import { _ } from '@joplin/lib/locale';
import { ButtonRowProps } from '../types';
const useInlineFormattingButtons = ({ selectionState, editorControl, readOnly, editorSettings }: ButtonRowProps) => {
const { bolded, italicized, inCode, inMath, inLink } = selectionState;
return useMemo(() => {
const inlineFormattingBtns: ButtonSpec[] = [];
inlineFormattingBtns.push({
icon: 'fa bold',
description: _('Bold'),
active: bolded,
onPress: editorControl.toggleBolded,
priority: 3,
disabled: readOnly,
});
inlineFormattingBtns.push({
icon: 'fa italic',
description: _('Italic'),
active: italicized,
onPress: editorControl.toggleItalicized,
priority: 2,
disabled: readOnly,
});
inlineFormattingBtns.push({
icon: 'text {;}',
description: _('Code'),
active: inCode,
onPress: editorControl.toggleCode,
priority: 2,
disabled: readOnly,
});
if (editorSettings.katexEnabled) {
inlineFormattingBtns.push({
icon: 'text ∑',
description: _('KaTeX'),
active: inMath,
onPress: editorControl.toggleMath,
priority: 1,
disabled: readOnly,
});
}
inlineFormattingBtns.push({
icon: 'fa link',
description: _('Link'),
active: inLink,
onPress: editorControl.showLinkDialog,
priority: -3,
disabled: readOnly,
});
return inlineFormattingBtns;
}, [readOnly, editorControl, editorSettings.katexEnabled, inLink, inMath, inCode, italicized, bolded]);
};
export default useInlineFormattingButtons;

View File

@ -1,63 +0,0 @@
import { useMemo } from 'react';
import { ButtonSpec } from '../types';
import { _ } from '@joplin/lib/locale';
import { ButtonRowProps } from '../types';
const useListButtons = ({ selectionState, editorControl, readOnly }: ButtonRowProps) => {
return useMemo(() => {
const listButtons: ButtonSpec[] = [];
listButtons.push({
icon: 'fa list-ul',
description: _('Unordered list'),
active: selectionState.inUnorderedList,
onPress: editorControl.toggleUnorderedList,
priority: -2,
disabled: readOnly,
});
listButtons.push({
icon: 'fa list-ol',
description: _('Ordered list'),
active: selectionState.inOrderedList,
onPress: editorControl.toggleOrderedList,
priority: -2,
disabled: readOnly,
});
listButtons.push({
icon: 'fa tasks',
description: _('Task list'),
active: selectionState.inChecklist,
onPress: editorControl.toggleTaskList,
priority: -2,
disabled: readOnly,
});
listButtons.push({
icon: 'ant indent-left',
description: _('Decrease indent level'),
onPress: editorControl.decreaseIndent,
priority: -1,
disabled: readOnly,
});
listButtons.push({
icon: 'ant indent-right',
description: _('Increase indent level'),
onPress: editorControl.increaseIndent,
priority: -1,
disabled: readOnly,
});
return listButtons;
}, [readOnly, editorControl, selectionState]);
};
export default useListButtons;

View File

@ -1,38 +0,0 @@
import { useMemo } from 'react';
import { ButtonSpec } from '../types';
import { ButtonRowProps } from '../types';
import { PluginStates, utils as pluginUtils } from '@joplin/lib/services/plugins/reducer';
import CommandService from '@joplin/lib/services/CommandService';
interface PluginButtonsRowProps extends ButtonRowProps {
pluginStates: PluginStates;
}
const usePluginButtons = (props: PluginButtonsRowProps) => {
return useMemo(() => {
const pluginButtons: ButtonSpec[] = [];
const pluginCommands =
pluginUtils
.commandNamesFromViews(props.pluginStates, 'editorToolbar')
// Remove separators
.filter(name => name !== '-');
const commandService = CommandService.instance();
for (const commandName of pluginCommands) {
const command = commandService.commandByName(commandName, { runtimeMustBeRegistered: true });
pluginButtons.push({
description: commandService.description(commandName),
icon: command.declaration.iconName ?? 'fas fa-cog',
onPress: async () => {
void commandService.execute(commandName);
},
});
}
return pluginButtons;
}, [props.pluginStates]);
};
export default usePluginButtons;

View File

@ -1,56 +0,0 @@
import { TextStyle, ViewStyle } from 'react-native';
import { EditorControl, EditorSettings } from '../types';
import SelectionFormatting from '@joplin/editor/SelectionFormatting';
import { SearchState } from '@joplin/editor/types';
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
export type OnPressListener = ()=> void;
export interface ButtonSpec {
// Name of an icon, as accepted by components/Icon.tsx
icon: string;
// Tooltip/accessibility label
description: string;
onPress: OnPressListener;
// Priority for showing the button in the main toolbar.
// Higher priority => more likely to be shown on the left of the toolbar
// Lower (negative) priority => more likely to be shown on the right side of the
// toolbar.
priority?: number;
// True if the button is connected to an enabled action.
// E.g. the cursor is in a header and the button is a header button.
active?: boolean;
disabled?: boolean;
visible?: boolean;
}
export interface ButtonGroup {
title: string;
items: ButtonSpec[];
}
export interface StyleSheetData {
themeId: number;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
styles: any;
}
type OnAttachCallback = ()=> void;
export interface MarkdownToolbarProps {
editorControl: EditorControl;
selectionState: SelectionFormatting;
searchState: SearchState;
editorSettings: EditorSettings;
pluginStates: PluginStates;
onAttach: OnAttachCallback;
style?: ViewStyle;
readOnly: boolean;
}
export interface ButtonRowProps extends MarkdownToolbarProps {
iconStyle: TextStyle;
}

View File

@ -7,10 +7,18 @@ import '@testing-library/jest-native';
import NoteEditor from './NoteEditor'; import NoteEditor from './NoteEditor';
import Setting from '@joplin/lib/models/Setting'; import Setting from '@joplin/lib/models/Setting';
import { _ } from '@joplin/lib/locale'; import { _ } from '@joplin/lib/locale';
import { MenuProvider } from 'react-native-popup-menu';
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils'; import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
import commandDeclarations from './commandDeclarations'; import commandDeclarations from './commandDeclarations';
import CommandService from '@joplin/lib/services/CommandService'; import CommandService, { RegisteredRuntime } from '@joplin/lib/services/CommandService';
import TestProviderStack from '../testing/TestProviderStack';
import createMockReduxStore from '../../utils/testing/createMockReduxStore';
import mockCommandRuntimes from '../EditorToolbar/testing/mockCommandRuntimes';
import setupGlobalStore from '../../utils/testing/setupGlobalStore';
import { Store } from 'redux';
import { AppState } from '../../utils/types';
let store: Store<AppState>;
let registeredRuntime: RegisteredRuntime;
describe('NoteEditor', () => { describe('NoteEditor', () => {
beforeAll(() => { beforeAll(() => {
@ -24,11 +32,19 @@ describe('NoteEditor', () => {
// Required to use ExtendedWebView // Required to use ExtendedWebView
await setupDatabaseAndSynchronizer(0); await setupDatabaseAndSynchronizer(0);
await switchClient(0); await switchClient(0);
store = createMockReduxStore();
setupGlobalStore(store);
registeredRuntime = mockCommandRuntimes(store);
});
afterEach(() => {
registeredRuntime.deregister();
}); });
it('should hide the markdown toolbar when the window is small', async () => { it('should hide the markdown toolbar when the window is small', async () => {
const wrappedNoteEditor = render( const wrappedNoteEditor = render(
<MenuProvider> <TestProviderStack store={store}>
<NoteEditor <NoteEditor
themeId={Setting.THEME_ARITIM_DARK} themeId={Setting.THEME_ARITIM_DARK}
initialText='Testing...' initialText='Testing...'
@ -41,7 +57,7 @@ describe('NoteEditor', () => {
onAttach={async ()=>{}} onAttach={async ()=>{}}
plugins={{}} plugins={{}}
/> />
</MenuProvider>, </TestProviderStack>,
); );
// Maps from screen height to whether the markdown toolbar should be visible. // Maps from screen height to whether the markdown toolbar should be visible.
@ -70,11 +86,11 @@ describe('NoteEditor', () => {
setRootHeight(height); setRootHeight(height);
await waitFor(async () => { await waitFor(async () => {
const showMoreButton = await screen.queryByLabelText(_('Show more actions')); const toolbarButton = await screen.queryByLabelText(_('Bold'));
if (visible) { if (visible) {
expect(showMoreButton).not.toBeNull(); expect(toolbarButton).not.toBeNull();
} else { } else {
expect(showMoreButton).toBeNull(); expect(toolbarButton).toBeNull();
} }
}); });
} }

View File

@ -16,7 +16,6 @@ import { editorFont } from '../global-style';
import { EditorControl as EditorBodyControl, ContentScriptData } from '@joplin/editor/types'; import { EditorControl as EditorBodyControl, ContentScriptData } from '@joplin/editor/types';
import { EditorControl, EditorSettings, SelectionRange, WebViewToEditorApi } from './types'; import { EditorControl, EditorSettings, SelectionRange, WebViewToEditorApi } from './types';
import { _ } from '@joplin/lib/locale'; import { _ } from '@joplin/lib/locale';
import MarkdownToolbar from './MarkdownToolbar/MarkdownToolbar';
import { ChangeEvent, EditorEvent, EditorEventType, SelectionRangeChangeEvent, UndoRedoDepthChangeEvent } from '@joplin/editor/events'; import { ChangeEvent, EditorEvent, EditorEventType, SelectionRangeChangeEvent, UndoRedoDepthChangeEvent } from '@joplin/editor/events';
import { EditorCommandType, EditorKeymap, EditorLanguageType, SearchState } from '@joplin/editor/types'; import { EditorCommandType, EditorKeymap, EditorLanguageType, SearchState } from '@joplin/editor/types';
import SelectionFormatting, { defaultSelectionFormatting } from '@joplin/editor/SelectionFormatting'; import SelectionFormatting, { defaultSelectionFormatting } from '@joplin/editor/SelectionFormatting';
@ -30,6 +29,7 @@ import { OnMessageEvent } from '../ExtendedWebView/types';
import { join, dirname } from 'path'; import { join, dirname } from 'path';
import * as mimeUtils from '@joplin/lib/mime-utils'; import * as mimeUtils from '@joplin/lib/mime-utils';
import uuid from '@joplin/lib/uuid'; import uuid from '@joplin/lib/uuid';
import EditorToolbar from '../EditorToolbar/EditorToolbar';
type ChangeEventHandler = (event: ChangeEvent)=> void; type ChangeEventHandler = (event: ChangeEvent)=> void;
type UndoRedoDepthChangeHandler = (event: UndoRedoDepthChangeEvent)=> void; type UndoRedoDepthChangeHandler = (event: UndoRedoDepthChangeEvent)=> void;
@ -184,7 +184,7 @@ const useEditorControl = (
setSearchState: OnSearchStateChangeCallback, setSearchState: OnSearchStateChangeCallback,
): EditorControl => { ): EditorControl => {
return useMemo(() => { return useMemo(() => {
const execCommand = (command: EditorCommandType) => { const execEditorCommand = (command: EditorCommandType) => {
void bodyControl.execCommand(command); void bodyControl.execCommand(command);
}; };
@ -229,25 +229,25 @@ const useEditorControl = (
}, },
toggleBolded() { toggleBolded() {
execCommand(EditorCommandType.ToggleBolded); execEditorCommand(EditorCommandType.ToggleBolded);
}, },
toggleItalicized() { toggleItalicized() {
execCommand(EditorCommandType.ToggleItalicized); execEditorCommand(EditorCommandType.ToggleItalicized);
}, },
toggleOrderedList() { toggleOrderedList() {
execCommand(EditorCommandType.ToggleNumberedList); execEditorCommand(EditorCommandType.ToggleNumberedList);
}, },
toggleUnorderedList() { toggleUnorderedList() {
execCommand(EditorCommandType.ToggleBulletedList); execEditorCommand(EditorCommandType.ToggleBulletedList);
}, },
toggleTaskList() { toggleTaskList() {
execCommand(EditorCommandType.ToggleCheckList); execEditorCommand(EditorCommandType.ToggleCheckList);
}, },
toggleCode() { toggleCode() {
execCommand(EditorCommandType.ToggleCode); execEditorCommand(EditorCommandType.ToggleCode);
}, },
toggleMath() { toggleMath() {
execCommand(EditorCommandType.ToggleMath); execEditorCommand(EditorCommandType.ToggleMath);
}, },
toggleHeaderLevel(level: number) { toggleHeaderLevel(level: number) {
const levelToCommand = [ const levelToCommand = [
@ -264,19 +264,19 @@ const useEditorControl = (
throw new Error(`Unsupported header level ${level}`); throw new Error(`Unsupported header level ${level}`);
} }
execCommand(levelToCommand[index]); execEditorCommand(levelToCommand[index]);
}, },
increaseIndent() { increaseIndent() {
execCommand(EditorCommandType.IndentMore); execEditorCommand(EditorCommandType.IndentMore);
}, },
decreaseIndent() { decreaseIndent() {
execCommand(EditorCommandType.IndentLess); execEditorCommand(EditorCommandType.IndentLess);
}, },
updateLink(label: string, url: string) { updateLink(label: string, url: string) {
bodyControl.updateLink(label, url); bodyControl.updateLink(label, url);
}, },
scrollSelectionIntoView() { scrollSelectionIntoView() {
execCommand(EditorCommandType.ScrollSelectionIntoView); execEditorCommand(EditorCommandType.ScrollSelectionIntoView);
}, },
showLinkDialog() { showLinkDialog() {
setLinkDialogVisible(true); setLinkDialogVisible(true);
@ -296,23 +296,23 @@ const useEditorControl = (
searchControl: { searchControl: {
findNext() { findNext() {
execCommand(EditorCommandType.FindNext); execEditorCommand(EditorCommandType.FindNext);
}, },
findPrevious() { findPrevious() {
execCommand(EditorCommandType.FindPrevious); execEditorCommand(EditorCommandType.FindPrevious);
}, },
replaceNext() { replaceNext() {
execCommand(EditorCommandType.ReplaceNext); execEditorCommand(EditorCommandType.ReplaceNext);
}, },
replaceAll() { replaceAll() {
execCommand(EditorCommandType.ReplaceAll); execEditorCommand(EditorCommandType.ReplaceAll);
}, },
showSearch() { showSearch() {
execCommand(EditorCommandType.ShowSearch); execEditorCommand(EditorCommandType.ShowSearch);
}, },
hideSearch() { hideSearch() {
execCommand(EditorCommandType.HideSearch); execEditorCommand(EditorCommandType.HideSearch);
}, },
setSearchState: setSearchStateCallback, setSearchState: setSearchStateCallback,
@ -535,20 +535,12 @@ function NoteEditor(props: Props, ref: any) {
} }
}, []); }, []);
const toolbar = <MarkdownToolbar const toolbarEditorState = useMemo(() => ({
style={{ selectionState,
// Don't show the markdown toolbar if there isn't enough space searchVisible: searchState.dialogVisible,
// for it: }), [selectionState, searchState.dialogVisible]);
flexShrink: 1,
}} const toolbar = <EditorToolbar editorState={toolbarEditorState} />;
editorSettings={editorSettings}
editorControl={editorControl}
selectionState={selectionState}
searchState={searchState}
pluginStates={props.plugins}
onAttach={props.onAttach}
readOnly={props.readOnly}
/>;
return ( return (
<View <View

View File

@ -1,14 +1,28 @@
import { EditorCommandType } from '@joplin/editor/types';
import { _ } from '@joplin/lib/locale';
import { CommandDeclaration } from '@joplin/lib/services/CommandService'; import { CommandDeclaration } from '@joplin/lib/services/CommandService';
export const enabledCondition = (_commandName: string) => { export const enabledCondition = (_commandName: string) => {
const output = [ const output = [
'!modalDialogVisible',
'!noteIsReadOnly', '!noteIsReadOnly',
]; ];
return output.filter(c => !!c).join(' && '); return output.filter(c => !!c).join(' && ');
}; };
const headerDeclarations = () => {
const result: CommandDeclaration[] = [];
for (let level = 1; level <= 5; level++) {
result.push({
name: `editor.textHeading${level}`,
iconName: `material format-header-${level}`,
label: () => _('Header %d', level),
});
}
return result;
};
const declarations: CommandDeclaration[] = [ const declarations: CommandDeclaration[] = [
{ {
name: 'insertText', name: 'insertText',
@ -34,6 +48,65 @@ const declarations: CommandDeclaration[] = [
{ {
name: 'editor.execCommand', name: 'editor.execCommand',
}, },
{
name: EditorCommandType.ToggleBolded,
label: () => _('Bold'),
iconName: 'material format-bold',
},
{
name: EditorCommandType.ToggleItalicized,
label: () => _('Italic'),
iconName: 'material format-italic',
},
...headerDeclarations(),
{
name: EditorCommandType.ToggleCode,
label: () => _('Code'),
iconName: 'material code-json',
},
{
// The 'editor.' prefix needs to be included because ToggleMath is not a legacy
// editor command. Without this, ToggleMath is not recognised as an editor command.
name: `editor.${EditorCommandType.ToggleMath}`,
label: () => _('Math'),
iconName: 'material sigma',
},
{
name: EditorCommandType.ToggleNumberedList,
label: () => _('Ordered list'),
iconName: 'material format-list-numbered',
},
{
name: EditorCommandType.ToggleBulletedList,
label: () => _('Unordered list'),
iconName: 'material format-list-bulleted',
},
{
name: EditorCommandType.ToggleCheckList,
label: () => _('Task list'),
iconName: 'material format-list-checks',
},
{
name: EditorCommandType.IndentLess,
label: () => _('Decrease indent level'),
iconName: 'ant indent-left',
},
{
name: EditorCommandType.IndentMore,
label: () => _('Increase indent level'),
iconName: 'ant indent-right',
},
{
name: EditorCommandType.ToggleSearch,
label: () => _('Search'),
iconName: 'material magnify',
},
{
name: EditorCommandType.EditLink,
label: () => _('Link'),
iconName: 'material link',
},
]; ];
export default declarations; export default declarations;

View File

@ -1,6 +1,6 @@
import CommandService, { CommandContext, CommandDeclaration } from '@joplin/lib/services/CommandService'; import CommandService, { CommandContext, CommandDeclaration } from '@joplin/lib/services/CommandService';
import { EditorControl } from '@joplin/editor/types'; import { EditorControl } from '@joplin/editor/types';
import { useEffect } from 'react'; import useNowEffect from '@joplin/lib/hooks/useNowEffect';
import commandDeclarations, { enabledCondition } from '../commandDeclarations'; import commandDeclarations, { enabledCondition } from '../commandDeclarations';
import Logger from '@joplin/utils/Logger'; import Logger from '@joplin/utils/Logger';
@ -34,7 +34,9 @@ const commandRuntime = (declaration: CommandDeclaration, editor: EditorControl)
}; };
const useEditorCommandHandler = (editorControl: EditorControl) => { const useEditorCommandHandler = (editorControl: EditorControl) => {
useEffect(() => { // useNowEffect: The command runtimes need to be registered before child components
// can render.
useNowEffect(() => {
const commandService = CommandService.instance(); const commandService = CommandService.instance();
for (const declaration of commandDeclarations) { for (const declaration of commandDeclarations) {
commandService.registerRuntime(declaration.name, commandRuntime(declaration, editorControl)); commandService.registerRuntime(declaration.name, commandRuntime(declaration, editorControl));
@ -45,7 +47,7 @@ const useEditorCommandHandler = (editorControl: EditorControl) => {
commandService.unregisterRuntime(declaration.name); commandService.unregisterRuntime(declaration.name);
} }
}; };
}); }, []);
}; };
export default useEditorCommandHandler; export default useEditorCommandHandler;

View File

@ -42,12 +42,12 @@ const WebBetaButton: React.FC<Props> = props => {
iconStyle={props.iconStyle} iconStyle={props.iconStyle}
/> />
<DismissibleDialog <DismissibleDialog
heading={_('Beta')}
size={DialogSize.Small} size={DialogSize.Small}
themeId={props.themeId} themeId={props.themeId}
visible={dialogVisible} visible={dialogVisible}
onDismiss={onHideDialog} onDismiss={onHideDialog}
> >
<Text variant='headlineMedium'>{_('Beta')}</Text>
<Text>{'At present, the web client is in beta. In the future, it may change significantly, or be removed.'}</Text> <Text>{'At present, the web client is in beta. In the future, it may change significantly, or be removed.'}</Text>
<View style={feedbackContainerStyles}> <View style={feedbackContainerStyles}>
<LinkButton onPress={onLeaveFeedback}>{_('Give feedback')}</LinkButton> <LinkButton onPress={onLeaveFeedback}>{_('Give feedback')}</LinkButton>

View File

@ -550,7 +550,14 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
try { try {
for (let i = 0; i < noteIds.length; i++) { for (let i = 0; i < noteIds.length; i++) {
await Note.moveToFolder(noteIds[i], folderId); await Note.moveToFolder(
noteIds[i],
folderId,
// By default, the note selection is preserved on mobile when a note is moved to
// a different folder. However, when moving notes from the note list, this shouldn't be
// the case:
{ dispatchOptions: { preserveSelection: false } },
);
} }
} catch (error) { } catch (error) {
alert(_n('This note could not be moved: %s', 'These notes could not be moved: %s', noteIds.length, error.message)); alert(_n('This note could not be moved: %s', 'These notes could not be moved: %s', noteIds.length, error.message));

View File

@ -9,16 +9,15 @@
import Setting from '@joplin/lib/models/Setting'; import Setting from '@joplin/lib/models/Setting';
import { themeStyle } from '@joplin/lib/theme'; import { themeStyle } from '@joplin/lib/theme';
import { Theme } from '@joplin/lib/themes/type';
import * as React from 'react'; import * as React from 'react';
import { ReactNode, useCallback, useState, useEffect } from 'react'; import { ReactNode, useCallback, useState, useEffect } from 'react';
import { View, ViewStyle } from 'react-native'; import { Platform, View, ViewStyle } from 'react-native';
import IconButton from '../../IconButton'; import IconButton from './IconButton';
import useKeyboardVisible from '../utils/hooks/useKeyboardVisible';
interface Props { interface Props {
children: ReactNode; children: ReactNode;
spaceApplicable: boolean;
themeId: number; themeId: number;
style?: ViewStyle; style?: ViewStyle;
} }
@ -44,7 +43,7 @@ const ToggleSpaceButton = (props: Props) => {
} }
}, [onDecreaseSpace]); }, [onDecreaseSpace]);
const theme: Theme = themeStyle(props.themeId); const theme = themeStyle(props.themeId);
const decreaseSpaceButton = ( const decreaseSpaceButton = (
<> <>
@ -77,15 +76,18 @@ const ToggleSpaceButton = (props: Props) => {
</> </>
); );
const { keyboardVisible } = useKeyboardVisible();
const spaceApplicable = keyboardVisible && Platform.OS === 'ios';
const style: ViewStyle = { const style: ViewStyle = {
marginBottom: props.spaceApplicable ? additionalSpace : 0, marginBottom: spaceApplicable ? additionalSpace : 0,
...props.style, ...props.style,
}; };
return ( return (
<View style={style}> <View style={style}>
{props.children} {props.children}
{ decreaseSpaceBtnVisible && props.spaceApplicable ? decreaseSpaceButton : null } { decreaseSpaceBtnVisible && spaceApplicable ? decreaseSpaceButton : null }
</View> </View>
); );
}; };

View File

@ -12,9 +12,19 @@ export default async () => {
} catch (error) { } catch (error) {
const errorName = (error as Errors).name; const errorName = (error as Errors).name;
let errorMessage = error.message; const errorMessage = error.message;
if (errorName === 'FingerprintScannerNotEnrolled' || errorName === 'FingerprintScannerNotAvailable') { if (errorName === 'FingerprintScannerNotEnrolled' || errorName === 'FingerprintScannerNotAvailable') {
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.'); // 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
// the check would be a problem if biometric unlock was disabled as a result of being
// broken. In this case, the user will never be able to unlock Joplin.
// Ref: https://github.com/laurent22/joplin/issues/10926
logger.warn('Biometric unlock is not setup on the device - skipping check');
return;
// 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.');
} }
error.message = _('Could not verify your identity: %s', errorMessage); error.message = _('Could not verify your identity: %s', errorMessage);

View File

@ -12,3 +12,4 @@ const makeTextButtonComponent = (type: ButtonType) => {
export const PrimaryButton = makeTextButtonComponent(ButtonType.Primary); export const PrimaryButton = makeTextButtonComponent(ButtonType.Primary);
export const SecondaryButton = makeTextButtonComponent(ButtonType.Secondary); export const SecondaryButton = makeTextButtonComponent(ButtonType.Secondary);
export const LinkButton = makeTextButtonComponent(ButtonType.Link); export const LinkButton = makeTextButtonComponent(ButtonType.Link);
export const DeleteButton = makeTextButtonComponent(ButtonType.Delete);

View File

@ -8,6 +8,7 @@ const Color = require('color');
const baseStyle = { const baseStyle = {
appearance: 'light', appearance: 'light',
fontSize: 16, fontSize: 16,
fontSizeLarge: 20,
noteViewerFontSize: 16, noteViewerFontSize: 16,
margin: 15, // No text and no interactive component should be within this margin margin: 15, // No text and no interactive component should be within this margin
itemMarginTop: 10, itemMarginTop: 10,

View File

@ -8,11 +8,10 @@ import NoteScreen from './Note';
import { setupDatabaseAndSynchronizer, switchClient, simulateReadOnlyShareEnv, supportDir, synchronizerStart, resourceFetcher, runWithFakeTimers } from '@joplin/lib/testing/test-utils'; import { setupDatabaseAndSynchronizer, switchClient, simulateReadOnlyShareEnv, supportDir, synchronizerStart, resourceFetcher, runWithFakeTimers } from '@joplin/lib/testing/test-utils';
import { waitFor as waitForWithRealTimers } from '@joplin/lib/testing/test-utils'; import { waitFor as waitForWithRealTimers } from '@joplin/lib/testing/test-utils';
import Note from '@joplin/lib/models/Note'; import Note from '@joplin/lib/models/Note';
import { AppState } from '../../utils/types'; import { AppState } from '../../../utils/types';
import { Store } from 'redux'; import { Store } from 'redux';
import createMockReduxStore from '../../utils/testing/createMockReduxStore'; import createMockReduxStore from '../../../utils/testing/createMockReduxStore';
import initializeCommandService from '../../utils/initializeCommandService'; import getWebViewDomById from '../../../utils/testing/getWebViewDomById';
import getWebViewDomById from '../../utils/testing/getWebViewDomById';
import { NoteEntity } from '@joplin/lib/services/database/types'; import { NoteEntity } from '@joplin/lib/services/database/types';
import Folder from '@joplin/lib/models/Folder'; import Folder from '@joplin/lib/models/Folder';
import BaseItem from '@joplin/lib/models/BaseItem'; import BaseItem from '@joplin/lib/models/BaseItem';
@ -22,11 +21,13 @@ import { getDisplayParentId } from '@joplin/lib/services/trash';
import { itemIsReadOnlySync, ItemSlice } from '@joplin/lib/models/utils/readOnly'; import { itemIsReadOnlySync, ItemSlice } from '@joplin/lib/models/utils/readOnly';
import { LayoutChangeEvent } from 'react-native'; import { LayoutChangeEvent } from 'react-native';
import shim from '@joplin/lib/shim'; import shim from '@joplin/lib/shim';
import getWebViewWindowById from '../../utils/testing/getWebViewWindowById'; import getWebViewWindowById from '../../../utils/testing/getWebViewWindowById';
import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl'; import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
import Setting from '@joplin/lib/models/Setting'; import Setting from '@joplin/lib/models/Setting';
import Resource from '@joplin/lib/models/Resource'; import Resource from '@joplin/lib/models/Resource';
import TestProviderStack from '../testing/TestProviderStack'; import TestProviderStack from '../../testing/TestProviderStack';
import setupGlobalStore from '../../../utils/testing/setupGlobalStore';
import CommandService from '@joplin/lib/services/CommandService';
interface WrapperProps { interface WrapperProps {
} }
@ -122,13 +123,22 @@ const openNoteActionsMenu = async () => {
}); });
}; };
const expectToBeEditing = async (editing: boolean) => {
await waitFor(() => {
const editButton = screen.queryByLabelText('Edit');
if (editing) {
expect(editButton).toBeNull();
} else {
expect(editButton).not.toBeNull();
}
});
};
const openEditor = async () => { const openEditor = async () => {
const editButton = await screen.findByLabelText('Edit'); const editButton = await screen.findByLabelText('Edit');
fireEvent.press(editButton); fireEvent.press(editButton);
await waitFor(() => { await expectToBeEditing(true);
expect(screen.queryByLabelText('Edit')).toBeNull();
});
}; };
describe('screens/Note', () => { describe('screens/Note', () => {
@ -138,7 +148,7 @@ describe('screens/Note', () => {
await switchClient(0); await switchClient(0);
store = createMockReduxStore(); store = createMockReduxStore();
initializeCommandService(store); setupGlobalStore(store);
// In order for note changes to be saved, note-screen-shared requires // In order for note changes to be saved, note-screen-shared requires
// that at least one folder exist. // that at least one folder exist.
@ -325,4 +335,15 @@ describe('screens/Note', () => {
} }
}); });
}); });
it('the toggleVisiblePanes command should start and stop editing', async () => {
await openNewNote({ title: 'To be edited', body: '...' });
render(<WrappedNoteScreen />);
await expectToBeEditing(false);
await CommandService.instance().execute('toggleVisiblePanes');
await expectToBeEditing(true);
await CommandService.instance().execute('toggleVisiblePanes');
await expectToBeEditing(false);
});
}); });

View File

@ -3,66 +3,66 @@ import uuid from '@joplin/lib/uuid';
import Setting from '@joplin/lib/models/Setting'; import Setting from '@joplin/lib/models/Setting';
import shim from '@joplin/lib/shim'; import shim from '@joplin/lib/shim';
import UndoRedoService from '@joplin/lib/services/UndoRedoService'; import UndoRedoService from '@joplin/lib/services/UndoRedoService';
import NoteBodyViewer from '../NoteBodyViewer/NoteBodyViewer'; import NoteBodyViewer from '../../NoteBodyViewer/NoteBodyViewer';
import checkPermissions from '../../utils/checkPermissions'; import checkPermissions from '../../../utils/checkPermissions';
import NoteEditor from '../NoteEditor/NoteEditor'; import NoteEditor from '../../NoteEditor/NoteEditor';
import * as React from 'react'; import * as React from 'react';
import { Keyboard, View, TextInput, StyleSheet, Linking, Share, NativeSyntheticEvent } from 'react-native'; import { Keyboard, View, TextInput, StyleSheet, Linking, Share, NativeSyntheticEvent } from 'react-native';
import { Platform, PermissionsAndroid } from 'react-native'; import { Platform, PermissionsAndroid } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
// const { MarkdownEditor } = require('@joplin/lib/../MarkdownEditor/index.js');
import Note from '@joplin/lib/models/Note'; import Note from '@joplin/lib/models/Note';
import BaseItem from '@joplin/lib/models/BaseItem'; import BaseItem from '@joplin/lib/models/BaseItem';
import Resource from '@joplin/lib/models/Resource'; import Resource from '@joplin/lib/models/Resource';
import Folder from '@joplin/lib/models/Folder'; import Folder from '@joplin/lib/models/Folder';
const Clipboard = require('@react-native-clipboard/clipboard').default; const Clipboard = require('@react-native-clipboard/clipboard').default;
const md5 = require('md5'); const md5 = require('md5');
import BackButtonService from '../../services/BackButtonService'; import BackButtonService from '../../../services/BackButtonService';
import NavService, { OnNavigateCallback as OnNavigateCallback } from '@joplin/lib/services/NavService'; import NavService, { OnNavigateCallback as OnNavigateCallback } from '@joplin/lib/services/NavService';
import { ModelType } from '@joplin/lib/BaseModel'; import { ModelType } from '@joplin/lib/BaseModel';
import FloatingActionButton from '../buttons/FloatingActionButton'; import FloatingActionButton from '../../buttons/FloatingActionButton';
const { fileExtension, safeFileExtension } = require('@joplin/lib/path-utils'); const { fileExtension, safeFileExtension } = require('@joplin/lib/path-utils');
import * as mimeUtils from '@joplin/lib/mime-utils'; import * as mimeUtils from '@joplin/lib/mime-utils';
import ScreenHeader, { MenuOptionType } from '../ScreenHeader'; import ScreenHeader, { MenuOptionType } from '../../ScreenHeader';
import NoteTagsDialog from './NoteTagsDialog'; import NoteTagsDialog from '../NoteTagsDialog';
import time from '@joplin/lib/time'; import time from '@joplin/lib/time';
import Checkbox from '../Checkbox'; import Checkbox from '../../Checkbox';
import { _, currentLocale } from '@joplin/lib/locale'; import { _, currentLocale } from '@joplin/lib/locale';
import { reg } from '@joplin/lib/registry'; import { reg } from '@joplin/lib/registry';
import ResourceFetcher from '@joplin/lib/services/ResourceFetcher'; import ResourceFetcher from '@joplin/lib/services/ResourceFetcher';
import { BaseScreenComponent } from '../base-screen'; import { BaseScreenComponent } from '../../base-screen';
import { themeStyle, editorFont } from '../global-style'; import { themeStyle, editorFont } from '../../global-style';
import shared, { BaseNoteScreenComponent, Props as BaseProps } from '@joplin/lib/components/shared/note-screen-shared'; import shared, { BaseNoteScreenComponent, Props as BaseProps } from '@joplin/lib/components/shared/note-screen-shared';
import { Asset, ImagePickerResponse, launchImageLibrary } from 'react-native-image-picker'; import SelectDateTimeDialog from '../../SelectDateTimeDialog';
import SelectDateTimeDialog from '../SelectDateTimeDialog'; import ShareExtension from '../../../utils/ShareExtension.js';
import ShareExtension from '../../utils/ShareExtension.js'; import CameraView from '../../CameraView/CameraView';
import CameraView from '../CameraView/CameraView';
import { FolderEntity, NoteEntity, ResourceEntity } from '@joplin/lib/services/database/types'; import { FolderEntity, NoteEntity, ResourceEntity } from '@joplin/lib/services/database/types';
import Logger from '@joplin/utils/Logger'; import Logger from '@joplin/utils/Logger';
import ImageEditor from '../NoteEditor/ImageEditor/ImageEditor'; import ImageEditor from '../../NoteEditor/ImageEditor/ImageEditor';
import promptRestoreAutosave from '../NoteEditor/ImageEditor/promptRestoreAutosave'; import promptRestoreAutosave from '../../NoteEditor/ImageEditor/promptRestoreAutosave';
import isEditableResource from '../NoteEditor/ImageEditor/isEditableResource'; import isEditableResource from '../../NoteEditor/ImageEditor/isEditableResource';
import VoiceTypingDialog from '../voiceTyping/VoiceTypingDialog'; import VoiceTypingDialog from '../../voiceTyping/VoiceTypingDialog';
import { isSupportedLanguage } from '../../services/voiceTyping/vosk'; import { isSupportedLanguage } from '../../../services/voiceTyping/vosk';
import { ChangeEvent as EditorChangeEvent, SelectionRangeChangeEvent, UndoRedoDepthChangeEvent } from '@joplin/editor/events'; import { ChangeEvent as EditorChangeEvent, SelectionRangeChangeEvent, UndoRedoDepthChangeEvent } from '@joplin/editor/events';
import { join } from 'path'; import { join } from 'path';
import { Dispatch } from 'redux'; import { Dispatch } from 'redux';
import { RefObject, useContext, useRef } from 'react'; import { RefObject, useContext } from 'react';
import { SelectionRange } from '../NoteEditor/types'; import { SelectionRange } from '../../NoteEditor/types';
import { getNoteCallbackUrl } from '@joplin/lib/callbackUrlUtils'; import { getNoteCallbackUrl } from '@joplin/lib/callbackUrlUtils';
import { AppState } from '../../utils/types'; import { AppState } from '../../../utils/types';
import restoreItems from '@joplin/lib/services/trash/restoreItems'; import restoreItems from '@joplin/lib/services/trash/restoreItems';
import { getDisplayParentTitle } from '@joplin/lib/services/trash'; import { getDisplayParentTitle } from '@joplin/lib/services/trash';
import { PluginStates, utils as pluginUtils } from '@joplin/lib/services/plugins/reducer'; import { PluginStates, utils as pluginUtils } from '@joplin/lib/services/plugins/reducer';
import pickDocument from '../../utils/pickDocument'; import debounce from '../../../utils/debounce';
import debounce from '../../utils/debounce';
import { focus } from '@joplin/lib/utils/focusHandler'; import { focus } from '@joplin/lib/utils/focusHandler';
import CommandService from '@joplin/lib/services/CommandService'; import CommandService, { RegisteredRuntime } from '@joplin/lib/services/CommandService';
import { ResourceInfo } from '../NoteBodyViewer/hooks/useRerenderHandler'; import { ResourceInfo } from '../../NoteBodyViewer/hooks/useRerenderHandler';
import getImageDimensions from '../../utils/image/getImageDimensions'; import getImageDimensions from '../../../utils/image/getImageDimensions';
import resizeImage from '../../utils/image/resizeImage'; import resizeImage from '../../../utils/image/resizeImage';
import { CameraResult } from '../CameraView/types'; import { CameraResult } from '../../CameraView/types';
import { DialogContext, DialogControl } from '../DialogManager'; import { DialogContext, DialogControl } from '../../DialogManager';
import { CommandRuntimeProps, EditorMode, PickerResponse } from './types';
import commands from './commands';
import { AttachFileAction, AttachFileOptions } from './commands/attachFile';
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const emptyArray: any[] = []; const emptyArray: any[] = [];
@ -84,6 +84,7 @@ interface Props extends BaseProps {
highlightedWords: string[]; highlightedWords: string[];
noteHash: string; noteHash: string;
toolbarEnabled: boolean; toolbarEnabled: boolean;
newNoteAttachFileAction: AttachFileAction;
} }
interface ComponentProps extends Props { interface ComponentProps extends Props {
@ -92,7 +93,7 @@ interface ComponentProps extends Props {
interface State { interface State {
note: NoteEntity; note: NoteEntity;
mode: 'view'|'edit'; mode: EditorMode;
readOnly: boolean; readOnly: boolean;
folder: FolderEntity|null; folder: FolderEntity|null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
@ -110,8 +111,6 @@ interface State {
noteResources: Record<string, ResourceInfo>; noteResources: Record<string, ResourceInfo>;
newAndNoTitleChangeNoteId: boolean|null; newAndNoTitleChangeNoteId: boolean|null;
HACK_webviewLoadingState: number;
undoRedoButtonState: { undoRedoButtonState: {
canUndo: boolean; canUndo: boolean;
canRedo: boolean; canRedo: boolean;
@ -150,6 +149,7 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
private folderPickerOptions_: any; private folderPickerOptions_: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
public dialogbox: any; public dialogbox: any;
private commandRegistration_: RegisteredRuntime|null = null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
public static navigationOptions(): any { public static navigationOptions(): any {
@ -178,15 +178,6 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
imageEditorResourceFilepath: null, imageEditorResourceFilepath: null,
newAndNoTitleChangeNoteId: null, newAndNoTitleChangeNoteId: null,
// HACK: For reasons I can't explain, when the WebView is present, the TextInput initially does not display (It's just a white rectangle with
// no visible text). It will only appear when tapping it or doing certain action like selecting text on the webview. The bug started to
// appear one day and did not go away - reverting to an old RN version did not help, undoing all
// the commits till a working version did not help. The bug also does not happen in the simulator which makes it hard to fix.
// Eventually, a way that "worked" is to add a 1px margin on top of the text input just after the webview has loaded, then removing this
// margin. This forces RN to update the text input and to display it. Maybe that hack can be removed once RN is upgraded.
// See https://github.com/laurent22/joplin/issues/1057
HACK_webviewLoadingState: 0,
undoRedoButtonState: { undoRedoButtonState: {
canUndo: false, canUndo: false,
canRedo: false, canRedo: false,
@ -292,7 +283,6 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
} }
}; };
this.takePhoto_onPress = this.takePhoto_onPress.bind(this);
this.cameraView_onPhoto = this.cameraView_onPhoto.bind(this); this.cameraView_onPhoto = this.cameraView_onPhoto.bind(this);
this.cameraView_onCancel = this.cameraView_onCancel.bind(this); this.cameraView_onCancel = this.cameraView_onCancel.bind(this);
this.properties_onPress = this.properties_onPress.bind(this); this.properties_onPress = this.properties_onPress.bind(this);
@ -308,13 +298,48 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
this.undoRedoService_stackChange = this.undoRedoService_stackChange.bind(this); this.undoRedoService_stackChange = this.undoRedoService_stackChange.bind(this);
this.screenHeader_undoButtonPress = this.screenHeader_undoButtonPress.bind(this); this.screenHeader_undoButtonPress = this.screenHeader_undoButtonPress.bind(this);
this.screenHeader_redoButtonPress = this.screenHeader_redoButtonPress.bind(this); this.screenHeader_redoButtonPress = this.screenHeader_redoButtonPress.bind(this);
this.onBodyViewerLoadEnd = this.onBodyViewerLoadEnd.bind(this);
this.onBodyViewerCheckboxChange = this.onBodyViewerCheckboxChange.bind(this); this.onBodyViewerCheckboxChange = this.onBodyViewerCheckboxChange.bind(this);
this.onUndoRedoDepthChange = this.onUndoRedoDepthChange.bind(this); this.onUndoRedoDepthChange = this.onUndoRedoDepthChange.bind(this);
this.voiceTypingDialog_onText = this.voiceTypingDialog_onText.bind(this); this.voiceTypingDialog_onText = this.voiceTypingDialog_onText.bind(this);
this.voiceTypingDialog_onDismiss = this.voiceTypingDialog_onDismiss.bind(this); this.voiceTypingDialog_onDismiss = this.voiceTypingDialog_onDismiss.bind(this);
} }
private registerCommands() {
if (this.commandRegistration_) return;
const dialogs = () => this.props.dialogs;
this.commandRegistration_ = CommandService.instance().componentRegisterCommands<CommandRuntimeProps>(
{
attachFile: this.attachFile.bind(this),
hideKeyboard: () => {
if (this.useEditorBeta()) {
this.editorRef?.current?.hideKeyboard();
} else {
Keyboard.dismiss();
}
},
insertText: this.insertText.bind(this),
get dialogs() {
return dialogs();
},
setCameraVisible: (visible) => {
this.setState({ showCamera: visible });
},
setTagDialogVisible: (visible) => {
if (!this.state.note || !this.state.note.id) return;
this.setState({ noteTagDialogShown: visible });
},
getMode: () => this.state.mode,
setMode: (mode: 'view'|'edit') => {
this.setState({ mode });
},
},
commands,
true,
);
}
private useEditorBeta(): boolean { private useEditorBeta(): boolean {
return this.props.useEditorBeta; return this.props.useEditorBeta;
} }
@ -378,7 +403,7 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
const themeId = this.props.themeId; const themeId = this.props.themeId;
const theme = themeStyle(themeId); const theme = themeStyle(themeId);
const cacheKey = [themeId, this.state.titleTextInputHeight, this.state.HACK_webviewLoadingState].join('_'); const cacheKey = [themeId, this.state.titleTextInputHeight].join('_');
if (this.styles_[cacheKey]) return this.styles_[cacheKey]; if (this.styles_[cacheKey]) return this.styles_[cacheKey];
this.styles_ = {}; this.styles_ = {};
@ -458,8 +483,6 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
paddingBottom: 10, // Added for iOS (Not needed for Android??) paddingBottom: 10, // Added for iOS (Not needed for Android??)
}; };
if (this.state.HACK_webviewLoadingState === 1) styles.titleTextInput.marginTop = 1;
this.styles_[cacheKey] = StyleSheet.create(styles); this.styles_[cacheKey] = StyleSheet.create(styles);
return this.styles_[cacheKey]; return this.styles_[cacheKey];
} }
@ -502,6 +525,19 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
// has already been granted, it doesn't slow down opening the note. If it hasn't // has already been granted, it doesn't slow down opening the note. If it hasn't
// been granted, the popup will open anyway. // been granted, the popup will open anyway.
void this.requestGeoLocationPermissions(); void this.requestGeoLocationPermissions();
if (this.props.newNoteAttachFileAction) {
setTimeout(async () => {
if (this.props.newNoteAttachFileAction === AttachFileAction.AttachDrawing) {
await this.drawPicture_onPress();
} else {
const options: AttachFileOptions = {
action: this.props.newNoteAttachFileAction,
};
await CommandService.instance().execute('attachFile', '', options);
}
}, 100);
}
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
@ -574,6 +610,9 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
// It cannot theoretically be undefined, since componentDidMount should always be called before // It cannot theoretically be undefined, since componentDidMount should always be called before
// componentWillUnmount, but with React Native the impossible often becomes possible. // componentWillUnmount, but with React Native the impossible often becomes possible.
if (this.undoRedoService_) this.undoRedoService_.off('stackChange', this.undoRedoService_stackChange); if (this.undoRedoService_) this.undoRedoService_.off('stackChange', this.undoRedoService_stackChange);
this.commandRegistration_?.deregister();
this.commandRegistration_ = null;
} }
private title_changeText(text: string) { private title_changeText(text: string) {
@ -636,11 +675,6 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
await shared.saveOneProperty(this, name, value); await shared.saveOneProperty(this, name, value);
} }
private async pickDocuments() {
const result = await pickDocument({ multiple: true });
return result;
}
public async resizeImage(localFilePath: string, targetPath: string, mimeType: string) { public async resizeImage(localFilePath: string, targetPath: string, mimeType: string) {
const maxSize = Resource.IMAGE_MAX_DIMENSION; const maxSize = Resource.IMAGE_MAX_DIMENSION;
const dimensions = await getImageDimensions(localFilePath); const dimensions = await getImageDimensions(localFilePath);
@ -720,7 +754,10 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
return newNote; return newNote;
} }
public async attachFile(pickerResponse: Asset, fileType: string): Promise<ResourceEntity|null> { public async attachFile(
pickerResponse: PickerResponse,
fileType: string,
): Promise<ResourceEntity|null> {
if (!pickerResponse) { if (!pickerResponse) {
// User has cancelled // User has cancelled
return null; return null;
@ -802,36 +839,6 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
return resource; return resource;
} }
private async attachPhoto_onPress() {
// the selection Limit should be specified. I think 200 is enough?
const response: ImagePickerResponse = await launchImageLibrary({ mediaType: 'photo', includeBase64: false, selectionLimit: 200 });
if (response.errorCode) {
reg.logger().warn('Got error from picker', response.errorCode);
return;
}
if (response.didCancel) {
reg.logger().info('User cancelled picker');
return;
}
for (const asset of response.assets) {
await this.attachFile(asset, 'image');
}
}
private async takePhoto_onPress() {
if (Platform.OS === 'web') {
const response = await pickDocument({ multiple: true, preferCamera: true });
for (const asset of response) {
await this.attachFile(asset, 'image');
}
} else {
this.setState({ showCamera: true });
}
}
private cameraView_onPhoto(data: CameraResult) { private cameraView_onPhoto(data: CameraResult) {
void this.attachFile( void this.attachFile(
data, data,
@ -935,25 +942,12 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
} }
}; };
private async attachFile_onPress() {
const response = await this.pickDocuments();
for (const asset of response) {
await this.attachFile(asset, 'all');
}
}
private toggleIsTodo_onPress() { private toggleIsTodo_onPress() {
shared.toggleIsTodo_onPress(this); shared.toggleIsTodo_onPress(this);
this.scheduleSave(); this.scheduleSave();
} }
private tags_onPress() {
if (!this.state.note || !this.state.note.id) return;
this.setState({ noteTagDialogShown: true });
}
private async share_onPress() { private async share_onPress() {
await Share.share({ await Share.share({
message: `${this.state.note.title}\n\n${this.state.note.body}`, message: `${this.state.note.title}\n\n${this.state.note.body}`,
@ -1059,37 +1053,8 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
return output; return output;
} }
public async showAttachMenu() {
// If the keyboard is editing a WebView, the standard Keyboard.dismiss()
// may not work. As such, we also need to call hideKeyboard on the editorRef
this.editorRef.current?.hideKeyboard();
const buttons = [];
// On iOS, it will show "local files", which means certain files saved from the browser
// and the iCloud files, but it doesn't include photos and images from the CameraRoll
//
// On Android, it will depend on the phone, but usually it will allow browsing all files and photos.
buttons.push({ text: _('Attach file'), id: 'attachFile' });
// Disabled on Android because it doesn't work due to permission issues, but enabled on iOS
// because that's only way to browse photos from the camera roll.
if (Platform.OS === 'ios') buttons.push({ text: _('Attach photo'), id: 'attachPhoto' });
buttons.push({ text: _('Take photo'), id: 'takePhoto' });
const buttonId = await this.props.dialogs.showMenu(_('Choose an option'), buttons);
if (buttonId === 'takePhoto') await this.takePhoto_onPress();
if (buttonId === 'attachFile') await this.attachFile_onPress();
if (buttonId === 'attachPhoto') await this.attachPhoto_onPress();
}
public onAttach = async (filePath?: string) => { public onAttach = async (filePath?: string) => {
if (filePath) { await CommandService.instance().execute('attachFile', filePath);
await this.attachFile({ uri: filePath }, 'all');
} else {
await this.showAttachMenu();
}
}; };
// private vosk_:Vosk; // private vosk_:Vosk;
@ -1183,7 +1148,7 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
if (canAttachPicture) { if (canAttachPicture) {
output.push({ output.push({
title: _('Attach...'), title: _('Attach...'),
onPress: () => this.showAttachMenu(), onPress: () => this.onAttach(),
disabled: readOnly, disabled: readOnly,
}); });
} }
@ -1227,14 +1192,25 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
}); });
} }
if (isSaved && !isDeleted) { const commandService = CommandService.instance();
const whenContext = commandService.currentWhenClauseContext();
const addButtonFromCommand = (commandName: string, title?: string) => {
if (commandName === '-') {
output.push({ isDivider: true });
} else {
output.push({ output.push({
title: _('Tags'), title: title ?? commandService.description(commandName),
onPress: () => { onPress: async () => {
this.tags_onPress(); void commandService.execute(commandName);
}, },
disabled: !commandService.isEnabled(commandName, whenContext),
}); });
} }
};
if (isSaved && !isDeleted) {
addButtonFromCommand('setTags');
}
output.push({ output.push({
title: isTodo ? _('Convert to note') : _('Convert to todo'), title: isTodo ? _('Convert to note') : _('Convert to todo'),
@ -1283,22 +1259,6 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
}); });
} }
const commandService = CommandService.instance();
const whenContext = commandService.currentWhenClauseContext();
const addButtonFromCommand = (commandName: string, title?: string) => {
if (commandName === '-') {
output.push({ isDivider: true });
} else {
output.push({
title: title ?? commandService.description(commandName),
onPress: async () => {
void commandService.execute(commandName);
},
disabled: !commandService.isEnabled(commandName, whenContext),
});
}
};
if (whenContext.inTrash) { if (whenContext.inTrash) {
addButtonFromCommand('permanentlyDeleteNote'); addButtonFromCommand('permanentlyDeleteNote');
} else { } else {
@ -1349,6 +1309,11 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
done = true; done = true;
} }
if (!this.noteEditorVisible()) {
logger.info(`Note editor is not visible - not setting focus on ${fieldToFocus}`);
done = true;
}
if (done) { if (done) {
shim.clearInterval(this.focusUpdateIID_); shim.clearInterval(this.focusUpdateIID_);
this.focusUpdateIID_ = null; this.focusUpdateIID_ = null;
@ -1401,15 +1366,6 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
return this.folderPickerOptions_; return this.folderPickerOptions_;
} }
public onBodyViewerLoadEnd() {
shim.setTimeout(() => {
this.setState({ HACK_webviewLoadingState: 1 });
shim.setTimeout(() => {
this.setState({ HACK_webviewLoadingState: 0 });
}, 50);
}, 5);
}
private onBodyViewerScroll = (scrollTop: number) => { private onBodyViewerScroll = (scrollTop: number) => {
this.lastBodyScroll = scrollTop; this.lastBodyScroll = scrollTop;
}; };
@ -1439,7 +1395,17 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
this.setState({ voiceTypingDialogShown: false }); this.setState({ voiceTypingDialogShown: false });
} }
private noteEditorVisible() {
return !this.state.showCamera && !this.state.showImageEditor;
}
public render() { public render() {
// Commands must be registered before child components can render.
// Calling this in the constructor won't work in strict mode, where
// componentWillUnmount (which removes the commands) can be called
// multiple times.
this.registerCommands();
if (this.state.isLoading) { if (this.state.isLoading) {
return ( return (
<View style={this.styles().screen}> <View style={this.styles().screen}>
@ -1492,7 +1458,6 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
onCheckboxChange={this.onBodyViewerCheckboxChange} onCheckboxChange={this.onBodyViewerCheckboxChange}
onMarkForDownload={this.onMarkForDownload} onMarkForDownload={this.onMarkForDownload}
onRequestEditResource={this.onEditResource} onRequestEditResource={this.onEditResource}
onLoadEnd={this.onBodyViewerLoadEnd}
onScroll={this.onBodyViewerScroll} onScroll={this.onBodyViewerScroll}
initialScroll={this.lastBodyScroll} initialScroll={this.lastBodyScroll}
pluginStates={this.props.plugins} pluginStates={this.props.plugins}
@ -1650,19 +1615,9 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
// which can cause some bugs where previously set state to another note would interfere // which can cause some bugs where previously set state to another note would interfere
// how the new note should be rendered // how the new note should be rendered
const NoteScreenWrapper = (props: Props) => { const NoteScreenWrapper = (props: Props) => {
const lastNonNullNoteIdRef = useRef(props.noteId);
if (props.noteId) {
lastNonNullNoteIdRef.current = props.noteId;
}
// This keeps the current note open even if it's no longer present in selectedNoteIds.
// This might happen, for example, if the selected note is moved to an unselected
// folder.
const noteId = lastNonNullNoteIdRef.current;
const dialogs = useContext(DialogContext); const dialogs = useContext(DialogContext);
return ( return (
<NoteScreenComponent key={noteId} dialogs={dialogs} {...props} /> <NoteScreenComponent key={props.noteId} dialogs={dialogs} {...props} />
); );
}; };
@ -1670,6 +1625,7 @@ const NoteScreen = connect((state: AppState) => {
return { return {
noteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null, noteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null,
noteHash: state.selectedNoteHash, noteHash: state.selectedNoteHash,
newNoteAttachFileAction: state.newNoteAttachFileAction,
itemType: state.selectedItemType, itemType: state.selectedItemType,
folders: state.folders, folders: state.folders,
searchQuery: state.searchQuery, searchQuery: state.searchQuery,

View File

@ -0,0 +1,109 @@
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
import { _ } from '@joplin/lib/locale';
import { CommandRuntimeProps } from '../types';
import { Platform } from 'react-native';
import pickDocument from '../../../../utils/pickDocument';
import { ImagePickerResponse, launchImageLibrary } from 'react-native-image-picker';
import Logger from '@joplin/utils/Logger';
const logger = Logger.create('attachFile');
export enum AttachFileAction {
TakePhoto = 'takePhoto',
AttachFile = 'attachFile',
AttachPhoto = 'attachPhoto',
AttachDrawing = 'attachDrawing',
}
export interface AttachFileOptions {
action?: AttachFileAction | null;
}
export const declaration: CommandDeclaration = {
name: 'attachFile',
label: () => _('Attach file'),
iconName: 'material attachment',
};
export const runtime = (props: CommandRuntimeProps): CommandRuntime => {
const takePhoto = async () => {
if (Platform.OS === 'web') {
const response = await pickDocument({ multiple: true, preferCamera: true });
for (const asset of response) {
await props.attachFile(asset, 'image');
}
} else {
props.setCameraVisible(true);
}
};
const attachFile = async () => {
const response = await pickDocument({ multiple: true });
for (const asset of response) {
await props.attachFile(asset, 'all');
}
};
const attachPhoto = async () => {
// the selection Limit should be specified. I think 200 is enough?
const response: ImagePickerResponse = await launchImageLibrary({ mediaType: 'photo', includeBase64: false, selectionLimit: 200 });
if (response.errorCode) {
logger.warn('Got error from picker', response.errorCode);
return;
}
if (response.didCancel) {
logger.info('User cancelled picker');
return;
}
for (const asset of response.assets) {
await props.attachFile(asset, 'image');
}
};
const showAttachMenu = async (action: AttachFileAction = null) => {
props.hideKeyboard();
let buttonId: AttachFileAction = null;
if (action) {
buttonId = action;
} else {
const buttons = [];
// On iOS, it will show "local files", which means certain files saved from the browser
// and the iCloud files, but it doesn't include photos and images from the CameraRoll
//
// On Android, it will depend on the phone, but usually it will allow browsing all files and photos.
buttons.push({ text: _('Attach file'), id: AttachFileAction.AttachFile });
// Disabled on Android because it doesn't work due to permission issues, but enabled on iOS
// because that's only way to browse photos from the camera roll.
if (Platform.OS === 'ios') buttons.push({ text: _('Attach photo'), id: AttachFileAction.AttachPhoto });
buttons.push({ text: _('Take photo'), id: AttachFileAction.TakePhoto });
buttonId = await props.dialogs.showMenu(_('Choose an option'), buttons) as AttachFileAction;
}
if (buttonId === AttachFileAction.TakePhoto) await takePhoto();
if (buttonId === AttachFileAction.AttachFile) await attachFile();
if (buttonId === AttachFileAction.AttachPhoto) await attachPhoto();
};
return {
execute: async (_context: CommandContext, filePath?: string, options: AttachFileOptions = null) => {
options = {
action: null,
...options,
};
if (filePath) {
await props.attachFile({ uri: filePath }, 'all');
} else {
await showAttachMenu(options.action);
}
},
enabledCondition: '!noteIsReadOnly',
};
};

View File

@ -0,0 +1,18 @@
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
import { _ } from '@joplin/lib/locale';
import { CommandRuntimeProps } from '../types';
export const declaration: CommandDeclaration = {
name: 'hideKeyboard',
label: () => _('Hide keyboard'),
iconName: 'material keyboard-close',
};
export const runtime = (props: CommandRuntimeProps): CommandRuntime => {
return {
execute: async (_context: CommandContext) => {
props.hideKeyboard();
},
enabledCondition: 'keyboardVisible',
};
};

View File

@ -0,0 +1,15 @@
// AUTO-GENERATED using `gulp buildScriptIndexes`
import * as attachFile from './attachFile';
import * as hideKeyboard from './hideKeyboard';
import * as setTags from './setTags';
import * as toggleVisiblePanes from './toggleVisiblePanes';
const index: any[] = [
attachFile,
hideKeyboard,
setTags,
toggleVisiblePanes,
];
export default index;
// AUTO-GENERATED using `gulp buildScriptIndexes`

View File

@ -0,0 +1,19 @@
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
import { _ } from '@joplin/lib/locale';
import { CommandRuntimeProps } from '../types';
export const declaration: CommandDeclaration = {
name: 'setTags',
label: () => _('Tags'),
iconName: 'material tag-multiple',
};
export const runtime = (props: CommandRuntimeProps): CommandRuntime => {
return {
execute: async (_context: CommandContext) => {
props.setTagDialogVisible(true);
},
enabledCondition: '!noteIsReadOnly',
};
};

View File

@ -0,0 +1,18 @@
import { CommandContext, CommandDeclaration, CommandRuntime } from '@joplin/lib/services/CommandService';
import { CommandRuntimeProps } from '../types';
export const declaration: CommandDeclaration = {
// For compatibility with the desktop app, this command is called "toggleVisiblePanes".
name: 'toggleVisiblePanes',
label: () => 'Start/stop editing',
};
export const runtime = (props: CommandRuntimeProps): CommandRuntime => {
return {
execute: async (_context: CommandContext) => {
// For now, the only two "panes" on mobile are view and edit.
const newMode = props.getMode() === 'edit' ? 'view' : 'edit';
props.setMode(newMode);
},
};
};

View File

@ -0,0 +1,22 @@
import { ResourceEntity } from '@joplin/lib/services/database/types';
import { DialogControl } from '../../DialogManager';
export interface PickerResponse {
uri?: string;
type?: string;
fileName?: string;
}
export type EditorMode = 'view'|'edit';
export interface CommandRuntimeProps {
attachFile(pickerResponse: PickerResponse, fileType: string): Promise<ResourceEntity|null>;
hideKeyboard(): void;
insertText(text: string): void;
getMode(): EditorMode;
setMode(mode: EditorMode): void;
setCameraVisible(visible: boolean): void;
setTagDialogVisible(visible: boolean): void;
dialogs: DialogControl;
}

View File

@ -35,6 +35,7 @@ interface Props {
showCompletedTodos: boolean; showCompletedTodos: boolean;
noteSelectionEnabled: boolean; noteSelectionEnabled: boolean;
selectedNoteIds: string[];
activeFolderId: string; activeFolderId: string;
selectedFolderId: string; selectedFolderId: string;
selectedTagId: string; selectedTagId: string;

View File

@ -9,9 +9,10 @@ import time from '@joplin/lib/time';
import { decryptedStatText, enableEncryptionConfirmationMessages, onSavePasswordClick, useInputMasterPassword, useInputPasswords, usePasswordChecker, useStats } from '@joplin/lib/components/EncryptionConfigScreen/utils'; import { decryptedStatText, enableEncryptionConfirmationMessages, onSavePasswordClick, useInputMasterPassword, useInputPasswords, usePasswordChecker, useStats } from '@joplin/lib/components/EncryptionConfigScreen/utils';
import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types'; import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types';
import { State } from '@joplin/lib/reducer'; import { State } from '@joplin/lib/reducer';
import { SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils'; import { masterKeyEnabled, SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils';
import { getDefaultMasterKey, setupAndDisableEncryption, toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils'; import { getDefaultMasterKey, setupAndDisableEncryption, toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { Divider, List } from 'react-native-paper';
import shim from '@joplin/lib/shim'; import shim from '@joplin/lib/shim';
interface Props { interface Props {
@ -34,8 +35,10 @@ const EncryptionConfigScreen = (props: Props) => {
const { passwordChecks, masterPasswordKeys } = usePasswordChecker(props.masterKeys, props.activeMasterKeyId, props.masterPassword, props.passwords); const { passwordChecks, masterPasswordKeys } = usePasswordChecker(props.masterKeys, props.activeMasterKeyId, props.masterPassword, props.passwords);
const { inputPasswords, onInputPasswordChange } = useInputPasswords(props.passwords); const { inputPasswords, onInputPasswordChange } = useInputPasswords(props.passwords);
const { inputMasterPassword, onMasterPasswordSave, onMasterPasswordChange } = useInputMasterPassword(props.masterKeys, props.activeMasterKeyId); const { inputMasterPassword, onMasterPasswordSave, onMasterPasswordChange } = useInputMasterPassword(props.masterKeys, props.activeMasterKeyId);
const [showDisabledKeys, setShowDisabledKeys] = useState(false);
const mkComps = []; const mkComps = [];
const disabledMkComps = [];
const nonExistingMasterKeyIds = props.notLoadedMasterKeys.slice(); const nonExistingMasterKeyIds = props.notLoadedMasterKeys.slice();
@ -78,6 +81,10 @@ const EncryptionConfigScreen = (props: Props) => {
flex: 1, flex: 1,
padding: theme.margin, padding: theme.margin,
}, },
disabledContainer: {
paddingLeft: theme.margin,
paddingRight: theme.margin,
},
}; };
return StyleSheet.create(styles); return StyleSheet.create(styles);
@ -85,7 +92,7 @@ const EncryptionConfigScreen = (props: Props) => {
const decryptedItemsInfo = props.encryptionEnabled ? <Text style={styles.normalText}>{decryptedStatText(stats)}</Text> : null; const decryptedItemsInfo = props.encryptionEnabled ? <Text style={styles.normalText}>{decryptedStatText(stats)}</Text> : null;
const renderMasterKey = (_num: number, mk: MasterKeyEntity) => { const renderMasterKey = (mk: MasterKeyEntity) => {
const theme = themeStyle(props.themeId); const theme = themeStyle(props.themeId);
const password = inputPasswords[mk.id] ? inputPasswords[mk.id] : ''; const password = inputPasswords[mk.id] ? inputPasswords[mk.id] : '';
@ -226,16 +233,19 @@ const EncryptionConfigScreen = (props: Props) => {
} }
}; };
for (let i = 0; i < props.masterKeys.filter(mk => masterKeyEnabled(mk)).length; i++) {
for (let i = 0; i < props.masterKeys.length; i++) {
const mk = props.masterKeys[i]; const mk = props.masterKeys[i];
mkComps.push(renderMasterKey(i + 1, mk)); mkComps.push(renderMasterKey(mk));
const idx = nonExistingMasterKeyIds.indexOf(mk.id); const idx = nonExistingMasterKeyIds.indexOf(mk.id);
if (idx >= 0) nonExistingMasterKeyIds.splice(idx, 1); if (idx >= 0) nonExistingMasterKeyIds.splice(idx, 1);
} }
for (let i = 0; i < props.masterKeys.filter(mk => !masterKeyEnabled(mk)).length; i++) {
const mk = props.masterKeys[i];
disabledMkComps.push(renderMasterKey(mk));
}
const onToggleButtonClick = async () => { const onToggleButtonClick = async () => {
if (props.encryptionEnabled) { if (props.encryptionEnabled) {
const ok = await shim.showConfirmationDialog(_('Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?')); const ok = await shim.showConfirmationDialog(_('Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?'));
@ -286,8 +296,8 @@ const EncryptionConfigScreen = (props: Props) => {
return ( return (
<View style={rootStyle}> <View style={rootStyle}>
<ScreenHeader title={_('Encryption Config')} /> <ScreenHeader title={_('Encryption Config')} />
<ScrollView style={styles.container}> <ScrollView>
{ <View style={styles.container}>
<View style={{ backgroundColor: theme.warningBackgroundColor, paddingTop: 5, paddingBottom: 5, paddingLeft: 10, paddingRight: 10 }}> <View style={{ backgroundColor: theme.warningBackgroundColor, paddingTop: 5, paddingBottom: 5, paddingLeft: 10, paddingRight: 10 }}>
<Text>{_('For more information about End-To-End Encryption (E2EE) and advice on how to enable it please check the documentation:')}</Text> <Text>{_('For more information about End-To-End Encryption (E2EE) and advice on how to enable it please check the documentation:')}</Text>
<TouchableOpacity <TouchableOpacity
@ -298,7 +308,6 @@ const EncryptionConfigScreen = (props: Props) => {
<Text>https://joplinapp.org/help/apps/sync/e2ee</Text> <Text>https://joplinapp.org/help/apps/sync/e2ee</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
}
<Text style={styles.titleText}>{_('Status')}</Text> <Text style={styles.titleText}>{_('Status')}</Text>
<Text style={styles.normalText}>{_('Encryption is: %s', props.encryptionEnabled ? _('Enabled') : _('Disabled'))}</Text> <Text style={styles.normalText}>{_('Encryption is: %s', props.encryptionEnabled ? _('Enabled') : _('Disabled'))}</Text>
@ -308,7 +317,18 @@ const EncryptionConfigScreen = (props: Props) => {
{passwordPromptComp} {passwordPromptComp}
{mkComps} {mkComps}
{nonExistingMasterKeySection} {nonExistingMasterKeySection}
<View style={{ flex: 1, height: 20 }}></View> </View>
<Divider />
<List.Accordion
title={_('Disabled keys')}
titleStyle={styles.titleText}
expanded={showDisabledKeys}
onPress={() => setShowDisabledKeys(st => !st)}
>
<View style={styles.disabledContainer}>
{disabledMkComps}
</View>
</List.Accordion>
</ScrollView> </ScrollView>
</View> </View>
); );

View File

@ -10,16 +10,16 @@
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; }; 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; };
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
46E31F54C547C341F605BB66 /* libPods-Joplin.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A5E1CD825FABD6C4E704EA54 /* libPods-Joplin.a */; };
4C036D13E81D8DB9640B0DC1 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF14612B39CE1556A9A31631 /* ExpoModulesProvider.swift */; }; 4C036D13E81D8DB9640B0DC1 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF14612B39CE1556A9A31631 /* ExpoModulesProvider.swift */; };
4D122473270878D700DE23E8 /* wtf.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D122472270878D700DE23E8 /* wtf.swift */; }; 4D122473270878D700DE23E8 /* wtf.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D122472270878D700DE23E8 /* wtf.swift */; };
5E556FC75AECECB13464A724 /* libPods-ShareExtension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FAC957496DFD2368FFE3C360 /* libPods-ShareExtension.a */; }; 57317DBFCCF429AEF0A019CB /* libPods-ShareExtension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C9F257EEF9EAC998DCD8BDEC /* libPods-ShareExtension.a */; };
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
AE152142260F770400217DCB /* ShareViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = AE152141260F770400217DCB /* ShareViewController.m */; }; AE152142260F770400217DCB /* ShareViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = AE152141260F770400217DCB /* ShareViewController.m */; };
AE82E4AF2599FA3A0013551B /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AE82E4AD2599FA3A0013551B /* MainInterface.storyboard */; }; AE82E4AF2599FA3A0013551B /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AE82E4AD2599FA3A0013551B /* MainInterface.storyboard */; };
AE82E4B32599FA3A0013551B /* ShareExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = AE82E4A82599FA3A0013551B /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; AE82E4B32599FA3A0013551B /* ShareExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = AE82E4A82599FA3A0013551B /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
BAD33BAD2BE9A08300E9F46A /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = BAD33BAC2BE9A08300E9F46A /* PrivacyInfo.xcprivacy */; }; BAD33BAD2BE9A08300E9F46A /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = BAD33BAC2BE9A08300E9F46A /* PrivacyInfo.xcprivacy */; };
BAD33BAE2BE9A08300E9F46A /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = BAD33BAC2BE9A08300E9F46A /* PrivacyInfo.xcprivacy */; }; BAD33BAE2BE9A08300E9F46A /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = BAD33BAC2BE9A08300E9F46A /* PrivacyInfo.xcprivacy */; };
D1BE6938B03F0BC60F98041F /* libPods-Joplin.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 800581C1ADC9CA9A7AC1BB75 /* libPods-Joplin.a */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@ -50,27 +50,19 @@
008F07F21AC5B25A0029DE68 /* main.jsbundle */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = main.jsbundle; sourceTree = "<group>"; }; 008F07F21AC5B25A0029DE68 /* main.jsbundle */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = main.jsbundle; sourceTree = "<group>"; };
00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
00E356F21AD99517003FC87E /* JoplinTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JoplinTests.m; sourceTree = "<group>"; }; 00E356F21AD99517003FC87E /* JoplinTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JoplinTests.m; sourceTree = "<group>"; };
0473A2D469A3555053E69327 /* Pods-ShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.release.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.release.xcconfig"; sourceTree = "<group>"; };
09056573D4C040FBD5FEB93A /* Pods-Joplin-JoplinTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Joplin-JoplinTests.debug.xcconfig"; path = "Target Support Files/Pods-Joplin-JoplinTests/Pods-Joplin-JoplinTests.debug.xcconfig"; sourceTree = "<group>"; };
13B07F961A680F5B00A75B9A /* Joplin.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Joplin.app; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07F961A680F5B00A75B9A /* Joplin.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Joplin.app; sourceTree = BUILT_PRODUCTS_DIR; };
13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = Joplin/AppDelegate.h; sourceTree = "<group>"; }; 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = Joplin/AppDelegate.h; sourceTree = "<group>"; };
13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.mm; path = Joplin/AppDelegate.mm; sourceTree = "<group>"; }; 13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.mm; path = Joplin/AppDelegate.mm; sourceTree = "<group>"; };
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Joplin/Images.xcassets; sourceTree = "<group>"; }; 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Joplin/Images.xcassets; sourceTree = "<group>"; };
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Joplin/Info.plist; sourceTree = "<group>"; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Joplin/Info.plist; sourceTree = "<group>"; };
13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Joplin/main.m; sourceTree = "<group>"; }; 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Joplin/main.m; sourceTree = "<group>"; };
14868D674CC065C7BF1C9944 /* Pods-Joplin.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Joplin.release.xcconfig"; path = "Target Support Files/Pods-Joplin/Pods-Joplin.release.xcconfig"; sourceTree = "<group>"; }; 4281BC1941ED8712A952DC60 /* Pods-ShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.debug.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.debug.xcconfig"; sourceTree = "<group>"; };
245A6EBAE2E874DB706B16DB /* Pods-Joplin-tvOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Joplin-tvOS.release.xcconfig"; path = "Target Support Files/Pods-Joplin-tvOS/Pods-Joplin-tvOS.release.xcconfig"; sourceTree = "<group>"; };
258F823D616BE3D6A52BC900 /* libPods-Joplin-tvOSTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Joplin-tvOSTests.a"; sourceTree = BUILT_PRODUCTS_DIR; };
2C91CD1424C7137D07789148 /* Pods-Joplin-JoplinTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Joplin-JoplinTests.release.xcconfig"; path = "Target Support Files/Pods-Joplin-JoplinTests/Pods-Joplin-JoplinTests.release.xcconfig"; sourceTree = "<group>"; };
2DA44D9A347489A29B995F73 /* Pods-Joplin-tvOSTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Joplin-tvOSTests.debug.xcconfig"; path = "Target Support Files/Pods-Joplin-tvOSTests/Pods-Joplin-tvOSTests.debug.xcconfig"; sourceTree = "<group>"; };
37DBC181C4AD99CBE0D07EEB /* Pods-Joplin-tvOSTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Joplin-tvOSTests.release.xcconfig"; path = "Target Support Files/Pods-Joplin-tvOSTests/Pods-Joplin-tvOSTests.release.xcconfig"; sourceTree = "<group>"; };
4D122471270878D600DE23E8 /* Joplin-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Joplin-Bridging-Header.h"; sourceTree = "<group>"; }; 4D122471270878D600DE23E8 /* Joplin-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Joplin-Bridging-Header.h"; sourceTree = "<group>"; };
4D122472270878D700DE23E8 /* wtf.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = wtf.swift; sourceTree = "<group>"; }; 4D122472270878D700DE23E8 /* wtf.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = wtf.swift; sourceTree = "<group>"; };
505CB61090817F4453631957 /* Pods-Joplin.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Joplin.debug.xcconfig"; path = "Target Support Files/Pods-Joplin/Pods-Joplin.debug.xcconfig"; sourceTree = "<group>"; }; 78131A70125DE0AE4D6BF72E /* Pods-Joplin.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Joplin.debug.xcconfig"; path = "Target Support Files/Pods-Joplin/Pods-Joplin.debug.xcconfig"; sourceTree = "<group>"; };
5DE39012F71F18423C665C57 /* Pods-ShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.debug.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.debug.xcconfig"; sourceTree = "<group>"; }; 800581C1ADC9CA9A7AC1BB75 /* libPods-Joplin.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Joplin.a"; sourceTree = BUILT_PRODUCTS_DIR; };
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = Joplin/LaunchScreen.storyboard; sourceTree = "<group>"; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = Joplin/LaunchScreen.storyboard; sourceTree = "<group>"; };
A3FEB746EE7F1B0FF28528E1 /* Pods-Joplin-tvOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Joplin-tvOS.debug.xcconfig"; path = "Target Support Files/Pods-Joplin-tvOS/Pods-Joplin-tvOS.debug.xcconfig"; sourceTree = "<group>"; }; 85EA9E81E151B08B5FFD2044 /* Pods-Joplin.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Joplin.release.xcconfig"; path = "Target Support Files/Pods-Joplin/Pods-Joplin.release.xcconfig"; sourceTree = "<group>"; };
A5E1CD825FABD6C4E704EA54 /* libPods-Joplin.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Joplin.a"; sourceTree = BUILT_PRODUCTS_DIR; };
AE152140260F770400217DCB /* ShareViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ShareViewController.h; path = Source/ShareExtension/ShareViewController.h; sourceTree = "<group>"; }; AE152140260F770400217DCB /* ShareViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ShareViewController.h; path = Source/ShareExtension/ShareViewController.h; sourceTree = "<group>"; };
AE152141260F770400217DCB /* ShareViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ShareViewController.m; path = Source/ShareExtension/ShareViewController.m; sourceTree = "<group>"; }; AE152141260F770400217DCB /* ShareViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ShareViewController.m; path = Source/ShareExtension/ShareViewController.m; sourceTree = "<group>"; };
AE7945DB259C9A2500051BE2 /* ShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ShareExtension.entitlements; sourceTree = "<group>"; }; AE7945DB259C9A2500051BE2 /* ShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ShareExtension.entitlements; sourceTree = "<group>"; };
@ -78,13 +70,12 @@
AE82E4A82599FA3A0013551B /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; AE82E4A82599FA3A0013551B /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
AE82E4AE2599FA3A0013551B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; }; AE82E4AE2599FA3A0013551B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
AE82E4B02599FA3A0013551B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; AE82E4B02599FA3A0013551B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
B61798F36B3BC123BF8EA4D9 /* libPods-Joplin-tvOS.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Joplin-tvOS.a"; sourceTree = BUILT_PRODUCTS_DIR; };
BAD33BAC2BE9A08300E9F46A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; }; BAD33BAC2BE9A08300E9F46A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
BEC9B285DDBCEBB2D7A812DE /* Pods-ShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.release.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.release.xcconfig"; sourceTree = "<group>"; };
C9F257EEF9EAC998DCD8BDEC /* libPods-ShareExtension.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ShareExtension.a"; sourceTree = BUILT_PRODUCTS_DIR; };
CF14612B39CE1556A9A31631 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-Joplin/ExpoModulesProvider.swift"; sourceTree = "<group>"; }; CF14612B39CE1556A9A31631 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-Joplin/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
ED2971642150620600B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS12.0.sdk/System/Library/Frameworks/JavaScriptCore.framework; sourceTree = DEVELOPER_DIR; }; ED2971642150620600B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS12.0.sdk/System/Library/Frameworks/JavaScriptCore.framework; sourceTree = DEVELOPER_DIR; };
F69B873C692CE22F1C4C9264 /* libPods-Joplin-JoplinTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Joplin-JoplinTests.a"; sourceTree = BUILT_PRODUCTS_DIR; };
FAC957496DFD2368FFE3C360 /* libPods-ShareExtension.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ShareExtension.a"; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -92,7 +83,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
46E31F54C547C341F605BB66 /* libPods-Joplin.a in Frameworks */, D1BE6938B03F0BC60F98041F /* libPods-Joplin.a in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -100,7 +91,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
5E556FC75AECECB13464A724 /* libPods-ShareExtension.a in Frameworks */, 57317DBFCCF429AEF0A019CB /* libPods-ShareExtension.a in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -146,11 +137,8 @@
children = ( children = (
ED297162215061F000B7C4FE /* JavaScriptCore.framework */, ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
ED2971642150620600B7C4FE /* JavaScriptCore.framework */, ED2971642150620600B7C4FE /* JavaScriptCore.framework */,
B61798F36B3BC123BF8EA4D9 /* libPods-Joplin-tvOS.a */, 800581C1ADC9CA9A7AC1BB75 /* libPods-Joplin.a */,
258F823D616BE3D6A52BC900 /* libPods-Joplin-tvOSTests.a */, C9F257EEF9EAC998DCD8BDEC /* libPods-ShareExtension.a */,
A5E1CD825FABD6C4E704EA54 /* libPods-Joplin.a */,
F69B873C692CE22F1C4C9264 /* libPods-Joplin-JoplinTests.a */,
FAC957496DFD2368FFE3C360 /* libPods-ShareExtension.a */,
); );
name = Frameworks; name = Frameworks;
sourceTree = "<group>"; sourceTree = "<group>";
@ -200,16 +188,10 @@
9CDB1D9DB6483D893504BFCB /* Pods */ = { 9CDB1D9DB6483D893504BFCB /* Pods */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
505CB61090817F4453631957 /* Pods-Joplin.debug.xcconfig */, 78131A70125DE0AE4D6BF72E /* Pods-Joplin.debug.xcconfig */,
14868D674CC065C7BF1C9944 /* Pods-Joplin.release.xcconfig */, 85EA9E81E151B08B5FFD2044 /* Pods-Joplin.release.xcconfig */,
09056573D4C040FBD5FEB93A /* Pods-Joplin-JoplinTests.debug.xcconfig */, 4281BC1941ED8712A952DC60 /* Pods-ShareExtension.debug.xcconfig */,
2C91CD1424C7137D07789148 /* Pods-Joplin-JoplinTests.release.xcconfig */, BEC9B285DDBCEBB2D7A812DE /* Pods-ShareExtension.release.xcconfig */,
A3FEB746EE7F1B0FF28528E1 /* Pods-Joplin-tvOS.debug.xcconfig */,
245A6EBAE2E874DB706B16DB /* Pods-Joplin-tvOS.release.xcconfig */,
2DA44D9A347489A29B995F73 /* Pods-Joplin-tvOSTests.debug.xcconfig */,
37DBC181C4AD99CBE0D07EEB /* Pods-Joplin-tvOSTests.release.xcconfig */,
5DE39012F71F18423C665C57 /* Pods-ShareExtension.debug.xcconfig */,
0473A2D469A3555053E69327 /* Pods-ShareExtension.release.xcconfig */,
); );
path = Pods; path = Pods;
sourceTree = "<group>"; sourceTree = "<group>";
@ -241,15 +223,15 @@
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Joplin" */; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Joplin" */;
buildPhases = ( buildPhases = (
335ACF4DE85695BEBB18D8A3 /* [CP] Check Pods Manifest.lock */, E76772393095384E60C0D95F /* [CP] Check Pods Manifest.lock */,
EB61CD887618E406C80EBC43 /* [Expo] Configure project */, EB61CD887618E406C80EBC43 /* [Expo] Configure project */,
13B07F871A680F5B00A75B9A /* Sources */, 13B07F871A680F5B00A75B9A /* Sources */,
13B07F8C1A680F5B00A75B9A /* Frameworks */, 13B07F8C1A680F5B00A75B9A /* Frameworks */,
13B07F8E1A680F5B00A75B9A /* Resources */, 13B07F8E1A680F5B00A75B9A /* Resources */,
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
CBC8354E4CF5CF4E15F2FCDE /* [CP] Copy Pods Resources */,
AE82E4B42599FA3A0013551B /* Embed App Extensions */, AE82E4B42599FA3A0013551B /* Embed App Extensions */,
C8F2067658ACF12DF7A17513 /* [CP] Embed Pods Frameworks */, E6849A86F6866D5E0DC90A55 /* [CP] Embed Pods Frameworks */,
63951A9079A5D0302FB331B7 /* [CP] Copy Pods Resources */,
); );
buildRules = ( buildRules = (
); );
@ -265,7 +247,7 @@
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = AE82E4B72599FA3A0013551B /* Build configuration list for PBXNativeTarget "ShareExtension" */; buildConfigurationList = AE82E4B72599FA3A0013551B /* Build configuration list for PBXNativeTarget "ShareExtension" */;
buildPhases = ( buildPhases = (
027E2AA6B101F8CFCA582EC1 /* [CP] Check Pods Manifest.lock */, 8B87E13D9DB3F38C8DC6F227 /* [CP] Check Pods Manifest.lock */,
AE82E4A42599FA3A0013551B /* Sources */, AE82E4A42599FA3A0013551B /* Sources */,
AE82E4A52599FA3A0013551B /* Frameworks */, AE82E4A52599FA3A0013551B /* Frameworks */,
AE82E4A62599FA3A0013551B /* Resources */, AE82E4A62599FA3A0013551B /* Resources */,
@ -355,71 +337,7 @@
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "set -e\n\nWITH_ENVIRONMENT=\"$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"$REACT_NATIVE_PATH/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $REACT_NATIVE_XCODE\"\n"; shellScript = "set -e\n\nWITH_ENVIRONMENT=\"$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"$REACT_NATIVE_PATH/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $REACT_NATIVE_XCODE\"\n";
}; };
027E2AA6B101F8CFCA582EC1 /* [CP] Check Pods Manifest.lock */ = { 63951A9079A5D0302FB331B7 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-ShareExtension-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
335ACF4DE85695BEBB18D8A3 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Joplin-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
C8F2067658ACF12DF7A17513 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Joplin/Pods-Joplin-frameworks.sh",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL-Universal/OpenSSL.framework/OpenSSL",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes",
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OpenSSL.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Joplin/Pods-Joplin-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
CBC8354E4CF5CF4E15F2FCDE /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
@ -483,6 +401,70 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Joplin/Pods-Joplin-resources.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Joplin/Pods-Joplin-resources.sh\"\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
8B87E13D9DB3F38C8DC6F227 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-ShareExtension-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
E6849A86F6866D5E0DC90A55 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Joplin/Pods-Joplin-frameworks.sh",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL-Universal/OpenSSL.framework/OpenSSL",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes",
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OpenSSL.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Joplin/Pods-Joplin-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
E76772393095384E60C0D95F /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Joplin-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
EB61CD887618E406C80EBC43 /* [Expo] Configure project */ = { EB61CD887618E406C80EBC43 /* [Expo] Configure project */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1; alwaysOutOfDate = 1;
@ -548,18 +530,18 @@
/* Begin XCBuildConfiguration section */ /* Begin XCBuildConfiguration section */
13B07F941A680F5B00A75B9A /* Debug */ = { 13B07F941A680F5B00A75B9A /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 505CB61090817F4453631957 /* Pods-Joplin.debug.xcconfig */; baseConfigurationReference = 78131A70125DE0AE4D6BF72E /* Pods-Joplin.debug.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements; CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
CURRENT_PROJECT_VERSION = 128; CURRENT_PROJECT_VERSION = 130;
DEVELOPMENT_TEAM = A9BXAFS6CT; DEVELOPMENT_TEAM = A9BXAFS6CT;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Joplin/Info.plist; INFOPLIST_FILE = Joplin/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.4; IPHONEOS_DEPLOYMENT_TARGET = 13.4;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 13.2.0; MARKETING_VERSION = 13.2.2;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
"-ObjC", "-ObjC",
@ -580,17 +562,17 @@
}; };
13B07F951A680F5B00A75B9A /* Release */ = { 13B07F951A680F5B00A75B9A /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 14868D674CC065C7BF1C9944 /* Pods-Joplin.release.xcconfig */; baseConfigurationReference = 85EA9E81E151B08B5FFD2044 /* Pods-Joplin.release.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements; CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
CURRENT_PROJECT_VERSION = 128; CURRENT_PROJECT_VERSION = 130;
DEVELOPMENT_TEAM = A9BXAFS6CT; DEVELOPMENT_TEAM = A9BXAFS6CT;
INFOPLIST_FILE = Joplin/Info.plist; INFOPLIST_FILE = Joplin/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.4; IPHONEOS_DEPLOYMENT_TARGET = 13.4;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 13.2.0; MARKETING_VERSION = 13.2.2;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
"-ObjC", "-ObjC",
@ -765,7 +747,7 @@
}; };
AE82E4B52599FA3A0013551B /* Debug */ = { AE82E4B52599FA3A0013551B /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 5DE39012F71F18423C665C57 /* Pods-ShareExtension.debug.xcconfig */; baseConfigurationReference = 4281BC1941ED8712A952DC60 /* Pods-ShareExtension.debug.xcconfig */;
buildSettings = { buildSettings = {
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
@ -776,14 +758,14 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 128; CURRENT_PROJECT_VERSION = 130;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = A9BXAFS6CT; DEVELOPMENT_TEAM = A9BXAFS6CT;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_FILE = ShareExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.4; IPHONEOS_DEPLOYMENT_TARGET = 13.4;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 13.2.0; MARKETING_VERSION = 13.2.2;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
@ -803,7 +785,7 @@
}; };
AE82E4B62599FA3A0013551B /* Release */ = { AE82E4B62599FA3A0013551B /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 0473A2D469A3555053E69327 /* Pods-ShareExtension.release.xcconfig */; baseConfigurationReference = BEC9B285DDBCEBB2D7A812DE /* Pods-ShareExtension.release.xcconfig */;
buildSettings = { buildSettings = {
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
@ -815,14 +797,14 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 128; CURRENT_PROJECT_VERSION = 130;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = A9BXAFS6CT; DEVELOPMENT_TEAM = A9BXAFS6CT;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_FILE = ShareExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.4; IPHONEOS_DEPLOYMENT_TARGET = 13.4;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 13.2.0; MARKETING_VERSION = 13.2.2;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",

View File

@ -1,116 +1,37 @@
{ {
"images": [ "images" : [
{ {
"filename": "ios_marketing1024x1024.png", "filename" : "ios_marketing1024x1024.png",
"idiom": "ios-marketing", "idiom" : "universal",
"size": "1024x1024", "platform" : "ios",
"scale": "1x" "size" : "1024x1024"
}, },
{ {
"filename": "iphone_notification20x20@2x.png", "appearances" : [
"idiom": "iphone",
"size": "20x20",
"scale": "2x"
},
{ {
"filename": "iphone_notification20x20@3x.png", "appearance" : "luminosity",
"idiom": "iphone", "value" : "dark"
"size": "20x20",
"scale": "3x"
},
{
"filename": "iphone_settings29x29@2x.png",
"idiom": "iphone",
"size": "29x29",
"scale": "2x"
},
{
"filename": "iphone_settings29x29@3x.png",
"idiom": "iphone",
"size": "29x29",
"scale": "3x"
},
{
"filename": "iphone_spotlight40x40@2x.png",
"idiom": "iphone",
"size": "40x40",
"scale": "2x"
},
{
"filename": "iphone_spotlight40x40@3x.png",
"idiom": "iphone",
"size": "40x40",
"scale": "3x"
},
{
"filename": "iphone_app60x60@2x.png",
"idiom": "iphone",
"size": "60x60",
"scale": "2x"
},
{
"filename": "iphone_app60x60@3x.png",
"idiom": "iphone",
"size": "60x60",
"scale": "3x"
},
{
"filename": "ipad_notification20x20.png",
"idiom": "ipad",
"size": "20x20",
"scale": "1x"
},
{
"filename": "ipad_notification20x20@2x.png",
"idiom": "ipad",
"size": "20x20",
"scale": "2x"
},
{
"filename": "ipad_settings29x29.png",
"idiom": "ipad",
"size": "29x29",
"scale": "1x"
},
{
"filename": "ipad_settings29x29@2x.png",
"idiom": "ipad",
"size": "29x29",
"scale": "2x"
},
{
"filename": "ipad_spotlight40x40.png",
"idiom": "ipad",
"size": "40x40",
"scale": "1x"
},
{
"filename": "ipad_spotlight40x40@2x.png",
"idiom": "ipad",
"size": "40x40",
"scale": "2x"
},
{
"filename": "ipad_app76x76.png",
"idiom": "ipad",
"size": "76x76",
"scale": "1x"
},
{
"filename": "ipad_app76x76@2x.png",
"idiom": "ipad",
"size": "76x76",
"scale": "2x"
},
{
"filename": "ipad_pro_app83.5x83.5@2x.png",
"idiom": "ipad",
"size": "83.5x83.5",
"scale": "2x"
} }
], ],
"info": { "filename" : "ios_marketing_dark1024x1024.png",
"version": 1, "idiom" : "universal",
"author": "xcode" "platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
} }
} }

View File

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 973 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Some files were not shown because too many files have changed in this diff Show More