1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-09-02 20:46:21 +02:00

Compare commits

..

1 Commits

Author SHA1 Message Date
Helmut K. C. Tessarek
417a3b05b9 ci: improve workflow
1. Run tests, iff the other prerequisites are met
2. In case of a translation PR, skip tests not related to translations
2024-12-03 16:27:56 -05:00
129 changed files with 1534 additions and 2068 deletions

View File

@@ -596,16 +596,6 @@ packages/app-mobile/components/DialogManager/types.js
packages/app-mobile/components/DismissibleDialog.js
packages/app-mobile/components/Dropdown.test.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.js
packages/app-mobile/components/ExtendedWebView/index.web.js
@@ -644,6 +634,18 @@ 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/watchEditorForTemplateChanges.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.js
packages/app-mobile/components/NoteEditor/SearchPanel.js
@@ -651,6 +653,7 @@ packages/app-mobile/components/NoteEditor/commandDeclarations.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.js
packages/app-mobile/components/NoteEditor/hooks/useKeyboardVisible.js
packages/app-mobile/components/NoteEditor/types.js
packages/app-mobile/components/NoteItem.js
packages/app-mobile/components/NoteList.js
@@ -667,7 +670,6 @@ packages/app-mobile/components/SelectDateTimeDialog.js
packages/app-mobile/components/SideMenu.js
packages/app-mobile/components/SideMenuContentNote.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/AccessibleView.js
packages/app-mobile/components/app-nav.js
@@ -751,13 +753,8 @@ packages/app-mobile/components/screens/ConfigScreen/plugins/utils/useUpdateState
packages/app-mobile/components/screens/ConfigScreen/types.js
packages/app-mobile/components/screens/JoplinCloudLoginScreen.js
packages/app-mobile/components/screens/LogScreen.js
packages/app-mobile/components/screens/Note/Note.test.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/types.js
packages/app-mobile/components/screens/Note.test.js
packages/app-mobile/components/screens/Note.js
packages/app-mobile/components/screens/NoteTagsDialog.js
packages/app-mobile/components/screens/Notes.js
packages/app-mobile/components/screens/SearchScreen/SearchResults.js
@@ -781,7 +778,6 @@ packages/app-mobile/services/AlarmServiceDriver.android.js
packages/app-mobile/services/AlarmServiceDriver.ios.js
packages/app-mobile/services/AlarmServiceDriver.web.js
packages/app-mobile/services/BackButtonService.js
packages/app-mobile/services/commands/stateToWhenClauseContext.js
packages/app-mobile/services/e2ee/RSA.react-native.js
packages/app-mobile/services/e2ee/crypto.js
packages/app-mobile/services/plugins/PlatformImplementation.js
@@ -823,7 +819,6 @@ packages/app-mobile/utils/fs-driver/testUtil/createFilesFromPathRecord.js
packages/app-mobile/utils/fs-driver/testUtil/verifyDirectoryMatches.js
packages/app-mobile/utils/getPackageInfo.js
packages/app-mobile/utils/getVersionInfoText.js
packages/app-mobile/utils/hooks/useKeyboardVisible.js
packages/app-mobile/utils/hooks/useOnLongPressProps.js
packages/app-mobile/utils/hooks/useReduceMotionEnabled.js
packages/app-mobile/utils/image/fileToImage.web.js
@@ -848,7 +843,6 @@ packages/app-mobile/utils/shim-init-react/shimInitShared.js
packages/app-mobile/utils/testing/createMockReduxStore.js
packages/app-mobile/utils/testing/getWebViewDomById.js
packages/app-mobile/utils/testing/getWebViewWindowById.js
packages/app-mobile/utils/testing/setupGlobalStore.js
packages/app-mobile/utils/types.js
packages/app-mobile/web/serviceWorker.js
packages/default-plugins/build.js
@@ -921,7 +915,6 @@ packages/editor/CodeMirror/utils/formatting/toggleSelectedLinesStartWith.js
packages/editor/CodeMirror/utils/formatting/types.js
packages/editor/CodeMirror/utils/getSearchState.js
packages/editor/CodeMirror/utils/growSelectionToNode.js
packages/editor/CodeMirror/utils/handleLinkEditRequests.js
packages/editor/CodeMirror/utils/handlePasteEvent.js
packages/editor/CodeMirror/utils/isCursorAtBeginning.js
packages/editor/CodeMirror/utils/isInSyntaxNode.js
@@ -1202,8 +1195,6 @@ packages/lib/services/interop/InteropService_Importer_Raw.js
packages/lib/services/interop/Module.test.js
packages/lib/services/interop/Module.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/joplinServer/personalizedUserContentBaseUrl.js
packages/lib/services/keychain/KeychainService.test.js

View File

@@ -55,6 +55,7 @@ echo "GIT_TAG_NAME=$GIT_TAG_NAME"
echo "BUILD_SEQUENCIAL=$BUILD_SEQUENCIAL"
echo "SERVER_REPOSITORY=$SERVER_REPOSITORY"
echo "SERVER_TAG_PREFIX=$SERVER_TAG_PREFIX"
echo "PR_TITLE=$PR_TITLE"
echo "IS_CONTINUOUS_INTEGRATION=$IS_CONTINUOUS_INTEGRATION"
echo "IS_PULL_REQUEST=$IS_PULL_REQUEST"
@@ -81,40 +82,6 @@ if [ $testResult -ne 0 ]; then
exit $testResult
fi
# =============================================================================
# Run test units
# =============================================================================
if [ "$RUN_TESTS" == "1" ]; then
echo "Step: Running tests..."
# On Linux, we run the Joplin Server tests using PostgreSQL
if [ "$IS_LINUX" == "1" ]; then
echo "Running Joplin Server tests using PostgreSQL..."
sudo docker compose --file docker-compose.db-dev.yml up -d
cmdResult=$?
if [ $cmdResult -ne 0 ]; then
exit $cmdResult
fi
export JOPLIN_TESTS_SERVER_DB=pg
else
echo "Running Joplin Server tests using SQLite..."
fi
# Need this because we're getting this error:
#
# @joplin/lib: FATAL ERROR: Ineffective mark-compacts near heap limit
# Allocation failed - JavaScript heap out of memory
#
# https://stackoverflow.com/questions/38558989
export NODE_OPTIONS="--max-old-space-size=32768"
yarn test-ci
testResult=$?
if [ $testResult -ne 0 ]; then
exit $testResult
fi
fi
# =============================================================================
# Run linter for pull requests only. We also don't want this to make the desktop
# release randomly fail.
@@ -172,6 +139,23 @@ if [ "$RUN_TESTS" == "1" ]; then
fi
fi
# =============================================================================
# Check whether this is a translation PR. There is no need to run the entire
# pipeline in such a case, thus exit early.
# =============================================================================
if [ "$RUN_TESTS" == "1" ]; then
# Due to the ancient bash release in macOS, the following is required, instead
# of using ${var,,}
PR_TITLE=$(echo $PR_TITLE |tr '[:upper:]' '[:lower:]')
echo "Step: Checking for translation PR..."
if [[ "$PR_TITLE" =~ ^.*(translation|(add|fix|update) .*language|\.po)( .*)?$ ]]; then
echo "It is a translation PR. Exit early."
exit 0
fi
fi
# =============================================================================
# Check .gitignore and .eslintignore files - they should be updated when
# new TypeScript files are added by running `yarn updateIgnored`.
@@ -185,7 +169,7 @@ if [ "$IS_LINUX" == "1" ]; then
# so that checkIgnoredFiles works.
git restore .gitignore .eslintignore
node packages/tools/checkIgnoredFiles.js
node packages/tools/checkIgnoredFiles.js
testResult=$?
if [ $testResult -ne 0 ]; then
exit $testResult
@@ -224,6 +208,40 @@ if [ "$IS_LINUX" == "1" ]; then
fi
fi
# =============================================================================
# Run test units
# =============================================================================
if [ "$RUN_TESTS" == "1" ]; then
echo "Step: Running tests..."
# On Linux, we run the Joplin Server tests using PostgreSQL
if [ "$IS_LINUX" == "1" ]; then
echo "Running Joplin Server tests using PostgreSQL..."
sudo docker compose --file docker-compose.db-dev.yml up -d
cmdResult=$?
if [ $cmdResult -ne 0 ]; then
exit $cmdResult
fi
export JOPLIN_TESTS_SERVER_DB=pg
else
echo "Running Joplin Server tests using SQLite..."
fi
# Need this because we're getting this error:
#
# @joplin/lib: FATAL ERROR: Ineffective mark-compacts near heap limit
# Allocation failed - JavaScript heap out of memory
#
# https://stackoverflow.com/questions/38558989
export NODE_OPTIONS="--max-old-space-size=32768"
yarn test-ci
testResult=$?
if [ $testResult -ne 0 ]; then
exit $testResult
fi
fi
# =============================================================================
# Find out if we should run the build or not. Electron-builder gets stuck when
# building PRs so we disable it in this case. The Linux build should provide
@@ -257,7 +275,7 @@ if [ "$IS_DESKTOP_RELEASE" == "1" ]; then
if [ "$IS_MACOS" == "1" ]; then
# This is to fix this error:
#
#
# Exit code: ENOENT. spawn /usr/bin/python ENOENT
#
# Ref: https://github.com/electron-userland/electron-builder/issues/6767#issuecomment-1096589528
@@ -273,14 +291,14 @@ if [ "$IS_DESKTOP_RELEASE" == "1" ]; then
USE_HARD_LINKS=false yarn dist
else
USE_HARD_LINKS=false yarn dist
fi
fi
elif [[ $IS_LINUX = 1 ]] && [ "$IS_SERVER_RELEASE" == "1" ]; then
echo "Step: Building Docker Image..."
cd "$ROOT_DIR"
yarn buildServerDocker --tag-name $GIT_TAG_NAME --push-images --repository $SERVER_REPOSITORY
else
echo "Step: Building but *not* publishing desktop application..."
if [ "$IS_MACOS" == "1" ]; then
# See above why we need to specify Python
alias python=$(which python3)
@@ -290,7 +308,7 @@ else
# https://www.electron.build/code-signing#how-to-disable-code-signing-during-the-build-process-on-macos
export CSC_IDENTITY_AUTO_DISCOVERY=false
npm pkg set 'build.mac.identity'=null --json
USE_HARD_LINKS=false yarn dist --publish=never
else
USE_HARD_LINKS=false yarn dist --publish=never

View File

@@ -81,7 +81,7 @@ jobs:
run: |
# https://yarnpkg.com/getting-started/install
corepack enable
# Login to Docker only if we're on a server release tag. If we run this on
# a pull request it will fail because the PR doesn't have access to
# secrets
@@ -115,6 +115,7 @@ jobs:
SERVER_REPOSITORY: joplin/server
SERVER_TAG_PREFIX: server
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
"${GITHUB_WORKSPACE}/.github/scripts/run_ci.sh"
@@ -189,7 +190,7 @@ jobs:
# Basic test to ensure that the created build is valid. It should exit with
# code 0 if it works.
docker run joplin/server:0.0.0-beta node dist/app.js migrate list
- name: Check HTTP request
run: |
# Need to pass environment variables:
@@ -201,23 +202,22 @@ jobs:
# Check if status code is correct
# if the actual_status DOES NOT include the expected_status
# it exits the process with code 1
expected_status="HTTP/1.1 200 OK"
actual_status=$(curl -I -X GET http://localhost:22300/api/ping | head -n 1)
if [[ ! "$actual_status" =~ "$expected_status" ]]; then
if [[ ! "$actual_status" =~ "$expected_status" ]]; then
echo 'Failed while checking the status code after request to /api/ping'
echo 'expected: ' $expected_status
echo 'actual: ' $actual_status
exit 1;
exit 1;
fi
# Check if the body response is correct
# if the actual_body is different of expected_body exit with code 1
expected_body='{"status":"ok","message":"Joplin Server is running"}'
actual_body=$(curl http://localhost:22300/api/ping)
if [[ "$actual_body" != "$expected_body" ]]; then
echo 'Failed while checking the body response after request to /api/ping'
exit 1;
fi

39
.gitignore vendored
View File

@@ -572,16 +572,6 @@ packages/app-mobile/components/DialogManager/types.js
packages/app-mobile/components/DismissibleDialog.js
packages/app-mobile/components/Dropdown.test.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.js
packages/app-mobile/components/ExtendedWebView/index.web.js
@@ -620,6 +610,18 @@ 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/watchEditorForTemplateChanges.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.js
packages/app-mobile/components/NoteEditor/SearchPanel.js
@@ -627,6 +629,7 @@ packages/app-mobile/components/NoteEditor/commandDeclarations.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.js
packages/app-mobile/components/NoteEditor/hooks/useKeyboardVisible.js
packages/app-mobile/components/NoteEditor/types.js
packages/app-mobile/components/NoteItem.js
packages/app-mobile/components/NoteList.js
@@ -643,7 +646,6 @@ packages/app-mobile/components/SelectDateTimeDialog.js
packages/app-mobile/components/SideMenu.js
packages/app-mobile/components/SideMenuContentNote.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/AccessibleView.js
packages/app-mobile/components/app-nav.js
@@ -727,13 +729,8 @@ packages/app-mobile/components/screens/ConfigScreen/plugins/utils/useUpdateState
packages/app-mobile/components/screens/ConfigScreen/types.js
packages/app-mobile/components/screens/JoplinCloudLoginScreen.js
packages/app-mobile/components/screens/LogScreen.js
packages/app-mobile/components/screens/Note/Note.test.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/types.js
packages/app-mobile/components/screens/Note.test.js
packages/app-mobile/components/screens/Note.js
packages/app-mobile/components/screens/NoteTagsDialog.js
packages/app-mobile/components/screens/Notes.js
packages/app-mobile/components/screens/SearchScreen/SearchResults.js
@@ -757,7 +754,6 @@ packages/app-mobile/services/AlarmServiceDriver.android.js
packages/app-mobile/services/AlarmServiceDriver.ios.js
packages/app-mobile/services/AlarmServiceDriver.web.js
packages/app-mobile/services/BackButtonService.js
packages/app-mobile/services/commands/stateToWhenClauseContext.js
packages/app-mobile/services/e2ee/RSA.react-native.js
packages/app-mobile/services/e2ee/crypto.js
packages/app-mobile/services/plugins/PlatformImplementation.js
@@ -799,7 +795,6 @@ packages/app-mobile/utils/fs-driver/testUtil/createFilesFromPathRecord.js
packages/app-mobile/utils/fs-driver/testUtil/verifyDirectoryMatches.js
packages/app-mobile/utils/getPackageInfo.js
packages/app-mobile/utils/getVersionInfoText.js
packages/app-mobile/utils/hooks/useKeyboardVisible.js
packages/app-mobile/utils/hooks/useOnLongPressProps.js
packages/app-mobile/utils/hooks/useReduceMotionEnabled.js
packages/app-mobile/utils/image/fileToImage.web.js
@@ -824,7 +819,6 @@ packages/app-mobile/utils/shim-init-react/shimInitShared.js
packages/app-mobile/utils/testing/createMockReduxStore.js
packages/app-mobile/utils/testing/getWebViewDomById.js
packages/app-mobile/utils/testing/getWebViewWindowById.js
packages/app-mobile/utils/testing/setupGlobalStore.js
packages/app-mobile/utils/types.js
packages/app-mobile/web/serviceWorker.js
packages/default-plugins/build.js
@@ -897,7 +891,6 @@ packages/editor/CodeMirror/utils/formatting/toggleSelectedLinesStartWith.js
packages/editor/CodeMirror/utils/formatting/types.js
packages/editor/CodeMirror/utils/getSearchState.js
packages/editor/CodeMirror/utils/growSelectionToNode.js
packages/editor/CodeMirror/utils/handleLinkEditRequests.js
packages/editor/CodeMirror/utils/handlePasteEvent.js
packages/editor/CodeMirror/utils/isCursorAtBeginning.js
packages/editor/CodeMirror/utils/isInSyntaxNode.js
@@ -1178,8 +1171,6 @@ packages/lib/services/interop/InteropService_Importer_Raw.js
packages/lib/services/interop/Module.test.js
packages/lib/services/interop/Module.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/joplinServer/personalizedUserContentBaseUrl.js
packages/lib/services/keychain/KeychainService.test.js

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

View File

@@ -1,21 +1,4 @@
<?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>
<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>
<ul>
<li>
<p><strong>Making content easier to perceive</strong>: We’ll make sure there are text alternatives for non-text content, provide captions for multimedia, and check that colour contrast is strong enough for easy reading.</p>
</li>
<li>
<p><strong>Improving operability</strong>: Every feature will be accessible through the keyboard, ensuring that users who can't rely on a mouse can still navigate easily.</p>
</li>
<li>
<p><strong>Enhancing comprehension</strong>: We’ll ensure that the text is readable, the layout is predictable, and input assistance is available for those who need it.</p>
</li>
</ul>
<p>We’ll also use ARIA standards and test for compatibility with assistive technologies to make sure everything works well with tools like screen readers.</p>
<p>To ensure we're on the right track, we’ll invite users with disabilities to test the app and help us identify any remaining barriers. Accessibility is something we’ll keep working on, so we'll continue testing, refining, and making improvements to ensure Joplin stays accessible to everyone. It’s a journey, and we’re committed to making the experience better for all our users.</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20241207-wcag2.jpg" alt="WCAG 2 logo"></p>
]]></description><link>https://joplinapp.org/news/20241207-project-2-wcag2</link><guid isPermaLink="false">20241207-project-2-wcag2</guid><pubDate>Sat, 07 Dec 2024 00:00:00 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[Project 1: Making voice typing even better for everyone]]></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 improvements to voice typing:</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>Thu, 28 Nov 2024 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Thu, 28 Nov 2024 00:00:00 GMT</pubDate><item><title><![CDATA[Project 1: Making voice typing even better for everyone]]></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 improvements to voice typing:</p>
<p>Right now, voice input works pretty well, but there’s one thing that still needs improvement: punctuation. Our current system is accurate, but it doesn’t pick up on things like commas or periods, which can make spoken text harder to understand. That’s why we’re looking into other voice recognition engines that can handle punctuation better while keeping the accuracy we need.</p>
<p>Improving voice input like this isn’t just about convenience – it’s about making our app more accessible to everyone. For people with visual impairments, being able to use voice input means they don’t have to rely on typing. Those with motor difficulties can control the app without needing to use their hands. And for users with dyslexia or other learning challenges, voice input can be much easier and more natural than typing out words.</p>
<p>We’re excited to experiment with this new feature, knowing it could make a real difference for people who rely on voice technology to get things done.</p>
@@ -399,4 +382,20 @@ sys 0m38.013s</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>
]]></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><item><title><![CDATA[Joplin 2.8 is available!]]></title><description><![CDATA[<p>As always a lot of changes and new features in this new version available on both desktop and mobile.</p>
<h2>Multiple profile support<a name="multiple-profile-support" href="#multiple-profile-support" class="heading-anchor">🔗</a></h2>
<p>Perhaps the most visible change in this version is the support for multiple profiles. You can now create as many application profile as you wish, each with their own settings, and easily switch from one to another. The main use case is to support for example a &quot;work&quot; profile and a &quot;personal&quot; profile, to allow you to keep things independent, and each profile can sync with a different sync target.</p>
<p>To create a new profile, open <strong>File &gt; Switch profile</strong> and select <strong>Create new profile</strong>, enter the profile name and press OK. The app will automatically switch to this new profile, which you can now configure.</p>
<p>To switch back to the previous profile, again open <strong>File &gt; Switch profile</strong> and select <strong>Default</strong>.</p>
<p>Note that profiles all share certain settings, such as language, font size, theme, etc. This is done so that you don't have reconfigure every details when switching profiles. Other settings such as sync configuration is per profile.</p>
<p>The feature is available on desktop only for now, and should be ported to mobile relatively soon.</p>
<h2>Save Mermaid graph as PNG/SVG<a name="save-mermaid-graph-as-png-svg" href="#save-mermaid-graph-as-png-svg" class="heading-anchor">🔗</a></h2>
<p>This convenient feature allows exporting a Mermaid graph as a PNG or SVG image, or allows copying the image as a DataUrl, which can then be pasted in any compatible text editor. Thanks Asrient for implementing this!</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20220606-mermaid-as-png.png" alt=""></p>
<h2>Publish a mini-website using Joplin Cloud<a name="publish-a-mini-website-using-joplin-cloud" href="#publish-a-mini-website-using-joplin-cloud" class="heading-anchor">🔗</a></h2>
<p>Joplin Cloud now supports publishing a note &quot;recursively&quot;, which means the notes and all the notes it is linked to. This allows easily publishing a simple website made of multiples and images.</p>
<p>To make use of this feature, simply select <strong>Also publish linked notes</strong> when publishing a note.</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20220606-publish-website.png" alt=""></p>
<h2>And more!<a name="and-more" href="#and-more" class="heading-anchor">🔗</a></h2>
<p>In total there are 38 changes to improve the app reliability, security and usability. Full changelog is at <a href="https://joplinapp.org/help/about/changelog/desktop">https://joplinapp.org/help/about/changelog/desktop</a></p>
]]></description><link>https://joplinapp.org/news/20220606-release-2-8</link><guid isPermaLink="false">20220606-release-2-8</guid><pubDate>Mon, 06 Jun 2022 00:00:00 GMT</pubDate><twitter-text></twitter-text></item></channel></rss>

View File

@@ -408,8 +408,6 @@ class Application extends BaseApplication {
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
// so execute it.
if (argv.length) {

View File

@@ -165,14 +165,18 @@ export default class FolderListWidget extends ListWidget {
const wasSelectedItemId = this.selectedJoplinItemId;
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
let newItems: any[] = [];
const orderFolders = (parentId: string) => {
this.logger().info('PARENT', parentId);
for (let i = 0; i < this.folders.length; i++) {
const f = this.folders[i];
const originalParent = this.folders_.find(f => f.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) {
newItems.push(f);
if (this.folderHasChildren_(this.folders, f.id)) orderFolders(f.id);

View File

@@ -22,6 +22,7 @@ const Setting = require('@joplin/lib/models/Setting').default;
const Revision = require('@joplin/lib/models/Revision').default;
const Logger = require('@joplin/utils/Logger').default;
const FsDriverNode = require('@joplin/lib/fs-driver-node').default;
const sharp = require('sharp');
const { shimInit } = require('@joplin/lib/shim-init-node.js');
const shim = require('@joplin/lib/shim').default;
const { _ } = require('@joplin/lib/locale');
@@ -31,14 +32,6 @@ const envFromArgs = require('@joplin/lib/envFromArgs');
const nodeSqlite = require('sqlite3');
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 fsDriver = new FsDriverNode();

View File

@@ -35,7 +35,7 @@
],
"owner": "Laurent Cozic"
},
"version": "3.2.2",
"version": "3.2.0",
"bin": "./main.js",
"engines": {
"node": ">=10.0.0"
@@ -72,7 +72,7 @@
"@joplin/tools": "~3.2",
"@types/fs-extra": "11.0.4",
"@types/jest": "29.5.12",
"@types/node": "18.19.50",
"@types/node": "18.19.48",
"@types/proper-lockfile": "^4.1.2",
"gulp": "4.0.2",
"jest": "29.7.0",

View File

@@ -99,11 +99,6 @@ export default class ElectronAppWrapper {
return null;
}
public allAppWindows() {
const allWindowIds = [...this.secondaryWindows_.keys(), defaultWindowId];
return allWindowIds.map(id => this.windowById(id));
}
public env() {
return this.env_;
}
@@ -363,9 +358,6 @@ export default class ElectronAppWrapper {
const electronWindowId = window?.id;
this.secondaryWindows_.set(windowId, { electronId: electronWindowId });
// Match the main window's zoom:
window.webContents.setZoomFactor(this.mainWindow().webContents.getZoomFactor());
window.once('close', () => {
this.secondaryWindows_.delete(windowId);

View File

@@ -29,7 +29,7 @@ import { reg } from '@joplin/lib/registry';
const packageInfo: PackageInfo = require('./packageInfo.js');
import DecryptionWorker from '@joplin/lib/services/DecryptionWorker';
import ClipperServer from '@joplin/lib/ClipperServer';
import { ipcRenderer } from 'electron';
import { ipcRenderer, webFrame } from 'electron';
const Menu = bridge().Menu;
const PluginManager = require('@joplin/lib/services/PluginManager');
import RevisionService from '@joplin/lib/services/RevisionService';
@@ -152,7 +152,7 @@ class Application extends BaseApplication {
}
if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'windowContentZoomFactor' || action.type === 'SETTING_UPDATE_ALL') {
bridge().setZoomFactor(Setting.value('windowContentZoomFactor') / 100);
webFrame.setZoomFactor(Setting.value('windowContentZoomFactor') / 100);
}
if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'linking.extraAllowedExtensions' || action.type === 'SETTING_UPDATE_ALL') {

View File

@@ -285,13 +285,6 @@ export class Bridge {
this.switchToWindow(defaultWindowId);
}
// zoom should be in the range [0..1]
public setZoomFactor(zoom: number) {
for (const window of this.electronWrapper_.allAppWindows()) {
window.webContents.setZoomFactor(zoom);
}
}
public showItemInFolder(fullPath: string) {
return require('electron').shell.showItemInFolder(toSystemSlashes(fullPath));
}

View File

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

View File

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

View File

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

View File

@@ -38,6 +38,7 @@ const incompatiblePluginIds = [
'ylc395.noteLinkSystem',
'outline',
'joplin.plugin.cmoptions',
'com.ckant.joplin-plugin-better-code-blocks',
// cSpell:enable
];

View File

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

View File

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

View File

@@ -121,7 +121,7 @@ const SlowSyncWarning = styled.div`
const syncTargetNames: string[] = [
'joplinCloud',
'dropbox',
'onedrive',
// 'onedrive',
'nextcloud',
'webdav',
'amazon_s3',

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
"version": "3.2.4",
"version": "3.2.3",
"description": "Joplin for Desktop",
"main": "main.js",
"private": true,
@@ -131,7 +131,7 @@
"@playwright/test": "1.45.3",
"@testing-library/react-hooks": "8.0.1",
"@types/jest": "29.5.12",
"@types/node": "18.19.50",
"@types/node": "18.19.48",
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"@types/react-redux": "7.1.33",

View File

@@ -52,9 +52,12 @@ export default class SpellCheckerServiceDriverNative extends SpellCheckerService
// If we pass an empty array, it disables spell checking
// https://github.com/electron/electron/issues/25228
if (effectiveLanguages.length === 0) {
this.session().setSpellCheckerLanguages([]);
return;
}
this.session().setSpellCheckerLanguages(effectiveLanguages);
this.session().setSpellCheckerEnabled(effectiveLanguages.length > 0);
logger.info(`Set effective languages to "${effectiveLanguages}"`);
}

View File

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

View File

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

View File

@@ -1,139 +0,0 @@
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

@@ -1,117 +0,0 @@
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

@@ -1,58 +0,0 @@
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

@@ -1,191 +0,0 @@
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

@@ -1,28 +0,0 @@
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

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

View File

@@ -1,54 +0,0 @@
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

@@ -1,40 +0,0 @@
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

@@ -1,30 +0,0 @@
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

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

View File

@@ -37,6 +37,10 @@ class ModalDialog extends React.Component<Props, State> {
this.styles_ = {};
const styles: Record<string, ViewStyle|TextStyle> = {
modalWrapper: {
flex: 1,
justifyContent: 'center',
},
modalContentWrapper: {
flex: 1,
flexDirection: 'column',
@@ -72,18 +76,20 @@ class ModalDialog extends React.Component<Props, State> {
const buttonBarEnabled = this.props.buttonBarEnabled !== false;
return (
<Modal transparent={true} visible={true} onRequestClose={() => {}} containerStyle={this.styles().modalContentWrapper}>
<Text style={this.styles().title}>{this.props.title}</Text>
<View style={this.styles().modalContentWrapper2}>{ContentComponent}</View>
<View style={this.styles().buttonRow}>
<View style={{ flex: 1 }}>
<Button disabled={!buttonBarEnabled} title={_('OK')} onPress={this.props.onOkPress}></Button>
<View style={this.styles().modalWrapper}>
<Modal transparent={true} visible={true} onRequestClose={() => {}} containerStyle={this.styles().modalContentWrapper}>
<Text style={this.styles().title}>{this.props.title}</Text>
<View style={this.styles().modalContentWrapper2}>{ContentComponent}</View>
<View style={this.styles().buttonRow}>
<View style={{ flex: 1 }}>
<Button disabled={!buttonBarEnabled} title={_('OK')} onPress={this.props.onOkPress}></Button>
</View>
<View style={{ flex: 1, marginLeft: 5 }}>
<Button disabled={!buttonBarEnabled} title={_('Cancel')} onPress={this.props.onCancelPress}></Button>
</View>
</View>
<View style={{ flex: 1, marginLeft: 5 }}>
<Button disabled={!buttonBarEnabled} title={_('Cancel')} onPress={this.props.onCancelPress}></Button>
</View>
</View>
</Modal>
</Modal>
</View>
);
}
}

View File

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

View File

@@ -0,0 +1,140 @@
// 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

@@ -0,0 +1,31 @@
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

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

View File

@@ -0,0 +1,124 @@
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

@@ -0,0 +1,74 @@
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

@@ -0,0 +1,134 @@
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

@@ -0,0 +1,83 @@
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

@@ -0,0 +1,34 @@
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

@@ -0,0 +1,67 @@
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

@@ -0,0 +1,63 @@
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

@@ -0,0 +1,38 @@
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

@@ -0,0 +1,56 @@
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,18 +7,10 @@ import '@testing-library/jest-native';
import NoteEditor from './NoteEditor';
import Setting from '@joplin/lib/models/Setting';
import { _ } from '@joplin/lib/locale';
import { MenuProvider } from 'react-native-popup-menu';
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
import commandDeclarations from './commandDeclarations';
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;
import CommandService from '@joplin/lib/services/CommandService';
describe('NoteEditor', () => {
beforeAll(() => {
@@ -32,19 +24,11 @@ describe('NoteEditor', () => {
// Required to use ExtendedWebView
await setupDatabaseAndSynchronizer(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 () => {
const wrappedNoteEditor = render(
<TestProviderStack store={store}>
<MenuProvider>
<NoteEditor
themeId={Setting.THEME_ARITIM_DARK}
initialText='Testing...'
@@ -57,7 +41,7 @@ describe('NoteEditor', () => {
onAttach={async ()=>{}}
plugins={{}}
/>
</TestProviderStack>,
</MenuProvider>,
);
// Maps from screen height to whether the markdown toolbar should be visible.
@@ -86,11 +70,11 @@ describe('NoteEditor', () => {
setRootHeight(height);
await waitFor(async () => {
const toolbarButton = await screen.queryByLabelText(_('Bold'));
const showMoreButton = await screen.queryByLabelText(_('Show more actions'));
if (visible) {
expect(toolbarButton).not.toBeNull();
expect(showMoreButton).not.toBeNull();
} else {
expect(toolbarButton).toBeNull();
expect(showMoreButton).toBeNull();
}
});
}

View File

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

View File

@@ -1,28 +1,14 @@
import { EditorCommandType } from '@joplin/editor/types';
import { _ } from '@joplin/lib/locale';
import { CommandDeclaration } from '@joplin/lib/services/CommandService';
export const enabledCondition = (_commandName: string) => {
const output = [
'!modalDialogVisible',
'!noteIsReadOnly',
];
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[] = [
{
name: 'insertText',
@@ -48,65 +34,6 @@ const declarations: CommandDeclaration[] = [
{
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;

View File

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

View File

@@ -42,12 +42,12 @@ const WebBetaButton: React.FC<Props> = props => {
iconStyle={props.iconStyle}
/>
<DismissibleDialog
heading={_('Beta')}
size={DialogSize.Small}
themeId={props.themeId}
visible={dialogVisible}
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>
<View style={feedbackContainerStyles}>
<LinkButton onPress={onLeaveFeedback}>{_('Give feedback')}</LinkButton>

View File

@@ -36,8 +36,7 @@ const PADDING_V = 10;
type OnPressCallback=()=> void;
export interface FolderPickerOptions {
visible: boolean;
disabled?: boolean;
enabled: boolean;
selectedFolderId?: string;
onValueChange?: OnValueChangedListener;
mustSelect?: boolean;
@@ -516,12 +515,10 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
});
}
const createTitleComponent = (hideableAfterTitleComponents: ReactElement) => {
const createTitleComponent = (disabled: boolean, hideableAfterTitleComponents: ReactElement) => {
const folderPickerOptions = this.props.folderPickerOptions;
if (folderPickerOptions && folderPickerOptions.visible) {
const hasSelectedNotes = this.props.selectedNoteIds.length > 0;
const disabled = this.props.folderPickerOptions.disabled ?? !hasSelectedNotes;
if (folderPickerOptions && folderPickerOptions.enabled) {
return (
<FolderPicker
themeId={themeId}
@@ -605,7 +602,7 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
{betaIconComp}
</>;
const titleComp = createTitleComponent(hideableRightComponents);
const titleComp = createTitleComponent(headerItemDisabled, hideableRightComponents);
const contextMenuStyle: ViewStyle = {
paddingTop: PADDING_V,

View File

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

View File

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

View File

@@ -8,10 +8,11 @@ import NoteScreen from './Note';
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 Note from '@joplin/lib/models/Note';
import { AppState } from '../../../utils/types';
import { AppState } from '../../utils/types';
import { Store } from 'redux';
import createMockReduxStore from '../../../utils/testing/createMockReduxStore';
import getWebViewDomById from '../../../utils/testing/getWebViewDomById';
import createMockReduxStore from '../../utils/testing/createMockReduxStore';
import initializeCommandService from '../../utils/initializeCommandService';
import getWebViewDomById from '../../utils/testing/getWebViewDomById';
import { NoteEntity } from '@joplin/lib/services/database/types';
import Folder from '@joplin/lib/models/Folder';
import BaseItem from '@joplin/lib/models/BaseItem';
@@ -21,12 +22,11 @@ import { getDisplayParentId } from '@joplin/lib/services/trash';
import { itemIsReadOnlySync, ItemSlice } from '@joplin/lib/models/utils/readOnly';
import { LayoutChangeEvent } from 'react-native';
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 Setting from '@joplin/lib/models/Setting';
import Resource from '@joplin/lib/models/Resource';
import TestProviderStack from '../../testing/TestProviderStack';
import setupGlobalStore from '../../../utils/testing/setupGlobalStore';
import TestProviderStack from '../testing/TestProviderStack';
interface WrapperProps {
}
@@ -138,7 +138,7 @@ describe('screens/Note', () => {
await switchClient(0);
store = createMockReduxStore();
setupGlobalStore(store);
initializeCommandService(store);
// In order for note changes to be saved, note-screen-shared requires
// that at least one folder exist.

View File

@@ -3,65 +3,66 @@ import uuid from '@joplin/lib/uuid';
import Setting from '@joplin/lib/models/Setting';
import shim from '@joplin/lib/shim';
import UndoRedoService from '@joplin/lib/services/UndoRedoService';
import NoteBodyViewer from '../../NoteBodyViewer/NoteBodyViewer';
import checkPermissions from '../../../utils/checkPermissions';
import NoteEditor from '../../NoteEditor/NoteEditor';
import NoteBodyViewer from '../NoteBodyViewer/NoteBodyViewer';
import checkPermissions from '../../utils/checkPermissions';
import NoteEditor from '../NoteEditor/NoteEditor';
import * as React from 'react';
import { Keyboard, View, TextInput, StyleSheet, Linking, Share, NativeSyntheticEvent } from 'react-native';
import { Platform, PermissionsAndroid } from 'react-native';
import { connect } from 'react-redux';
// const { MarkdownEditor } = require('@joplin/lib/../MarkdownEditor/index.js');
import Note from '@joplin/lib/models/Note';
import BaseItem from '@joplin/lib/models/BaseItem';
import Resource from '@joplin/lib/models/Resource';
import Folder from '@joplin/lib/models/Folder';
const Clipboard = require('@react-native-clipboard/clipboard').default;
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 { ModelType } from '@joplin/lib/BaseModel';
import FloatingActionButton from '../../buttons/FloatingActionButton';
import FloatingActionButton from '../buttons/FloatingActionButton';
const { fileExtension, safeFileExtension } = require('@joplin/lib/path-utils');
import * as mimeUtils from '@joplin/lib/mime-utils';
import ScreenHeader, { MenuOptionType } from '../../ScreenHeader';
import NoteTagsDialog from '../NoteTagsDialog';
import ScreenHeader, { MenuOptionType } from '../ScreenHeader';
import NoteTagsDialog from './NoteTagsDialog';
import time from '@joplin/lib/time';
import Checkbox from '../../Checkbox';
import Checkbox from '../Checkbox';
import { _, currentLocale } from '@joplin/lib/locale';
import { reg } from '@joplin/lib/registry';
import ResourceFetcher from '@joplin/lib/services/ResourceFetcher';
import { BaseScreenComponent } from '../../base-screen';
import { themeStyle, editorFont } from '../../global-style';
import { BaseScreenComponent } from '../base-screen';
import { themeStyle, editorFont } from '../global-style';
import shared, { BaseNoteScreenComponent, Props as BaseProps } from '@joplin/lib/components/shared/note-screen-shared';
import SelectDateTimeDialog from '../../SelectDateTimeDialog';
import ShareExtension from '../../../utils/ShareExtension.js';
import CameraView from '../../CameraView/CameraView';
import { Asset, ImagePickerResponse, launchImageLibrary } from 'react-native-image-picker';
import SelectDateTimeDialog from '../SelectDateTimeDialog';
import ShareExtension from '../../utils/ShareExtension.js';
import CameraView from '../CameraView/CameraView';
import { FolderEntity, NoteEntity, ResourceEntity } from '@joplin/lib/services/database/types';
import Logger from '@joplin/utils/Logger';
import ImageEditor from '../../NoteEditor/ImageEditor/ImageEditor';
import promptRestoreAutosave from '../../NoteEditor/ImageEditor/promptRestoreAutosave';
import isEditableResource from '../../NoteEditor/ImageEditor/isEditableResource';
import VoiceTypingDialog from '../../voiceTyping/VoiceTypingDialog';
import { isSupportedLanguage } from '../../../services/voiceTyping/vosk';
import ImageEditor from '../NoteEditor/ImageEditor/ImageEditor';
import promptRestoreAutosave from '../NoteEditor/ImageEditor/promptRestoreAutosave';
import isEditableResource from '../NoteEditor/ImageEditor/isEditableResource';
import VoiceTypingDialog from '../voiceTyping/VoiceTypingDialog';
import { isSupportedLanguage } from '../../services/voiceTyping/vosk';
import { ChangeEvent as EditorChangeEvent, SelectionRangeChangeEvent, UndoRedoDepthChangeEvent } from '@joplin/editor/events';
import { join } from 'path';
import { Dispatch } from 'redux';
import { RefObject, useContext, useRef } from 'react';
import { SelectionRange } from '../../NoteEditor/types';
import { RefObject, useContext } from 'react';
import { SelectionRange } from '../NoteEditor/types';
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 { getDisplayParentTitle } from '@joplin/lib/services/trash';
import { PluginStates, utils as pluginUtils } from '@joplin/lib/services/plugins/reducer';
import debounce from '../../../utils/debounce';
import pickDocument from '../../utils/pickDocument';
import debounce from '../../utils/debounce';
import { focus } from '@joplin/lib/utils/focusHandler';
import CommandService, { RegisteredRuntime } from '@joplin/lib/services/CommandService';
import { ResourceInfo } from '../../NoteBodyViewer/hooks/useRerenderHandler';
import getImageDimensions from '../../../utils/image/getImageDimensions';
import resizeImage from '../../../utils/image/resizeImage';
import { CameraResult } from '../../CameraView/types';
import { DialogContext, DialogControl } from '../../DialogManager';
import { CommandRuntimeProps, PickerResponse } from './types';
import commands from './commands';
import CommandService from '@joplin/lib/services/CommandService';
import { ResourceInfo } from '../NoteBodyViewer/hooks/useRerenderHandler';
import getImageDimensions from '../../utils/image/getImageDimensions';
import resizeImage from '../../utils/image/resizeImage';
import { CameraResult } from '../CameraView/types';
import { DialogContext, DialogControl } from '../DialogManager';
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const emptyArray: any[] = [];
@@ -149,7 +150,6 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
private folderPickerOptions_: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
public dialogbox: any;
private commandRegistration_: RegisteredRuntime|null = null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
public static navigationOptions(): any {
@@ -292,6 +292,7 @@ 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_onCancel = this.cameraView_onCancel.bind(this);
this.properties_onPress = this.properties_onPress.bind(this);
@@ -314,38 +315,6 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
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 });
},
},
commands,
true,
);
}
private useEditorBeta(): boolean {
return this.props.useEditorBeta;
}
@@ -605,9 +574,6 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
// It cannot theoretically be undefined, since componentDidMount should always be called before
// componentWillUnmount, but with React Native the impossible often becomes possible.
if (this.undoRedoService_) this.undoRedoService_.off('stackChange', this.undoRedoService_stackChange);
this.commandRegistration_?.deregister();
this.commandRegistration_ = null;
}
private title_changeText(text: string) {
@@ -670,6 +636,11 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
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) {
const maxSize = Resource.IMAGE_MAX_DIMENSION;
const dimensions = await getImageDimensions(localFilePath);
@@ -749,10 +720,7 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
return newNote;
}
public async attachFile(
pickerResponse: PickerResponse,
fileType: string,
): Promise<ResourceEntity|null> {
public async attachFile(pickerResponse: Asset, fileType: string): Promise<ResourceEntity|null> {
if (!pickerResponse) {
// User has cancelled
return null;
@@ -834,6 +802,36 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
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) {
void this.attachFile(
data,
@@ -937,12 +935,25 @@ 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() {
shared.toggleIsTodo_onPress(this);
this.scheduleSave();
}
private tags_onPress() {
if (!this.state.note || !this.state.note.id) return;
this.setState({ noteTagDialogShown: true });
}
private async share_onPress() {
await Share.share({
message: `${this.state.note.title}\n\n${this.state.note.body}`,
@@ -1048,8 +1059,37 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
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) => {
await CommandService.instance().execute('attachFile', filePath);
if (filePath) {
await this.attachFile({ uri: filePath }, 'all');
} else {
await this.showAttachMenu();
}
};
// private vosk_:Vosk;
@@ -1143,7 +1183,7 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
if (canAttachPicture) {
output.push({
title: _('Attach...'),
onPress: () => this.onAttach(),
onPress: () => this.showAttachMenu(),
disabled: readOnly,
});
}
@@ -1187,24 +1227,13 @@ 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 (isSaved && !isDeleted) {
addButtonFromCommand('setTags');
output.push({
title: _('Tags'),
onPress: () => {
this.tags_onPress();
},
});
}
output.push({
@@ -1254,6 +1283,22 @@ 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) {
addButtonFromCommand('permanentlyDeleteNote');
} else {
@@ -1338,8 +1383,7 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
public folderPickerOptions() {
const options = {
visible: !this.state.readOnly,
disabled: false,
enabled: !this.state.readOnly,
selectedFolderId: this.state.folder ? this.state.folder.id : null,
onValueChange: this.folderPickerOptions_valueChanged,
};
@@ -1347,7 +1391,7 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
if (
this.folderPickerOptions_
&& options.selectedFolderId === this.folderPickerOptions_.selectedFolderId
&& options.visible === this.folderPickerOptions_.visible
&& options.enabled === this.folderPickerOptions_.enabled
) {
return this.folderPickerOptions_;
}
@@ -1395,12 +1439,6 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
}
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) {
return (
<View style={this.styles().screen}>
@@ -1611,19 +1649,9 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
// which can cause some bugs where previously set state to another note would interfere
// how the new note should be rendered
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);
return (
<NoteScreenComponent key={noteId} dialogs={dialogs} {...props} />
<NoteScreenComponent key={props.noteId} dialogs={dialogs} {...props} />
);
};

View File

@@ -1,87 +0,0 @@
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 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 () => {
props.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 props.dialogs.showMenu(_('Choose an option'), buttons);
if (buttonId === 'takePhoto') await takePhoto();
if (buttonId === 'attachFile') await attachFile();
if (buttonId === 'attachPhoto') await attachPhoto();
};
return {
execute: async (_context: CommandContext, filePath?: string) => {
if (filePath) {
await props.attachFile({ uri: filePath }, 'all');
} else {
await showAttachMenu();
}
},
enabledCondition: '!noteIsReadOnly',
};
};

View File

@@ -1,18 +0,0 @@
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

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

View File

@@ -1,19 +0,0 @@
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

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

View File

@@ -214,11 +214,11 @@ class NotesScreenComponent extends BaseScreenComponent<ComponentProps, State> {
public folderPickerOptions() {
const options = {
visible: this.props.noteSelectionEnabled,
enabled: this.props.noteSelectionEnabled,
mustSelect: true,
};
if (this.folderPickerOptions_ && options.visible === this.folderPickerOptions_.visible) return this.folderPickerOptions_;
if (this.folderPickerOptions_ && options.enabled === this.folderPickerOptions_.enabled) return this.folderPickerOptions_;
this.folderPickerOptions_ = options;
return this.folderPickerOptions_;

View File

@@ -86,7 +86,7 @@ const SearchScreenComponent: React.FC<Props> = props => {
<ScreenHeader
title={_('Search')}
folderPickerOptions={{
visible: props.noteSelectionEnabled,
enabled: props.noteSelectionEnabled,
mustSelect: true,
}}
showSideMenuButton={false}

View File

@@ -78,10 +78,6 @@ const useWhisper = ({ locale, provider, onSetPreview, onText }: UseVoiceTypingPr
setMustDownloadModel(!(await builder.isDownloaded()));
}, [builder]);
useEffect(() => () => {
void voiceTypingRef.current?.stop();
}, []);
return [error, mustDownloadModel, voiceTyping];
};

View File

@@ -10,16 +10,16 @@
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; };
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
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 */; };
4D122473270878D700DE23E8 /* wtf.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D122472270878D700DE23E8 /* wtf.swift */; };
57317DBFCCF429AEF0A019CB /* libPods-ShareExtension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C9F257EEF9EAC998DCD8BDEC /* libPods-ShareExtension.a */; };
5E556FC75AECECB13464A724 /* libPods-ShareExtension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FAC957496DFD2368FFE3C360 /* libPods-ShareExtension.a */; };
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
AE152142260F770400217DCB /* ShareViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = AE152141260F770400217DCB /* ShareViewController.m */; };
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, ); }; };
BAD33BAD2BE9A08300E9F46A /* 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 */
/* Begin PBXContainerItemProxy section */
@@ -50,19 +50,27 @@
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>"; };
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; };
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>"; };
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>"; };
13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Joplin/main.m; 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>"; };
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>"; };
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>"; };
4D122472270878D700DE23E8 /* wtf.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = wtf.swift; 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>"; };
800581C1ADC9CA9A7AC1BB75 /* libPods-Joplin.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Joplin.a"; sourceTree = BUILT_PRODUCTS_DIR; };
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>"; };
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>"; };
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = Joplin/LaunchScreen.storyboard; 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>"; };
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>"; };
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>"; };
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>"; };
@@ -70,12 +78,13 @@
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>"; };
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>"; };
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>"; };
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; };
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 */
/* Begin PBXFrameworksBuildPhase section */
@@ -83,7 +92,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
D1BE6938B03F0BC60F98041F /* libPods-Joplin.a in Frameworks */,
46E31F54C547C341F605BB66 /* libPods-Joplin.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -91,7 +100,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
57317DBFCCF429AEF0A019CB /* libPods-ShareExtension.a in Frameworks */,
5E556FC75AECECB13464A724 /* libPods-ShareExtension.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -137,8 +146,11 @@
children = (
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
ED2971642150620600B7C4FE /* JavaScriptCore.framework */,
800581C1ADC9CA9A7AC1BB75 /* libPods-Joplin.a */,
C9F257EEF9EAC998DCD8BDEC /* libPods-ShareExtension.a */,
B61798F36B3BC123BF8EA4D9 /* libPods-Joplin-tvOS.a */,
258F823D616BE3D6A52BC900 /* libPods-Joplin-tvOSTests.a */,
A5E1CD825FABD6C4E704EA54 /* libPods-Joplin.a */,
F69B873C692CE22F1C4C9264 /* libPods-Joplin-JoplinTests.a */,
FAC957496DFD2368FFE3C360 /* libPods-ShareExtension.a */,
);
name = Frameworks;
sourceTree = "<group>";
@@ -188,10 +200,16 @@
9CDB1D9DB6483D893504BFCB /* Pods */ = {
isa = PBXGroup;
children = (
78131A70125DE0AE4D6BF72E /* Pods-Joplin.debug.xcconfig */,
85EA9E81E151B08B5FFD2044 /* Pods-Joplin.release.xcconfig */,
4281BC1941ED8712A952DC60 /* Pods-ShareExtension.debug.xcconfig */,
BEC9B285DDBCEBB2D7A812DE /* Pods-ShareExtension.release.xcconfig */,
505CB61090817F4453631957 /* Pods-Joplin.debug.xcconfig */,
14868D674CC065C7BF1C9944 /* Pods-Joplin.release.xcconfig */,
09056573D4C040FBD5FEB93A /* Pods-Joplin-JoplinTests.debug.xcconfig */,
2C91CD1424C7137D07789148 /* Pods-Joplin-JoplinTests.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;
sourceTree = "<group>";
@@ -223,15 +241,15 @@
isa = PBXNativeTarget;
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Joplin" */;
buildPhases = (
E76772393095384E60C0D95F /* [CP] Check Pods Manifest.lock */,
335ACF4DE85695BEBB18D8A3 /* [CP] Check Pods Manifest.lock */,
EB61CD887618E406C80EBC43 /* [Expo] Configure project */,
13B07F871A680F5B00A75B9A /* Sources */,
13B07F8C1A680F5B00A75B9A /* Frameworks */,
13B07F8E1A680F5B00A75B9A /* Resources */,
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
CBC8354E4CF5CF4E15F2FCDE /* [CP] Copy Pods Resources */,
AE82E4B42599FA3A0013551B /* Embed App Extensions */,
E6849A86F6866D5E0DC90A55 /* [CP] Embed Pods Frameworks */,
63951A9079A5D0302FB331B7 /* [CP] Copy Pods Resources */,
C8F2067658ACF12DF7A17513 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
@@ -247,7 +265,7 @@
isa = PBXNativeTarget;
buildConfigurationList = AE82E4B72599FA3A0013551B /* Build configuration list for PBXNativeTarget "ShareExtension" */;
buildPhases = (
8B87E13D9DB3F38C8DC6F227 /* [CP] Check Pods Manifest.lock */,
027E2AA6B101F8CFCA582EC1 /* [CP] Check Pods Manifest.lock */,
AE82E4A42599FA3A0013551B /* Sources */,
AE82E4A52599FA3A0013551B /* Frameworks */,
AE82E4A62599FA3A0013551B /* Resources */,
@@ -337,7 +355,71 @@
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";
};
63951A9079A5D0302FB331B7 /* [CP] Copy Pods Resources */ = {
027E2AA6B101F8CFCA582EC1 /* [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;
};
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;
buildActionMask = 2147483647;
files = (
@@ -401,70 +483,6 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Joplin/Pods-Joplin-resources.sh\"\n";
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 */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
@@ -530,18 +548,18 @@
/* Begin XCBuildConfiguration section */
13B07F941A680F5B00A75B9A /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 78131A70125DE0AE4D6BF72E /* Pods-Joplin.debug.xcconfig */;
baseConfigurationReference = 505CB61090817F4453631957 /* Pods-Joplin.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
CURRENT_PROJECT_VERSION = 129;
CURRENT_PROJECT_VERSION = 128;
DEVELOPMENT_TEAM = A9BXAFS6CT;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Joplin/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 13.2.1;
MARKETING_VERSION = 13.2.0;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -562,17 +580,17 @@
};
13B07F951A680F5B00A75B9A /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 85EA9E81E151B08B5FFD2044 /* Pods-Joplin.release.xcconfig */;
baseConfigurationReference = 14868D674CC065C7BF1C9944 /* Pods-Joplin.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
CURRENT_PROJECT_VERSION = 129;
CURRENT_PROJECT_VERSION = 128;
DEVELOPMENT_TEAM = A9BXAFS6CT;
INFOPLIST_FILE = Joplin/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 13.2.1;
MARKETING_VERSION = 13.2.0;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -747,7 +765,7 @@
};
AE82E4B52599FA3A0013551B /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 4281BC1941ED8712A952DC60 /* Pods-ShareExtension.debug.xcconfig */;
baseConfigurationReference = 5DE39012F71F18423C665C57 /* Pods-ShareExtension.debug.xcconfig */;
buildSettings = {
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
@@ -758,14 +776,14 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 129;
CURRENT_PROJECT_VERSION = 128;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = A9BXAFS6CT;
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = ShareExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 13.2.1;
MARKETING_VERSION = 13.2.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_LDFLAGS = (
@@ -785,7 +803,7 @@
};
AE82E4B62599FA3A0013551B /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = BEC9B285DDBCEBB2D7A812DE /* Pods-ShareExtension.release.xcconfig */;
baseConfigurationReference = 0473A2D469A3555053E69327 /* Pods-ShareExtension.release.xcconfig */;
buildSettings = {
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
@@ -797,14 +815,14 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 129;
CURRENT_PROJECT_VERSION = 128;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = A9BXAFS6CT;
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = ShareExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 13.2.1;
MARKETING_VERSION = 13.2.0;
MTL_FAST_MATH = YES;
OTHER_LDFLAGS = (
"$(inherited)",

View File

@@ -1029,7 +1029,7 @@ PODS:
- Yoga
- react-native-image-resizer (3.0.10):
- React-Core
- react-native-netinfo (11.3.3):
- react-native-netinfo (11.3.2):
- React-Core
- react-native-quick-crypto (0.7.5):
- DoubleConversion
@@ -1056,7 +1056,7 @@ PODS:
- Yoga
- react-native-rsa-native (2.0.5):
- React
- react-native-saf-x (3.2.3):
- react-native-saf-x (3.2.0):
- React-Core
- react-native-safe-area-context (4.10.8):
- React-Core
@@ -1725,10 +1725,10 @@ SPEC CHECKSUMS:
react-native-get-random-values: 21325b2244dfa6b58878f51f9aa42821e7ba3d06
react-native-image-picker: d3a65af2538ac5407e5329e50f057fb2456f15f8
react-native-image-resizer: fd0c333eca55147bd55c5e054cac95dcd0da6814
react-native-netinfo: 9af975c142e5673d643093aa5afdfa26f46b71b4
react-native-netinfo: 076df4f9b07f6670acf4ce9a75aac8d34c2e2ccc
react-native-quick-crypto: 7085e4e4607e7e8fa57f4193f994d5262d351e45
react-native-rsa-native: 12132eb627797529fdb1f0d22fd0f8f9678df64a
react-native-saf-x: e24866280d63484ed861601da2726fa24b1a1a5d
react-native-saf-x: 4eb5df05a53de42a943e8c460e1fff8d1d0df770
react-native-safe-area-context: b7daa1a8df36095a032dff095a1ea8963cb48371
react-native-slider: 03f213d3ffbf919b16298c7896c1b60101d8e137
react-native-sqlite-storage: f6d515e1c446d1e6d026aa5352908a25d4de3261
@@ -1776,4 +1776,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 11a2f8ebab99f816b8905858bff8a86a196b1f7e
COCOAPODS: 1.16.2
COCOAPODS: 1.16.1

View File

@@ -30,9 +30,9 @@
"@react-native-clipboard/clipboard": "1.14.1",
"@react-native-community/datetimepicker": "8.2.0",
"@react-native-community/geolocation": "3.3.0",
"@react-native-community/netinfo": "11.3.3",
"@react-native-community/netinfo": "11.3.2",
"@react-native-community/push-notification-ios": "1.11.0",
"@react-native-community/slider": "4.5.3",
"@react-native-community/slider": "4.5.2",
"assert-browserify": "2.0.0",
"buffer": "6.0.3",
"color": "3.2.1",
@@ -100,7 +100,7 @@
"@tsconfig/react-native": "2.0.2",
"@types/fs-extra": "11.0.4",
"@types/jest": "29.5.12",
"@types/node": "18.19.50",
"@types/node": "18.19.48",
"@types/react": "18.3.3",
"@types/react-native": "0.70.6",
"@types/react-redux": "7.1.33",

View File

@@ -12,7 +12,7 @@ import BaseModel from '@joplin/lib/BaseModel';
import BaseService from '@joplin/lib/services/BaseService';
import ResourceService from '@joplin/lib/services/ResourceService';
import KvStore from '@joplin/lib/services/KvStore';
import NoteScreen from './components/screens/Note/Note';
import NoteScreen from './components/screens/Note';
import UpgradeSyncTargetScreen from './components/screens/UpgradeSyncTargetScreen';
import Setting, { AppType, Env } from '@joplin/lib/models/Setting';
import PoorManIntervals from '@joplin/lib/PoorManIntervals';
@@ -27,7 +27,7 @@ import SyncTargetJoplinCloud from '@joplin/lib/SyncTargetJoplinCloud';
import SyncTargetOneDrive from '@joplin/lib/SyncTargetOneDrive';
import initProfile from '@joplin/lib/services/profileConfig/initProfile';
const VersionInfo = require('react-native-version-info').default;
import { Keyboard, BackHandler, Animated, StatusBar, Platform, Dimensions } from 'react-native';
const { Keyboard, BackHandler, Animated, StatusBar, Platform, Dimensions } = require('react-native');
import { AppState as RNAppState, EmitterSubscription, View, Text, Linking, NativeEventSubscription, Appearance, ActivityIndicator } from 'react-native';
import getResponsiveValue from './components/getResponsiveValue';
import NetInfo from '@react-native-community/netinfo';
@@ -443,9 +443,6 @@ const appReducer = (state = appDefaultState, action: any) => {
newState.isOnMobileData = action.isOnMobileData;
break;
case 'KEYBOARD_VISIBLE_CHANGE':
newState = { ...state, keyboardVisible: action.visible };
break;
}
} catch (error) {
error.message = `In reducer: ${error.message} Action: ${JSON.stringify(action)}`;
@@ -856,8 +853,6 @@ class AppComponent extends React.Component {
private urlOpenListener_: EmitterSubscription|null = null;
private appStateChangeListener_: NativeEventSubscription|null = null;
private themeChangeListener_: NativeEventSubscription|null = null;
private keyboardShowListener_: EmitterSubscription|null = null;
private keyboardHideListener_: EmitterSubscription|null = null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
private dropdownAlert_ = (_data: any) => new Promise<any>(res => res);
private callbackUrl: string|null = null;
@@ -1043,19 +1038,6 @@ class AppComponent extends React.Component {
await setupNotifications(this.props.dispatch);
this.keyboardShowListener_ = Keyboard.addListener('keyboardDidShow', () => {
this.props.dispatch({
type: 'KEYBOARD_VISIBLE_CHANGE',
visible: true,
});
});
this.keyboardHideListener_ = Keyboard.addListener('keyboardDidHide', () => {
this.props.dispatch({
type: 'KEYBOARD_VISIBLE_CHANGE',
visible: false,
});
});
// Setting.setValue('encryption.masterPassword', 'WRONG');
// setTimeout(() => NavService.go('EncryptionConfig'), 2000);
}
@@ -1092,15 +1074,6 @@ class AppComponent extends React.Component {
this.quickActionShortcutListener_.remove();
this.quickActionShortcutListener_ = undefined;
}
if (this.keyboardShowListener_) {
this.keyboardShowListener_.remove();
this.keyboardShowListener_ = undefined;
}
if (this.keyboardHideListener_) {
this.keyboardHideListener_.remove();
this.keyboardHideListener_ = undefined;
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied

View File

@@ -1,16 +0,0 @@
// This extends the generic stateToWhenClauseContext (potentially shared by
// all apps) with additional properties specific to the desktop app. So in
// general, any desktop component should import this file, and not the lib
// one.
import libStateToWhenClauseContext, { WhenClauseContextOptions } from '@joplin/lib/services/commands/stateToWhenClauseContext';
import { AppState } from '../../utils/types';
const stateToWhenClauseContext = (state: AppState, options: WhenClauseContextOptions = null) => {
return {
...libStateToWhenClauseContext(state, options),
keyboardVisible: state.keyboardVisible,
};
};
export default stateToWhenClauseContext;

View File

@@ -75,7 +75,7 @@ class Whisper implements VoiceTypingSession {
public async stop() {
if (this.sessionId === null) {
logger.debug('Session already closed.');
logger.warn('Session already closed.');
return;
}

View File

@@ -11,7 +11,6 @@ export const DEFAULT_ROUTE = {
const appDefaultState: AppState = {
smartFilterId: undefined,
...defaultState,
keyboardVisible: false,
route: DEFAULT_ROUTE,
noteSelectionEnabled: false,
noteSideMenuOptions: null,

View File

@@ -3,6 +3,9 @@ import { themeStyle } from '../components/global-style';
export default (themeId: number) => {
const theme = themeStyle(themeId);
return {
root: theme.rootStyle,
root: {
flex: 1,
backgroundColor: theme.backgroundColor,
},
};
};

View File

@@ -1,12 +1,11 @@
import Setting from '@joplin/lib/models/Setting';
import CommandService, { CommandDeclaration, CommandRuntime } from '@joplin/lib/services/CommandService';
import stateToWhenClauseContext from '@joplin/lib/services/commands/stateToWhenClauseContext';
import { AppState } from './types';
import { Store } from 'redux';
import editorCommandDeclarations from '../components/NoteEditor/commandDeclarations';
import noteCommands from '../components/screens/Note/commands';
import globalCommands from '../commands';
import libCommands from '@joplin/lib/commands';
import stateToWhenClauseContext from '../services/commands/stateToWhenClauseContext';
interface CommandSpecification {
declaration: CommandDeclaration;
@@ -26,9 +25,6 @@ const initializeCommandService = (store: Store<AppState, any>) => {
for (const declaration of editorCommandDeclarations) {
CommandService.instance().registerDeclaration(declaration);
}
for (const command of noteCommands) {
CommandService.instance().registerDeclaration(command.declaration);
}
registerCommands(globalCommands);
registerCommands(libCommands);
};

View File

@@ -2,13 +2,14 @@ import reducer from '@joplin/lib/reducer';
import { createStore } from 'redux';
import appDefaultState from '../appDefaultState';
import Setting from '@joplin/lib/models/Setting';
import { AppState } from '../types';
const testReducer = (state: AppState|undefined, action: unknown) => {
state ??= {
...appDefaultState,
settings: Setting.toPlainObject(),
};
const defaultState = {
...appDefaultState,
// Mocking theme in the default state is necessary to prevent "Theme not set!" warnings.
settings: { theme: Setting.THEME_LIGHT },
};
const testReducer = (state = defaultState, action: unknown) => {
return reducer(state, action);
};

View File

@@ -1,16 +0,0 @@
import { Store } from 'redux';
import { AppState } from '../types';
import initializeCommandService from '../initializeCommandService';
import BaseSyncTarget from '@joplin/lib/BaseSyncTarget';
import NavService from '@joplin/lib/services/NavService';
import BaseModel from '@joplin/lib/BaseModel';
// Sets a given Redux store as global
const setupGlobalStore = (store: Store<AppState>) => {
BaseModel.dispatch = store.dispatch;
BaseSyncTarget.dispatch = store.dispatch;
NavService.dispatch = store.dispatch;
initializeCommandService(store);
};
export default setupGlobalStore;

View File

@@ -3,7 +3,6 @@ import { State } from '@joplin/lib/reducer';
export interface AppState extends State {
showPanelsDialog: boolean;
isOnMobileData: boolean;
keyboardVisible: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
route: any;
smartFilterId: string;

View File

@@ -34,7 +34,6 @@ import biDirectionalTextExtension from './utils/biDirectionalTextExtension';
import searchExtension from './utils/searchExtension';
import isCursorAtBeginning from './utils/isCursorAtBeginning';
import overwriteModeExtension from './utils/overwriteModeExtension';
import handleLinkEditRequests, { showLinkEditor } from './utils/handleLinkEditRequests';
// Newer versions of CodeMirror by default use Chrome's EditContext API.
// While this might be stable enough for desktop use, it causes significant
@@ -98,6 +97,12 @@ const createEditor = (
}
};
const notifyLinkEditRequest = () => {
props.onEvent({
kind: EditorEventType.EditLink,
});
};
const globalSpellcheckEnabled = () => {
return editor.contentDOM.spellcheck;
@@ -179,7 +184,10 @@ const createEditor = (
keyCommand('Mod-`', toggleCode),
keyCommand('Mod-[', decreaseIndent),
keyCommand('Mod-]', increaseIndent),
keyCommand('Mod-k', showLinkEditor),
keyCommand('Mod-k', (_: EditorView) => {
notifyLinkEditRequest();
return true;
}),
keyCommand('Tab', (view: EditorView) => {
if (settings.autocompleteMarkup) {
return insertOrIncreaseIndent(view);
@@ -281,11 +289,6 @@ const createEditor = (
notifySelectionFormattingChange(viewUpdate);
}),
handleLinkEditRequests(() => {
props.onEvent({
kind: EditorEventType.EditLink,
});
}),
],
doc: initialText,
}),

View File

@@ -10,9 +10,8 @@ import {
} from '../markdown/markdownCommands';
import duplicateLine from './duplicateLine';
import sortSelectedLines from './sortSelectedLines';
import { closeSearchPanel, findNext, findPrevious, openSearchPanel, replaceAll, replaceNext, searchPanelOpen } from '@codemirror/search';
import { closeSearchPanel, findNext, findPrevious, openSearchPanel, replaceAll, replaceNext } from '@codemirror/search';
import { focus } from '@joplin/lib/utils/focusHandler';
import { showLinkEditor } from '../utils/handleLinkEditRequests';
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
export type EditorCommandFunction = (editor: EditorView, ...args: any[])=> void|any;
@@ -72,15 +71,6 @@ const editorCommands: Record<EditorCommandType, EditorCommandFunction> = {
[EditorCommandType.UndoSelection]: undoSelection,
[EditorCommandType.RedoSelection]: redoSelection,
[EditorCommandType.EditLink]: showLinkEditor,
[EditorCommandType.ToggleSearch]: (view) => {
if (searchPanelOpen(view.state)) {
return closeSearchPanel(view);
} else {
return openSearchPanel(view);
}
},
[EditorCommandType.ShowSearch]: openSearchPanel,
[EditorCommandType.HideSearch]: closeSearchPanel,
[EditorCommandType.FindNext]: findNext,

View File

@@ -1,25 +0,0 @@
import { EditorState, StateEffect } from '@codemirror/state';
import { EditorView } from '@codemirror/view';
export const showLinkEditorEffect = StateEffect.define<void>();
export const showLinkEditor = (view: EditorView) => {
view.dispatch({
effects: [
showLinkEditorEffect.of(),
],
});
return true;
};
const handleLinkEditRequests = (onShowEditor: ()=> void) => [
EditorState.transactionExtender.of(tr => {
if (tr.effects.some(e => e.is(showLinkEditorEffect))) {
onShowEditor();
}
return null;
}),
];
export default handleLinkEditRequests;

View File

@@ -33,7 +33,6 @@ export enum EditorCommandType {
InsertHorizontalRule = 'textHorizontalRule',
// Find commands
ToggleSearch = 'textSearch',
ShowSearch = 'find',
HideSearch = 'hideSearchDialog',
FindNext = 'findNext',
@@ -41,8 +40,6 @@ export enum EditorCommandType {
ReplaceNext = 'replace',
ReplaceAll = 'replaceAll',
EditLink = 'textLink',
// Editing and navigation commands
ScrollSelectionIntoView = 'scrollSelectionIntoView',
DeleteLine = 'deleteLine',

View File

@@ -1,7 +1,7 @@
{
"name": "@joplin/fork-htmlparser2",
"description": "Fast & forgiving HTML/XML/RSS parser",
"version": "4.1.56",
"version": "4.1.52",
"author": "Felix Boehm <me@feedic.com>",
"publishConfig": {
"access": "public"
@@ -46,7 +46,7 @@
},
"devDependencies": {
"@types/jest": "29.5.12",
"@types/node": "18.19.50",
"@types/node": "18.19.48",
"@typescript-eslint/eslint-plugin": "6.21.0",
"@typescript-eslint/parser": "6.21.0",
"coveralls": "3.1.1",

View File

@@ -2,7 +2,7 @@
"name": "@joplin/fork-sax",
"description": "An evented streaming XML parser in JavaScript",
"author": "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me/)",
"version": "1.2.60",
"version": "1.2.56",
"main": "lib/sax.js",
"publishConfig": {
"access": "public"

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/fork-uslug",
"version": "1.0.21",
"version": "1.0.17",
"description": "A permissive slug generator that works with unicode.",
"author": "Jeremy Selier <jerem.selier@gmail.com>",
"publishConfig": {

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/htmlpack",
"version": "3.2.4",
"version": "3.2.0",
"description": "Pack an HTML file and all its linked resources into a single HTML file",
"main": "dist/index.js",
"types": "src/index.ts",
@@ -15,7 +15,7 @@
"license": "MIT",
"dependencies": {
"@adobe/css-tools": "4.4.0",
"@joplin/fork-htmlparser2": "^4.1.56",
"@joplin/fork-htmlparser2": "^4.1.52",
"datauri": "4.1.0",
"fs-extra": "11.2.0",
"html-entities": "1.4.0"

View File

@@ -1,5 +1,4 @@
import * as fs from 'fs-extra';
import { pathExistsSync } from 'fs-extra';
const Entities = require('html-entities').AllHtmlEntities;
const htmlparser2 = require('@joplin/fork-htmlparser2');
const Datauri = require('datauri/sync');
@@ -103,7 +102,6 @@ const processLinkTag = (baseDir: string, _name: string, attrs: any): string => {
const filePath = `${baseDir}/${href}`;
if (!pathExistsSync(filePath)) return null;
const content = fs.readFileSync(filePath, 'utf8');
return `<style>${processCssContent(dirname(filePath), content)}</style>`;
};

View File

@@ -33,7 +33,7 @@ export default class SyncTargetOneDrive extends BaseSyncTarget {
}
public static label() {
return _('OneDrive');
return `${_('OneDrive')} (Deprecated)`;
}
public static description() {

View File

@@ -668,15 +668,6 @@ const builtInMetadata = (Setting: typeof SettingType) => {
storage: SettingStorage.File,
isGlobal: true,
},
'editor.toolbarButtons': {
value: [] as string[],
public: false,
type: SettingItemType.Array,
storage: SettingStorage.File,
isGlobal: true,
appTypes: [AppType.Mobile],
label: () => 'buttons included in the editor toolbar',
},
'notes.columns': {
value: defaultListColumns(),
public: false,

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/lib",
"version": "3.2.4",
"version": "3.2.0",
"description": "Joplin Core library",
"author": "Laurent Cozic",
"homepage": "",
@@ -24,7 +24,7 @@
"@types/jsdom": "21.1.7",
"@types/markdown-it": "13.0.9",
"@types/mustache": "4.2.5",
"@types/node": "18.19.50",
"@types/node": "18.19.48",
"@types/node-rsa": "1.1.4",
"@types/react": "18.3.3",
"@types/uuid": "9.0.7",
@@ -43,15 +43,15 @@
"@adobe/css-tools": "4.4.0",
"@aws-sdk/client-s3": "3.296.0",
"@aws-sdk/s3-request-presigner": "3.296.0",
"@joplin/fork-htmlparser2": "^4.1.56",
"@joplin/fork-sax": "^1.2.60",
"@joplin/fork-uslug": "^1.0.21",
"@joplin/htmlpack": "^3.2.4",
"@joplin/onenote-converter": "^3.2.4",
"@joplin/renderer": "^3.2.4",
"@joplin/turndown": "^4.0.78",
"@joplin/turndown-plugin-gfm": "^1.0.60",
"@joplin/utils": "^3.2.4",
"@joplin/fork-htmlparser2": "^4.1.52",
"@joplin/fork-sax": "^1.2.56",
"@joplin/fork-uslug": "^1.0.17",
"@joplin/htmlpack": "~3.2",
"@joplin/onenote-converter": "0.0.1",
"@joplin/renderer": "~3.2",
"@joplin/turndown": "^4.0.74",
"@joplin/turndown-plugin-gfm": "^1.0.56",
"@joplin/utils": "~3.2",
"@types/nanoid": "3.0.0",
"adm-zip": "0.5.12",
"async-mutex": "0.5.0",

View File

@@ -257,7 +257,7 @@ export default class CommandService extends BaseService {
}
}
public componentRegisterCommands<ComponentType>(component: ComponentType, commands: ComponentCommandSpec<ComponentType>[], allowMultiple?: boolean): RegisteredRuntime {
public componentRegisterCommands<ComponentType>(component: ComponentType, commands: ComponentCommandSpec<ComponentType>[], allowMultiple?: boolean) {
const runtimeHandles: RegisteredRuntime[] = [];
for (const command of commands) {
runtimeHandles.push(

View File

@@ -1,11 +1,10 @@
import CommandService from '../CommandService';
import { stateUtils } from '../../reducer';
import focusEditorIfEditorCommand from './focusEditorIfEditorCommand';
import { WhenClauseContext } from './stateToWhenClauseContext';
const separatorItem = { type: 'separator' };
export interface ToolbarButtonInfo {
type: 'button';
name: string;
tooltip: string;
iconName: string;
@@ -14,16 +13,6 @@ export interface ToolbarButtonInfo {
title: string;
}
interface SeparatorItem extends Omit<Partial<ToolbarButtonInfo>, 'type'> {
type: 'separator';
}
export const separatorItem: SeparatorItem = {
type: 'separator',
};
export type ToolbarItem = ToolbarButtonInfo|SeparatorItem;
interface ToolbarButtonCacheItem {
info: ToolbarButtonInfo;
}
@@ -45,7 +34,8 @@ export default class ToolbarButtonUtils {
return this.service_;
}
private commandToToolbarButton(commandName: string, whenClauseContext: WhenClauseContext): ToolbarButtonInfo {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
private commandToToolbarButton(commandName: string, whenClauseContext: any): ToolbarButtonInfo {
const newEnabled = this.service.isEnabled(commandName, whenClauseContext);
const newTitle = this.service.title(commandName);
@@ -59,14 +49,13 @@ export default class ToolbarButtonUtils {
const command = this.service.commandByName(commandName, { runtimeMustBeRegistered: true });
const output: ToolbarButtonInfo = {
type: 'button',
const output = {
name: commandName,
tooltip: this.service.label(commandName),
iconName: command.declaration.iconName,
enabled: newEnabled,
onClick: async () => {
await this.service.execute(commandName);
void this.service.execute(commandName);
void focusEditorIfEditorCommand(commandName, this.service);
},
title: newTitle,
@@ -83,12 +72,13 @@ export default class ToolbarButtonUtils {
// the output also won't change. Invididual toolbarButtonInfo also won't changed
// if the state they use hasn't changed. This is to avoid useless renders of the toolbars.
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
public commandsToToolbarButtons(commandNames: string[], whenClauseContext: any): ToolbarItem[] {
const output: ToolbarItem[] = [];
public commandsToToolbarButtons(commandNames: string[], whenClauseContext: any): ToolbarButtonInfo[] {
const output: ToolbarButtonInfo[] = [];
for (const commandName of commandNames) {
if (commandName === '-') {
output.push(separatorItem);
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
output.push(separatorItem as any);
continue;
}

View File

@@ -15,10 +15,6 @@ const { escapeHtml } = require('../../string-utils.js');
import { assetsToHeaders } from '@joplin/renderer';
import getPluginSettingValue from '../plugins/utils/getPluginSettingValue';
import { LinkRenderingType } from '@joplin/renderer/MdToHtml';
import Logger from '@joplin/utils/Logger';
import { parseRenderedNoteMetadata } from './utils';
const logger = Logger.create('InteropService_Exporter_Html');
export default class InteropService_Exporter_Html extends InteropService_Exporter_Base {
@@ -129,11 +125,8 @@ export default class InteropService_Exporter_Html extends InteropService_Exporte
},
},
});
const noteContent = [];
const metadata = parseRenderedNoteMetadata(result.html ? result.html : '');
if (!metadata.printTitle) logger.info('Not printing title because joplin-metadata-print-title tag is set to false');
if (metadata.printTitle && item.title) noteContent.push(`<div class="exported-note-title">${escapeHtml(item.title)}</div>`);
if (item.title) noteContent.push(`<div class="exported-note-title">${escapeHtml(item.title)}</div>`);
if (result.html) noteContent.push(result.html);
const libRootPath = dirname(dirname(__dirname));
@@ -143,15 +136,11 @@ export default class InteropService_Exporter_Html extends InteropService_Exporte
for (let i = 0; i < result.pluginAssets.length; i++) {
const asset = result.pluginAssets[i];
const filePath = asset.pathIsAbsolute ? asset.path : `${libRootPath}/node_modules/@joplin/renderer/assets/${asset.name}`;
if (!(await shim.fsDriver().exists(filePath))) {
logger.warn(`File does not exist and cannot be exported: ${filePath}`);
} else {
const destPath = `${dirname(noteFilePath)}/pluginAssets/${asset.name}`;
const dir = dirname(destPath);
await shim.fsDriver().mkdir(dir);
this.createdDirs_.push(dir);
await shim.fsDriver().copy(filePath, destPath);
}
const destPath = `${dirname(noteFilePath)}/pluginAssets/${asset.name}`;
const dir = dirname(destPath);
await shim.fsDriver().mkdir(dir);
this.createdDirs_.push(dir);
await shim.fsDriver().copy(filePath, destPath);
}
const fullHtml = `

View File

@@ -1,35 +0,0 @@
import { RenderedNoteMetadata, parseRenderedNoteMetadata } from './utils';
describe('interop/utils', () => {
test.each<[string, RenderedNoteMetadata]>([
[
'',
{ printTitle: true },
],
[
'<!-- joplin-metadata-print-title = false -->',
{ printTitle: false },
],
[
'<!-- joplin-metadata-print-title = true -->',
{ printTitle: true },
],
[
'<!-- joplin-metadata-print-title = 0 -->',
{ printTitle: false },
],
[
'<!-- joplin-metadata-print-title = 1 -->',
{ printTitle: true },
],
[
'<!-- joplin-metadata-print-title = 0 -->',
{ printTitle: false },
],
])('should parse metadata from the note HTML body', async (bodyHtml, expected) => {
const actual = parseRenderedNoteMetadata(bodyHtml);
expect(actual).toEqual(expected);
});
});

View File

@@ -1,25 +0,0 @@
/* eslint-disable import/prefer-default-export */
export interface RenderedNoteMetadata {
printTitle: boolean;
}
export const parseRenderedNoteMetadata = (noteHtml: string) => {
const output: RenderedNoteMetadata = {
printTitle: true,
};
// <!-- joplin-metadata-print-title = false -->
const match = noteHtml.match(/<!--[\s]+joplin-metadata-(.*?)[\s]+=[\s]+(.*?)[\s]+-->/);
if (match) {
const [, propName, propValue] = match;
if (propName === 'print-title') {
output.printTitle = propValue.toLowerCase() === 'true' || propValue === '1';
} else {
throw new Error(`Unknown view metadata: ${propName}`);
}
}
return output;
};

View File

@@ -1,7 +1,7 @@
import { Day, msleep } from '@joplin/utils/time';
import Folder from '../../models/Folder';
import Note from '../../models/Note';
import { setupDatabaseAndSynchronizer, simulateReadOnlyShareEnv, switchClient } from '../../testing/test-utils';
import { setupDatabaseAndSynchronizer, switchClient } from '../../testing/test-utils';
import permanentlyDeleteOldItems from './permanentlyDeleteOldItems';
import Setting from '../../models/Setting';
@@ -75,42 +75,4 @@ describe('permanentlyDeleteOldItems', () => {
expect(await Folder.count()).toBe(1);
});
it('should not auto-delete read-only items', async () => {
const shareId = 'testShare';
// Simulates a folder having been deleted a long time ago
const longTimeAgo = 1000;
const readOnlyFolder = await Folder.save({
title: 'Read-only folder',
share_id: shareId,
deleted_time: longTimeAgo,
});
const readOnlyNote1 = await Note.save({
title: 'Read-only note',
parent_id: readOnlyFolder.id,
share_id: shareId,
deleted_time: longTimeAgo,
});
const readOnlyNote2 = await Note.save({
title: 'Read-only note 2',
share_id: shareId,
deleted_time: longTimeAgo,
});
const writableNote = await Note.save({
title: 'Editable note',
deleted_time: longTimeAgo,
});
const cleanup = simulateReadOnlyShareEnv(shareId);
await permanentlyDeleteOldItems(Day);
// Should preserve only the read-only items.
expect(await Folder.load(readOnlyFolder.id)).toBeTruthy();
expect(await Note.load(readOnlyNote1.id)).toBeTruthy();
expect(await Note.load(readOnlyNote2.id)).toBeTruthy();
expect(await Note.load(writableNote.id)).toBeFalsy();
cleanup();
});
});

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