1
0
mirror of https://github.com/laurent22/joplin.git synced 2026-02-01 07:49:31 +02:00

Compare commits

...

52 Commits

Author SHA1 Message Date
Laurent Cozic
c5808b60a6 update 2026-01-26 14:49:40 +00:00
Laurent Cozic
e548b3ee74 Merge branch 'dev' into lazy_load 2026-01-26 14:48:04 +00:00
Laurent Cozic
8e2b6ca296 Chore: Improved performance log consistency, and log to console on Android production too 2026-01-26 14:47:19 +00:00
Laurent Cozic
0172bb0ad8 Chore: Fix error in release-android script when the main apk is not built 2026-01-26 14:46:33 +00:00
Laurent Cozic
bd0bef121c Merge branch 'dev' into lazy_load 2026-01-26 14:35:05 +00:00
Laurent Cozic
6569dff1ac Chore: Improved performance log consistency, and log to console on Android production too 2026-01-26 14:34:31 +00:00
Laurent Cozic
5226a2e630 Chore: Fix error in release-android script when the main apk is not built 2026-01-26 14:29:25 +00:00
Laurent Cozic
1d38e443ba Chore: Do not create GitHub release when there is no APK to publish 2026-01-26 13:58:57 +00:00
Laurent Cozic
09ccc2f25d update 2026-01-26 13:50:10 +00:00
Laurent Cozic
5ad19b7261 Chore: Make Android app profileable 2026-01-26 13:46:27 +00:00
Arda Kılıçdağı
70293478a2 All: Translation: Update tr_TR.po (#14193) 2026-01-25 18:14:39 -05:00
custiq
3aaa20254f All: Translation: Update fi_FI.po (#14189) 2026-01-24 23:46:32 -05:00
renovate[bot]
42c248f7ca Update dependency @types/serviceworker to v0.0.160 (#14190)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-24 09:11:54 +00:00
Joplin Bot
ac1e94a8df Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-01-23 06:47:06 +00:00
renovate[bot]
daff4496cf Update dependency turndown to v7.2.2 (#14181)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-23 02:15:02 +00:00
Joplin Bot
1e00078228 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-01-23 01:56:25 +00:00
renovate[bot]
03a1de9370 Update dependency @rollup/plugin-commonjs to v28.0.9 (#14175)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 17:46:44 +00:00
renovate[bot]
55ef256c65 Update dependency rate-limiter-flexible to v7.4.0 (#14174)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 17:46:38 +00:00
renovate[bot]
6d115db16f Update dependency @types/yargs to v17.0.34 (#14173)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 16:25:50 +00:00
renovate[bot]
5853031fde Update dependency @types/serviceworker to v0.0.159 (#14172)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 09:05:01 +00:00
renovate[bot]
47db2ae962 Update dependency @types/nodemailer to v6.4.21 (#14171)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 09:02:54 +00:00
Laurent Cozic
b960a2a8b0 Doc: Updated JSB contact link 2026-01-21 09:27:55 +00:00
renovate[bot]
fcaa7d2a98 Update dependency lint-staged to v16.2.6 (#14165)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-20 18:18:59 +00:00
renovate[bot]
99284ae135 Update dependency lint-staged to v16.2.0 (#14162)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-20 13:20:03 +00:00
Joplin Bot
66ae58c81b Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-01-19 18:43:09 +00:00
Laurent Cozic
484d6a866d Doc: Remove "(Pre-release)" marker from Android changelog since all versions are pre-releases 2026-01-19 18:03:05 +00:00
Laurent Cozic
b45fd09e38 Merge branch 'release-3.5' into dev 2026-01-19 16:44:41 +00:00
Laurent Cozic
903a369c13 Android 3.5.9 2026-01-19 16:43:41 +00:00
Laurent Cozic
1fb79315e4 Chore: lock files 2026-01-19 16:13:04 +00:00
Henry Heino
4dc021b523 Android: Remove unnecessary READ_PHONE_STATE permission (#14157) 2026-01-19 16:04:56 +00:00
Joplin Bot
bbb4b46dd9 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-01-19 02:01:31 +00:00
renovate[bot]
063dc46f50 Update dependency dotenv to v17.2.3 (#14155)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-19 00:02:36 +00:00
renovate[bot]
aa400b52be Update dependency short-uuid to v5 (#14156)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-19 00:02:26 +00:00
renovate[bot]
be7de2f08a Update dependency dotenv to v17.2.2 (#14145)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-18 22:01:23 +00:00
renovate[bot]
f8a129e4dc Update dependency npm-package-json-lint to v9 (#14146)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-18 22:01:11 +00:00
Laurent Cozic
c5d9646908 Desktop release v3.6.2 2026-01-18 11:33:16 +00:00
Henry Heino
876ec80911 Desktop: Fixes #14084: .onepkg import: Fix Unicode issues, support Linux and MacOS (#14094)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-01-18 11:31:48 +00:00
mrjo118
4051f88ce7 Chore: Fix intermittent Synchronizer.revisions test failure (#14096)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-01-18 11:31:42 +00:00
Laurent Cozic
f194c111e4 All: Fixes #14144: Application crashes when profile database has been analyzed 2026-01-18 11:30:05 +00:00
Henry Heino
e386246bc9 Chore: Sync fuzzer: Improve error logging (#14108)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-01-18 11:29:32 +00:00
Henry Heino
292b269f1d Desktop: Resolves #14086: Accessibility: Include accessibility information in exported PDFs (#14111)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-01-18 11:29:25 +00:00
renovate[bot]
b2fc43da2b Update dependency short-uuid to v4.2.2 (#14114)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-01-18 11:29:17 +00:00
Henry Heino
4a23a1ed3e Desktop: Fixes #14092: Built-in plugins: Upgrade Freehand Drawing to v4.3.0 (#14123)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-01-18 11:29:07 +00:00
Henry Heino
c8878a18bf Desktop, Mobile: Editor: Inline rendering: Render inline HTML (colorized text, superscript, subscript, strikethrough) (#14133) 2026-01-18 11:28:15 +00:00
Henry Heino
340fba7af5 Server: Fixes #14107: Fix warning when unsharing folder (#14134) 2026-01-18 11:25:52 +00:00
Henry Heino
271c4f4a2a Server: Fixes #14131: Allow changing the password for the admin account when SAML is enabled (#14135) 2026-01-18 11:25:38 +00:00
Henry Heino
c9dba20f59 Chore: Sync fuzzer: Allow specifying a set of initial actions (#14136)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2026-01-18 11:25:07 +00:00
renovate[bot]
b474cc206a Update dependency dotenv to v17 (#14138)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-18 11:24:46 +00:00
Milo Ivir
9d4df8cc6e All: Translation: Update hr_HR.po (#14140) 2026-01-17 20:57:39 -05:00
Joplin Bot
a4ddfe1f58 Doc: Auto-update documentation
Auto-updated using release-website.sh
2026-01-17 18:38:35 +00:00
renovate[bot]
7d15215e66 Update dependency react-native-device-info to v14.1.1 (#14132)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-17 14:23:02 +00:00
Laurent Cozic
449555c8e9 Desktop release v3.5.12 2026-01-17 11:21:02 +00:00
73 changed files with 3066 additions and 1843 deletions

View File

@@ -1061,6 +1061,8 @@ packages/editor/CodeMirror/extensions/rendering/replaceBulletLists.js
packages/editor/CodeMirror/extensions/rendering/replaceCheckboxes.js
packages/editor/CodeMirror/extensions/rendering/replaceDividers.js
packages/editor/CodeMirror/extensions/rendering/replaceFormatCharacters.js
packages/editor/CodeMirror/extensions/rendering/replaceInlineHtml.test.js
packages/editor/CodeMirror/extensions/rendering/replaceInlineHtml.js
packages/editor/CodeMirror/extensions/rendering/types.js
packages/editor/CodeMirror/extensions/rendering/utils/makeBlockReplaceExtension.js
packages/editor/CodeMirror/extensions/rendering/utils/makeInlineReplaceExtension.js
@@ -1101,6 +1103,7 @@ 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/htmlNodeInfo.js
packages/editor/CodeMirror/utils/isCursorAtBeginning.js
packages/editor/CodeMirror/utils/isInSyntaxNode.js
packages/editor/CodeMirror/utils/markdown/codeBlockLanguages/allLanguages.js
@@ -1841,17 +1844,19 @@ packages/tools/checkIgnoredFiles.js
packages/tools/checkLibPaths.test.js
packages/tools/checkLibPaths.js
packages/tools/convertThemesToCss.js
packages/tools/fuzzer/ActionRunner.js
packages/tools/fuzzer/ActionTracker.js
packages/tools/fuzzer/Client.js
packages/tools/fuzzer/ClientPool.js
packages/tools/fuzzer/Server.js
packages/tools/fuzzer/constants.js
packages/tools/fuzzer/doRandomAction.js
packages/tools/fuzzer/model/FolderRecord.js
packages/tools/fuzzer/sync-fuzzer.js
packages/tools/fuzzer/types.js
packages/tools/fuzzer/utils/ProgressBar.js
packages/tools/fuzzer/utils/SeededRandom.js
packages/tools/fuzzer/utils/diffSortedStringArrays.test.js
packages/tools/fuzzer/utils/diffSortedStringArrays.js
packages/tools/fuzzer/utils/getNumberProperty.js
packages/tools/fuzzer/utils/getProperty.js
packages/tools/fuzzer/utils/getStringProperty.js

7
.gitignore vendored
View File

@@ -1035,6 +1035,8 @@ packages/editor/CodeMirror/extensions/rendering/replaceBulletLists.js
packages/editor/CodeMirror/extensions/rendering/replaceCheckboxes.js
packages/editor/CodeMirror/extensions/rendering/replaceDividers.js
packages/editor/CodeMirror/extensions/rendering/replaceFormatCharacters.js
packages/editor/CodeMirror/extensions/rendering/replaceInlineHtml.test.js
packages/editor/CodeMirror/extensions/rendering/replaceInlineHtml.js
packages/editor/CodeMirror/extensions/rendering/types.js
packages/editor/CodeMirror/extensions/rendering/utils/makeBlockReplaceExtension.js
packages/editor/CodeMirror/extensions/rendering/utils/makeInlineReplaceExtension.js
@@ -1075,6 +1077,7 @@ 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/htmlNodeInfo.js
packages/editor/CodeMirror/utils/isCursorAtBeginning.js
packages/editor/CodeMirror/utils/isInSyntaxNode.js
packages/editor/CodeMirror/utils/markdown/codeBlockLanguages/allLanguages.js
@@ -1815,17 +1818,19 @@ packages/tools/checkIgnoredFiles.js
packages/tools/checkLibPaths.test.js
packages/tools/checkLibPaths.js
packages/tools/convertThemesToCss.js
packages/tools/fuzzer/ActionRunner.js
packages/tools/fuzzer/ActionTracker.js
packages/tools/fuzzer/Client.js
packages/tools/fuzzer/ClientPool.js
packages/tools/fuzzer/Server.js
packages/tools/fuzzer/constants.js
packages/tools/fuzzer/doRandomAction.js
packages/tools/fuzzer/model/FolderRecord.js
packages/tools/fuzzer/sync-fuzzer.js
packages/tools/fuzzer/types.js
packages/tools/fuzzer/utils/ProgressBar.js
packages/tools/fuzzer/utils/SeededRandom.js
packages/tools/fuzzer/utils/diffSortedStringArrays.test.js
packages/tools/fuzzer/utils/diffSortedStringArrays.js
packages/tools/fuzzer/utils/getNumberProperty.js
packages/tools/fuzzer/utils/getProperty.js
packages/tools/fuzzer/utils/getStringProperty.js

View File

@@ -0,0 +1,21 @@
# Add a minSdkVersion to prevent the dangerous READ_PHONE_STATE
# permission from being added.
# See:
# - Upstream issue report: https://github.com/oblador/react-native-vector-icons/issues/1861
# - About the permission: https://developer.android.com/reference/android/Manifest.permission#READ_PHONE_STATE
# - StackOverflow post with discussion and alternate workarounds: https://stackoverflow.com/questions/39668549/why-has-the-read-phone-state-permission-been-added
diff --git a/android/build.gradle b/android/build.gradle
index a16b4ad6d1871cf5cf73ef7ebeaf8bd4d662b134..9871afb5fbf8e687370e08f54d884ecd7dde7e7c 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -37,6 +37,10 @@ android {
}
compileSdkVersion safeExtGet('compileSdkVersion', 31)
+
+ defaultConfig {
+ minSdkVersion safeExtGet('minSdkVersion', 24)
+ }
}
dependencies {

View File

@@ -0,0 +1,21 @@
# Add a minSdkVersion to prevent the dangerous READ_PHONE_STATE
# permission from being added.
# See:
# - Upstream issue report: https://github.com/oblador/react-native-vector-icons/issues/1861
# - About the permission: https://developer.android.com/reference/android/Manifest.permission#READ_PHONE_STATE
# - StackOverflow post with discussion and alternate workarounds: https://stackoverflow.com/questions/39668549/why-has-the-read-phone-state-permission-been-added
diff --git a/android/build.gradle b/android/build.gradle
index d42bd23123644cc324051e9c7ec4635de286315a..640996df60fe7769f69b30b35f771eb9cf0b75d4 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -37,6 +37,10 @@ android {
}
compileSdkVersion safeExtGet('compileSdkVersion', 31)
+
+ defaultConfig {
+ minSdkVersion safeExtGet('minSdkVersion', 24)
+ }
}
dependencies {

View File

@@ -0,0 +1,21 @@
# Add a minSdkVersion to prevent the dangerous READ_PHONE_STATE
# permission from being added.
# See:
# - Upstream issue report: https://github.com/oblador/react-native-vector-icons/issues/1861
# - About the permission: https://developer.android.com/reference/android/Manifest.permission#READ_PHONE_STATE
# - StackOverflow post with discussion and alternate workarounds: https://stackoverflow.com/questions/39668549/why-has-the-read-phone-state-permission-been-added
diff --git a/android/build.gradle b/android/build.gradle
index 170ec0ff9befe0f9155aaf5e1b84133cfd87be99..e6a0ab4a019ee67c5af7761ae8bb35f18b05c590 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -37,6 +37,10 @@ android {
}
compileSdkVersion safeExtGet('compileSdkVersion', 31)
+
+ defaultConfig {
+ minSdkVersion safeExtGet('minSdkVersion', 24)
+ }
}
dependencies {

View File

@@ -0,0 +1,21 @@
# Add a minSdkVersion to prevent the dangerous READ_PHONE_STATE
# permission from being added.
# See:
# - Upstream issue report: https://github.com/oblador/react-native-vector-icons/issues/1861
# - About the permission: https://developer.android.com/reference/android/Manifest.permission#READ_PHONE_STATE
# - StackOverflow post with discussion and alternate workarounds: https://stackoverflow.com/questions/39668549/why-has-the-read-phone-state-permission-been-added
diff --git a/android/build.gradle b/android/build.gradle
index 3b22f9de66795ee01dbaa29655727ee7ddba3cc8..325daa88d33f066b3826e5031ce281793710af2d 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -37,6 +37,10 @@ android {
}
compileSdkVersion safeExtGet('compileSdkVersion', 31)
+
+ defaultConfig {
+ minSdkVersion safeExtGet('minSdkVersion', 24)
+ }
}
dependencies {

View File

@@ -86,9 +86,9 @@
"gulp": "4.0.2",
"husky": "9.1.7",
"lerna": "3.22.1",
"lint-staged": "16.1.6",
"lint-staged": "16.2.6",
"madge": "8.0.0",
"npm-package-json-lint": "8.0.0",
"npm-package-json-lint": "9.0.0",
"typescript": "5.8.3"
},
"dependencies": {

Binary file not shown.

View File

@@ -95,6 +95,9 @@ export default class InteropServiceHelper {
// Allows users to override the CSS page size.
// See https://github.com/laurent22/joplin/issues/13096
preferCSSPageSize: true,
// Include accessibility information in the output:
generateTaggedPDF: true,
});
resolve(data);
} catch (error) {

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
"version": "3.6.1",
"version": "3.6.2",
"description": "Joplin for Desktop",
"main": "main.bundle.js",
"private": true,

View File

@@ -0,0 +1,90 @@
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
exports.default = notarizeFile;
const fs_1 = require('fs');
const notarize_1 = require('@electron/notarize');
const execCommand = require('./execCommand');
const child_process_1 = require('child_process');
const util_1 = require('util');
const execAsync = (0, util_1.promisify)(child_process_1.exec);
// Same appId in electron-builder.
const appId = 'net.cozic.joplin-desktop';
function isDesktopAppTag(tagName) {
if (!tagName) { return false; }
return tagName[0] === 'v';
}
async function notarizeFile(filePath) {
if (process.platform !== 'darwin') { return; }
console.info(`Checking if notarization should be done on: ${filePath}`);
if (!process.env.IS_CONTINUOUS_INTEGRATION || !isDesktopAppTag(process.env.GIT_TAG_NAME)) {
console.info(`Either not running in CI or not processing a desktop app tag - skipping notarization. process.env.IS_CONTINUOUS_INTEGRATION = ${process.env.IS_CONTINUOUS_INTEGRATION}; process.env.GIT_TAG_NAME = ${process.env.GIT_TAG_NAME}`);
return;
}
if (!process.env.APPLE_ID || !process.env.APPLE_ID_PASSWORD) {
console.warn('Environment variables APPLE_ID and APPLE_ID_PASSWORD not found - notarization will NOT be done.');
return;
}
if (!(0, fs_1.existsSync)(filePath)) {
throw new Error(`Cannot find file at: ${filePath}`);
}
// Every x seconds we print something to stdout, otherwise CI may timeout
// the task after 10 minutes, and Apple notarization can take more time.
const waitingIntervalId = setInterval(() => {
console.info('.');
}, 60000);
const isPkg = filePath.endsWith('.pkg');
console.info(`Notarizing ${filePath}`);
try {
if (isPkg) {
await execAsync(`xcrun notarytool submit "${filePath}" ` +
`--apple-id "${process.env.APPLE_ID}" ` +
`--password "${process.env.APPLE_ID_PASSWORD}" ` +
`--team-id "${process.env.APPLE_ASC_PROVIDER}" ` +
'--wait', { maxBuffer: 1024 * 1024 });
} else {
await (0, notarize_1.notarize)({
appBundleId: appId,
appPath: filePath,
// Apple Developer email address
appleId: process.env.APPLE_ID,
// App-specific password: https://support.apple.com/en-us/HT204397
appleIdPassword: process.env.APPLE_ID_PASSWORD,
// When Apple ID is attached to multiple providers (eg if the
// account has been used to build multiple apps for different
// companies), in that case the provider "Team Short Name" (also
// known as "ProviderShortname") must be provided.
//
// Use this to get it:
//
// xcrun altool --list-providers -u APPLE_ID -p APPLE_ID_PASSWORD
// ascProvider: process.env.APPLE_ASC_PROVIDER,
// In our case, the team ID is the same as the legacy ASC_PROVIDER
teamId: process.env.APPLE_ASC_PROVIDER,
tool: 'notarytool',
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
});
}
} catch (error) {
console.error(error);
process.exit(1);
}
clearInterval(waitingIntervalId);
// It appears that electron-notarize doesn't staple the app, but without
// this we were still getting the malware warning when launching the app.
// Stapling the app means attaching the notarization ticket to it, so that
// if the user is offline, macOS can still check if the app was notarized.
// So it seems to be more or less optional, but at least in our case it
// wasn't.
console.info('Stapling notarization ticket to the file...');
const staplerCmd = `xcrun stapler staple "${filePath}"`;
console.info(`> ${staplerCmd}`);
console.info(await execCommand(staplerCmd));
console.info(`Validating stapled file: ${filePath}`);
try {
await execAsync(`spctl -a -vv -t install "${filePath}"`);
} catch (error) {
console.error(`Failed validating stapled file: ${filePath}:`, error);
}
console.info(`Done notarizing ${filePath}`);
}
// # sourceMappingURL=notarizeFile.js.map

View File

@@ -132,6 +132,17 @@ android {
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
}
profileable {
// Release-like build that allows profiling with Android Studio Profiler
initWith release
signingConfig signingConfigs.debug
// Required for Android Studio Profiler to attach
debuggable false
// Keeps symbols for better stack traces in profiler
minifyEnabled false
// Use release variants of dependencies that don't have profileable
matchingFallbacks = ['release']
}
}
}

View File

@@ -46,6 +46,9 @@
android:theme="@style/AppTheme"
android:supportsRtl="true">
<!-- Enable profiling in release builds (Android 10+) -->
<profileable android:shell="true" />
<!--
2018-12-16: Changed android:launchMode from "singleInstance" to "singleTop" for Firebase notification
Previously singleInstance was necessary to prevent multiple instance of the RN app from running at the same time, but maybe no longer needed.

View File

@@ -345,6 +345,7 @@
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/react-native-image-picker/RNImagePickerPrivacyInfo.bundle",
"${PODS_ROOT}/../../node_modules/@react-native-vector-icons/fontawesome5/fonts/FontAwesome5_Brands.ttf",
"${PODS_ROOT}/../../node_modules/@react-native-vector-icons/fontawesome5/fonts/FontAwesome5_Regular.ttf",
@@ -364,6 +365,7 @@
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNImagePickerPrivacyInfo.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Brands.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Regular.ttf",

View File

@@ -1406,7 +1406,7 @@ PODS:
- React-jsiexecutor
- React-RCTFBReactNativeSpec
- ReactCommon/turbomodule/core
- react-native-alarm-notification (3.5.0):
- react-native-alarm-notification (3.6.0):
- React
- react-native-document-picker (10.1.7):
- DoubleConversion
@@ -1514,7 +1514,7 @@ PODS:
- Yoga
- react-native-rsa-native (2.0.5):
- React
- react-native-saf-x (3.5.1):
- react-native-saf-x (3.6.0):
- React-Core
- react-native-safe-area-context (5.6.1):
- React-Core
@@ -1904,7 +1904,7 @@ PODS:
- React-Core
- RNDateTimePicker (8.4.5):
- React-Core
- RNDeviceInfo (14.0.4):
- RNDeviceInfo (14.1.1):
- React-Core
- RNExitApp (2.0.0):
- React-Core
@@ -1912,7 +1912,7 @@ PODS:
- React-Core
- RNFS (2.20.0):
- React-Core
- RNLocalize (3.5.2):
- RNLocalize (3.5.4):
- React-Core
- RNQuickAction (0.3.13):
- React
@@ -2306,7 +2306,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90
DoubleConversion: 76ab83afb40bddeeee456813d9c04f67f78771b5
DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb
EXAV: ae28256069c4cdde93d185c007d8f68d92902c2e
EXConstants: 98bcf0f22b820f9b28f9fee55ff2daededadd2f8
Expo: c8f323f74218c45c46e27eed40d8a53ba50667c3
@@ -2319,7 +2319,7 @@ SPEC CHECKSUMS:
fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6
FBLazyVector: 84b955f7b4da8b895faf5946f73748267347c975
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
hermes-engine: 314be5250afa5692b57b4dd1705959e1973a8ebe
JoplinCommonShareExtension: a8b60b02704d85a7305627912c0240e94af78db7
JoplinRNShareExtension: e158a4b53ee0aa9cd3037a16221dc8adbd6f7860
@@ -2355,7 +2355,7 @@ SPEC CHECKSUMS:
React-logger: 8edfcedc100544791cd82692ca5a574240a16219
React-Mapbuffer: c3f4b608e4a59dd2f6a416ef4d47a14400194468
React-microtasksnativemodule: 054f34e9b82f02bd40f09cebd4083828b5b2beb6
react-native-alarm-notification: a4326a743df72a94d361a4c3a21515556f650341
react-native-alarm-notification: 846df1df72eca38e711409b9c064a5c635ff1c32
react-native-document-picker: b6419b766863408dacbdf5e97b2f3a694c611150
react-native-geolocation: ec15ffebc53790314885eb9e5f2132132fbc2600
react-native-get-random-values: d16467cf726c618e9c7a8c3c39c31faa2244bbba
@@ -2364,7 +2364,7 @@ SPEC CHECKSUMS:
react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187
react-native-quick-crypto: b475b71e7fa4dbf3446be55e8ad4ef2c58ac4f7f
react-native-rsa-native: a7931cdda1f73a8576a46d7f431378c5550f0c38
react-native-saf-x: 404f0f9a29cc6bf21d88582e054c45a11b28c22b
react-native-saf-x: 50d176763ed692b379c190bf55ae7293a3ee09bb
react-native-safe-area-context: 2243039f43d10cb1ea30ec5ac57fc6d1448413f4
react-native-sqlite-storage: 0c84826214baaa498796c7e46a5ccc9a82e114ed
react-native-vector-icons: a45ecc326ec090450f152dfc7076ce1173331ce5
@@ -2409,11 +2409,11 @@ SPEC CHECKSUMS:
RNCClipboard: f6679d470d0da2bce2a37b0af7b9e0bf369ecda5
RNCPushNotificationIOS: 6c4ca3388c7434e4a662b92e4dfeeee858e6f440
RNDateTimePicker: 8c12d12e8660697c2e176d2f98775764431c141f
RNDeviceInfo: d863506092aef7e7af3a1c350c913d867d795047
RNDeviceInfo: bcce8752b5043a623fe3c26789679b473f705d3c
RNExitApp: 4432b9b7cc5ccec9f91c94e507849891282befd4
RNFileViewer: 4b5d83358214347e4ab2d4ca8d5c1c90d869e251
RNFS: 89de7d7f4c0f6bafa05343c578f61118c8282ed8
RNLocalize: 3c4d0abd777a546fa77bdb6caef85a87fb9ea349
RNLocalize: d7859f87f1083349c73aa089e360af33ef89efc2
RNQuickAction: c2c8f379e614428be0babe4d53a575739667744d
RNSecureRandom: b64d263529492a6897e236a22a2c4249aa1b53dc
RNShare: 40ace3f87cd881869e8085aced9dc16b425c74aa

View File

@@ -35,11 +35,11 @@
"@react-native-community/netinfo": "11.4.1",
"@react-native-community/push-notification-ios": "1.11.0",
"@react-native-documents/picker": "10.1.7",
"@react-native-vector-icons/fontawesome5": "12.3.0",
"@react-native-vector-icons/fontawesome5": "patch:@react-native-vector-icons/fontawesome5@npm%3A12.3.0#~/.yarn/patches/@react-native-vector-icons-fontawesome5-npm-12.3.0-a1ca46610f.patch",
"@react-native-vector-icons/get-image": "12.3.0",
"@react-native-vector-icons/ionicons": "12.3.0",
"@react-native-vector-icons/material-design-icons": "12.4.0",
"@react-native-vector-icons/material-icons": "12.4.0",
"@react-native-vector-icons/ionicons": "patch:@react-native-vector-icons/ionicons@npm%3A12.3.0#~/.yarn/patches/@react-native-vector-icons-ionicons-npm-12.3.0-9bd4746f3f.patch",
"@react-native-vector-icons/material-design-icons": "patch:@react-native-vector-icons/material-design-icons@npm%3A12.4.0#~/.yarn/patches/@react-native-vector-icons-material-design-icons-npm-12.4.0-890f7f618b.patch",
"@react-native-vector-icons/material-icons": "patch:@react-native-vector-icons/material-icons@npm%3A12.4.0#~/.yarn/patches/@react-native-vector-icons-material-icons-npm-12.4.0-94138e627b.patch",
"assert-browserify": "2.0.0",
"buffer": "6.0.3",
"color": "3.2.1",
@@ -59,7 +59,7 @@
"punycode": "2.3.1",
"react": "19.0.0",
"react-native": "0.79.2",
"react-native-device-info": "14.0.4",
"react-native-device-info": "14.1.1",
"react-native-dropdownalert": "5.2.0",
"react-native-exit-app": "2.0.0",
"react-native-file-viewer": "2.1.5",
@@ -114,7 +114,7 @@
"@types/node": "18.19.130",
"@types/react": "19.0.14",
"@types/react-redux": "7.1.33",
"@types/serviceworker": "0.0.158",
"@types/serviceworker": "0.0.160",
"@types/tar-stream": "3.1.4",
"babel-jest": "29.7.0",
"babel-loader": "9.1.3",

View File

@@ -11,7 +11,6 @@ import Alarm from '@joplin/lib/models/Alarm';
import time from '@joplin/lib/time';
import Logger from '@joplin/utils/Logger';
import NoteScreen from './components/screens/Note/Note';
import UpgradeSyncTargetScreen from './components/screens/UpgradeSyncTargetScreen';
import Setting, { } from '@joplin/lib/models/Setting';
import PoorManIntervals from '@joplin/lib/PoorManIntervals';
import { NotesParent, serializeNotesParent } from '@joplin/lib/reducer';
@@ -36,15 +35,6 @@ import reduxSharedMiddleware from '@joplin/lib/components/shared/reduxSharedMidd
const { AppNav } = require('./components/app-nav.js');
import Folder from '@joplin/lib/models/Folder';
import NotesScreen from './components/screens/Notes/Notes';
import TagsScreen from './components/screens/tags';
import ConfigScreen from './components/screens/ConfigScreen/ConfigScreen';
const { FolderScreen } = require('./components/screens/folder.js');
import LogScreen from './components/screens/LogScreen';
import StatusScreen from './components/screens/status';
import SearchScreen from './components/screens/SearchScreen';
const { OneDriveLoginScreen } = require('./components/screens/onedrive-login.js');
import EncryptionConfigScreen from './components/screens/encryption-config';
import DropboxLoginScreen from './components/screens/dropbox-login.js';
import { MenuProvider } from 'react-native-popup-menu';
import SideMenu, { SideMenuPosition } from './components/SideMenu';
import SideMenuContent from './components/side-menu-content';
@@ -63,11 +53,47 @@ const SyncTargetAmazonS3 = require('@joplin/lib/SyncTargetAmazonS3.js');
import SyncTargetJoplinServerSAML from '@joplin/lib/SyncTargetJoplinServerSAML';
import BiometricPopup from './components/biometrics/BiometricPopup';
import { isCallbackUrl, parseCallbackUrl, CallbackUrlCommand } from '@joplin/lib/callbackUrlUtils';
import JoplinCloudLoginScreen from './components/screens/JoplinCloudLoginScreen';
import SyncTargetNone from '@joplin/lib/SyncTargetNone';
// Lazy-loaded screens for faster startup
const TagsScreen = React.lazy(() => import('./components/screens/tags'));
const ConfigScreen = React.lazy(() => import('./components/screens/ConfigScreen/ConfigScreen'));
const FolderScreen = React.lazy(async () => {
// @ts-expect-error JS file without type declarations
const m: { FolderScreen: React.ComponentType } = await import('./components/screens/folder.js');
return { default: m.FolderScreen };
});
const LogScreen = React.lazy(() => import('./components/screens/LogScreen'));
const StatusScreen = React.lazy(() => import('./components/screens/status'));
const SearchScreen = React.lazy(() => import('./components/screens/SearchScreen'));
const OneDriveLoginScreen = React.lazy(async () => {
// @ts-expect-error JS file without type declarations
const m: { OneDriveLoginScreen: React.ComponentType } = await import('./components/screens/onedrive-login.js');
return { default: m.OneDriveLoginScreen };
});
const EncryptionConfigScreen = React.lazy(() => import('./components/screens/encryption-config'));
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- JS file without type declarations
const DropboxLoginScreen = React.lazy(async (): Promise<{ default: any }> => {
return await import('./components/screens/dropbox-login.js');
});
const JoplinCloudLoginScreen = React.lazy(() => import('./components/screens/JoplinCloudLoginScreen'));
const UpgradeSyncTargetScreen = React.lazy(() => import('./components/screens/UpgradeSyncTargetScreen'));
const ShareManager = React.lazy(() => import('./components/screens/ShareManager'));
const ProfileSwitcher = React.lazy(() => import('./components/ProfileSwitcher/ProfileSwitcher'));
const ProfileEditor = React.lazy(() => import('./components/ProfileSwitcher/ProfileEditor'));
const NoteRevisionViewer = React.lazy(() => import('./components/screens/NoteRevisionViewer'));
const DocumentScanner = React.lazy(() => import('./components/screens/DocumentScanner/DocumentScanner'));
const SyncWizard = React.lazy(() => import('./components/SyncWizard/SyncWizard'));
// SsoLoginScreen needs special handling due to its factory pattern
const SsoLoginScreen = React.lazy(async () => {
const [{ default: SsoLoginScreenFactory }, { default: SamlShared }] = await Promise.all([
import('./components/screens/SsoLoginScreen'),
import('@joplin/lib/components/shared/SamlShared'),
]);
return { default: SsoLoginScreenFactory(new SamlShared()) };
});
SyncTargetRegistry.addClass(SyncTargetNone);
SyncTargetRegistry.addClass(SyncTargetOneDrive);
@@ -85,29 +111,21 @@ import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
import setupNotifications from './utils/setupNotifications';
import { loadMasterKeysFromSettings } from '@joplin/lib/services/e2ee/utils';
import { Theme, ThemeAppearance } from '@joplin/lib/themes/type';
import ProfileSwitcher from './components/ProfileSwitcher/ProfileSwitcher';
import ProfileEditor from './components/ProfileSwitcher/ProfileEditor';
import sensorInfo, { SensorInfo } from './components/biometrics/sensorInfo';
import { setDispatch } from './services/profiles';
import { ReactNode } from 'react';
import autodetectTheme, { onSystemColorSchemeChange } from './utils/autodetectTheme';
import PluginRunnerWebView from './components/plugins/PluginRunnerWebView';
import { refreshFolders, scheduleRefreshFolders } from '@joplin/lib/folders-screen-utils';
import ShareManager from './components/screens/ShareManager';
import { setDateFormat, setTimeFormat, setTimeLocale } from '@joplin/utils/time';
import DialogManager from './components/DialogManager';
import { AppState } from './utils/types';
import { getDisplayParentId } from '@joplin/lib/services/trash';
import PluginNotification from './components/plugins/PluginNotification';
import FocusControl from './components/accessibility/FocusControl/FocusControl';
import SsoLoginScreen from './components/screens/SsoLoginScreen';
import SamlShared from '@joplin/lib/components/shared/SamlShared';
import NoteRevisionViewer from './components/screens/NoteRevisionViewer';
import DocumentScanner from './components/screens/DocumentScanner/DocumentScanner';
import buildStartupTasks from './utils/buildStartupTasks';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import appReducer from './utils/appReducer';
import SyncWizard from './components/SyncWizard/SyncWizard';
const logger = Logger.create('root');
const perfLogger = PerformanceLogger.create();
@@ -711,7 +729,7 @@ class AppComponent extends React.Component<AppComponentProps, AppComponentState>
OneDriveLogin: { screen: OneDriveLoginScreen },
DropboxLogin: { screen: DropboxLoginScreen },
JoplinCloudLogin: { screen: JoplinCloudLoginScreen },
JoplinServerSamlLogin: { screen: SsoLoginScreen(new SamlShared()) },
JoplinServerSamlLogin: { screen: SsoLoginScreen },
EncryptionConfig: { screen: EncryptionConfigScreen },
UpgradeSyncTarget: { screen: UpgradeSyncTargetScreen },
ShareManager: { screen: ShareManager },
@@ -759,11 +777,15 @@ class AppComponent extends React.Component<AppComponentProps, AppComponentState>
<View style={{ flexGrow: 1, flexShrink: 1, flexBasis: '100%' }}>
<SafeAreaView style={{ flex: 1 }} titleBarUnderlayColor={theme.backgroundColor2}>
<View style={{ flex: 1, backgroundColor: theme.backgroundColor }}>
{ shouldShowMainContent && <AppNav screens={appNavInit} dispatch={this.props.dispatch} /> }
<React.Suspense fallback={<View/>}>
{ shouldShowMainContent && <AppNav screens={appNavInit} dispatch={this.props.dispatch} /> }
</React.Suspense>
</View>
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied */}
<DropdownAlert alert={(func: any) => (this.dropdownAlert_ = func)} />
<SyncWizard/>
<React.Suspense fallback={null}>
<SyncWizard/>
</React.Suspense>
</SafeAreaView>
</View>
</SideMenu>

View File

@@ -1,6 +1,6 @@
import PluginAssetsLoader from '../PluginAssetsLoader';
import AlarmService from '@joplin/lib/services/AlarmService';
import Logger, { TargetType } from '@joplin/utils/Logger';
import Logger, { LogLevel, TargetType } from '@joplin/utils/Logger';
import BaseModel from '@joplin/lib/BaseModel';
import BaseService from '@joplin/lib/services/BaseService';
import ResourceService from '@joplin/lib/services/ResourceService';
@@ -200,11 +200,8 @@ const buildStartupTasks = (
const mainLogger = new Logger();
mainLogger.addTarget(TargetType.Database, { database: logDatabase, source: 'm' });
mainLogger.setLevel(Logger.LEVEL_INFO);
if (Setting.value('env') === 'dev') {
mainLogger.addTarget(TargetType.Console);
mainLogger.setLevel(Logger.LEVEL_DEBUG);
}
mainLogger.addTarget(TargetType.Console);
mainLogger.setLevel(Setting.value('env') === 'dev' ? LogLevel.Debug : LogLevel.Info);
Logger.initializeGlobalLogger(mainLogger);
initLib(mainLogger);

View File

@@ -13,8 +13,8 @@
"url": "git+https://github.com/laurent22/joplin.git"
},
"devDependencies": {
"@types/yargs": "17.0.33",
"joplin-plugin-freehand-drawing": "4.2.0",
"@types/yargs": "17.0.34",
"joplin-plugin-freehand-drawing": "4.3.0",
"ts-node": "10.9.2",
"typescript": "5.8.3"
},

View File

@@ -3,39 +3,17 @@ import { EditorSelection } from '@codemirror/state';
import { EditorView } from '@codemirror/view';
import uslug from '@joplin/fork-uslug/lib/uslug';
import { SyntaxNodeRef } from '@lezer/common';
import htmlNodeInfo from '../utils/htmlNodeInfo';
const jumpToHash = (view: EditorView, hash: string) => {
const state = view.state;
const timeout = 1_000; // Maximum time to spend parsing the syntax tree
let targetLocation: number|undefined = undefined;
const removeQuotes = (quoted: string) => quoted.replace(/^["'](.*)["']$/, '$1');
const makeEnterNode = (offset: number) => (node: SyntaxNodeRef) => {
const nodeToText = (node: SyntaxNodeRef) => {
return state.sliceDoc(node.from + offset, node.to + offset);
};
// Returns the attribute with the given name for [node]
const getHtmlNodeAttr = (node: SyntaxNodeRef, attrName: string) => {
if (node.from === node.to) return null; // Empty
const content = node.node.resolveInner(node.from + 1);
// Search for the "id" attribute
const attributes = content.getChildren('Attribute');
for (const attribute of attributes) {
const nameNode = attribute.getChild('AttributeName');
const valueNode = attribute.getChild('AttributeValue');
if (nameNode && valueNode) {
const name = nodeToText(nameNode).toLowerCase().replace(/^"(.*)"$/, '$1');
if (name === attrName) {
return removeQuotes(nodeToText(valueNode));
}
}
}
return null;
};
const found = targetLocation !== undefined;
if (found) return false; // Skip this node
@@ -46,13 +24,14 @@ const jumpToHash = (view: EditorView, hash: string) => {
.replace(/^#+\s/, '') // Leading #s in headers
.replace(/\n-+$/, ''); // Trailing --s in headers
matches = hash === uslug(nodeText);
} else if (node.name === 'HTMLTag' || node.name === 'HTMLBlock') {
} else if (node.name === 'HTMLBlock') {
// CodeMirror adds HTML information to Markdown documents using overlays attached
// to HTMLTag and HTMLBlock nodes.
// Use .enter to enter the overlay and visit the HTML nodes:
node.node.enter(node.from, 1).toTree().iterate({ enter: makeEnterNode(node.from) });
} else if (node.name === 'OpenTag') {
matches = getHtmlNodeAttr(node, 'id') === hash || getHtmlNodeAttr(node, 'name') === hash;
} else if (node.name === 'OpenTag' || node.name === 'HTMLTag') {
const htmlNodeDetails = htmlNodeInfo(node, state);
matches = htmlNodeDetails.getAttr('id') === hash || htmlNodeDetails.getAttr('name') === hash;
}
if (matches) {

View File

@@ -4,6 +4,7 @@ import replaceBulletLists from './replaceBulletLists';
import replaceCheckboxes from './replaceCheckboxes';
import replaceDividers from './replaceDividers';
import replaceFormatCharacters from './replaceFormatCharacters';
import replaceInlineHtml from './replaceInlineHtml';
export default () => {
return [
@@ -13,5 +14,6 @@ export default () => {
replaceBackslashEscapes,
replaceDividers,
addFormattingClasses,
replaceInlineHtml,
];
};

View File

@@ -0,0 +1,27 @@
import { EditorSelection } from '@codemirror/state';
import createTestEditor from '../../testing/createTestEditor';
import replaceInlineHtml from './replaceInlineHtml';
const createEditor = async (initialMarkdown: string, expectedTags: string[] = ['HTMLTag']) => {
const editor = await createTestEditor(
initialMarkdown,
EditorSelection.cursor(0),
expectedTags,
[replaceInlineHtml],
);
return editor;
};
describe('replaceInlineHtml', () => {
test.each([
{ markdown: '<sup>Test</sup>', expectedTagsQuery: 'sup' },
{ markdown: '<strike>Test</strike>', expectedTagsQuery: 'strike' },
{ markdown: 'Test: <span style="color: red;">Test</span>', expectedTagsQuery: 'span[style]' },
{ markdown: 'Test: <span style="color: rgb(123, 0, 0);">Test</span>', expectedTagsQuery: 'span[style]' },
])('should render inline HTML (case %#)', async ({ markdown, expectedTagsQuery }) => {
// Add additional newlines: Ensure that the cursor isn't initially on the same line as the content to be rendered:
const editor = await createEditor(`\n\n${markdown}\n\n`);
expect(editor.contentDOM.querySelector(expectedTagsQuery)).toBeTruthy();
});
});

View File

@@ -0,0 +1,90 @@
import makeInlineReplaceExtension from './utils/makeInlineReplaceExtension';
import { Decoration } from '@codemirror/view';
import htmlNodeInfo, { HtmlNodeInfo } from '../../utils/htmlNodeInfo';
import { SyntaxNodeRef } from '@lezer/common';
import { EditorState } from '@codemirror/state';
const hideDecoration = Decoration.replace({});
type OnRenderTagContent = (openingTag: HtmlNodeInfo)=> Decoration;
const createHtmlReplacementExtension = (tagName: string, onRenderContent: OnRenderTagContent) => {
const isMatchingTag = (info: HtmlNodeInfo) => {
return info.tagName().toLowerCase() === tagName;
};
const isMatchingOpeningTag = (info: HtmlNodeInfo) => {
return isMatchingTag(info) && info.opening;
};
const isMatchingClosingTag = (info: HtmlNodeInfo) => {
return isMatchingTag(info) && info.closing;
};
const findClosingTag = (openingTag: SyntaxNodeRef, state: EditorState) => {
const openingTagInfo = htmlNodeInfo(openingTag, state);
// Self-closing?
if (openingTagInfo.closing) {
return openingTag;
}
let cursor = openingTag.node.nextSibling;
let nestedTagCounter = 1;
// Find the matching closing tag
for (; !!cursor && nestedTagCounter > 0; cursor = cursor.nextSibling) {
const info = htmlNodeInfo(cursor, state);
if (isMatchingOpeningTag(info)) {
nestedTagCounter ++;
} else if (isMatchingClosingTag(info)) {
nestedTagCounter --;
}
if (nestedTagCounter === 0) {
break;
}
}
return cursor;
};
const hideTags = makeInlineReplaceExtension({
createDecoration: (node, state) => {
const info = htmlNodeInfo(node, state);
return info && isMatchingTag(info) ? hideDecoration : null;
},
});
const styleContent = makeInlineReplaceExtension({
createDecoration: (node, state) => {
const info = htmlNodeInfo(node, state);
if (!info || !isMatchingOpeningTag(info)) return null;
return onRenderContent(info);
},
getDecorationRange(node, state) {
const closingTag = findClosingTag(node, state);
if (closingTag) {
return [node.to, closingTag.from];
} else {
return null;
}
},
});
return [hideTags, styleContent];
};
export default [
createHtmlReplacementExtension('sub', () => Decoration.mark({ tagName: 'sub' })),
createHtmlReplacementExtension('sup', () => Decoration.mark({ tagName: 'sup' })),
createHtmlReplacementExtension('strike', () => Decoration.mark({ tagName: 'strike' })),
createHtmlReplacementExtension('span', (info) => {
const styles = info.getAttr('style') ?? '';
const colorMatch = styles.match(/color:\s*(#?[a-z0-9A-Z]+|rgba?\([0-9, ]+\))(;|$)/);
return Decoration.mark({
attributes: {
style: colorMatch ? `color: ${colorMatch[1]};` : '',
},
});
}),
].flat();

View File

@@ -0,0 +1,88 @@
import { EditorState } from '@codemirror/state';
import { SyntaxNodeRef } from '@lezer/common';
export interface HtmlNodeInfo {
node: SyntaxNodeRef;
opening: boolean;
closing: boolean;
from: number;
to: number;
tagName: ()=> string;
getAttr: (attributeName: string)=> string;
}
type OnGetNodeContent = (node: SyntaxNodeRef)=> string;
const removeQuotes = (quoted: string) => quoted.replace(/^["'](.*)["']$/, '$1');
const getHtmlNodeAttr = (node: SyntaxNodeRef, attrName: string, getText: OnGetNodeContent) => {
if (node.from === node.to) return null; // Empty
const content = node.node.resolveInner(node.from + 1);
// Search for the "id" attribute
const attributes = content.getChildren('Attribute');
for (const attribute of attributes) {
const nameNode = attribute.getChild('AttributeName');
const valueNode = attribute.getChild('AttributeValue');
if (nameNode && valueNode) {
const name = getText(nameNode).toLowerCase().replace(/^"(.*)"$/, '$1');
if (name === attrName) {
return removeQuotes(getText(valueNode));
}
}
}
return null;
};
// Utility function to access CodeMirror HTML node information, based on
// the corresponding Markdown node.
const htmlNodeInfo = (node: SyntaxNodeRef, state: EditorState, offset = 0): HtmlNodeInfo|null => {
// Already an HTML node?
if (node.name === 'OpenTag' || node.name === 'CloseTag' || node.name === 'SelfClosingTag') {
const getNodeText = (childNode: SyntaxNodeRef) => state.sliceDoc(childNode.from + offset, childNode.to + offset);
const selfClosing = node.name === 'SelfClosingTag';
return {
node,
opening: node.name === 'OpenTag' || selfClosing,
closing: node.name === 'CloseTag' || selfClosing,
from: node.from + offset,
to: node.to + offset,
tagName: () => {
const nodeText = getNodeText(node).trim();
const tagNameMatch = nodeText.match(/^<\/?([^>\s]+)/);
if (tagNameMatch) {
return tagNameMatch[1];
}
return null;
},
getAttr: (name: string) => {
return getHtmlNodeAttr(node, name, getNodeText);
},
};
}
// Convert Markdown HTML nodes to HTML nodes
if (node.name === 'HTMLTag' || node.name === 'HTMLBlock') {
const globalOffset = node.from + offset;
let resolved: HtmlNodeInfo|null = null;
// CodeMirror adds HTML information to Markdown documents using overlays attached
// to HTMLTag and HTMLBlock nodes.
// Use .enter to enter the overlay and visit the HTML nodes:
node.node.enter(node.from, 1).toTree().iterate({
enter: (subNode) => {
resolved ??= htmlNodeInfo(subNode, state, globalOffset);
return !resolved;
},
});
return resolved;
}
return null;
};
export default htmlNodeInfo;

View File

@@ -314,7 +314,11 @@ export default class JoplinDatabase extends Database {
throw new Error(`\`notes_fts\` (${countFieldsNotesFts} fields) must have the same number of fields as \`items_fts\` (${countFieldsItemsFts} fields) for the search engine BM25 algorithm to work`);
}
const tableRows = await this.selectAll('SELECT name FROM sqlite_master WHERE type=\'table\'');
interface TableRow {
name: string;
}
const tableRows: TableRow[] = await this.selectAll('SELECT name FROM sqlite_master WHERE type=\'table\'');
for (let i = 0; i < tableRows.length; i++) {
let pragmas: Row[] = [];
@@ -322,7 +326,7 @@ export default class JoplinDatabase extends Database {
try {
if (tableName === 'android_metadata') continue;
if (tableName === 'table_fields') continue;
if (tableName === 'sqlite_sequence') continue;
if (tableName.startsWith('sqlite_')) continue;
if (tableName.indexOf('notes_fts') === 0) continue;
if (tableName.indexOf('items_fts') === 0) continue;
if (tableName === 'notes_spellfix') continue;

View File

@@ -130,7 +130,7 @@ export default class PerformanceLogger {
const startTime = performance.now();
this.lastLogTime_ = startTime;
PerformanceLogger.logDebug_(`${name}: Start at ${formatAbsoluteTime(startTime)}`);
PerformanceLogger.log_(`${name}: Start at ${formatAbsoluteTime(startTime)}`);
const onEnd = () => {
const now = performance.now();
@@ -140,12 +140,7 @@ export default class PerformanceLogger {
performance.measure(name, `${uniqueTaskId}-start`, `${uniqueTaskId}-end`);
}
const duration = now - startTime;
// Increase the log level for long-running tasks
const isLong = duration >= Second / 10;
const log = isLong ? PerformanceLogger.log_ : PerformanceLogger.logDebug_;
log(`${name}: End at ${formatAbsoluteTime(now)} (took ${formatTaskDuration(now - startTime)})`);
PerformanceLogger.log_(`${name}: End at ${formatAbsoluteTime(now)} (took ${formatTaskDuration(now - startTime)})`);
};
return {
onEnd,

View File

@@ -26,11 +26,6 @@ export interface ArchiveExtractOptions {
extractTo: string;
}
export interface CabExtractOptions extends ArchiveExtractOptions {
// Only files matching the pattern will be extracted
fileNamePattern: string;
}
export interface ZipEntry {
entryName: string;
name: string;
@@ -276,8 +271,4 @@ export default class FsDriverBase {
public async zipExtract(_options: ArchiveExtractOptions): Promise<ZipEntry[]> {
throw new Error('Not implemented: zipExtract');
}
public async cabExtract(_options: CabExtractOptions) {
throw new Error('Not implemented: cabExtract.');
}
}

View File

@@ -1,8 +1,6 @@
import AdmZip = require('adm-zip');
import FsDriverBase, { Stat, ZipEntry, ArchiveExtractOptions, CabExtractOptions } from './fs-driver-base';
import FsDriverBase, { Stat, ZipEntry, ArchiveExtractOptions } from './fs-driver-base';
import time from './time';
import { execCommand } from '@joplin/utils';
import { extname } from 'path';
const md5File = require('md5-file');
const fs = require('fs-extra');
@@ -218,25 +216,4 @@ export default class FsDriverNode extends FsDriverBase {
zip.extractAllTo(options.extractTo, false);
return zip.getEntries();
}
public async cabExtract(options: CabExtractOptions) {
if (process.platform !== 'win32') {
throw new Error('Extracting CAB archives is only supported on Windows.');
}
const source = this.resolve(options.source);
const extractTo = this.resolve(options.extractTo);
if (extname(source).toLowerCase() !== '.cab') {
throw new Error(`Invalid file extension. Expected .CAB. Was ${extname(source)}`);
}
// See https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/expand
await execCommand([
'expand.exe',
source,
`-f:${options.fileNamePattern}`,
extractTo,
], { quiet: true });
}
}

View File

@@ -145,8 +145,7 @@ export default class InteropService {
fileExtensions: [
'zip',
'one',
// .onepkg is a CAB archive, which Joplin can currently only extract on Windows
...(shim.isWindows() ? ['onepkg'] : []),
'onepkg',
],
sources: [FileSystemItem.File],
isNoteArchive: false, // Tells whether the file can contain multiple notes (eg. Enex or Jex format)

View File

@@ -42,6 +42,19 @@ const normalizeNoteForSnapshot = (body: string) => {
return removeItemIds(removeDefaultCss(body));
};
// A single Markdown string is much easier to visually compare during snapshot testing.
// Prefer notesToMarkdownString to normalizeNoteForSnapshot when the exact output HTML
// doesn't matter.
const notesToMarkdownString = (notes: NoteEntity[]) => {
const converter = new HtmlToMd();
return notes.map(note => {
return [
`# Note: ${note.title}`,
converter.parse(normalizeNoteForSnapshot(note.body)),
].join('\n\n');
}).sort().join('\n\n\n');
};
// This file is ignored if not running in CI. Look at onenote-converter/README.md and jest.config.js for more information
describe('InteropService_Importer_OneNote', () => {
let tempDir: string;
@@ -329,4 +342,10 @@ describe('InteropService_Importer_OneNote', () => {
expect(normalizeNoteForSnapshot(importedNote.body)).toMatchSnapshot('EmbeddedFiles');
});
it('should correctly import .onepkg notebooks', async () => {
const notes = await importNote(`${supportDir}/onenote/test.onepkg`);
expect(notesToMarkdownString(notes)).toMatchSnapshot();
});
});

View File

@@ -47,30 +47,13 @@ export default class InteropService_Importer_OneNote extends InteropService_Impo
if (fileExtension === '.zip') {
logger.info('Unzipping files...');
await shim.fsDriver().zipExtract({ source: sourcePath, extractTo: targetPath });
} else if (fileExtension === '.one') {
} else if (fileExtension === '.one' || fileExtension === '.onepkg') {
logger.info('Copying file...');
const outputDirectory = join(targetPath, fileNameNoExtension);
await shim.fsDriver().mkdir(outputDirectory);
await shim.fsDriver().copy(sourcePath, join(outputDirectory, basename(sourcePath)));
} else if (fileExtension === '.onepkg') {
// Change the file extension so that the archive can be extracted
const archivePath = join(targetPath, `${fileNameNoExtension}.cab`);
await shim.fsDriver().copy(sourcePath, archivePath);
const extractPath = join(targetPath, fileNameNoExtension);
await shim.fsDriver().mkdir(extractPath);
await shim.fsDriver().cabExtract({
source: archivePath,
extractTo: extractPath,
// Only the .one files are used--there's no need to extract
// other files.
fileNamePattern: '*.one',
});
await this.fixIncorrectLatin1Decoding_(extractPath);
} else {
throw new Error(`Unknown file extension: ${fileExtension}`);
}
@@ -101,7 +84,7 @@ export default class InteropService_Importer_OneNote extends InteropService_Impo
const notebookFilePath = join(unzipTempDirectory, notebookFile.path);
// In some cases, the OneNote zip file can include folders and other files
// that shouldn't be imported directly. Skip these:
if (!['.one', '.onetoc2'].includes(extname(notebookFilePath).toLowerCase())) {
if (!['.one', '.onepkg', '.onetoc2'].includes(extname(notebookFilePath).toLowerCase())) {
logger.info('Skipping non-OneNote file:', notebookFile.path);
skippedFiles.push(notebookFile.path);
continue;
@@ -323,47 +306,4 @@ export default class InteropService_Importer_OneNote extends InteropService_Impo
changed: true,
};
}
// Works around a decoding issue in which file names are extracted as latin1 strings,
// rather than UTF-8 strings. For example, OneNote seems to encode filenames as UTF-8 in .onepkg files.
// However, EXPAND.EXE reads the filenames as latin1. As a result, "é.one" becomes
// "é.one" when extracted from the archive.
// This workaround re-encodes filenames as UTF-8.
private async fixIncorrectLatin1Decoding_(parentDir: string) {
// Only seems to be necessary on Windows.
if (!shim.isWindows()) return;
const fixEncoding = async (basePath: string, fileName: string) => {
const originalPath = join(basePath, fileName);
let newPath;
let fixedFileName = Buffer.from(fileName, 'latin1').toString('utf8');
if (fixedFileName !== fileName) {
// In general, the path shouldn't start with "."s or contain path separators.
// However, if it does, these characters might cause import errors, so remove them:
fixedFileName = fixedFileName.replace(/^\.+/, '');
fixedFileName = fixedFileName.replace(/[/\\]/g, ' ');
// Avoid path traversal: Ensure that the file path is contained within the base directory
const newFullPathSafe = shim.fsDriver().resolveRelativePathWithinDir(basePath, fixedFileName);
await shim.fsDriver().move(originalPath, newFullPathSafe);
newPath = newFullPathSafe;
} else {
newPath = originalPath;
}
if (await shim.fsDriver().isDirectory(originalPath)) {
const children = await shim.fsDriver().readDirStats(newPath, { recursive: false });
for (const child of children) {
await fixEncoding(originalPath, child.path);
}
}
};
const stats = await shim.fsDriver().readDirStats(parentDir, { recursive: false });
for (const stat of stats) {
await fixEncoding(parentDir, stat.path);
}
}
}

View File

@@ -153,6 +153,133 @@ jeudi 23 octobre 2025
- [x] Documenter configuration synchro JBS saml pour un utilisateur (case cochée)"
`;
exports[`InteropService_Importer_OneNote should correctly import .onepkg notebooks 1`] = `
"# Note: A
A
- [Test](:/id-here "Test")
# Note: A test
A test
A test
Tuesday, January 13, 2026
1:44 PM
…test…
# Note: Another section
Another section
- [Page 1](:/id-here "Page 1")
- [Page 2](:/id-here "Page 2")
# Note: B
B
- [Test page](:/id-here "Test page")
# Note: Page 1
Page 1
Page 1
Tuesday, January 13, 2026
1:42 PM
Test
# Note: Page 2
Page 2
Page 2
Tuesday, January 13, 2026
1:42 PM
![](:/id-here)
&nbsp;
# Note: Test
Test
Test
Tuesday, January 13, 2026
1:44 PM
# Note: Test page
Test page
Test page
Tuesday, January 13, 2026
1:45 PM
# Note: Testing…
Testing…
Testing…
Friday, November 28, 2025
2:47 PM
&nbsp;
&nbsp;
Link to page: [Page 2](onenote:https://d.docs.live.net/4c230b31b0dfb50f/Documents/OneNote%20Notebooks/test/Another%20section.one#Page%202&section-id={C271F3B1-5F22-457F-9DEA-F2B938D9B3D7}&page-id={62800B88-EC08-4170-BDB6-885CBB47FF99}&end)
# Note: Tést!
Tést!
- [Testing…](:/id-here "Testing…")
- [A test](:/id-here "A test")
# Note: Untitled Page 1
Untitled Page
Tuesday, January 13, 2026
1:45 PM
# Note: ⅀⸨ Unicode ⸩
⅀⸨ Unicode ⸩
- [Untitled Page 1](:/id-here "Untitled Page 1")"
`;
exports[`InteropService_Importer_OneNote should correctly import math formulas: Math 1`] = `
" Math

View File

@@ -253,16 +253,16 @@ describe('Synchronizer.revisions', () => {
const getNoteRevisions = () => {
return Revision.allByType(BaseModel.TYPE_NOTE, note.id);
};
jest.advanceTimersByTime(200);
jest.advanceTimersByTime(500);
await Note.save({ id: note.id, title: 'note REV0' });
jest.advanceTimersByTime(200);
jest.advanceTimersByTime(500);
await revisionService().collectRevisions(); // REV0
expect(await getNoteRevisions()).toHaveLength(1);
const interimTime = Date.now();
jest.advanceTimersByTime(200);
jest.advanceTimersByTime(500);
await Note.save({ id: note.id, title: 'note REV1' });
await revisionService().collectRevisions(); // REV1
@@ -273,6 +273,10 @@ describe('Synchronizer.revisions', () => {
await switchClient(2);
await synchronizerStart();
// Prevent a race condition whereby a revision is downloaded via the sync, then one of the same revisions is updated within the same millisecond via
// deleteOldRevisions, and therefore is not uploaded via the sync because the sync_time matches
jest.advanceTimersByTime(500);
const revisions = await getNoteRevisions();
expect(revisions).toHaveLength(2);
expect(revisions[0].title_diff).toBe('[{"diffs":[[1,"note REV0"]],"start1":0,"start2":0,"length1":0,"length2":9}]');

View File

@@ -459,7 +459,7 @@ export function getPlans(stripeConfig: StripePublicConfig): Record<PlanName, Pla
featureLabelsOn: getFeatureLabelsByPlan(PlanName.JoplinServerBusiness, true),
featureLabelsOff: getFeatureLabelsByPlan(PlanName.JoplinServerBusiness, false),
cfaLabel: _('Get a quote'),
cfaUrl: 'mailto:jsb-inquiry@joplin.cloud?subject=Joplin%20Server%20Business%20inquiry',
cfaUrl: 'https://tally.so/r/D4BlOE',
footnote: '',
learnMoreUrl: 'https://joplinapp.org/help/apps/joplin_server_business',
hostingType: PlanHostingType.Self,

View File

@@ -17,6 +17,12 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "adler2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "aho-corasick"
version = "0.7.15"
@@ -93,7 +99,7 @@ dependencies = [
"cc",
"cfg-if",
"libc",
"miniz_oxide",
"miniz_oxide 0.4.4",
"object",
"rustc-demangle",
]
@@ -113,12 +119,30 @@ version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]]
name = "cab"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "171228650e6721d5acc0868a462cd864f49ac5f64e4a42cde270406e64e404d2"
dependencies = [
"byteorder",
"flate2",
"lzxd",
"time",
]
[[package]]
name = "cc"
version = "1.0.96"
@@ -168,6 +192,24 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "crc32fast"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
dependencies = [
"cfg-if",
]
[[package]]
name = "deranged"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587"
dependencies = [
"powerfmt",
]
[[package]]
name = "either"
version = "1.11.0"
@@ -204,6 +246,16 @@ dependencies = [
"once_cell",
]
[[package]]
name = "flate2"
version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369"
dependencies = [
"crc32fast",
"miniz_oxide 0.8.9",
]
[[package]]
name = "getrandom"
version = "0.1.16"
@@ -270,6 +322,12 @@ version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "lzxd"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b29dffab797218e12e4df08ef5d15ab9efca2504038b1b32b9b32fc844b39c9"
[[package]]
name = "memchr"
version = "2.7.6"
@@ -302,6 +360,22 @@ dependencies = [
"autocfg",
]
[[package]]
name = "miniz_oxide"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
"adler2",
"simd-adler32",
]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-traits"
version = "0.2.19"
@@ -455,6 +529,12 @@ version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.17"
@@ -553,6 +633,7 @@ version = "0.0.1"
dependencies = [
"askama",
"bytes",
"cab",
"color-eyre",
"console_error_panic_hook",
"encoding_rs",
@@ -652,6 +733,12 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "simd-adler32"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
[[package]]
name = "siphasher"
version = "0.3.11"
@@ -710,6 +797,25 @@ dependencies = [
"once_cell",
]
[[package]]
name = "time"
version = "0.3.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
dependencies = [
"deranged",
"num-conv",
"powerfmt",
"serde",
"time-core",
]
[[package]]
name = "time-core"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
[[package]]
name = "tracing"
version = "0.1.40"

View File

@@ -28,10 +28,31 @@ function normalizeAndWriteFile(filePath, data) {
fs.writeFileSync(filePath, data);
}
function fileReader(path) {
const fd = fs.openSync(path);
const size = fs.fstatSync(fd).size;
return {
read: (position, length) => {
const data = Buffer.alloc(length);
const sizeRead = fs.readSync(fd, data, { length, position });
// Make data.size match the number of bytes read:
return data.subarray(0, sizeRead);
},
size: () => {
return size;
},
close: () => {
fs.closeSync(fd);
},
};
}
module.exports = {
mkdirSyncRecursive,
isDirectory,
readDir,
removePrefix,
normalizeAndWriteFile,
fileReader,
};

View File

@@ -1,4 +1,7 @@
use std::io::{Read, Seek};
pub type ApiResult<T> = std::result::Result<T, std::io::Error>;
pub trait FileHandle: Read + Seek {}
pub trait FileApiDriver: Send + Sync {
fn is_directory(&self, path: &str) -> ApiResult<bool>;
@@ -7,6 +10,7 @@ pub trait FileApiDriver: Send + Sync {
fn write_file(&self, path: &str, data: &[u8]) -> ApiResult<()>;
fn make_dir(&self, path: &str) -> ApiResult<()>;
fn exists(&self, path: &str) -> ApiResult<bool>;
fn open_file(&self, path: &str) -> ApiResult<Box<dyn FileHandle>>;
// These functions correspond to the similarly-named
// NodeJS path functions and should behave like the NodeJS

View File

@@ -1,6 +1,7 @@
pub mod api;
pub use api::ApiResult;
pub use api::FileApiDriver;
pub use api::FileHandle;
use lazy_static::lazy_static;
use std::sync::Arc;

View File

@@ -1,5 +1,6 @@
use super::ApiResult;
use super::FileApiDriver;
use super::FileHandle;
use std::fs;
use std::path;
use std::path::Path;
@@ -26,6 +27,10 @@ impl FileApiDriver for FileApiDriverImpl {
fs::read(path)
}
fn open_file(&self, path: &str) -> ApiResult<Box<dyn FileHandle>> {
Ok(Box::new(fs::File::open(path)?))
}
fn write_file(&self, path: &str, data: &[u8]) -> ApiResult<()> {
fs::write(path, data)
}
@@ -72,6 +77,8 @@ impl FileApiDriver for FileApiDriverImpl {
}
}
impl FileHandle for fs::File {}
#[cfg(test)]
mod test {
use crate::file_api::FileApiDriver;

View File

@@ -1,5 +1,7 @@
use super::ApiResult;
use super::FileApiDriver;
use super::FileHandle;
use std::io::{BufReader, Read, Seek, SeekFrom};
use wasm_bindgen::JsValue;
use wasm_bindgen::prelude::wasm_bindgen;
use web_sys::js_sys;
@@ -31,6 +33,27 @@ extern "C" {
#[wasm_bindgen(js_name = readDir, catch)]
fn read_dir_js(path: &str) -> std::result::Result<JsValue, JsValue>;
#[wasm_bindgen(js_name = fileReader, catch)]
fn open_file_handle(path: &str) -> std::result::Result<JsFileHandle, JsValue>;
}
#[wasm_bindgen]
extern "C" {
type JsFileHandle;
#[wasm_bindgen(structural, method, catch)]
fn read(
this: &JsFileHandle,
offset: usize,
size: usize,
) -> std::result::Result<Uint8Array, JsValue>;
#[wasm_bindgen(structural, method)]
fn size(this: &JsFileHandle) -> usize;
#[wasm_bindgen(structural, method, catch)]
fn close(this: &JsFileHandle) -> std::result::Result<(), JsValue>;
}
#[wasm_bindgen(module = "fs")]
@@ -97,6 +120,16 @@ impl FileApiDriver for FileApiDriverImpl {
}
}
fn open_file(&self, path: &str) -> ApiResult<Box<dyn FileHandle>> {
match open_file_handle(path) {
Ok(handle) => {
let file = BufReader::new(SeekableFileHandle { handle, offset: 0 });
Ok(Box::new(file))
}
Err(e) => Err(handle_error(e, &format!("opening file {}", path))),
}
}
fn write_file(&self, path: &str, data: &[u8]) -> ApiResult<()> {
if let Err(error) = write_file(path, data) {
Err(handle_error(error, &format!("writing file {}", path)))
@@ -138,3 +171,87 @@ impl FileApiDriver for FileApiDriverImpl {
join_path(path_1, path_2).unwrap().as_string().unwrap()
}
}
struct SeekableFileHandle {
handle: JsFileHandle,
offset: usize,
}
impl Read for SeekableFileHandle {
fn read(&mut self, out: &mut [u8]) -> std::io::Result<usize> {
let file_size = self.handle.size();
let bytes_remaining = if self.offset < file_size {
file_size - self.offset
} else {
0
};
let maximum_read_size = bytes_remaining.min(out.len());
match self.handle.read(self.offset, maximum_read_size) {
Ok(data) => {
let data = data.to_vec();
let size = data.len();
self.offset += size;
// Verify that handle.read respected the maximum length:
if size > out.len() {
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
"Invariant violation: Size read must be less than or equal to the maximum_read_size.",
));
}
let (target_mem, padding) = out.split_at_mut(size);
target_mem.copy_from_slice(&data);
padding.fill(0);
Ok(size)
}
Err(error) => {
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Read failed: {:?}.", error),
));
}
}
}
}
impl Seek for SeekableFileHandle {
fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
match pos {
SeekFrom::Start(pos) => {
self.offset = pos as usize;
}
SeekFrom::Current(offset) => {
// Disallow seeking to a negative position
if offset < 0 && (-offset) as usize > self.offset {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Attempted to seek before the beginning of the file.",
));
}
self.offset = (self.offset as i64 + offset) as usize;
}
SeekFrom::End(offset) => {
self.offset = self.handle.size();
self.seek(SeekFrom::Current(offset))?;
}
}
Ok(self.offset as u64)
}
}
impl Drop for SeekableFileHandle {
fn drop(&mut self) {
if let Err(error) = self.handle.close() {
// Use web_sys directly -- log_warn! can't be used from within the parser-utils package:
let message: JsValue =
format!("OneNote converter: Failed to close file: Error: {error:?}").into();
web_sys::console::warn_1(&message);
}
}
}
impl FileHandle for BufReader<SeekableFileHandle> {}

View File

@@ -9,6 +9,7 @@ pub mod parse;
pub mod reader;
pub use errors::Result;
pub use file_api::FileHandle;
pub use file_api::fs_driver;
pub type Reader<'a, 'b> = &'b mut crate::reader::Reader<'a>;

View File

@@ -76,10 +76,17 @@ impl Parser {
pub fn parse_section(&mut self, path: String) -> Result<Section> {
log!("Parsing section: {:?}", path);
let data = fs_driver().read_file(path.as_str())?;
self.parse_section_from_data(&data, &path)
}
/// Parse a OneNote section file from a byte array.
/// The [path] is used to provide debugging information and determine
/// the name of the section file.
pub fn parse_section_from_data(&mut self, data: &[u8], path: &str) -> Result<Section> {
let store = parse_onestore(&mut Reader::new(&data))?;
if store.get_type() != OneStoreType::Section {
return Err(ErrorKind::NotASectionFile { file: path }.into());
return Err(ErrorKind::NotASectionFile { file: String::from(path) }.into());
}
let filename = fs_driver()

View File

@@ -30,6 +30,7 @@ uuid = "1.1.2"
widestring = "1.0.2"
wasm-bindgen = "0.2"
lazy_static = "1.4"
cab = "0.6.0"
parser = { path = "../parser" }
parser-utils = { path = "../parser-utils" }

View File

@@ -1,9 +1,10 @@
use color_eyre::eyre::{Result, eyre};
pub use parser::Parser;
use std::panic;
use sanitize_filename::sanitize;
use std::{io::Read, panic};
use wasm_bindgen::{JsError, prelude::wasm_bindgen};
use parser_utils::{fs_driver, log};
use parser_utils::{FileHandle, fs_driver, log};
mod errors;
mod notebook;
@@ -34,8 +35,6 @@ fn _main(input_path: &str, output_dir: &str, base_path: &str) -> Result<()> {
}
pub fn convert(path: &str, output_dir: &str, base_path: &str) -> Result<()> {
let mut parser = Parser::new();
let extension: String = fs_driver().get_file_extension(path);
match extension.as_str() {
@@ -47,7 +46,7 @@ pub fn convert(path: &str, output_dir: &str, base_path: &str) -> Result<()> {
return Ok(());
}
let section = parser.parse_section(path.to_owned())?;
let section = Parser::new().parse_section(path.to_owned())?;
let section_output_dir = fs_driver().get_output_path(base_path, output_dir, path);
section::Renderer::new().render(&section, section_output_dir.to_owned())?;
@@ -56,7 +55,7 @@ pub fn convert(path: &str, output_dir: &str, base_path: &str) -> Result<()> {
let _name: String = fs_driver().get_file_name(path).expect("Missing file name");
log!("Parsing .onetoc2 file: {}", _name);
let notebook = parser.parse_notebook(path.to_owned())?;
let notebook = Parser::new().parse_notebook(path.to_owned())?;
let notebook_name = fs_driver()
.get_parent_dir(path)
@@ -71,8 +70,66 @@ pub fn convert(path: &str, output_dir: &str, base_path: &str) -> Result<()> {
notebook::Renderer::new().render(&notebook, &notebook_name, &notebook_output_dir)?;
}
".onepkg" => {
let file_data = fs_driver().open_file(path)?;
convert_onepkg(file_data, output_dir)?;
}
ext => return Err(eyre!("Invalid file extension: {}, file: {}", ext, path)),
}
Ok(())
}
fn convert_onepkg(file_data: Box<dyn FileHandle>, output_dir: &str) -> Result<()> {
// .onepkg files are cabinet files
let mut cabinet = cab::Cabinet::new(file_data)?;
let file_paths: Vec<String> = cabinet
.folder_entries()
.flat_map(|folder| folder.file_entries())
.map(|entry| String::from(entry.name()))
.collect();
let build_output_dir = |file_path_in_archive: &str| -> Result<(String, String)> {
let mut output_path = String::from(output_dir);
// Split on both "\"s and "/"s since CAB archives seem to use Windows-style paths,
// where both / and \ are valid path separators.
let is_path_separator = |c| c == '\\' || c == '/';
let path_segments: Vec<&str> = file_path_in_archive.split(is_path_separator).collect();
let path_segments_without_filename = &path_segments[0..path_segments.len() - 1];
for part in path_segments_without_filename {
output_path = fs_driver().join(&output_path, &sanitize(part));
fs_driver().make_dir(&output_path)?;
}
let file_name = path_segments.last().unwrap_or(&"");
Ok((output_path, sanitize(file_name)))
};
let mut parser = Parser::new();
for file_path in file_paths {
log!("File path {file_path}");
if !file_path.ends_with(".one") {
log!("Skipping non-section file {file_path}");
continue;
}
log!("Rendering {file_path}");
let data = {
let mut file_data = cabinet.read_file(&file_path)?;
let mut data = Vec::new();
file_data.read_to_end(&mut data)?;
data
};
let (output_path, file_name) = build_output_dir(&file_path)?;
let section = parser.parse_section_from_data(&data, &file_name)?;
section::Renderer::new().render(&section, output_path)?;
}
Ok(())
}

View File

@@ -54,7 +54,7 @@
"prettycron": "0.10.0",
"qrcode": "1.5.4",
"query-string": "7.1.3",
"rate-limiter-flexible": "7.3.2",
"rate-limiter-flexible": "7.4.0",
"raw-body": "3.0.1",
"samlify": "2.10.1",
"sqlite3": "5.1.6",
@@ -77,15 +77,15 @@
"@types/mustache": "4.2.6",
"@types/node": "18.19.130",
"@types/node-os-utils": "1.3.4",
"@types/nodemailer": "6.4.20",
"@types/yargs": "17.0.33",
"@types/nodemailer": "6.4.21",
"@types/yargs": "17.0.34",
"@types/zxcvbn": "4.4.5",
"gulp": "4.0.2",
"jest": "29.7.0",
"jest-expect-message": "1.1.3",
"jsdom": "26.1.0",
"node-mocks-http": "1.17.2",
"short-uuid": "4.2.0",
"short-uuid": "5.2.0",
"source-map-support": "0.5.21",
"typescript": "5.8.3"
}

View File

@@ -123,7 +123,12 @@ export default class ItemModel extends BaseModel<Item> {
const share = await this.models().share().load(resource.jop_share_id, { fields: ['id', 'owner_id'] });
if (!share) {
modelLogger.warn('cannot find the share associated with this item. Action:', action, 'User:', user, 'Resource:', resource);
// Don't warn in the case where the share doesn't exist. This can happen, for example, when
// unsharing a folder.
// See https://github.com/laurent22/joplin/issues/14107.
if (resource.owner_id !== user.id) {
modelLogger.warn('cannot find the share associated with this item. Action:', action, 'User:', user.email, 'Resource:', resource);
}
} else {
if (share.owner_id !== user.id) {
const shareUser = await this.models().shareUser().byShareAndUserId(share.id, user.id);

View File

@@ -6,6 +6,7 @@ import { ErrorForbidden } from '../../utils/errors';
import { execRequest, execRequestC } from '../../utils/testing/apiUtils';
import { beforeAllDb, afterAllTests, beforeEachDb, koaAppContext, createUserAndSession, models, parseHtml, checkContextError, expectHttpError, expectThrow } from '../../utils/testing/testUtils';
import { uuidgen } from '@joplin/lib/uuid';
import config from '../../config';
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
async function postUser(sessionId: string, email: string, password: string = null, props: any = null): Promise<User> {
@@ -349,4 +350,29 @@ describe('index/users', () => {
expect(await models().application().count()).toBe(0);
});
test.each([
{ isExternal: true, expectedDisabled: true },
{ isExternal: false, expectedDisabled: false },
])('should disable password fields for external users, enable for internal users (case: %j)', async ({
isExternal, expectedDisabled,
}) => {
const { user, session } = await createUserAndSession();
config().SAML_ENABLED = true;
try {
await models().user().save({
id: user.id,
is_external: isExternal ? 1 : 0,
}, { skipValidation: true });
const userHtml = await getUserHtml(session.id, user.id);
const doc = parseHtml(userHtml);
expect(
doc.querySelector<HTMLInputElement>('input[name=password]').disabled,
).toBe(expectedDisabled);
} finally {
config().SAML_ENABLED = false;
}
});
});

View File

@@ -119,7 +119,9 @@ router.get('users/:id', async (path: SubPath, ctx: AppContext, formUser: User =
view.content.hasFlags = !!userFlagViews.length;
view.content.userFlagViews = userFlagViews;
view.content.stripePortalUrl = stripePortalUrl();
view.content.disabledIfExternalAuth = isUsingExternalAuth(config()) ? 'disabled' : '';
const isExternalAuth = isUsingExternalAuth(config());
view.content.disabledIfExternalAuth = isExternalAuth && user.is_external ? 'disabled' : '';
view.jsFiles.push('zxcvbn');
view.cssFiles.push('index/user');

View File

@@ -0,0 +1,484 @@
import uuid from '@joplin/lib/uuid';
import Client from './Client';
import ClientPool from './ClientPool';
import { assertIsFolder, assertIsNote, FuzzContext, ItemId, RandomFolderOptions } from './types';
import { strict as assert } from 'assert';
import Logger from '@joplin/utils/Logger';
import retryWithCount from './utils/retryWithCount';
import { Second } from '@joplin/utils/time';
const logger = Logger.create('ActionRunner');
export interface ActionSpec {
key: string;
options: Record<string, string|number>;
}
export default class ActionRunner {
public constructor(private context_: FuzzContext, private clientPool_: ClientPool, private activeClient_: Client) {}
public switchClient(client: Client) {
this.activeClient_ = client;
}
public async syncAndCheckState() {
await this.activeClient_.sync();
// .checkState can fail occasionally due to incomplete
// syncs (perhaps because the server is still processing
// share-related changes?). Allow this to be retried:
await retryWithCount(async () => {
await this.clientPool_.checkState();
}, {
count: 4,
delayOnFailure: count => count * Second * 2,
onFail: async () => {
logger.info('.checkState failed. Syncing all clients...');
await this.clientPool_.syncAll();
},
});
}
private buildActions_() {
const { actions, schema, addAction } = getActions(this.context_, this.clientPool_, this.activeClient_);
addAction('switchClient', async ({ id }) => {
if (typeof id !== 'number') throw new Error(`clientId must be a number. Was ${id}`);
this.switchClient(this.clientPool_.clientById(id));
return true;
}, { id: () => this.clientPool_.getClientId(this.clientPool_.randomClient()) });
addAction('syncAndCheckState', async () => {
await this.syncAndCheckState();
return true;
}, {});
return { actions, schema };
}
private validateActions_(specs: ActionSpec[]) {
const { schema } = this.buildActions_();
const errors = [];
for (const spec of specs) {
const currentActionLabel = JSON.stringify([spec.key, spec.options]);
const supportedOptions = schema.get(spec.key);
if (!supportedOptions) {
errors.push(
`In ${currentActionLabel}: Unknown action: ${spec.key}. Available action keys: ${JSON.stringify([...schema.keys()])}`,
);
continue;
}
const providedOptions = Object.keys(spec.options);
for (const option of providedOptions) {
if (!supportedOptions.includes(option)) {
errors.push(`In ${currentActionLabel}: Unknown option: ${option}. Supported options: ${JSON.stringify(supportedOptions)}`);
}
}
}
if (errors.length) {
throw new Error(`Validation failed:\n- ${errors.join('\n- ')}`);
}
}
public async doActions(specs: ActionSpec[]) {
this.validateActions_(specs);
for (const spec of specs) {
const { actions } = this.buildActions_();
const action = actions.get(spec.key);
if (!action) throw new Error(`Not found: ${spec.key}`);
await action(spec.options);
}
}
public async doRandomAction() {
// Avoid running special actions (e.g. "comment")
const { actions } = this.buildActions_();
const actionKeys = [...actions.keys()].filter(key => {
// Avoid choosing certain actions:
return key !== 'syncAndCheckState' && key !== 'switchClient' && key !== 'comment';
});
let result = false;
while (!result) { // Loop until an action was done
const randomAction = this.context_.randomFrom(actionKeys);
logger.info(`Action: ${randomAction} in ${this.activeClient_.email}`);
result = await actions.get(randomAction)({ });
if (!result) {
logger.info(` ${randomAction} was skipped (preconditions not met).`);
}
}
}
}
type ActionDefaults = { [key: string]: ()=> unknown|Promise<unknown> };
type ActionOptions<Defaults extends ActionDefaults> = {
[t in keyof Defaults]: Awaited<ReturnType<Defaults[t]>>
};
type UnknownActionOptions = Record<string, unknown>;
type ActionFunction<Options extends UnknownActionOptions>
= (options: Options)=> Promise<boolean>;
// Creates an action function, with defaults applied and logging
const createActionFunction = <Defaults extends ActionDefaults> (
key: string, action: ActionFunction<ActionOptions<Defaults>>, defaults: Defaults,
): ActionFunction<Partial<ActionOptions<Defaults>>> => {
return async (options) => {
const builtOptions: Record<string, unknown> = {};
for (const key in defaults) {
const defaultValue = await defaults[key]();
builtOptions[key] = options[key] ?? defaultValue;
}
logger.info('Run action:', JSON.stringify([key, builtOptions]));
return action(builtOptions as ActionOptions<Defaults>);
};
};
const getActions = (context: FuzzContext, clientPool: ClientPool, client: Client) => {
const selectOrCreateWriteableFolder = async () => {
let parentId = (await client.randomFolder({ includeReadOnly: false }))?.id;
// Create a toplevel folder to serve as this
// folder's parent if none exist yet
if (!parentId) {
parentId = uuid.create();
await client.createFolder({
parentId: '',
id: parentId,
title: 'Parent folder',
});
}
return parentId;
};
const defaultNoteProperties = {
published: false,
};
const selectOrCreateWriteableNote = async () => {
const options = { includeReadOnly: false };
let note = await client.randomNote(options);
if (!note) {
await client.createNote({
...defaultNoteProperties,
parentId: await selectOrCreateWriteableFolder(),
id: uuid.create(),
title: 'Test note',
body: 'Body',
});
note = await client.randomNote(options);
assert.ok(note, 'should have selected a random note');
}
return note.id;
};
const noteById = (id: ItemId) => {
const note = client.itemById(id);
assertIsNote(note);
return note;
};
const folderById = (id: ItemId) => {
const folder = client.itemById(id);
assertIsFolder(folder);
return folder;
};
const folderByIdOrRandom = (id: ItemId|undefined, randomOptions: RandomFolderOptions) => {
if (id !== undefined) {
return folderById(id);
} else {
return client.randomFolder(randomOptions);
}
};
const schema = new Map<string, string[]>; // Maps from keys to supported options
const actions = new Map<string, ActionFunction<UnknownActionOptions>>();
const addAction = <T extends ActionDefaults> (key: string, action: ActionFunction<ActionOptions<T>>, defaults: T) => {
actions.set(key, createActionFunction(key, action, defaults) as ActionFunction<UnknownActionOptions>);
schema.set(key, Object.keys(defaults));
};
const undefinedId = (): ItemId|undefined => undefined;
addAction('newSubfolder', async ({ parentId, id }) => {
await client.createRandomFolder({ parentId, id, quiet: false });
return true;
}, {
parentId: selectOrCreateWriteableFolder,
id: undefinedId,
});
addAction('newToplevelFolder', async ({ id }) => {
await client.createRandomFolder({ parentId: '', id, quiet: false });
return true;
}, { id: undefinedId });
addAction('newNote', async ({ parentId, id }) => {
await client.createRandomNote({ parentId, id });
return true;
}, {
parentId: selectOrCreateWriteableFolder,
id: undefinedId,
});
addAction('renameNote', async ({ id }) => {
await client.updateNote({
...noteById(id),
title: `Renamed (${context.randInt(0, 1000)})`,
});
return true;
}, { id: selectOrCreateWriteableNote });
addAction('updateNoteBody', async ({ id }) => {
const note = noteById(id);
await client.updateNote({
...note,
body: `${note.body}\n\nUpdated.\n`,
});
return true;
}, { id: selectOrCreateWriteableNote });
addAction('moveNote', async ({ noteId, targetFolderId }) => {
const note = noteById(noteId);
const newParent = await folderByIdOrRandom(targetFolderId, {
filter: folder => folder.id !== note.parentId,
includeReadOnly: false,
});
if (!newParent) return false;
await client.moveItem(note.id, newParent.id);
return true;
}, {
noteId: selectOrCreateWriteableNote,
targetFolderId: undefinedId,
});
addAction('deleteNote', async ({ id }) => {
const validatedNote = noteById(id); // Ensure, e.g., that the note exists
await client.deleteNote(validatedNote.id);
return true;
}, { id: selectOrCreateWriteableNote });
const randomClientOnDifferentAccount = () => {
const other = clientPool.randomClient(c => !c.hasSameAccount(client));
if (!other) return undefined;
return clientPool.getClientId(other);
};
addAction('shareFolder', async ({ otherClientId, folderId, readOnly }) => {
// No suitable client?
if (otherClientId === undefined) return false;
const other = clientPool.clientById(otherClientId);
const target = await folderByIdOrRandom(folderId, {
filter: candidate => {
const isToplevel = !candidate.parentId;
const ownedByCurrent = candidate.ownedByEmail === client.email;
const alreadyShared = isToplevel && candidate.isSharedWith(other.email);
return isToplevel && ownedByCurrent && !alreadyShared;
},
includeReadOnly: true,
});
if (!target) return false;
await client.shareFolder(target.id, other, { readOnly });
return true;
}, {
otherClientId: randomClientOnDifferentAccount,
folderId: undefinedId,
readOnly: () => context.randInt(0, 2) === 1 && context.isJoplinCloud,
});
addAction('unshareFolder', async ({ folderId, clientId }) => {
const target = await folderByIdOrRandom(folderId, {
filter: candidate => {
return candidate.isRootSharedItem && candidate.ownedByEmail === client.email;
},
includeReadOnly: true,
});
if (!target) return false;
const recipientEmail = () => {
if (clientId !== undefined) {
if (clientId === 'all') {
return 'all';
}
const email = clientPool.clientById(clientId).email;
assert.ok(target.shareRecipients.includes(email), `Not shared with ${email}.`);
return email;
}
const recipientIndex = context.randInt(-1, target.shareRecipients.length);
if (recipientIndex === -1) return 'all';
const recipientEmail = target.shareRecipients[recipientIndex];
return recipientEmail;
};
const email = recipientEmail();
if (email === 'all') { // Completely remove the share
await client.deleteAssociatedShare(target.id);
} else {
const recipient = clientPool.clientsByEmail(email)[0];
assert.ok(recipient, `invalid state -- recipient ${recipientEmail} should exist`);
await client.removeFromShare(target.id, recipient);
}
return true;
}, {
folderId: undefinedId,
clientId: (): number|'all'|undefined => undefined,
});
addAction('deleteFolder', async ({ folderId }) => {
await client.deleteFolder(folderId);
return true;
}, {
folderId: selectOrCreateWriteableFolder,
});
addAction('moveFolderToToplevel', async ({ folderId }) => {
if (!folderId) return false;
await client.deleteFolder(folderId);
return true;
}, {
folderId: async () => (await client.randomFolder({
// Don't choose items that are already toplevel
filter: item => !!item.parentId,
includeReadOnly: false,
}))?.id,
});
addAction('moveFolderTo', async ({ sourceFolderId, newParentId }) => {
const target = await folderByIdOrRandom(sourceFolderId, {
// Don't move shared folders (should not be allowed by the GUI in the main apps).
filter: item => !item.isRootSharedItem,
includeReadOnly: false,
});
if (!target) return false;
const targetDescendants = new Set(await client.allFolderDescendants(target.id));
const newParent = await folderByIdOrRandom(newParentId, {
filter: (item) => {
// Avoid making the folder a child of itself
return !targetDescendants.has(item.id);
},
includeReadOnly: false,
});
if (!newParent) return false;
await client.moveItem(target.id, newParent.id);
return true;
}, {
sourceFolderId: undefinedId,
newParentId: undefinedId,
});
addAction('newClientOnSameAccount', async ({ welcomeNoteCount }) => {
logger.info(`Syncing a new client on the same account ${welcomeNoteCount > 0 ? `(with ${welcomeNoteCount} initial notes)` : ''}`);
const createClientInitialNotes = async (client: Client) => {
if (welcomeNoteCount === 0) return;
// Create a new folder. Usually, new clients have a default set of
// welcome notes when first syncing.
const parentFolder = await client.createRandomFolder({ parentId: '', quiet: false });
for (let i = 0; i < welcomeNoteCount; i++) {
await client.createRandomNote({ parentId: parentFolder.id });
}
};
await client.sync();
const other = await clientPool.newWithSameAccount(client);
await createClientInitialNotes(other);
// Sometimes, a delay is needed between client creation
// and initial sync. Retry the initial sync and the checkState
// on failure:
await retryWithCount(async () => {
await other.sync();
await other.checkState();
}, {
delayOnFailure: (count) => Second * count,
count: 3,
onFail: async (error) => {
logger.warn('other.sync/other.checkState failed with', error, 'retrying...');
},
});
await client.sync();
return true;
}, {
welcomeNoteCount: () => context.randInt(0, 30),
});
addAction('removeClientsOnSameAccount', async () => {
const others = clientPool.othersWithSameAccount(client);
if (others.length === 0) return false;
for (const otherClient of others) {
assert.notEqual(otherClient, client);
await otherClient.close();
}
return true;
}, {});
addAction('createOrUpdateMany', async ({ count }) => {
await client.createOrUpdateMany(count);
return true;
}, {
count: () => context.randInt(1, 512),
});
addAction('publishNote', async ({ id }) => {
const note = id ? noteById(id) : await client.randomNote({
includeReadOnly: true,
});
if (!note || note.published) return false;
await client.publishNote(note.id);
return true;
}, {
id: undefinedId,
});
addAction('unpublishNote', async ({ id }) => {
const note = id ? noteById(id) : await client.randomNote({ includeReadOnly: true });
if (!note || !note.published) return false;
await client.unpublishNote(note.id);
return true;
}, { id: undefinedId });
addAction('sync', async () => {
await client.sync();
return true;
}, {});
addAction('comment', async ({ message }) => {
logger.info(`Action: Comment: ${JSON.stringify(message)}`);
return true;
}, { message: () => '' });
return { actions, schema, addAction };
};

View File

@@ -10,11 +10,43 @@ interface ClientInfo {
email: string;
}
interface ActionLogEntry {
action: string;
source: string;
}
class ActionTracker {
private idToActionLog_: Map<ItemId, ActionLogEntry[]> = new Map();
private idToItem_: Map<ItemId, TreeItem> = new Map();
private tree_: Map<string, ClientData> = new Map();
public constructor(private readonly context_: FuzzContext) {}
public getActionLog(id: ItemId) {
return [...(this.idToActionLog_.get(id) ?? [])];
}
public printActionLog(id: ItemId) {
const logEntries = this.getActionLog(id);
if (logEntries.length === 0) {
process.stdout.write('N/A\n');
return;
}
const log = logEntries
.map(item => `in:${item.source}: ${item.action}`)
.join('\n');
process.stdout.write(`${log}\n`);
}
private logAction_(item: ItemId|TreeItem, action: string, source: string) {
const itemId = typeof item === 'string' ? item : item.id;
const log = this.idToActionLog_.get(itemId) ?? [];
this.idToActionLog_.set(itemId, log);
log.push({ action, source });
}
private getToplevelParent_(item: ItemId|TreeItem) {
let itemId = typeof item === 'string' ? item : item.id;
const originalItemId = itemId;
@@ -99,6 +131,14 @@ class ActionTracker {
});
}
const logAction = (item: ItemId|TreeItem, action: string) => {
this.logAction_(item, action, clientId);
};
const updateItem = (id: ItemId, newValue: TreeItem, changeLabel: string) => {
logAction(id, changeLabel);
this.idToItem_.set(id, newValue);
};
const getChildIds = (itemId: ItemId) => {
const item = this.idToItem_.get(itemId);
if (!item || !isFolder(item)) return [];
@@ -207,10 +247,12 @@ class ActionTracker {
}
this.idToItem_.delete(id);
logAction(id, 'removed');
} else {
const idIsUnused = removeRootItem(item.id);
if (idIsUnused) {
this.idToItem_.delete(id);
logAction(id, 'removed');
}
}
@@ -223,8 +265,9 @@ class ActionTracker {
}
}
};
const mapItems = <T> (map: (item: TreeItem)=> T) => {
const workList: ItemId[] = [...this.tree_.get(clientId).childIds];
const mapItems = <T> (map: (item: TreeItem)=> T, startFolder?: FolderRecord) => {
const startIds: readonly ItemId[] = (startFolder ?? this.tree_.get(clientId)).childIds;
const workList = [...startIds];
const result: T[] = [];
while (workList.length > 0) {
@@ -244,6 +287,8 @@ class ActionTracker {
return result;
};
const descendants = (folder: FolderRecord) => mapItems(item => item, folder);
const getAllFolders = () => {
return mapItems((item): FolderRecord => {
return isFolder(item) ? item : null;
@@ -286,7 +331,13 @@ class ActionTracker {
childIds: otherSubTree.childIds.filter(childId => childId !== id),
});
this.idToItem_.set(id, targetItem.withRemovedFromShare(shareWith.email));
const updateLabel = `remove ${shareWith.email} from share`;
updateItem(
id, targetItem.withRemovedFromShare(shareWith.email), updateLabel,
);
for (const item of descendants(targetItem)) {
logAction(item, updateLabel);
}
this.checkRep_();
};
@@ -297,9 +348,9 @@ class ActionTracker {
assert.ok(!!data.parentId, `note ${data.id} should have a parentId`);
assert.ok(!this.idToItem_.has(data.id), `note ${data.id} should not yet exist`);
this.idToItem_.set(data.id, {
updateItem(data.id, {
...data,
});
}, 'created');
addChild(data.parentId, data.id);
this.checkRep_();
@@ -308,14 +359,26 @@ class ActionTracker {
updateNote: (data: NoteData) => {
assertWriteable(data.parentId);
const oldItem = this.idToItem_.get(data.id);
const oldItem = this.idToItem_.get(data.id) as NoteData;
assert.ok(oldItem, `note ${data.id} should exist`);
assert.ok(!!data.parentId, `note ${data.id} should have a parentId`);
// Additional debugging information about what changed:
const changedFieldsInfo = Object.entries(data)
.filter(([key, newValue]) => {
const itemKey = key as keyof NoteData;
// isShared is a virtual property
return key !== 'isShared'
&& oldItem[itemKey] !== newValue;
})
.map(([key]) => {
return key;
});
removeChild(oldItem.parentId, data.id);
this.idToItem_.set(data.id, {
updateItem(data.id, {
...data,
});
}, `updated (changed fields: ${JSON.stringify(changedFieldsInfo)})`);
addChild(data.parentId, data.id);
this.checkRep_();
@@ -325,14 +388,14 @@ class ActionTracker {
const parentId = data.parentId ?? '';
assertWriteable(parentId);
this.idToItem_.set(data.id, new FolderRecord({
updateItem(data.id, new FolderRecord({
...data,
parentId: parentId ?? '',
childIds: getChildIds(data.id),
sharedWith: [],
ownedByEmail: clientId,
isShared: false,
}));
}), 'created');
addChild(data.parentId, data.id);
this.checkRep_();
@@ -365,6 +428,8 @@ class ActionTracker {
return Promise.resolve();
},
shareFolder: (id: ItemId, shareWith: ClientInfo, options: ShareOptions) => {
assert.notEqual(client.email, shareWith.email, 'Cannot share a folder with the same client');
const itemToShare = this.idToItem_.get(id);
assertIsFolder(itemToShare);
@@ -381,10 +446,17 @@ class ActionTracker {
childIds: [...shareWithChildIds, id],
});
this.idToItem_.set(
id, itemToShare.withShared(shareWith.email, options.readOnly),
updateItem(
id,
itemToShare.withShared(shareWith.email, options.readOnly),
`shared with ${shareWith.email}`,
);
// Additional logging
for (const item of descendants(itemToShare)) {
logAction(item, `ancestor shared with ${shareWith.email}`);
}
this.checkRep_();
return Promise.resolve();
},
@@ -397,7 +469,11 @@ class ActionTracker {
removeFromShare(id, { email: recipient });
}
this.idToItem_.set(id, targetItem.withUnshared());
updateItem(id, targetItem.withUnshared(), 'unshared');
for (const item of descendants(targetItem)) {
logAction(item, 'parent share removed');
}
this.checkRep_();
return Promise.resolve();
@@ -426,9 +502,10 @@ class ActionTracker {
removeChild(item.parentId, itemId);
addChild(newParentId, itemId);
this.idToItem_.set(
updateItem(
itemId,
isFolder(item) ? item.withParent(newParentId) : { ...item, parentId: newParentId },
`moved to id:${newParentId}`,
);
this.checkRep_();
@@ -441,10 +518,10 @@ class ActionTracker {
assert.ok(!oldItem.published, 'should not be published');
this.idToItem_.set(id, {
updateItem(id, {
...oldItem,
published: true,
});
}, 'published');
this.checkRep_();
return Promise.resolve();
@@ -455,10 +532,10 @@ class ActionTracker {
assert.ok(!isFolder(oldItem), 'folders cannot be unpublished');
assert.ok(oldItem.published, 'should be published');
this.idToItem_.set(id, {
updateItem(id, {
...oldItem,
published: false,
});
}, 'unpublished');
this.checkRep_();
return Promise.resolve();
@@ -525,6 +602,13 @@ class ActionTracker {
const noteIndex = this.context_.randInt(0, notes.length);
return notes.length ? notes[noteIndex] : null;
},
// Note: Does not verify that the current client has access to the item
itemById: (id: ItemId) => {
const item = this.idToItem_.get(id);
if (!item) throw new Error(`No item found with ID ${id}`);
return item;
},
};
return tracker;
}

View File

@@ -22,6 +22,7 @@ import Stream = require('stream');
import ProgressBar from './utils/ProgressBar';
import logDiffDebug from './utils/logDiffDebug';
import { NoteEntity } from '@joplin/lib/services/database/types';
import diffSortedStringArrays from './utils/diffSortedStringArrays';
const logger = Logger.create('Client');
@@ -98,6 +99,12 @@ interface CreateOrUpdateOptions {
quiet?: boolean;
}
interface CreateRandomItemOptions extends CreateOrUpdateOptions {
parentId: ItemId;
id?: ItemId;
quiet?: boolean;
}
class Client implements ActionableClient {
public readonly email: string;
@@ -251,6 +258,7 @@ class Client implements ActionableClient {
this.childProcess_.close();
this.closed_ = true;
logger.info('Closed client ', this.email);
}
public onClose(listener: OnCloseListener) {
@@ -313,7 +321,8 @@ class Client implements ActionableClient {
this.bufferedChildProcessStderr_ = [];
process.stdout.write('CLI debug session. Enter a blank line or "exit" to exit.\n');
process.stdout.write('To review a transcript of all interactions with this client,\n');
process.stdout.write('enter "[transcript]".\n\n');
process.stdout.write('enter "[transcript]". To log information about a particular item\n');
process.stdout.write('enter "[item:...id here...]".\n\n');
process.stdout.write(cliProcessPromptString);
const isExitRequest = (input: string) => {
@@ -328,6 +337,10 @@ class Client implements ActionableClient {
lastInput = await readline.question('');
if (lastInput === '[transcript]') {
process.stdout.write(`\n\n# Transcript\n\n${this.getTranscript()}\n\n# End transcript\n\n`);
} else if (lastInput.startsWith('[item:') && lastInput.endsWith(']')) {
let id = lastInput.substring('[item:'.length);
id = id.substring(0, id.length - 1);
this.globalActionTracker_.printActionLog(id);
} else if (!isExitRequest(lastInput)) {
this.childProcess_.writeStdin(`${lastInput}\n`);
}
@@ -471,11 +484,11 @@ class Client implements ActionableClient {
let parentId = (await this.randomFolder({ includeReadOnly: false }))?.id;
const createSubfolder = this.context_.randInt(0, 100) < 10;
if (!parentId || createSubfolder) {
const folder = await this.createRandomFolder(parentId, { quiet: true });
const folder = await this.createRandomFolder({ parentId, quiet: true });
parentId = folder.id;
}
await this.createRandomNote(parentId, { quiet: true });
await this.createRandomNote({ parentId, quiet: true });
},
update: async (targetNote: NoteData) => {
const keep = targetNote.body.substring(
@@ -513,16 +526,15 @@ class Client implements ActionableClient {
bar.complete();
}
public async createRandomFolder(parentId: ItemId, options: CreateOrUpdateOptions) {
public async createRandomFolder({ quiet, parentId, id }: CreateRandomItemOptions) {
const titleLength = this.context_.randInt(1, 128);
const folderId = uuid.create();
const folder = {
parentId: parentId,
id: folderId,
id: id ?? uuid.create(),
title: this.context_.randomString(titleLength).replace(/\n/g, ' '),
};
await this.createFolder(folder, options);
await this.createFolder(folder, { quiet });
return folder;
}
@@ -582,11 +594,14 @@ class Client implements ActionableClient {
logDiffDebug(lastActualNote.title, expected.title);
logDiffDebug(lastActualNote.body, expected.body);
}
// Log all transactions associated with the item
this.globalActionTracker_.printActionLog(expected.id);
throw error;
}
}
public async createRandomNote(parentId: string, { quiet = false }: CreateOrUpdateOptions = { }) {
public async createRandomNote({ parentId, id, quiet = false }: CreateRandomItemOptions) {
const titleLength = this.context_.randInt(0, 256);
const bodyLength = this.context_.randInt(0, 2000);
await this.createNote({
@@ -594,7 +609,7 @@ class Client implements ActionableClient {
parentId,
title: this.context_.randomString(titleLength),
body: this.context_.randomString(bodyLength),
id: uuid.create(),
id: id ?? uuid.create(),
}, { quiet });
}
@@ -793,6 +808,10 @@ class Client implements ActionableClient {
return this.tracker_.randomNote(options);
}
public itemById(itemId: ItemId) {
return this.tracker_.itemById(itemId);
}
public async checkState() {
logger.info('Check state', this.label);
@@ -814,6 +833,35 @@ class Client implements ActionableClient {
}
};
const assertSameIds = (actualSorted: ItemSlice[], expectedSorted: ItemSlice[], testLabel: string) => {
const actualIds = actualSorted.map(i => i.id);
const expectedIds = expectedSorted.map(i => i.id);
const { missing, unexpected } = diffSortedStringArrays(actualIds, expectedIds);
if (missing.length || unexpected.length) {
const idLogs = (ids: string[]) => {
const output = [];
for (const id of ids) {
const log = this.globalActionTracker_.getActionLog(id);
output.push(`\nid:${id}`);
output.push(log.map(item => `\t${item.source}: ${item.action}`).join('\n'));
}
return output.join('\n');
};
throw new Error([
`IDs were different (${testLabel}):`,
missing.length && `- Expected ${JSON.stringify(missing)} to be present, but were missing.`,
unexpected.length && `- Present but should not have been: ${JSON.stringify(unexpected)}`,
'\n',
'Logs:',
idLogs(missing),
idLogs(unexpected),
].filter(line => !!line).join('\n'));
}
};
const checkNoteState = async () => {
const notes = [...await this.listNotes()];
const expectedNotes = [...await this.tracker_.listNotes()];
@@ -823,6 +871,7 @@ class Client implements ActionableClient {
assertNoAdjacentEqualIds(notes, 'notes');
assertNoAdjacentEqualIds(expectedNotes, 'expectedNotes');
assertSameIds(notes, expectedNotes, 'should have the same note IDs');
assert.deepEqual(notes, expectedNotes, 'should have the same notes as the expected state');
};
@@ -835,6 +884,7 @@ class Client implements ActionableClient {
assertNoAdjacentEqualIds(folders, 'folders');
assertNoAdjacentEqualIds(expectedFolders, 'expectedFolders');
assertSameIds(folders, expectedFolders, 'should have the same folder IDs');
assert.deepEqual(folders, expectedFolders, 'should have the same folders as the expected state');
};

View File

@@ -41,7 +41,7 @@ export default class ClientPool {
});
}
public async createInitialItemsAndSync() {
public async createRandomInitialItemsAndSync() {
for (const client of this.clients) {
logger.info('Creating items for ', client.email);
const actionCount = this.context_.randomFrom([0, 10, 100]);
@@ -55,6 +55,18 @@ export default class ClientPool {
return this.clients.filter(client => client.email === email);
}
public clientById(id: number) {
const client = this.clients[id];
if (!client) throw new Error(`Not found: ${id}`);
return client;
}
public getClientId(client: Client): number {
const index = this.clients.indexOf(client);
if (index === -1) throw new Error(`Not found: ${client}`);
return index;
}
public randomClient(filter: ClientFilter = ()=>true) {
const clients = this.clients_.filter(filter);
return clients[

View File

@@ -1,271 +0,0 @@
import uuid from '@joplin/lib/uuid';
import Client from './Client';
import ClientPool from './ClientPool';
import { FuzzContext } from './types';
import { strict as assert } from 'assert';
import Logger from '@joplin/utils/Logger';
import retryWithCount from './utils/retryWithCount';
import { Second } from '@joplin/utils/time';
const logger = Logger.create('doRandomAction');
const doRandomAction = async (context: FuzzContext, client: Client, clientPool: ClientPool) => {
const selectOrCreateParentFolder = async () => {
let parentId = (await client.randomFolder({ includeReadOnly: false }))?.id;
// Create a toplevel folder to serve as this
// folder's parent if none exist yet
if (!parentId) {
parentId = uuid.create();
await client.createFolder({
parentId: '',
id: parentId,
title: 'Parent folder',
});
}
return parentId;
};
const defaultNoteProperties = {
published: false,
};
const selectOrCreateWriteableNote = async () => {
const options = { includeReadOnly: false };
let note = await client.randomNote(options);
if (!note) {
await client.createNote({
...defaultNoteProperties,
parentId: await selectOrCreateParentFolder(),
id: uuid.create(),
title: 'Test note',
body: 'Body',
});
note = await client.randomNote(options);
assert.ok(note, 'should have selected a random note');
}
return note;
};
const actions = {
newSubfolder: async () => {
const parentId = await selectOrCreateParentFolder();
await client.createRandomFolder(parentId, { quiet: false });
return true;
},
newToplevelFolder: async () => {
await client.createRandomFolder('', { quiet: false });
return true;
},
newNote: async () => {
const parentId = await selectOrCreateParentFolder();
await client.createRandomNote(parentId);
return true;
},
renameNote: async () => {
const note = await selectOrCreateWriteableNote();
await client.updateNote({
...note,
title: `Renamed (${context.randInt(0, 1000)})`,
});
return true;
},
updateNoteBody: async () => {
const note = await selectOrCreateWriteableNote();
await client.updateNote({
...note,
body: `${note.body}\n\nUpdated.\n`,
});
return true;
},
moveNote: async () => {
const note = await selectOrCreateWriteableNote();
const targetParent = await client.randomFolder({
filter: folder => folder.id !== note.parentId,
includeReadOnly: false,
});
if (!targetParent) return false;
await client.moveItem(note.id, targetParent.id);
return true;
},
deleteNote: async () => {
const target = await client.randomNote({ includeReadOnly: false });
if (!target) return false;
await client.deleteNote(target.id);
return true;
},
shareFolder: async () => {
const other = clientPool.randomClient(c => !c.hasSameAccount(client));
if (!other) return false;
const target = await client.randomFolder({
filter: candidate => {
const isToplevel = !candidate.parentId;
const ownedByCurrent = candidate.ownedByEmail === client.email;
const alreadyShared = isToplevel && candidate.isSharedWith(other.email);
return isToplevel && ownedByCurrent && !alreadyShared;
},
includeReadOnly: true,
});
if (!target) return false;
const readOnly = context.randInt(0, 2) === 1 && context.isJoplinCloud;
await client.shareFolder(target.id, other, { readOnly });
return true;
},
unshareFolder: async () => {
const target = await client.randomFolder({
filter: candidate => {
return candidate.isRootSharedItem && candidate.ownedByEmail === client.email;
},
includeReadOnly: true,
});
if (!target) return false;
const recipientIndex = context.randInt(-1, target.shareRecipients.length);
if (recipientIndex === -1) { // Completely remove the share
await client.deleteAssociatedShare(target.id);
} else {
const recipientEmail = target.shareRecipients[recipientIndex];
const recipient = clientPool.clientsByEmail(recipientEmail)[0];
assert.ok(recipient, `invalid state -- recipient ${recipientEmail} should exist`);
await client.removeFromShare(target.id, recipient);
}
return true;
},
deleteFolder: async () => {
const target = await client.randomFolder({ includeReadOnly: false });
if (!target) return false;
await client.deleteFolder(target.id);
return true;
},
moveFolderToToplevel: async () => {
const target = await client.randomFolder({
// Don't choose items that are already toplevel
filter: item => !!item.parentId,
includeReadOnly: false,
});
if (!target) return false;
await client.moveItem(target.id, '');
return true;
},
moveFolderTo: async () => {
const target = await client.randomFolder({
// Don't move shared folders (should not be allowed by the GUI in the main apps).
filter: item => !item.isRootSharedItem,
includeReadOnly: false,
});
if (!target) return false;
const targetDescendants = new Set(await client.allFolderDescendants(target.id));
const newParent = await client.randomFolder({
filter: (item) => {
// Avoid making the folder a child of itself
return !targetDescendants.has(item.id);
},
includeReadOnly: false,
});
if (!newParent) return false;
await client.moveItem(target.id, newParent.id);
return true;
},
newClientOnSameAccount: async () => {
const welcomeNoteCount = context.randInt(0, 30);
logger.info(`Syncing a new client on the same account ${welcomeNoteCount > 0 ? `(with ${welcomeNoteCount} initial notes)` : ''}`);
const createClientInitialNotes = async (client: Client) => {
if (welcomeNoteCount === 0) return;
// Create a new folder. Usually, new clients have a default set of
// welcome notes when first syncing.
const parentFolder = await client.createRandomFolder('', { quiet: false });
for (let i = 0; i < welcomeNoteCount; i++) {
await client.createRandomNote(parentFolder.id);
}
};
await client.sync();
const other = await clientPool.newWithSameAccount(client);
await createClientInitialNotes(other);
// Sometimes, a delay is needed between client creation
// and initial sync. Retry the initial sync and the checkState
// on failure:
await retryWithCount(async () => {
await other.sync();
await other.checkState();
}, {
delayOnFailure: (count) => Second * count,
count: 3,
onFail: async (error) => {
logger.warn('other.sync/other.checkState failed with', error, 'retrying...');
},
});
await client.sync();
return true;
},
removeClientsOnSameAccount: async () => {
const others = clientPool.othersWithSameAccount(client);
if (others.length === 0) return false;
for (const otherClient of others) {
assert.notEqual(otherClient, client);
await otherClient.close();
}
return true;
},
createOrUpdateMany: async () => {
await client.createOrUpdateMany(context.randInt(1, 512));
return true;
},
publishNote: async () => {
const note = await client.randomNote({
includeReadOnly: true,
});
if (!note || note.published) return false;
await client.publishNote(note.id);
return true;
},
unpublishNote: async () => {
const note = await client.randomNote({ includeReadOnly: true });
if (!note || !note.published) return false;
await client.unpublishNote(note.id);
return true;
},
};
const actionKeys = [...Object.keys(actions)] as (keyof typeof actions)[];
let result = false;
while (!result) { // Loop until an action was done
const randomAction = context.randomFrom(actionKeys);
logger.info(`Action: ${randomAction} in ${client.email}`);
result = await actions[randomAction]();
if (!result) {
logger.info(` ${randomAction} was skipped (preconditions not met).`);
}
}
};
export default doRandomAction;

View File

@@ -0,0 +1,26 @@
{
"clientCount": 2,
"actions": [
["switchClient", { "id": 0 }],
"sync",
"// Switch to the second client & share a folder",
["switchClient", { "id": 1 }],
["newToplevelFolder", { "id": "11111111111111111111111111111112" }],
["newNote", { "id": "11111111111111111111111111111113", "parentId": "11111111111111111111111111111112" }],
["shareFolder", { "folderId": "11111111111111111111111111111112", "otherClientId": 0, "readOnly": false }],
"syncAndCheckState",
"// Switch to the first client & remove a note from the folder",
["switchClient", { "id": 0 }],
["newToplevelFolder", { "id": "11111111111111111111111111111111" }],
"syncAndCheckState",
["moveNote", { "noteId": "11111111111111111111111111111113", "targetFolderId": "11111111111111111111111111111111" }],
"syncAndCheckState",
"// Switch back to the second client and add a new client to the same account",
["switchClient", { "id": 0 }],
"newClientOnSameAccount"
]
}

View File

@@ -5,15 +5,14 @@ import Logger, { TargetType } from '@joplin/utils/Logger';
import Server from './Server';
import { CleanupTask, FuzzContext } from './types';
import ClientPool from './ClientPool';
import retryWithCount from './utils/retryWithCount';
import SeededRandom from './utils/SeededRandom';
import { env } from 'process';
import yargs = require('yargs');
import openDebugSession from './utils/openDebugSession';
import { Second } from '@joplin/utils/time';
import { packagesDir } from './constants';
import doRandomAction from './doRandomAction';
import ActionRunner, { ActionSpec } from './ActionRunner';
import randomString from './utils/randomString';
import { readFile } from 'fs/promises';
const { shimInit } = require('@joplin/lib/shim-init-node');
const globalLogger = new Logger();
@@ -48,6 +47,7 @@ interface Options {
randomStrings: boolean;
clientCount: number;
keepAccountsOnClose: boolean;
setupActions: ActionSpec[];
serverPath: string;
isJoplinCloud: boolean;
@@ -137,11 +137,16 @@ const main = async (options: Options) => {
options.clientCount,
task => { cleanupTasks.push(task); },
);
await clientPool.createInitialItemsAndSync();
const actionRunner = new ActionRunner(fuzzContext, clientPool, clientPool.randomClient());
logger.info('Starting setup:');
await actionRunner.doActions(options.setupActions);
logger.info('Starting randomized actions:');
const maxSteps = options.maximumSteps;
for (let stepIndex = 1; maxSteps <= 0 || stepIndex <= maxSteps; stepIndex++) {
const client = clientPool.randomClient();
actionRunner.switchClient(client);
// Ensure that the client starts up-to-date with the other synced clients.
await client.sync();
@@ -152,23 +157,10 @@ const main = async (options: Options) => {
if (actionsBeforeFullSync > 1) {
logger.info('Sub-step', subStepIndex, '/', actionsBeforeFullSync, '(in step', stepIndex, ')');
}
await doRandomAction(fuzzContext, client, clientPool);
await actionRunner.doRandomAction();
}
await client.sync();
// .checkState can fail occasionally due to incomplete
// syncs (perhaps because the server is still processing
// share-related changes?). Allow this to be retried:
await retryWithCount(async () => {
await clientPool.checkState();
}, {
count: 4,
delayOnFailure: count => count * Second * 2,
onFail: async () => {
logger.info('.checkState failed. Syncing all clients...');
await clientPool.syncAll();
},
});
await actionRunner.syncAndCheckState();
}
} catch (error) {
logger.error('ERROR', error);
@@ -185,6 +177,64 @@ const main = async (options: Options) => {
}
};
const readSetupFile = async (path: string) => {
const setupActionFile = await readFile(path, 'utf-8');
const setupData = JSON.parse(setupActionFile);
const errorLabel = `Reading ${path}.`;
const readNumber = <T extends object> (key: keyof T, parent: T) => {
if (typeof parent[key] !== 'number') {
throw new Error(`${errorLabel} Expected key ${String(key)} to be a number. Was ${typeof parent[key]}.`);
}
return parent[key];
};
const readArray = <T extends object> (key: keyof T, parent: T) => {
if (!Array.isArray(parent[key])) {
throw new Error(`${errorLabel} Expected key ${String(key)} to be an array. Was ${typeof parent[key]}.`);
}
return parent[key];
};
const clientCount = readNumber('clientCount', setupData);
const initialActions: unknown[] = readArray('actions', setupData);
const actions: ActionSpec[] = initialActions
.map((action: unknown, index: number) => {
if (typeof action === 'string') {
const isComment = action.startsWith('//');
if (isComment) {
return { key: 'comment', options: { message: action.substring(2).trimStart() } };
}
return { key: action, options: {} };
}
if (!Array.isArray(action) || action.length < 1 || action.length > 2) {
throw new Error(`${errorLabel} In 'actions'. Expected an array of length 1 or 2. (Reading item ${JSON.stringify(action)} at index: ${index})`);
}
const key = action[0];
const options = action[1] ?? {};
return { key, options } as ActionSpec;
});
return { clientCount, setupActions: actions };
};
const defaultSetupActions = (clientCount: number) => {
const actions = [];
for (let i = 0; i < clientCount; i++) {
actions.push(
{ key: 'switchClient', options: { id: i } },
{ key: 'createOrUpdateMany', options: {} },
{ key: 'sync', options: {} },
);
}
return actions;
};
void yargs
.usage('$0 <cmd>')
@@ -239,22 +289,39 @@ void yargs
'This also enables testing for some Joplin Cloud-specific features (e.g. read-only shares).',
].join(''),
},
'setup': {
type: 'string',
default: '',
defaultDescription: [
'A path: If provided, this should point to a JSON file containing actions to run during startup. ',
'The JSON file should contain an object similar to { "actions": [ ["newNote", {}] ], "clientCount": 1 }.',
].join(''),
},
});
},
async (argv) => {
const serverPath = argv.joplinCloud ? argv.joplinCloud : join(packagesDir, 'server');
let setupData = undefined;
if (argv.setup) {
setupData = await readSetupFile(argv.setup);
}
const clientCount = setupData?.clientCount ?? argv.clients;
await main({
seed: argv.seed,
maximumSteps: argv.steps,
clientCount: argv.clients,
clientCount,
serverPath: serverPath,
isJoplinCloud: !!argv.joplinCloud,
maximumStepsBetweenSyncs: argv['steps-between-syncs'],
keepAccountsOnClose: argv.keepAccounts,
enableE2ee: argv.enableE2ee,
randomStrings: argv.randomStrings,
setupActions: setupData?.setupActions ?? defaultSetupActions(clientCount),
});
},
)
.demandCommand()
.help()
.argv;

View File

@@ -42,6 +42,10 @@ export const assertIsFolder: (item: TreeItem)=> asserts item is FolderRecord = i
throw new Error(`Expected item with ID ${item?.id} to be a folder.`);
}
};
export const assertIsNote: (item: TreeItem)=> asserts item is NoteData = item => {
if (!item) throw new Error(`Item ${item} is not a note`);
if (isFolder(item)) throw new Error(`Expected item with ID ${item?.id} to be a note.`);
};
export interface FuzzContext {
serverUrl: string;
@@ -88,6 +92,7 @@ export interface ActionableClient {
allFolderDescendants(parentId: ItemId): Promise<ItemId[]>;
randomFolder(options: RandomFolderOptions): Promise<FolderRecord>;
randomNote(options: RandomNoteOptions): Promise<NoteData>;
itemById(id: ItemId): TreeItem;
}
export interface UserData {

View File

@@ -0,0 +1,34 @@
import diffSortedStringArrays from './diffSortedStringArrays';
describe('diffSortedStringArrays', () => {
test.each([
[
['a'],
[],
{ unexpected: ['a'], missing: [] },
],
[
['a', 'b'],
['a', 'b'],
{ unexpected: [], missing: [] },
],
[
[],
['a'],
{ unexpected: [], missing: ['a'] },
],
[
['a'],
['b', 'c'],
{ unexpected: ['a'], missing: ['b', 'c'] },
],
[
['a', 'b', 'c', 'd', 'e', 'f'],
['a', 'e'],
{ unexpected: ['b', 'c', 'd', 'f'], missing: [] },
],
])('should diff string arrays: %j, %j', (actual, expected, diff) => {
expect(diffSortedStringArrays(actual, expected)).toEqual(diff);
});
});

View File

@@ -0,0 +1,58 @@
// Input arrays must be sorted
const diffSortedStringArrays = (actual: string[], expected: string[]) => {
const missing = [];
const unexpected = [];
let indexActual = 0;
let indexExpected = 0;
for (;
indexActual < actual.length && indexExpected < expected.length;
indexActual++, indexExpected++
) {
const itemActual = actual[indexActual];
const itemExpected = expected[indexExpected];
if (itemActual !== itemExpected) {
let found = false;
// Case 1: The expected item is present eventually, after a few unexpected item:
for (let i = indexActual + 1; i < actual.length; i++) {
if (actual[i] === itemExpected) {
// Everything before the found item shouldn't have been present:
unexpected.push(actual.slice(indexActual, i));
indexActual = i;
found = true;
break;
}
}
// Case 2: The expected item wasn't present at all:
if (!found) {
missing.push(itemExpected);
// Revisit the current item in "actual" on the next loop:
indexActual --;
}
}
}
// Handle any unexpected items at the end
if (indexActual < actual.length) {
unexpected.push(actual.slice(indexActual));
}
if (indexExpected < expected.length) {
missing.push(expected.slice(indexExpected));
}
return {
// Items that were present in the actual state, but are missing from the expected state
unexpected: unexpected.flat(),
// Items that were present in the expected state, but missing from the actual state.
missing: missing.flat(),
};
};
export default diffSortedStringArrays;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -7,6 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: Türkçe\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
"Last-Translator: Arda Kılıçdağı <arda@kilicdagi.com>\n"
"Language-Team: Turkish (Turkey)\n"
"Language: tr_TR\n"
@@ -328,7 +330,7 @@ msgstr "A5"
#: packages/lib/models/settings/builtInMetadata.ts:1121
msgid "ABC musical notation: Options"
msgstr ""
msgstr "ABC müzik notasyonu: Seçenekler"
#: packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.tsx:62
#: packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.tsx:240
@@ -424,7 +426,7 @@ msgstr "Gövde Ekle"
#: packages/editor/ProseMirror/plugins/tablePlugin.ts:31
msgid "Add column"
msgstr ""
msgstr "Sütun ekle"
#: packages/app-mobile/components/buttons/FloatingActionButton.tsx:66
#: packages/app-mobile/components/ComboBox.tsx:103
@@ -441,9 +443,8 @@ msgid "Add recipient:"
msgstr "Alıcı ekle:"
#: packages/editor/ProseMirror/plugins/tablePlugin.ts:26
#, fuzzy
msgid "Add row"
msgstr "Yeni bir şey ekle"
msgstr "Satır ekle"
#: packages/app-mobile/components/TagEditor.tsx:281
msgid "Add tags:"
@@ -801,7 +802,7 @@ msgstr "Beta"
#: packages/app-mobile/components/NoteEditor/commandDeclarations.ts:123
msgid "Block code"
msgstr ""
msgstr "Blok kod"
#: packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.ts:55
#: packages/app-mobile/components/NoteEditor/commandDeclarations.ts:80
@@ -919,9 +920,8 @@ msgid "Cannot change encrypted item"
msgstr "Şifrelenmiş öğe değiştirilemiyor"
#: packages/lib/commands/convertNoteToMarkdown.ts:42
#, fuzzy
msgid "Cannot convert read-only item: \"%s\""
msgstr "Yeni bir not oluşturulamadı: %s"
msgstr "Salt okunur öğe dönüştürülemiyor: %s"
#: packages/lib/models/Note.ts:622
msgid "Cannot copy note to \"%s\" notebook"
@@ -1077,7 +1077,7 @@ msgstr "Kontrol ediliyor... Lütfen bekleyin."
#: packages/app-desktop/gui/NoteContentPropertiesDialog.tsx:114
msgid "Chinese/Japanese/Korean characters"
msgstr ""
msgstr "Çince/Japonca/Korece karakterler"
#: packages/app-mobile/components/screens/Note/commands/attachFile.ts:98
msgid "Choose an option"
@@ -1244,7 +1244,7 @@ msgstr "Komut"
#: packages/app-cli/app/command-keymap.ts:30
msgid "COMMAND"
msgstr ""
msgstr "KOMUT"
#: packages/app-desktop/plugins/GotoAnything.tsx:783
msgid "Command palette"
@@ -1309,9 +1309,8 @@ msgid "Configuration"
msgstr "Yapılandırma"
#: packages/app-cli/app/command-keymap.ts:24
#, fuzzy
msgid "Configured keyboard shortcuts:"
msgstr "Klavye Kısayolları"
msgstr "Ayarlanmış klavye Kısayolları:"
#: packages/lib/models/settings/builtInMetadata.ts:1296
msgid "Configures the size of scrollbars used in the app."
@@ -1386,9 +1385,8 @@ msgid "Convert it"
msgstr "Dönüştür"
#: packages/lib/commands/convertNoteToMarkdown.ts:18
#, fuzzy
msgid "Convert to Markdown"
msgstr "Notu Markdown'a dönüştür"
msgstr "Markdown'a dönüştür"
#: packages/app-mobile/components/screens/Note/Note.tsx:1350
msgid "Convert to note"
@@ -1494,9 +1492,8 @@ msgid "Could not connect to plugin repository."
msgstr "Eklenti sunucusuna bağlanılamadı."
#: packages/lib/commands/convertNoteToMarkdown.ts:70
#, fuzzy
msgid "Could not convert notes to Markdown: %s"
msgstr "Not Markdown'a dönüştürülemedi: %s"
msgstr "Notlar Markdown'a dönüştürülemedi: %s"
#: packages/app-desktop/InteropServiceHelper.ts:235
msgid "Could not export notes: %s"
@@ -1767,9 +1764,8 @@ msgid "Delete attachment \"%s\"?"
msgstr "\"%s\" eki silinsin mi?"
#: packages/editor/ProseMirror/plugins/tablePlugin.ts:41
#, fuzzy
msgid "Delete column"
msgstr "Satırı sil"
msgstr "Sütunu sil"
#: packages/server/src/services/TaskService.ts:27
msgid "Delete expired sessions"
@@ -1810,19 +1806,19 @@ msgid "Delete profile \"%s\""
msgstr "\"%s\" profilini sil"
#: packages/app-desktop/gui/ProfileEditor.tsx:147
#, fuzzy
msgid ""
"Delete profile \"%s\"?\n"
"\n"
"All data, including notes, notebooks and tags will be permanently deleted."
msgstr ""
"Not defterleri, etiketler ve notlar da dahil olmak üzere tüm veriler kalıcı "
"olarak silinecek."
"“%s” profili silinsin mi?\n"
"\n"
"Notlar, not defterleri ve etiketler dâhil tüm veriler kalıcı olarak "
"silinecektir."
#: packages/editor/ProseMirror/plugins/tablePlugin.ts:36
#, fuzzy
msgid "Delete row"
msgstr "Notu sil"
msgstr "Satırı sil"
#: packages/app-mobile/components/ScreenHeader/index.tsx:487
msgid "Delete selected notes"
@@ -1835,6 +1831,8 @@ msgid ""
"All notes associated with this tag will remain, but the tag will be removed "
"from all notes."
msgstr ""
"\"%s\" etiketini silmek ister misiniz? Bu etiketle ilişkili tüm notlar "
"kalacak, ancak etiket tüm notlardan kaldırılacaktır."
#: packages/app-desktop/gui/WindowCommandsAndDialogs/commands/deleteFolder.ts:38
#: packages/app-mobile/components/side-menu-content.tsx:414
@@ -2017,7 +2015,7 @@ msgstr "Notla ilgili tüm bilgileri görüntüler."
#: packages/app-cli/app/command-keymap.ts:14
msgid "Displays the configured keyboard shortcuts."
msgstr ""
msgstr "Yapılandırılmış klavye kısayollarını görüntüler."
#: packages/app-cli/app/command-cat.ts:14
msgid "Displays the given note."
@@ -2237,9 +2235,8 @@ msgid "Edit profile configuration..."
msgstr "Profil ayarlarını düzenle…"
#: packages/app-mobile/components/screens/tags.tsx:64
#, fuzzy
msgid "Edit tag"
msgstr "Notu düzenle."
msgstr "Etiketi düzenle"
#: packages/app-desktop/gui/MainScreen.tsx:129
#: packages/app-desktop/gui/NoteContentPropertiesDialog.tsx:151
@@ -2349,9 +2346,8 @@ msgid "Enable abbreviation syntax"
msgstr "Kısaltma söz dizimini etkinleştir"
#: packages/lib/models/settings/builtInMetadata.ts:1093
#, fuzzy
msgid "Enable ABC musical notation support"
msgstr "Fountain söz dizimi desteğini etkinleştir"
msgstr "ABC müzik notasyon desteğini etkinleştir"
#: packages/lib/models/settings/builtInMetadata.ts:1095
msgid "Enable audio player"
@@ -3133,9 +3129,8 @@ msgid "Import or export your data"
msgstr "Verini içeri veya dışarı aktar"
#: packages/app-desktop/gui/WindowCommandsAndDialogs/commands/importFrom.ts:20
#, fuzzy
msgid "Import..."
msgstr "Dışa aktarılıyor..."
msgstr "İçe aktar"
#: packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteImportButton.tsx:76
msgid "Imported successfully!"
@@ -3515,7 +3510,7 @@ msgstr "Not tarihçesini şu kadar süre tut"
#: packages/lib/models/settings/builtInMetadata.ts:2037
msgid "Keep notes in the trash for"
msgstr "Çöp kutusunda notları şu kadar süre tut: "
msgstr "Çöp kutusunda notları şu kadar süre tut:"
#: packages/lib/models/settings/builtInMetadata.ts:1470
msgid "Keyboard Mode"
@@ -3535,7 +3530,7 @@ msgstr "Keychain Desteği: %s"
#: packages/app-cli/app/command-keymap.ts:30
msgid "KEYS"
msgstr ""
msgstr "TUŞLAR"
#: packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.tsx:74
msgid "Keys that need upgrading"
@@ -3763,6 +3758,8 @@ msgid ""
"Manage your profiles. You can rename or delete profiles. The active profile "
"cannot be deleted."
msgstr ""
"Profillerini yönet. Profilleri yeniden adlandırabilir veya silebilirsin. O "
"an kullanılan aktif profil silinemez."
#. `generate-ppk`
#: packages/app-cli/app/command-e2ee.ts:19
@@ -3923,7 +3920,6 @@ msgstr[0] "%d not \"%s\" not defterine taşınsın mı?"
msgstr[1] "%d not \"%s\" not defterine taşınsın mı?"
#: packages/app-desktop/gui/WindowCommandsAndDialogs/commands/deleteFolder.ts:34
#, fuzzy
msgid ""
"Move %d notebooks to the trash?\n"
"\n"
@@ -4174,7 +4170,7 @@ msgstr "Güncelleme bulunamadı"
#: packages/lib/components/shared/SamlShared.ts:12
msgid "No URL for SAML authentication set."
msgstr ""
msgstr "URL veya SAML doğrulaması ayarlanmamış."
#: packages/app-cli/app/command-share.ts:188
#: packages/app-cli/app/command-share.ts:208
@@ -4516,7 +4512,7 @@ msgstr "Senkronizasyon Sihirbazını Aç…"
#: packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx:635
msgid "Open-source licences"
msgstr ""
msgstr "Açık kaynaklı lisanslar"
#: packages/app-desktop/gui/NoteEditor/utils/contextMenu.ts:87
msgid "Open..."
@@ -4555,6 +4551,8 @@ msgid ""
"Options that should be used whenever rendering ABC code. It must be a JSON5 "
"object. The full list of options is available at: %s"
msgstr ""
"ABC kodu her işlendiğinde kullanılacak seçenekler. Bir JSON5 nesnesi "
"olmalıdır. Seçeneklerin tam listesi şu adreste mevcuttur: %s"
#: packages/app-mobile/components/NoteEditor/commandDeclarations.ts:103
msgid "Ordered list"
@@ -4920,9 +4918,8 @@ msgid "Profile name"
msgstr "Profil adı"
#: packages/app-desktop/gui/ProfileEditor.tsx:120
#, fuzzy
msgid "Profile name cannot be empty"
msgstr "Şifre boş olamaz"
msgstr "Profil adı boş olamaz"
#: packages/app-desktop/gui/ProfileEditor.tsx:116
#: packages/app-desktop/gui/WindowCommandsAndDialogs/commands/addProfile.ts:18
@@ -5115,11 +5112,8 @@ msgid "Remove"
msgstr "Sil"
#: packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.tsx:134
#, fuzzy
msgid "Remove %d tags from all notes? This cannot be undone."
msgstr ""
"Model silinip yeniden indirilsin mi?\n"
"Bu işlem geri alınamaz."
msgstr "Tüm notlardan %d etiket kaldırılsın mı? Bu işlem geri alınamaz."
#: packages/app-mobile/components/TagEditor.tsx:136
msgid "Remove %s"
@@ -5536,7 +5530,7 @@ msgstr "Ana not defterini seç"
#: packages/app-desktop/gui/WindowCommandsAndDialogs/commands/importFrom.ts:42
msgid "Select the type of file to be imported:"
msgstr ""
msgstr "İçe aktarılacak dosya türünü seçin:"
#: packages/app-mobile/components/ComboBox.tsx:378
msgid "Selected: %s"
@@ -5861,7 +5855,7 @@ msgstr "Kaynak: "
#: packages/app-cli/app/command-keymap.ts:35
msgid "SPACE"
msgstr ""
msgstr "ALAN"
#: packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.tsx:456
msgid "Spacer"
@@ -6172,18 +6166,16 @@ msgid "Tab moves focus"
msgstr "Tab odak hareket ettirir"
#: packages/app-mobile/components/NoteEditor/commandDeclarations.ts:118
#, fuzzy
msgid "Table"
msgstr "Etkinleştir"
msgstr "Tablo"
#: packages/lib/models/settings/builtInMetadata.ts:1440
msgid "Tabloid"
msgstr "Tablo"
#: packages/app-mobile/components/screens/tags.tsx:206
#, fuzzy
msgid "Tag: %s"
msgstr "Kullanım: %s"
msgstr "Etiket: %s"
#: packages/app-cli/app/command-import.ts:58
#: packages/app-desktop/gui/ImportScreen.tsx:94
@@ -6404,7 +6396,6 @@ msgid "The note \"%s\" has been successfully restored to the notebook \"%s\"."
msgstr "“%s” notu başarılı bir şekilde “%s” not defterine geri yüklendi."
#: packages/lib/commands/convertNoteToMarkdown.ts:64
#, fuzzy
msgid ""
"The note has been converted to Markdown and the original note has been moved "
"to the trash"
@@ -6412,7 +6403,8 @@ msgid_plural ""
"The notes have been converted to Markdown and the original notes have been "
"moved to the trash"
msgstr[0] "Not Markdown'a dönüştürüldü ve orijinal not çöp kutusuna taşındı"
msgstr[1] "Not Markdown'a dönüştürüldü ve orijinal not çöp kutusuna taşındı"
msgstr[1] ""
"Notlar Markdown'a dönüştürüldü ve orijinal notlar çöp kutusuna taşındı"
#: packages/app-desktop/gui/TrashNotification/TrashNotification.tsx:45
msgid "The note was successfully moved to the trash."
@@ -6991,7 +6983,7 @@ msgstr "Şimdi dene"
#: packages/app-cli/app/command-keymap.ts:30
msgid "TYPE"
msgstr ""
msgstr "TİP"
#: packages/app-cli/app/command-help.ts:72
msgid ""

View File

@@ -54,7 +54,7 @@
"@types/mustache": "4.2.6",
"@types/node": "18.19.130",
"@types/node-fetch": "2.6.13",
"@types/yargs": "17.0.33",
"@types/yargs": "17.0.34",
"gettext-extractor": "3.8.0",
"gulp": "4.0.2",
"html-entities": "1.4.0",

View File

@@ -197,6 +197,10 @@
"ios-v13.5.2": true,
"android-v3.5.8": true,
"ios-v13.5.3": true,
"v3.5.11": true
"v3.5.11": true,
"v3.5.12": true,
"v3.6.1": true,
"v3.6.2": true,
"android-v3.5.9": true
}
}

View File

@@ -186,6 +186,12 @@ async function createRelease(projectName: string, releaseConfig: ReleaseConfig,
}
const uploadToGitHubRelease = async (projectName: string, tagName: string, isPreRelease: boolean, releaseFiles: Record<string, Release>) => {
const allPublishDisabled = Object.values(releaseFiles).every(r => !r.publish);
if (allPublishDisabled) {
console.info('All release files have publishing disabled - skipping GitHub release creation');
return;
}
console.info(`Creating GitHub release ${tagName}...`);
const releaseOptions = { isPreRelease: isPreRelease };
@@ -323,10 +329,15 @@ async function main() {
await uploadToGitHubRelease(mainProjectName, tagName, isPreRelease, releaseFiles);
console.info(`Main download URL: ${releaseFiles['main'].downloadUrl}`);
if (releaseFiles['main']) console.info(`Main download URL: ${releaseFiles['main'].downloadUrl}`);
const changelogPath = `${rootDir}/readme/about/changelog/android.md`;
await completeReleaseWithChangelog(changelogPath, version, tagName, 'Android', isPreRelease);
// When creating the changelog, we always set `isPrerelease` to `false` - this is because we
// only ever publish pre-releases, and it's only later that we manually promote some of them to
// stable releases. So having "(Pre-release)" for each Android version in the changelog is
// meaningless and would be incorrect for the versions that are stable ones.
await completeReleaseWithChangelog(changelogPath, version, tagName, 'Android', false);
}
main().catch((error) => {

View File

@@ -16,7 +16,7 @@
"dependencies": {
"@joplin/utils": "~3.6",
"@koa/cors": "3.4.3",
"dotenv": "16.6.1",
"dotenv": "17.2.3",
"file-type": "16.5.4",
"fs-extra": "11.3.2",
"knex": "3.1.0",

View File

@@ -11,7 +11,7 @@
"browserify": "14.5.0",
"rollup": "0.50.1",
"standard": "17.1.2",
"turndown": "7.2.1",
"turndown": "7.2.2",
"turndown-attendant": "0.0.3"
},
"files": [

View File

@@ -14,7 +14,7 @@
"jsdom": "26.1.0"
},
"devDependencies": {
"@rollup/plugin-commonjs": "28.0.8",
"@rollup/plugin-commonjs": "28.0.9",
"@rollup/plugin-node-resolve": "16.0.3",
"@rollup/plugin-replace": "6.0.2",
"browserify": "14.5.0",

View File

@@ -1,10 +1,14 @@
# Joplin Android Changelog
## [android-v3.5.8](https://github.com/laurent22/joplin/releases/tag/android-v3.5.8) (Pre-release) - 2026-01-10T10:08:46Z
## [android-v3.5.9](https://github.com/laurent22/joplin/releases/tag/android-v3.5.9) - 2026-01-19T16:24:19Z
- Improved: Remove unnecessary READ_PHONE_STATE permission (#14157 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
## [android-v3.5.8](https://github.com/laurent22/joplin/releases/tag/android-v3.5.8) - 2026-01-10T10:08:46Z
- Fixed: Fixed keyboard input issue in note title (#14070) (#13544 by [@mrjo118](https://github.com/mrjo118))
## [android-v3.5.7](https://github.com/laurent22/joplin/releases/tag/android-v3.5.7) (Pre-release) - 2026-01-08T19:30:28Z
## [android-v3.5.7](https://github.com/laurent22/joplin/releases/tag/android-v3.5.7) - 2026-01-08T19:30:28Z
- New: Rich Text Editor: Add shortcuts for inserting code blocks (#14055 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Improved: Accessibility: In-editor rendering: Fix rendered checkboxes are very small on mobile (#14056 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
@@ -16,18 +20,18 @@
- Fixed: Images sometimes don't render until you click somewhere in the note (#14019) (#13963 by [@bwat47](https://github.com/bwat47))
- Fixed: Insert time command not respecting locale settings (#13994) (#13229 by [@HIJOdelIDANII](https://github.com/HIJOdelIDANII))
## [android-v3.5.6](https://github.com/laurent22/joplin/releases/tag/android-v3.5.6) (Pre-release) - 2025-12-27T20:34:44Z
## [android-v3.5.6](https://github.com/laurent22/joplin/releases/tag/android-v3.5.6) - 2025-12-27T20:34:44Z
- Revert "All: Apache Tomcat WebDAV compatibility for sync (#13614)"
## [android-v3.5.5](https://github.com/laurent22/joplin/releases/tag/android-v3.5.5) (Pre-release) - 2025-12-26T10:53:20Z
## [android-v3.5.5](https://github.com/laurent22/joplin/releases/tag/android-v3.5.5) - 2025-12-26T10:53:20Z
- Improved: Update js-draw to v1.33.0 (#13990 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Improved: Updated packages react-native-webview (v13.16.0)
- Fixed: Editor: Fix search/replace UI is partially off-screen on small-screen devices (#13978 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Fixed: Feature flags: Fix "voice typing" feature flag (#13981 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
## [android-v3.5.4](https://github.com/laurent22/joplin/releases/tag/android-v3.5.4) (Pre-release) - 2025-12-23T20:00:18Z
## [android-v3.5.4](https://github.com/laurent22/joplin/releases/tag/android-v3.5.4) - 2025-12-23T20:00:18Z
- Improved: Accessibility: Dark mode: Improve contrast of conflicts notebook title, error messages in "Logs" (#13925 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Improved: Attempt to fix application hang when opening the camera (#13974 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
@@ -40,7 +44,7 @@
- Fixed: Rich Text Editor: Fix indent/de-indent buttons do nothing when not in a list (#13961 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Fixed: Toolbar editor: Fix toolbar editor dismiss button is rendered outside the dialog on small screens (#13976 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
## [android-v3.5.3](https://github.com/laurent22/joplin/releases/tag/android-v3.5.3) (Pre-release) - 2025-12-14T13:46:02Z
## [android-v3.5.3](https://github.com/laurent22/joplin/releases/tag/android-v3.5.3) - 2025-12-14T13:46:02Z
- New: Add a link to the list of open-source licenses (5caec16)
- New: Add the ability to rename and delete tags (#13731 by [@mrjo118](https://github.com/mrjo118))
@@ -54,7 +58,7 @@
- Fixed: Markdown import incorrectly parses a link as a file path (#12172)
- Fixed: Rich Text Editor: Fix table delete row/delete column buttons can't remove the last row/column from a table (#13877 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
## [android-v3.5.1](https://github.com/laurent22/joplin/releases/tag/android-v3.5.1) (Pre-release) - 2025-11-29T12:33:28Z
## [android-v3.5.1](https://github.com/laurent22/joplin/releases/tag/android-v3.5.1) - 2025-11-29T12:33:28Z
- New: Add support for mixed case tags (#12931 by [@mrjo118](https://github.com/mrjo118))
- New: Add the ability to search on the tag list screen (#13733 by [@mrjo118](https://github.com/mrjo118))
@@ -130,13 +134,13 @@
- Fixed: Treat unclosed quotes as fully quoted search terms, to prevent malformed match expression error (#13564) (#13319 by [@mrjo118](https://github.com/mrjo118))
- Fixed: When creating a conflict, ensure the latest note contents are used to create the conflict (#13552) (#13531 by [@mrjo118](https://github.com/mrjo118))
## [android-v3.4.7](https://github.com/laurent22/joplin/releases/tag/android-v3.4.7) (Pre-release) - 2025-09-09T08:09:54Z
## [android-v3.4.7](https://github.com/laurent22/joplin/releases/tag/android-v3.4.7) - 2025-09-09T08:09:54Z
- Fixed: Fix error when saving in-editor rendering-related settings (#13105) (#13103 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Fixed: Fix light bar shown above header in dark mode (#13132 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Fixed: Plugins: Fix renderer plugins that use the `settingValue` API (#13131 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
## [android-v3.4.6](https://github.com/laurent22/joplin/releases/tag/android-v3.4.6) (Pre-release) - 2025-09-01T12:02:41Z
## [android-v3.4.6](https://github.com/laurent22/joplin/releases/tag/android-v3.4.6) - 2025-09-01T12:02:41Z
- Fixed: Fix "edit profile" button is partially off-screen (#13084) (#13015 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Fixed: Fix shadow shown above the screen header (#13074 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
@@ -144,7 +148,7 @@
- Fixed: Plugin API: Fix compatibility with certain plugins targetting the desktop app (#13077 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Fixed: Plugins: Fix plugin panel buttons are off-screen on recent versions of Android (#13080 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
## [android-v3.4.5](https://github.com/laurent22/joplin/releases/tag/android-v3.4.5) (Pre-release) - 2025-08-27T06:27:53Z
## [android-v3.4.5](https://github.com/laurent22/joplin/releases/tag/android-v3.4.5) - 2025-08-27T06:27:53Z
- New: Add a "highlight active line" setting (#12967 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- New: Rich Text Editor: Add basic support for collapsible &lt;details&gt; blocks (#12946 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
@@ -167,7 +171,7 @@
- Fixed: Rich Text Editor: Fix additional blank lines added around list items on save (#12935 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Fixed: Shared folders: Fix moving shared subfolder to top-level briefly marks it as a top-level share (#12964 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
## [android-v3.4.4](https://github.com/laurent22/joplin/releases/tag/android-v3.4.4) (Pre-release) - 2025-08-10T09:31:45Z
## [android-v3.4.4](https://github.com/laurent22/joplin/releases/tag/android-v3.4.4) - 2025-08-10T09:31:45Z
- Improved: Allow editing code blocks from the Rich Text Editor (#12906) (#12841 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Improved: Fixed missing filename when a file is shared with the app (#12895) (#12858 by [@klaas0](https://github.com/klaas0))
@@ -180,7 +184,7 @@
- Fixed: Fix switching between note and todo on mobile (#12849) (#12822 by [@mrjo118](https://github.com/mrjo118))
- Fixed: Rich Text Editor: Make initial search behavior match the Markdown editor (#12878) (#12844 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
## [android-v3.4.3](https://github.com/laurent22/joplin/releases/tag/android-v3.4.3) (Pre-release) - 2025-08-04T17:38:13Z
## [android-v3.4.3](https://github.com/laurent22/joplin/releases/tag/android-v3.4.3) - 2025-08-04T17:38:13Z
- New: Add a Rich Text Editor (#12748 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Improved: Performance: Improve Rich Text Editor startup performance (#12819 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
@@ -191,12 +195,12 @@
- Fixed: Improve usability of inline search in notes (#12791) (#12783 by [@mrjo118](https://github.com/mrjo118))
- Fixed: Markdown editor: Make list indentation size equivalent to four spaces (#12794) (#12573 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
## [android-v3.4.2](https://github.com/laurent22/joplin/releases/tag/android-v3.4.2) (Pre-release) - 2025-07-25T08:30:30Z
## [android-v3.4.2](https://github.com/laurent22/joplin/releases/tag/android-v3.4.2) - 2025-07-25T08:30:30Z
- Improved: Updated packages react-native-paper (v5.13.5)
- Fixed: Fix title bar is partially hidden by the screen header (#12785 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
## [android-v3.4.1](https://github.com/laurent22/joplin/releases/tag/android-v3.4.1) (Pre-release) - 2025-07-24T10:57:44Z
## [android-v3.4.1](https://github.com/laurent22/joplin/releases/tag/android-v3.4.1) - 2025-07-24T10:57:44Z
- New: Add Joplin Server SAML support (#11865 by [@ttcchhmm](https://github.com/ttcchhmm))
- New: Add delete line, duplicate line and sort selected lines buttons to editor toolbar (#12555 by [@mrjo118](https://github.com/mrjo118))
@@ -230,32 +234,32 @@
- Fixed: Fix voice typing fails to start on certain devices (#12351 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Fixed: Moving sub-notebook of shared notebook should unshare it (#12647) (#12089)
## [android-v3.3.11](https://github.com/laurent22/joplin/releases/tag/android-v3.3.11) (Pre-release) - 2025-07-09T22:51:55Z
## [android-v3.3.11](https://github.com/laurent22/joplin/releases/tag/android-v3.3.11) - 2025-07-09T22:51:55Z
- Fixed: Biometrics: Fix notebook list can still be accessed when the app is locked (#12691 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
## [android-v3.3.10](https://github.com/laurent22/joplin/releases/tag/android-v3.3.10) (Pre-release) - 2025-06-10T08:07:25Z
## [android-v3.3.10](https://github.com/laurent22/joplin/releases/tag/android-v3.3.10) - 2025-06-10T08:07:25Z
- New: Add additional checks when updating sidebar state (#12428 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
## [android-v3.3.9](https://github.com/laurent22/joplin/releases/tag/android-v3.3.9) (Pre-release) - 2025-06-09T17:11:04Z
## [android-v3.3.9](https://github.com/laurent22/joplin/releases/tag/android-v3.3.9) - 2025-06-09T17:11:04Z
- Fixed: Voice typing: Fix memory leak (#12402 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
## [android-v3.3.8](https://github.com/laurent22/joplin/releases/tag/android-v3.3.8) (Pre-release) - 2025-05-01T15:45:35Z
## [android-v3.3.8](https://github.com/laurent22/joplin/releases/tag/android-v3.3.8) - 2025-05-01T15:45:35Z
- New: Force quick action shortcuts to have the same size (#12195 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Improved: Increase space between new note/to-do buttons (#12194 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Fixed: Fix new note menu size (#12193) (#12191 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
## [android-v3.3.7](https://github.com/laurent22/joplin/releases/tag/android-v3.3.7) (Pre-release) - 2025-04-29T13:06:06Z
## [android-v3.3.7](https://github.com/laurent22/joplin/releases/tag/android-v3.3.7) - 2025-04-29T13:06:06Z
- New: Add plural forms for notes, users, hours, minutes, days (#12171 by [@SilverGreen93](https://github.com/SilverGreen93))
- Improved: Allow new note and new to-do buttons to wrap (#12163 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Improved: Update immer to v9.0.21 (#12182 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Fixed: Editor: Allow syntax highlighting within ==highlight==s (#12167) (#12110 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
## [android-v3.3.6](https://github.com/laurent22/joplin/releases/tag/android-v3.3.6) (Pre-release) - 2025-04-24T08:34:56Z
## [android-v3.3.6](https://github.com/laurent22/joplin/releases/tag/android-v3.3.6) - 2025-04-24T08:34:56Z
- Improved: Improve UI for downloading updated models (#12145 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Improved: Note editor: Hash links: Move cursor to header or anchor associated with link target (#12129 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
@@ -266,7 +270,7 @@
- Fixed: Markdown Editor: Fix numbered sublist renumbering (#12091 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Fixed: Settings: Fix desktop-specific setting visible in note &gt; advanced (#12146 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
## [android-v3.3.5](https://github.com/laurent22/joplin/releases/tag/android-v3.3.5) (Pre-release) - 2025-04-07T19:31:26Z
## [android-v3.3.5](https://github.com/laurent22/joplin/releases/tag/android-v3.3.5) - 2025-04-07T19:31:26Z
- New: Add "swap line up" and "swap line down" to toolbar extended options (#12053 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- New: Plugins: Add command to hide the plugin panel viewer (#12018 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
@@ -283,12 +287,12 @@
- Fixed: Restoring a note which was in a deleted notebook (#12016) (#11934)
- Fixed: Voice typing: Fix incorrectly-calculated audio length (#12012 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
## [android-v3.3.4](https://github.com/laurent22/joplin/releases/tag/android-v3.3.4) (Pre-release) - 2025-03-21T18:07:00Z
## [android-v3.3.4](https://github.com/laurent22/joplin/releases/tag/android-v3.3.4) - 2025-03-21T18:07:00Z
- Improved: Voice typing: Improve processing with larger models (#11983 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Improved: Voice typing: Improve re-download button UI (#11979) (#11955 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
## [android-v3.3.3](https://github.com/laurent22/joplin/releases/tag/android-v3.3.3) (Pre-release) - 2025-03-16T10:29:52Z
## [android-v3.3.3](https://github.com/laurent22/joplin/releases/tag/android-v3.3.3) - 2025-03-16T10:29:52Z
- New: Add setting migration for ocr.enabled (ab86b95)
- Improved: Accessibility: Improve focus handling in the note actions menu and modal dialogs (#11929 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
@@ -298,7 +302,7 @@
- Fixed: Make tab size consistent between Markdown editor and viewer (and RTE) (#11940) (#11673)
- Fixed: Voice typing: Fix potential output duplication when finalizing voice typing (#11953 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
## [android-v3.3.2](https://github.com/laurent22/joplin/releases/tag/android-v3.3.2) (Pre-release) - 2025-03-03T22:35:08Z
## [android-v3.3.2](https://github.com/laurent22/joplin/releases/tag/android-v3.3.2) - 2025-03-03T22:35:08Z
- Improved: Improve encryption config screen accessibility (#11874) (#11846 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Improved: Switch default library used for Whisper voice typing (#11881 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
@@ -307,7 +311,7 @@
- Fixed: Fix disabled encryption keys list showing enabled keys (#11861) (#11858 by [@pedr](https://github.com/pedr))
- Fixed: Fix voice recorder crash (#11876) (#11864 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
## [android-v3.3.1](https://github.com/laurent22/joplin/releases/tag/android-v3.3.1) (Pre-release) - 2025-02-19T16:01:54Z
## [android-v3.3.1](https://github.com/laurent22/joplin/releases/tag/android-v3.3.1) - 2025-02-19T16:01:54Z
- New: Add support for plugin editor views (#11831)
- Improved: Accessibility: Improve contrast of faded URLs in Markdown editor (#11635 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
@@ -337,17 +341,17 @@
- Fixed: Use alternative fix to set the sqlite CursorWindow size to 50mb (#11726) (#11571 by [@mrjo118](https://github.com/mrjo118))
- Security: Improve comment escaping (#11706 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
## [android-v3.2.7](https://github.com/laurent22/joplin/releases/tag/android-v3.2.7) (Pre-release) - 2025-01-13T17:03:36Z
## [android-v3.2.7](https://github.com/laurent22/joplin/releases/tag/android-v3.2.7) - 2025-01-13T17:03:36Z
- Fixed: Clicking on an external note link from within a note logs an error (#11619) (#11455 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Fixed: Fix clicking "Draw picture" results in blank screen with very old WebView versions (#11604 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
## [android-v3.2.5](https://github.com/laurent22/joplin/releases/tag/android-v3.2.5) (Pre-release) - 2025-01-07T23:35:43Z
## [android-v3.2.5](https://github.com/laurent22/joplin/releases/tag/android-v3.2.5) - 2025-01-07T23:35:43Z
- Improved: Allow re-downloading voice typing models on URL change and error (#11557 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Improved: Upgrade js-draw to 1.26.0 (#11589 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
## [android-v3.2.4](https://github.com/laurent22/joplin/releases/tag/android-v3.2.4) (Pre-release) - 2025-01-06T12:50:23Z
## [android-v3.2.4](https://github.com/laurent22/joplin/releases/tag/android-v3.2.4) - 2025-01-06T12:50:23Z
- New: Plugin API: Add support for the renderMarkup command (#11494 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Improved: Accessibility: Improve sidemenu notebook list accessibility (#11556 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
@@ -362,7 +366,7 @@
- Fixed: Fix missing "Insert Time" button (#11542) (#11539 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Fixed: Locked out of mobile app due to broken fingerprint scanner (#10926)
## [android-v3.2.3](https://github.com/laurent22/joplin/releases/tag/android-v3.2.3) (Pre-release) - 2024-12-11T13:58:14Z
## [android-v3.2.3](https://github.com/laurent22/joplin/releases/tag/android-v3.2.3) - 2024-12-11T13:58:14Z
- New: Accessibility: Add checked/unchecked accessibility information to the "sort notes by" dialog (#11411 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- New: Translation: Add sk_SK.po (Slovak) (#11433 by [@dodog](https://github.com/dodog))
@@ -384,7 +388,7 @@
- Fixed: Fix the error caused by undefined isCodeBlock_ (turndown-plugin-gfm) (#11471 by Manabu Nakazawa)
- Fixed: Upgrade CodeMirror packages (#11440) (#11318 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
## [android-v3.2.2](https://github.com/laurent22/joplin/releases/tag/android-v3.2.2) (Pre-release) - 2024-11-19T01:12:43Z
## [android-v3.2.2](https://github.com/laurent22/joplin/releases/tag/android-v3.2.2) - 2024-11-19T01:12:43Z
- Improved: Accessibility: Improve dialog accessibility (#11395 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Improved: Deprecated OneDrive sync method (e36f377)
@@ -393,7 +397,7 @@
- Fixed: Fix race condition which may cause data loss, particularly before or after pasting text in the note editor (#11334) (#11317 by [@mrjo118](https://github.com/mrjo118))
- Fixed: Fix vertical alignment of checkboxes when text wraps over multiple lines (226a8b3)
## [android-v3.2.1](https://github.com/laurent22/joplin/releases/tag/android-v3.2.1) (Pre-release) - 2024-11-10T14:23:47Z
## [android-v3.2.1](https://github.com/laurent22/joplin/releases/tag/android-v3.2.1) - 2024-11-10T14:23:47Z
- New: Add new encryption methods based on native crypto libraries (#10696 by Self Not Found)
- New: Add setting to disable markup autocompletion (#11222 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
@@ -411,17 +415,17 @@
- Fixed: Handle callback url triggered app launch (#11280) (#9204 by [@tiberiusteng](https://github.com/tiberiusteng))
- Fixed: Upgrade react-native-quick-crypto to v0.7.5 (#11294 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
## [android-v3.1.8](https://github.com/laurent22/joplin/releases/tag/android-v3.1.8) (Pre-release) - 2024-11-09T13:02:33Z
## [android-v3.1.8](https://github.com/laurent22/joplin/releases/tag/android-v3.1.8) - 2024-11-09T13:02:33Z
- Fixed: Fix error on creating new notes if the user is a share recipient (#11326) (#11325 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Fixed: Fix new note button is pushed off-screen on certain Android devices (#11323) (#11276 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Fixed: Fix sharing to Joplin causes back navigation to get stuck (#11355) (#11324 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
## [android-v3.1.7](https://github.com/laurent22/joplin/releases/tag/android-v3.1.7) (Pre-release) - 2024-11-04T20:27:52Z
## [android-v3.1.7](https://github.com/laurent22/joplin/releases/tag/android-v3.1.7) - 2024-11-04T20:27:52Z
- Fixed: Fix search result note hidden after powering on device (#11297) (#11197 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
## [android-v3.1.6](https://github.com/laurent22/joplin/releases/tag/android-v3.1.6) (Pre-release) - 2024-10-17T22:13:06Z
## [android-v3.1.6](https://github.com/laurent22/joplin/releases/tag/android-v3.1.6) - 2024-10-17T22:13:06Z
- Improved: Downgrade CodeMirror packages to fix various Android regressions (#11170 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Improved: Plugins: Name webview root attribute so that it can be styled (75b8caf)
@@ -431,7 +435,7 @@
- Fixed: Fix new note/edit buttons only work if pressed quickly (#11185) (#11183 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Fixed: Fix regression: Search screen not hidden when cached for search result navigation (#11131) (#11130 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
## [android-v3.1.5](https://github.com/laurent22/joplin/releases/tag/android-v3.1.5) (Pre-release) - 2024-10-11T22:11:20Z
## [android-v3.1.5](https://github.com/laurent22/joplin/releases/tag/android-v3.1.5) - 2024-10-11T22:11:20Z
- Improved: Downgrade CodeMirror packages to fix various Android regressions (#11170 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Improved: Plugins: Name webview root attribute so that it can be styled (75b8caf)
@@ -441,7 +445,7 @@
- Fixed: Fix new note/edit buttons only work if pressed quickly (#11185) (#11183 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Fixed: Fix regression: Search screen not hidden when cached for search result navigation (#11131) (#11130 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
## [android-v3.1.4](https://github.com/laurent22/joplin/releases/tag/android-v3.1.4) (Pre-release) - 2024-09-24T14:21:42Z
## [android-v3.1.4](https://github.com/laurent22/joplin/releases/tag/android-v3.1.4) - 2024-09-24T14:21:42Z
- Improved: Automatically detect and use operating system theme by default (5beb80b)
- Improved: Make pressing "back" navigate to the previous note after following a link (#11086) (#11082 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
@@ -461,7 +465,7 @@
- Fixed: Move accessibility focus to the first note action menu item on open (#11031) (#10253 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Fixed: WebDAV synchronisation not working because of URL encoding differences (#11076) (#10608 by [@pedr](https://github.com/pedr))
## [android-v3.1.3](https://github.com/laurent22/joplin/releases/tag/android-v3.1.3) (Pre-release) - 2024-09-02T12:16:46Z
## [android-v3.1.3](https://github.com/laurent22/joplin/releases/tag/android-v3.1.3) - 2024-09-02T12:16:46Z
- Improved: Added feature flag to disable sync lock support (#10925) (#10407)
- Improved: Make feature flags advanced settings by default (700ffa2)
@@ -473,7 +477,7 @@
- Fixed: Fixed italic support in Fountain documents (5fdd088)
- Fixed: Markdown editor: Fix toggling bulleted lists when items start with asterisks (#10902) (#10891 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
## [android-v3.1.2](https://github.com/laurent22/joplin/releases/tag/android-v3.1.2) (Pre-release) - 2024-08-10T11:44:30Z
## [android-v3.1.2](https://github.com/laurent22/joplin/releases/tag/android-v3.1.2) - 2024-08-10T11:44:30Z
- Fixed: Fix WebDAV sync on mobile (#10849) (#10848 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Improved: Improve RTL support in the Markdown editor (#10810 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
@@ -490,26 +494,26 @@
- Fixed: Remove search bar from plugins screen (#10648) (#10596 by Siddhant Paritosh Rao)
- Fixed: Show notification in case Joplin Cloud credential is not valid anymore (#10649) (#10645 by [@pedr](https://github.com/pedr))
## [android-v3.0.9](https://github.com/laurent22/joplin/releases/tag/android-v3.0.9) (Pre-release) - 2024-07-28T14:00:59Z
## [android-v3.0.9](https://github.com/laurent22/joplin/releases/tag/android-v3.0.9) - 2024-07-28T14:00:59Z
- Fixed: #10677: Following a link to a previously open note wouldn't work (#10750) (#10677 by [@pedr](https://github.com/pedr))
- Fixed: Fix manual resource download mode (#10748 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
## [android-v3.0.8](https://github.com/laurent22/joplin/releases/tag/android-v3.0.8) (Pre-release) - 2024-07-06T10:26:06Z
## [android-v3.0.8](https://github.com/laurent22/joplin/releases/tag/android-v3.0.8) - 2024-07-06T10:26:06Z
- Fixed: Fix sidebar performance regression with many nested notebooks (#10676) (#10674 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
## [android-v3.0.7](https://github.com/laurent22/joplin/releases/tag/android-v3.0.7) (Pre-release) - 2024-07-01T15:47:15Z
## [android-v3.0.7](https://github.com/laurent22/joplin/releases/tag/android-v3.0.7) - 2024-07-01T15:47:15Z
- Improved: Set min version for synchronising to 3.0.0 (e4b8976)
- Fixed: Show notification in case Joplin Cloud credential is not valid anymore (#10649) (#10645 by [@pedr](https://github.com/pedr))
## [android-v3.0.6](https://github.com/laurent22/joplin/releases/tag/android-v3.0.6) (Pre-release) - 2024-06-29T09:41:10Z
## [android-v3.0.6](https://github.com/laurent22/joplin/releases/tag/android-v3.0.6) - 2024-06-29T09:41:10Z
- Updated Chinese and German translation (#10660 by [@cedecode](https://github.com/cedecode))
- Fixed: Fix refocusing the note editor (#10644) (#10637 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
## [android-v3.0.5](https://github.com/laurent22/joplin/releases/tag/android-v3.0.5) (Pre-release) - 2024-06-19T12:02:12Z
## [android-v3.0.5](https://github.com/laurent22/joplin/releases/tag/android-v3.0.5) - 2024-06-19T12:02:12Z
- Improved: Don't render empty title page for Fountain (#10631 by [@XPhyro](https://github.com/XPhyro))
- Improved: Don't show an "expand" arrow by "Installed plugins" when no plugins are installed (#10583 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
@@ -528,7 +532,7 @@
- Fixed: Plugin settings screen: Fix plugin states not set correctly when installing multiple plugins at once (#10580 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Fixed: Plugin settings: Fix plugins without settings can't be disabled without reinstall (#10579 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
## [android-v3.0.4](https://github.com/laurent22/joplin/releases/tag/android-v3.0.4) (Pre-release) - 2024-06-12T20:38:44Z
## [android-v3.0.4](https://github.com/laurent22/joplin/releases/tag/android-v3.0.4) - 2024-06-12T20:38:44Z
- New: Add Joplin Cloud account information to configuration screen (#10553 by [@pedr](https://github.com/pedr))
- New: Add button on Synchronization to Joplin Cloud login screen (#10569 by [@pedr](https://github.com/pedr))
@@ -558,7 +562,7 @@
- Fixed: Fix plugins not reloaded when the plugin runner reloads (#10540 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Fixed: Maintain cursor position when changing list indentation (#10441) (#10439 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
## [android-v3.0.3](https://github.com/laurent22/joplin/releases/tag/android-v3.0.3) (Pre-release) - 2024-04-27T11:21:48Z
## [android-v3.0.3](https://github.com/laurent22/joplin/releases/tag/android-v3.0.3) - 2024-04-27T11:21:48Z
- Improved: Display a message when Joplin Cloud user don't have access to email to note feature (#10322 by [@pedr](https://github.com/pedr))
- Improved: Make editor styles closer to desktop (#10377 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
@@ -573,7 +577,7 @@
- Fixed: Fix sync icon off-center (#10350) (#10351 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Fixed: Plugins: Fix API incompatibility in arguments to `onMessage` listeners in panels (#10375 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
## [android-v3.0.2](https://github.com/laurent22/joplin/releases/tag/android-v3.0.2) (Pre-release) - 2024-04-15T18:06:46Z
## [android-v3.0.2](https://github.com/laurent22/joplin/releases/tag/android-v3.0.2) - 2024-04-15T18:06:46Z
- Improved: Allow marking a plugin as mobile-only or desktop-only (#10229) (#10206 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Improved: Allow marking items as "ignored" in sync status (#10261) (#10245 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
@@ -604,7 +608,7 @@
- Fixed: Plugin API: Fix unable to require `@codemirror/search` (#10205 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Fixed: Plugins: Fix event listener memory leak when disabling/uninstalling plugins (#10280 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
## [android-v3.0.1](https://github.com/laurent22/joplin/releases/tag/android-v3.0.1) (Pre-release) - 2024-03-21T18:27:47Z
## [android-v3.0.1](https://github.com/laurent22/joplin/releases/tag/android-v3.0.1) - 2024-03-21T18:27:47Z
- New: Add support for Markdown editor plugins (#10086 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- New: Add support for plugin panels and dialogs (#10121 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
@@ -628,23 +632,23 @@
- Fixed: Plugins: Fix warning after reloading plugins (#10165 by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Fixed: Shows only the real folders in the dropdown of parent folders. (#10147) (#10143 by [@Sidd-R](https://github.com/Sidd-R))
## [android-v2.14.9](https://github.com/laurent22/joplin/releases/tag/android-v2.14.9) (Pre-release) - 2024-02-26T19:56:11Z
## [android-v2.14.9](https://github.com/laurent22/joplin/releases/tag/android-v2.14.9) - 2024-02-26T19:56:11Z
- Fixed: Note editor: Support older WebView versions (#9986) (#9521 by Henry Heino)
- Fixed: Sort notebooks in a case-insensitive way (#9996)
## [android-v2.14.8](https://github.com/laurent22/joplin/releases/tag/android-v2.14.8) (Pre-release) - 2024-02-22T22:29:24Z
## [android-v2.14.8](https://github.com/laurent22/joplin/releases/tag/android-v2.14.8) - 2024-02-22T22:29:24Z
- Improved: Immediately sort notes after toggling a checkbox (5820f63)
- Fixed: Fix auto-indentation in some types of code blocks (#9972) (#9971 by Henry Heino)
## [android-v2.14.7](https://github.com/laurent22/joplin/releases/tag/android-v2.14.7) (Pre-release) - 2024-02-19T10:40:10Z
## [android-v2.14.7](https://github.com/laurent22/joplin/releases/tag/android-v2.14.7) - 2024-02-19T10:40:10Z
- Improved: Migrate profile in preparation for trash feature (115eb5d)
- Improved: Updated packages tar-stream (v3.1.7)
- Fixed: Fix full text search broken on Android 7 and earlier (#9914) (#9905 by Henry Heino)
## [android-v2.14.6](https://github.com/laurent22/joplin/releases/tag/android-v2.14.6) (Pre-release) - 2024-02-09T12:41:18Z
## [android-v2.14.6](https://github.com/laurent22/joplin/releases/tag/android-v2.14.6) - 2024-02-09T12:41:18Z
- Improved: Improve search engine error handling when preparing text for search (#9871 by Henry Heino)
- Improved: Updated packages @js-draw/material-icons (v1.16.1), @react-native-community/netinfo (v11.2.1), @react-native-community/slider (v4.5.0), async-mutex (v0.4.1), follow-redirects (v1.15.5), js-draw (v1.16.1), moment (v2.30.1), react-native-document-picker (v9.1.0), react-native-localize (v3.0.6), react-native-paper (v5.11.7), react-native-safe-area-context (v4.8.2), react-native-share (v10.0.2), react-native-webview (v13.6.4), sass (v1.69.7), sharp (v0.33.2), sqlite3 (v5.1.7)
@@ -654,7 +658,7 @@
- Fixed: Fix share to Joplin when only "All notes" has been opened (#9876) (#9863 by Henry Heino)
- Fixed: Increase space available for Notebook icon (#9877) (#9475 by [@pedr](https://github.com/pedr))
## [android-v2.14.5](https://github.com/laurent22/joplin/releases/tag/android-v2.14.5) (Pre-release) - 2024-02-02T23:09:50Z
## [android-v2.14.5](https://github.com/laurent22/joplin/releases/tag/android-v2.14.5) - 2024-02-02T23:09:50Z
- Improved: Allow note viewer to extend to the edge of the screen while pinch zooming (#9820) (#9819 by Henry Heino)
- Improved: Do not allow switching the sync target if not all resources are downloaded (#9263)
@@ -667,7 +671,7 @@
- Fixed: Fix note editor errors/logs not sent to Joplin's logs (#9808) (#9807 by Henry Heino)
- Fixed: Fix synchronization happens every 10 seconds even if nothing has changed (#9814) (#9800 by Henry Heino)
## [android-v2.14.4](https://github.com/laurent22/joplin/releases/tag/android-v2.14.4) (Pre-release) - 2024-01-26T10:46:28Z
## [android-v2.14.4](https://github.com/laurent22/joplin/releases/tag/android-v2.14.4) - 2024-01-26T10:46:28Z
- New: Add support for showing only lines of log that contain a filter (#9728 by Henry Heino)
- Improved: Allow setting a minimum app version on the sync target (#9778)
@@ -677,18 +681,18 @@
- Improved: Updated packages @js-draw/material-icons (v1.15.0), follow-redirects (v1.15.4), fs-extra (v11.2.0), js-draw (v1.15.0), react, react-native-device-info (v10.12.0), react-native-image-picker (v7.1.0), react-native-paper (v5.11.5), react-native-vector-icons (v10.0.3), sharp (v0.33.1)
- Fixed: Fix AWS S3 sync error (#9696) (#8891 by Henry Heino)
## [android-v2.14.3](https://github.com/laurent22/joplin/releases/tag/android-v2.14.3) (Pre-release) - 2024-01-06T12:30:29Z
## [android-v2.14.3](https://github.com/laurent22/joplin/releases/tag/android-v2.14.3) - 2024-01-06T12:30:29Z
- Improved: Fix table-of-contents links to headings with duplicate content (#9610) (#9594 by Henry Heino)
- Improved: Improve sync by reducing how often note list is sorted (f95ee68)
- Improved: Render mermaid diagrams in dark mode when Joplin is in dark mode (#9631) (#3201 by Henry Heino)
- Improved: Updated packages deprecated-react-native-prop-types (v5), react-native-paper (v5.11.4)
## [android-v2.14.2](https://github.com/laurent22/joplin/releases/tag/android-v2.14.2) (Pre-release) - 2023-12-31T16:14:25Z
## [android-v2.14.2](https://github.com/laurent22/joplin/releases/tag/android-v2.14.2) - 2023-12-31T16:14:25Z
- Improved: Updated packages react-native-get-random-values (v1.10.0)
## [android-v2.14.1](https://github.com/laurent22/joplin/releases/tag/android-v2.14.1) (Pre-release) - 2023-12-29T22:12:14Z
## [android-v2.14.1](https://github.com/laurent22/joplin/releases/tag/android-v2.14.1) - 2023-12-29T22:12:14Z
- Improved: CodeMirror 6 markdown editor: Support highlighting more languages (#9563) (#9562 by Henry Heino)
- Improved: Don't attach empty drawings when a user exits without saving (#9386) (#9377 by Henry Heino)
@@ -710,22 +714,22 @@
- Fixed: Fix tooltips don't disappear on some devices (upgrade to js-draw 1.13.2) (#9401) (#9374 by Henry Heino)
- Fixed: Sidebar is not dismissed when creating a note (#9376)
## [android-v2.13.10](https://github.com/laurent22/joplin/releases/tag/android-v2.13.10) (Pre-release) - 2023-12-01T11:16:17Z
## [android-v2.13.10](https://github.com/laurent22/joplin/releases/tag/android-v2.13.10) - 2023-12-01T11:16:17Z
- Improved: Drawing: Revert recent changes to input system (#9426) (#9427 by Henry Heino)
## [android-v2.13.9](https://github.com/laurent22/joplin/releases/tag/android-v2.13.9) (Pre-release) - 2023-11-30T17:55:54Z
## [android-v2.13.9](https://github.com/laurent22/joplin/releases/tag/android-v2.13.9) - 2023-11-30T17:55:54Z
- Improved: Don't attach empty drawings when a user exits without saving (#9386) (#9377 by Henry Heino)
- Fixed: Fix tooltips don't disappear on some devices (upgrade to js-draw 1.13.2) (#9401) (#9374 by Henry Heino)
## [android-v2.13.8](https://github.com/laurent22/joplin/releases/tag/android-v2.13.8) (Pre-release) - 2023-11-26T12:37:00Z
## [android-v2.13.8](https://github.com/laurent22/joplin/releases/tag/android-v2.13.8) - 2023-11-26T12:37:00Z
- Fixed: Fix to-dos options toggle don't toggle a rerender (#9364) (#9361 by [@pedr](https://github.com/pedr))
- Fixed: Fix new note/to-do buttons not visible on app startup in some cases (#9329) (#9328 by Henry Heino)
- Fixed: Sidebar is not dismissed when creating a note (#9376)
## [android-v2.13.7](https://github.com/laurent22/joplin/releases/tag/android-v2.13.7) (Pre-release) - 2023-11-16T13:17:53Z
## [android-v2.13.7](https://github.com/laurent22/joplin/releases/tag/android-v2.13.7) - 2023-11-16T13:17:53Z
- Improved: Add more space between settings title and description (#9270) (#9258 by Henry Heino)
- Improved: Fade settings screen icons (#9268) (#9260 by Henry Heino)
@@ -739,7 +743,7 @@
- Fixed: Fix settings save confirmation not shown when navigating to encryption/profile/log screens (#9313) (#9312 by Henry Heino)
- Fixed: Restore scroll position when returning to the note viewer from the editor or camera (#9324) (#9321 by Henry Heino)
## [android-v2.13.6](https://github.com/laurent22/joplin/releases/tag/android-v2.13.6) (Pre-release) - 2023-11-09T19:45:21Z
## [android-v2.13.6](https://github.com/laurent22/joplin/releases/tag/android-v2.13.6) - 2023-11-09T19:45:21Z
- Improved: Add a "Retry all" button when multiple resources could not be downloaded (#9158)
- Improved: Image editor: Allow loading from save when the image editor is reloaded in the background (#9135) (#9134 by Henry Heino)
@@ -751,13 +755,13 @@
- Fixed: Fix search highlighting (#9206) (#9207 by Henry Heino)
- Fixed: Image editor resets on theme change (#9190) (#9188 by Henry Heino)
## [android-v2.13.5](https://github.com/laurent22/joplin/releases/tag/android-v2.13.5) (Pre-release) - 2023-10-30T22:49:19Z
## [android-v2.13.5](https://github.com/laurent22/joplin/releases/tag/android-v2.13.5) - 2023-10-30T22:49:19Z
- Improved: Allow searching by note ID or using a callback URL (3667bf3)
- Improved: Updated packages @react-native-community/datetimepicker (v7.6.0), react-native-device-info (v10.11.0), react-native-webview (v13.6.2)
- Fixed: Beta editor: Fix image timestamps not updated after editing (#9176) (#9175 by Henry Heino)
## [android-v2.13.4](https://github.com/laurent22/joplin/releases/tag/android-v2.13.4) (Pre-release) - 2023-10-24T18:29:09Z
## [android-v2.13.4](https://github.com/laurent22/joplin/releases/tag/android-v2.13.4) - 2023-10-24T18:29:09Z
- Improved: Allow modifying a resource metadata only when synchronising (#9114)
- Improved: Support for plural translations (#9033)
@@ -767,7 +771,7 @@
- Fixed: Fixed issues related to sharing notes on read-only notebooks (1c7d22e)
- Fixed: Improve list toggle logic (#9103) (#9066 by Henry Heino)
## [android-v2.13.2](https://github.com/laurent22/joplin/releases/tag/android-v2.13.2) (Pre-release) - 2023-10-07T16:42:16Z
## [android-v2.13.2](https://github.com/laurent22/joplin/releases/tag/android-v2.13.2) - 2023-10-07T16:42:16Z
- New: Add share button to log screen (#8364 by Henry Heino)
- New: Add support for drawing pictures (#7588 by Henry Heino)
@@ -787,18 +791,18 @@
- Fixed: Hide the keyboard when showing the attach dialog (#8911) (#8774 by Henry Heino)
- Fixed: Prevent accessibility tools from focusing the notes list when it's invisible (#8799) (#8798 by Henry Heino)
## [android-v2.12.3](https://github.com/laurent22/joplin/releases/tag/android-v2.12.3) (Pre-release) - 2023-09-11T20:01:44Z
## [android-v2.12.3](https://github.com/laurent22/joplin/releases/tag/android-v2.12.3) - 2023-09-11T20:01:44Z
- Improved: Add screen reader labels to search/note actions buttons (#8797) (#8796 by Henry Heino)
- Improved: Improve accessibility of side menu (#8839 by Henry Heino)
- Fixed: Fix older Android versions unable to set alarms (#8837) (#8789 by Henry Heino)
- Fixed: Revert to `react-native-sidemenu-updated` for navigation drawers (#8820) (#8791 by Henry Heino)
## [android-v2.12.2](https://github.com/laurent22/joplin/releases/tag/android-v2.12.2) (Pre-release) - 2023-08-22T13:15:18Z
## [android-v2.12.2](https://github.com/laurent22/joplin/releases/tag/android-v2.12.2) - 2023-08-22T13:15:18Z
- Improved: Only include "armeabi-v7a", "x86", "arm64-v8a", "x86_64" in APK (4e2d366)
## [android-v2.12.1](https://github.com/laurent22/joplin/releases/tag/android-v2.12.1) (Pre-release) - 2023-08-19T22:32:39Z
## [android-v2.12.1](https://github.com/laurent22/joplin/releases/tag/android-v2.12.1) - 2023-08-19T22:32:39Z
- New: Add JEX export (#8428 by Henry Heino)
- New: Add support for Joplin Cloud email to note functionality (#8460 by [@pedr](https://github.com/pedr))
@@ -824,40 +828,40 @@
- Fixed: Unrevert #7953: Migrate to react-native-drawer-layout (#8379) (#7918 by Henry Heino)
- Security: Prevent XSS when passing specially encoded string to a link (57b4198)
## [android-v2.11.32](https://github.com/laurent22/joplin/releases/tag/android-v2.11.32) (Pre-release) - 2023-07-03T11:33:54Z
## [android-v2.11.32](https://github.com/laurent22/joplin/releases/tag/android-v2.11.32) - 2023-07-03T11:33:54Z
- Improved: Allow configuring voice typing model URL (2aab85f)
## [android-v2.11.31](https://github.com/laurent22/joplin/releases/tag/android-v2.11.31) (Pre-release) - 2023-06-25T14:26:21Z
## [android-v2.11.31](https://github.com/laurent22/joplin/releases/tag/android-v2.11.31) - 2023-06-25T14:26:21Z
- Improved: Upgrade E2EE encryption method to AES-256 (#7686)
## [android-v2.11.30](https://github.com/laurent22/joplin/releases/tag/android-v2.11.30) (Pre-release) - 2023-06-20T15:21:15Z
## [android-v2.11.30](https://github.com/laurent22/joplin/releases/tag/android-v2.11.30) - 2023-06-20T15:21:15Z
- New: Add support for Voice Typing for most languages (#8309)
## [android-v2.11.27](https://github.com/laurent22/joplin/releases/tag/android-v2.11.27) (Pre-release) - 2023-06-10T15:58:58Z
## [android-v2.11.27](https://github.com/laurent22/joplin/releases/tag/android-v2.11.27) - 2023-06-10T15:58:58Z
- Upgraded to React Native 0.71
- Improved: Updated packages @react-native-community/datetimepicker (v7), buildTools, domutils (v3.1.0), react-native-document-picker (v8.2.1), react-native-safe-area-context (v4.5.3), tar (v6.1.15)
## [android-v2.11.26](https://github.com/laurent22/joplin/releases/tag/android-v2.11.26) (Pre-release) - 2023-06-08T16:13:02Z
## [android-v2.11.26](https://github.com/laurent22/joplin/releases/tag/android-v2.11.26) - 2023-06-08T16:13:02Z
- Improved: Updated packages @react-native-community/datetimepicker (v7), buildTools, domutils (v3.1.0), react-native-document-picker (v8.2.1), react-native-safe-area-context (v4.5.3), tar (v6.1.15)
- Fixed: Allow certain HTML anchor tags (#8286)
- Fixed: Fix alarms for latest Android versions (#8229)
- Fixed: Fix sharing data with the app (#8285)
## [android-v2.11.25](https://github.com/laurent22/joplin/releases/tag/android-v2.11.25) (Pre-release) - 2023-06-03T16:40:08Z
## [android-v2.11.25](https://github.com/laurent22/joplin/releases/tag/android-v2.11.25) - 2023-06-03T16:40:08Z
- Fixed: Fix Vosk logic (60b3921)
- Fixed: Fixed error "Download interrupted" when downloading resources from Joplin Cloud/Server.
## [android-v2.11.24](https://github.com/laurent22/joplin/releases/tag/android-v2.11.24) (Pre-release) - 2023-06-02T15:22:04Z
## [android-v2.11.24](https://github.com/laurent22/joplin/releases/tag/android-v2.11.24) - 2023-06-02T15:22:04Z
- Improved: Write to note in realtime using voice typing (7779879)
## [android-v2.11.23](https://github.com/laurent22/joplin/releases/tag/android-v2.11.23) (Pre-release) - 2023-06-01T17:19:16Z
## [android-v2.11.23](https://github.com/laurent22/joplin/releases/tag/android-v2.11.23) - 2023-06-01T17:19:16Z
- Improved: Auto-detect language on start (e48d55c)
- Improved: Implement parenting of notebooks (#7980) (#8193 by [@jcgurango](https://github.com/jcgurango))
@@ -869,59 +873,59 @@
- Security: Disable SVG tag support in editor to prevent XSS (caf6606)
- Security: Prevent XSS by sanitizing certain HTML attributes (9e90d90)
## [android-v2.11.22](https://github.com/laurent22/joplin/releases/tag/android-v2.11.22) (Pre-release) - 2023-05-14T13:44:28Z
## [android-v2.11.22](https://github.com/laurent22/joplin/releases/tag/android-v2.11.22) - 2023-05-14T13:44:28Z
- Fixed: Fix "Download interrupted" error (b023f58)
## [android-v2.11.21](https://github.com/laurent22/joplin/releases/tag/android-v2.11.21) (Pre-release) - 2023-05-14T11:05:15Z
## [android-v2.11.21](https://github.com/laurent22/joplin/releases/tag/android-v2.11.21) - 2023-05-14T11:05:15Z
- Improved: Updated packages react-native-paper (v5.6.0)
## [android-v2.11.16](https://github.com/laurent22/joplin/releases/tag/android-v2.11.16) (Pre-release) - 2023-05-12T12:43:08Z
## [android-v2.11.16](https://github.com/laurent22/joplin/releases/tag/android-v2.11.16) - 2023-05-12T12:43:08Z
- Improved: Sync as soon as the app starts, and immediately after changing a note (3eb44d2)
## [android-v2.11.14](https://github.com/laurent22/joplin/releases/tag/android-v2.11.14) (Pre-release) - 2023-05-10T12:24:40Z
## [android-v2.11.14](https://github.com/laurent22/joplin/releases/tag/android-v2.11.14) - 2023-05-10T12:24:40Z
- Improved: Translate Welcome notes (#8154)
## [android-v2.11.13](https://github.com/laurent22/joplin/releases/tag/android-v2.11.13) (Pre-release) - 2023-05-08T20:28:29Z
## [android-v2.11.13](https://github.com/laurent22/joplin/releases/tag/android-v2.11.13) - 2023-05-08T20:28:29Z
- Improved: Tells whether Hermes engine is enabled or not (5ecae17)
## [android-v2.11.10](https://github.com/laurent22/joplin/releases/tag/android-v2.11.10) (Pre-release) - 2023-05-08T10:26:14Z
## [android-v2.11.10](https://github.com/laurent22/joplin/releases/tag/android-v2.11.10) - 2023-05-08T10:26:14Z
- Improved: Disable Hermes engine (e9e9986)
- Fixed: Fix voice typing (d5eeb12)
## [android-v2.11.7](https://github.com/laurent22/joplin/releases/tag/android-v2.11.7) (Pre-release) - 2023-05-07T14:29:08Z
## [android-v2.11.7](https://github.com/laurent22/joplin/releases/tag/android-v2.11.7) - 2023-05-07T14:29:08Z
- Fixed crash when starting voice typing.
## [android-v2.11.6](https://github.com/laurent22/joplin/releases/tag/android-v2.11.6) (Pre-release) - 2023-05-07T13:53:31Z
## [android-v2.11.6](https://github.com/laurent22/joplin/releases/tag/android-v2.11.6) - 2023-05-07T13:53:31Z
- Disabled Hermes engine
## [android-v2.11.5](https://github.com/laurent22/joplin/releases/tag/android-v2.11.5) (Pre-release) - 2023-05-07T12:14:21Z
## [android-v2.11.5](https://github.com/laurent22/joplin/releases/tag/android-v2.11.5) - 2023-05-07T12:14:21Z
- Improved: Improved Vosk support (beta, fr only) (#8131)
- Improved: Updated packages react-native-share (v8.2.2), reselect (v4.1.8), sharp (v0.32.0)
## [android-v2.11.4](https://github.com/laurent22/joplin/releases/tag/android-v2.11.4) (Pre-release) - 2023-05-03T11:57:27Z
## [android-v2.11.4](https://github.com/laurent22/joplin/releases/tag/android-v2.11.4) - 2023-05-03T11:57:27Z
- New: Add support for offline speech to text (Beta - FR only) (#8115)
- Improved: Updated packages @react-native-community/netinfo (v9.3.9), aws, react-native-document-picker (v8.2.0), react-native-paper (v5.5.2), react-native-safe-area-context (v4.5.1), sass (v1.60.0)
- Fixed: Fixed sync crash (#8056) (#8017 by Arun Kumar)
- Fixed: Fixes issue where the note body is not updated after attaching a file (991c120)
## [android-v2.11.2](https://github.com/laurent22/joplin/releases/tag/android-v2.11.2) (Pre-release) - 2023-04-09T12:04:06Z
## [android-v2.11.2](https://github.com/laurent22/joplin/releases/tag/android-v2.11.2) - 2023-04-09T12:04:06Z
- Improved: Resolve #8022: Editor syntax highlighting was broken (#8023) (#8022 by Henry Heino)
- Improved: Updated packages @react-native-community/netinfo (v9.3.8)
- Fixed: Removed `MasterKey` from Sync Status report (#8026) (#7940 by Arun Kumar)
- Security: Prevent bypassing fingerprint lock on certain devices (6b72f86)
## [android-v2.11.1](https://github.com/laurent22/joplin/releases/tag/android-v2.11.1) (Pre-release) - 2023-04-08T08:49:19Z
## [android-v2.11.1](https://github.com/laurent22/joplin/releases/tag/android-v2.11.1) - 2023-04-08T08:49:19Z
- New: Add log info for biometrics feature (efdbaeb)
- New: Add setting to enable/disable the markdown toolbar (#7929 by Henry Heino)
@@ -929,11 +933,11 @@
- Fixed: Fix OneDrive sync attempting to call method on `null` variable (#7987) (#7986 by Henry Heino)
- Updated packages @lezer/highlight (v1.1.4), fs-extra (v11.1.1), jsdom (v21.1.1), markdown-it-multimd-table (v4.2.1), nanoid (v3.3.6), node-persist (v3.1.3), nodemon (v2.0.22), react-native-document-picker (v8.1.4), react-native-image-picker (v5.3.1), react-native-paper (v5.4.1), react-native-share (v8.2.1), sass (v1.59.3), sqlite3 (v5.1.6), turndown (v7.1.2), yargs (v17.7.1)
## [android-v2.10.9](https://github.com/laurent22/joplin/releases/tag/android-v2.10.9) (Pre-release) - 2023-03-22T18:40:57Z
## [android-v2.10.9](https://github.com/laurent22/joplin/releases/tag/android-v2.10.9) - 2023-03-22T18:40:57Z
- Improved: Mark biometrics feature as beta and ensure no call is made if it is not enabled (e44a934)
## [android-v2.10.8](https://github.com/laurent22/joplin/releases/tag/android-v2.10.8) (Pre-release) - 2023-02-28T18:09:21Z
## [android-v2.10.8](https://github.com/laurent22/joplin/releases/tag/android-v2.10.8) - 2023-02-28T18:09:21Z
- Improved: Stop synchronization with unsupported WebDAV providers (#7819) (#7661 by [@julien](https://github.com/julien))
- Fixed: Custom sort order not synchronized (#7729) (#6956 by Tao Klerks)
@@ -944,20 +948,20 @@
- Fixed: Hide main content while biometric is enabled and not authenticated (#7781) (#7762 by [@pedr](https://github.com/pedr))
- Fixed: Sharing pictures to Joplin creates recurring duplications (#7807) (#7791 by [@jd1378](https://github.com/jd1378))
## [android-v2.10.6](https://github.com/laurent22/joplin/releases/tag/android-v2.10.6) (Pre-release) - 2023-02-10T16:22:28Z
## [android-v2.10.6](https://github.com/laurent22/joplin/releases/tag/android-v2.10.6) - 2023-02-10T16:22:28Z
- Improved: Add create sub-notebook feature (#7728) (#1044 by [@carlosngo](https://github.com/carlosngo))
- Fixed: Fix double-scroll issue in long notes (#7701) (#7700 by Henry Heino)
- Fixed: Fix startup error (#7688) (#7687 by Henry Heino)
- Fixed: Sharing file to Joplin does not work (#7691)
## [android-v2.10.5](https://github.com/laurent22/joplin/releases/tag/android-v2.10.5) (Pre-release) - 2023-01-21T14:21:23Z
## [android-v2.10.5](https://github.com/laurent22/joplin/releases/tag/android-v2.10.5) - 2023-01-21T14:21:23Z
- Improved: Improve dialogue spacing in Fountain renderer (#7628) (#7627 by [@Elleo](https://github.com/Elleo))
- Improved: Improve filesystem sync performance (#7637) (#6942 by [@jd1378](https://github.com/jd1378))
- Fixed: Fixes non-working alarms (138bc81)
## [android-v2.10.4](https://github.com/laurent22/joplin/releases/tag/android-v2.10.4) (Pre-release) - 2023-01-14T17:30:34Z
## [android-v2.10.4](https://github.com/laurent22/joplin/releases/tag/android-v2.10.4) - 2023-01-14T17:30:34Z
- New: Add support for multiple profiles (6bb52d5)
- Improved: Configurable editor font size (#7596 by Henry Heino)
@@ -968,19 +972,19 @@
- Fixed: Fixed issue when floating keyboard is visible (#7593) (#6682 by Henry Heino)
- Fixed: Remove gray line around text editor (#7595) (#7594 by Henry Heino)
## [android-v2.10.3](https://github.com/laurent22/joplin/releases/tag/android-v2.10.3) (Pre-release) - 2023-01-05T11:29:06Z
## [android-v2.10.3](https://github.com/laurent22/joplin/releases/tag/android-v2.10.3) - 2023-01-05T11:29:06Z
- New: Add support for locking the app using biometrics (f10d9f7)
- Improved: Make the new text editor the default one (f5ef318)
- Fixed: Fixed proxy timeout setting UI (275c80a)
- Fixed: Settings save button visible even when no settings have been changed (#7503)
## [android-v2.10.2](https://github.com/laurent22/joplin/releases/tag/android-v2.10.2) (Pre-release) - 2023-01-02T17:44:15Z
## [android-v2.10.2](https://github.com/laurent22/joplin/releases/tag/android-v2.10.2) - 2023-01-02T17:44:15Z
- New: Add support for realtime search (767213c)
- Fixed: Enable autocorrect with spellcheck (#7532) (#6175 by Henry Heino)
## [android-v2.10.1](https://github.com/laurent22/joplin/releases/tag/android-v2.10.1) (Pre-release) - 2022-12-29T13:55:48Z
## [android-v2.10.1](https://github.com/laurent22/joplin/releases/tag/android-v2.10.1) - 2022-12-29T13:55:48Z
- Improved: Switch license to AGPL-3.0 (faf0a4e)
- Improved: Tag search case insensitive (#7368 by [@JackGruber](https://github.com/JackGruber))
@@ -992,16 +996,16 @@
- Fixed: Update CodeMirror (#7262) (#7253 by Henry Heino)
- Security: Fix XSS when a specially crafted string is passed to the renderer (762b4e8)
## [android-v2.9.8](https://github.com/laurent22/joplin/releases/tag/android-v2.9.8) (Pre-release) - 2022-11-01T15:45:36Z
## [android-v2.9.8](https://github.com/laurent22/joplin/releases/tag/android-v2.9.8) - 2022-11-01T15:45:36Z
- Updated translations
## [android-v2.9.7](https://github.com/laurent22/joplin/releases/tag/android-v2.9.7) (Pre-release) - 2022-10-30T10:25:01Z
## [android-v2.9.7](https://github.com/laurent22/joplin/releases/tag/android-v2.9.7) - 2022-10-30T10:25:01Z
- Fixed: Fixed notebook icons alignment (ea6b7ca)
- Fixed: Fixed crash when attaching a file.
## [android-v2.9.6](https://github.com/laurent22/joplin/releases/tag/android-v2.9.6) (Pre-release) - 2022-10-23T16:23:25Z
## [android-v2.9.6](https://github.com/laurent22/joplin/releases/tag/android-v2.9.6) - 2022-10-23T16:23:25Z
- New: Add monochrome icon (#6954 by Tom Bursch)
- Fixed: Fix file system sync issues (#6943 by [@jd1378](https://github.com/jd1378))
@@ -1009,12 +1013,12 @@
- Fixed: Fixed notebook icon spacing (633c9ac)
- Fixed: Support non-ASCII characters in OneDrive (#6916) (#6838 by Self Not Found)
## [android-v2.9.5](https://github.com/laurent22/joplin/releases/tag/android-v2.9.5) (Pre-release) - 2022-10-11T13:52:00Z
## [android-v2.9.5](https://github.com/laurent22/joplin/releases/tag/android-v2.9.5) - 2022-10-11T13:52:00Z
- Improved: Disable multi-highlighting to fix context menu (9b348fd)
- Improved: Display icon for all notebooks if at least one notebook has an icon (ec97dd8)
## [android-v2.9.3](https://github.com/laurent22/joplin/releases/tag/android-v2.9.3) (Pre-release) - 2022-10-07T11:12:56Z
## [android-v2.9.3](https://github.com/laurent22/joplin/releases/tag/android-v2.9.3) - 2022-10-07T11:12:56Z
- Improved: Convert empty bolded regions to bold-italic regions in beta editor (#6807) (#6808 by Henry Heino)
- Improved: Increase the attachment size limit to 200MB (#6848 by Self Not Found)
@@ -1026,7 +1030,7 @@
- Fixed: Fix resources sync when proxy is set (#6817) (#6688 by Self Not Found)
- Fixed: Fixed crash when trying to move note to notebook (#6898)
## [android-v2.9.2](https://github.com/laurent22/joplin/releases/tag/android-v2.9.2) (Pre-release) - 2022-09-01T11:14:58Z
## [android-v2.9.2](https://github.com/laurent22/joplin/releases/tag/android-v2.9.2) - 2022-09-01T11:14:58Z
- New: Add Markdown toolbar (#6753 by Henry Heino)
- New: Add long-press tooltips (#6758 by Henry Heino)
@@ -1038,7 +1042,7 @@
- Fixed: Fixed Android filesystem sync (resources) (#6789) (#6779 by [@jd1378](https://github.com/jd1378))
- Fixed: Fixed handling of normal paths in filesystem sync (#6792) (#6791 by [@jd1378](https://github.com/jd1378))
## [android-v2.9.1](https://github.com/laurent22/joplin/releases/tag/android-v2.9.1) (Pre-release) - 2022-08-12T17:14:49Z
## [android-v2.9.1](https://github.com/laurent22/joplin/releases/tag/android-v2.9.1) - 2022-08-12T17:14:49Z
- New: Add alt text/roles to some buttons to improve accessibility (#6616 by Henry Heino)
- New: Add keyboard-activatable markdown commands (e.g. bold, italicize) on text editor (#6707 by Henry Heino)
@@ -1051,7 +1055,7 @@
- Fixed: Note links with HTML notation did not work (#6515)
- Fixed: Scroll selection into view in beta editor when window resizes (#6610) (#5949 by Henry Heino)
## [android-v2.8.1](https://github.com/laurent22/joplin/releases/tag/android-v2.8.1) (Pre-release) - 2022-05-18T13:35:01Z
## [android-v2.8.1](https://github.com/laurent22/joplin/releases/tag/android-v2.8.1) - 2022-05-18T13:35:01Z
- Improved: Allow filtering tags in tag dialog (#6221 by [@shinglyu](https://github.com/shinglyu))
- Improved: Automatically start sync after setting the sync parameters (ff066ba)
@@ -1069,7 +1073,7 @@
- Fixed: Support inserting attachments from Beta Editor (#6325) (#6324 by Henry Heino)
- Fixed: The camera button remains clickable after taking a photo bug (#6222 by [@shinglyu](https://github.com/shinglyu))
## [android-v2.7.2](https://github.com/laurent22/joplin/releases/tag/android-v2.7.2) (Pre-release) - 2022-02-12T12:51:29Z
## [android-v2.7.2](https://github.com/laurent22/joplin/releases/tag/android-v2.7.2) - 2022-02-12T12:51:29Z
- New: Add additional time format HH.mm (#6086 by [@vincentjocodes](https://github.com/vincentjocodes))
- Improved: Do not duplicate resources when duplicating a note (721d008)
@@ -1087,11 +1091,11 @@
- Improved: Update Mermaid: 8.12.1 -&gt; 8.13.5 (#5831 by Helmut K. C. Tessarek)
- Fixed: Links in flowchart Mermaid diagrams (#5830) (#5801 by Helmut K. C. Tessarek)
## [android-v2.6.5](https://github.com/laurent22/joplin/releases/tag/android-v2.6.5) (Pre-release) - 2021-12-13T09:41:18Z
## [android-v2.6.5](https://github.com/laurent22/joplin/releases/tag/android-v2.6.5) - 2021-12-13T09:41:18Z
- Fixed: Fixed "Invalid lock client type" error when migrating sync target (e0e93c4)
## [android-v2.6.4](https://github.com/laurent22/joplin/releases/tag/android-v2.6.4) (Pre-release) - 2021-12-01T11:38:49Z
## [android-v2.6.4](https://github.com/laurent22/joplin/releases/tag/android-v2.6.4) - 2021-12-01T11:38:49Z
- Improved: Also duplicate resources when duplicating a note (c0a8c33)
- Improved: Improved S3 sync error handling and reliability, and upgraded S3 SDK (#5312 by Lee Matos)
@@ -1100,7 +1104,7 @@
- Fixed: Fixed opening attachments (6950c40)
- Fixed: Handle duplicate attachments when the parent notebook is shared (#5796)
## [android-v2.6.3](https://github.com/laurent22/joplin/releases/tag/android-v2.6.3) (Pre-release) - 2021-11-21T16:59:46Z
## [android-v2.6.3](https://github.com/laurent22/joplin/releases/tag/android-v2.6.3) - 2021-11-21T16:59:46Z
- New: Add date format YYYY/MM/DD (#5759 by Helmut K. C. Tessarek)
- New: Add support for faster built-in sync locks (#5662)
@@ -1113,18 +1117,18 @@
- Fixed: Fixed issue with parts of HTML notes not being displayed in some cases (#5687)
- Fixed: Sharing multiple notebooks via Joplin Server with the same user results in an error (#5721)
## [android-v2.6.1](https://github.com/laurent22/joplin/releases/tag/android-v2.6.1) (Pre-release) - 2021-11-02T20:49:53Z
## [android-v2.6.1](https://github.com/laurent22/joplin/releases/tag/android-v2.6.1) - 2021-11-02T20:49:53Z
- Improved: Upgraded React Native from 0.64 to 0.66 (66e79cc)
- Fixed: Fixed potential infinite loop when Joplin Server session is invalid (c5569ef)
## [android-v2.5.5](https://github.com/laurent22/joplin/releases/tag/android-v2.5.5) (Pre-release) - 2021-10-31T11:03:16Z
## [android-v2.5.5](https://github.com/laurent22/joplin/releases/tag/android-v2.5.5) - 2021-10-31T11:03:16Z
- New: Add padding around beta text editor (365e152)
- Improved: Capitalise first word of sentence in beta editor (4128be9)
- Fixed: Do not render very large code blocks to prevent app from freezing (#5593)
## [android-v2.5.3](https://github.com/laurent22/joplin/releases/tag/android-v2.5.3) (Pre-release) - 2021-10-28T21:47:18Z
## [android-v2.5.3](https://github.com/laurent22/joplin/releases/tag/android-v2.5.3) - 2021-10-28T21:47:18Z
- New: Add support for public-private key pairs and improved master password support (#5438)
- New: Added mechanism to migrate default settings to new values (72db8e4)
@@ -1139,13 +1143,13 @@
- Fixed: Fix default sync target (4b39d30)
## [android-v2.4.2](https://github.com/laurent22/joplin/releases/tag/android-v2.4.2) (Pre-release) - 2021-09-22T17:02:37Z
## [android-v2.4.2](https://github.com/laurent22/joplin/releases/tag/android-v2.4.2) - 2021-09-22T17:02:37Z
- Improved: Allow disabling any master key, including default or active one (9407efd)
- Improved: Update Mermaid 8.10.2 -&gt; 8.12.1 and fix gitGraph crash (#5448) (#5295 by Helmut K. C. Tessarek)
- Fixed: Misinterpreted search term after filter in quotation marks (#5445) (#5444 by [@JackGruber](https://github.com/JackGruber))
## [android-v2.4.1](https://github.com/laurent22/joplin/releases/tag/android-v2.4.1) (Pre-release) - 2021-08-30T13:37:34Z
## [android-v2.4.1](https://github.com/laurent22/joplin/releases/tag/android-v2.4.1) - 2021-08-30T13:37:34Z
- New: Add a way to disable a master key (7faa58e)
- New: Add support for single master password, to simplify handling of multiple encryption keys (ce89ee5)
@@ -1155,21 +1159,21 @@
- Improved: Show the used tags first in the tagging dialog (#5315 by [@JackGruber](https://github.com/JackGruber))
- Fixed: Fixed crash when a required master key does not exist (#5391)
## [android-v2.3.4](https://github.com/laurent22/joplin/releases/tag/android-v2.3.4) (Pre-release) - 2021-08-15T13:27:57Z
## [android-v2.3.4](https://github.com/laurent22/joplin/releases/tag/android-v2.3.4) - 2021-08-15T13:27:57Z
- Fixed: Bump highlight.js to v11.2 (#5278) (#5245 by Roman Musin)
## [android-v2.3.3](https://github.com/laurent22/joplin/releases/tag/android-v2.3.3) (Pre-release) - 2021-08-12T20:46:15Z
## [android-v2.3.3](https://github.com/laurent22/joplin/releases/tag/android-v2.3.3) - 2021-08-12T20:46:15Z
- Improved: Improved E2EE usability by making its state a property of the sync target (#5276)
## [android-v2.2.5](https://github.com/laurent22/joplin/releases/tag/android-v2.2.5) (Pre-release) - 2021-08-11T10:54:38Z
## [android-v2.2.5](https://github.com/laurent22/joplin/releases/tag/android-v2.2.5) - 2021-08-11T10:54:38Z
- Revert "Plugins: Add ability to make dialogs fit the application window (#5219)" as it breaks several plugin webviews.
- Revert "Resolves #4810, Resolves #4610: Fix AWS S3 sync error and upgrade framework to v3 (#5212)" due to incompatibility with some AWS providers.
- Improved: Upgraded React Native to v0.64 (afb7e1a)
## [android-v2.2.3](https://github.com/laurent22/joplin/releases/tag/android-v2.2.3) (Pre-release) - 2021-08-09T18:48:29Z
## [android-v2.2.3](https://github.com/laurent22/joplin/releases/tag/android-v2.2.3) - 2021-08-09T18:48:29Z
- Improved: Ensure that timestamps are not changed when sharing or unsharing a note (cafaa9c)
- Improved: Fix AWS S3 sync error and upgrade framework to v3 (#5212) (#4810 by Lee Matos)
@@ -1177,7 +1181,7 @@
- Improved: Make sync icon spin in the right direction (#5275) (#4588 by Lee Matos)
- Fixed: Fixed issue with orphaned resource being created in case of a resource conflict (#5223)
## [android-v2.2.1](https://github.com/laurent22/joplin/releases/tag/android-v2.2.1) (Pre-release) - 2021-07-13T17:37:38Z
## [android-v2.2.1](https://github.com/laurent22/joplin/releases/tag/android-v2.2.1) - 2021-07-13T17:37:38Z
- New: Added improved editor (beta)
- Improved: Disable backup to Google Drive (#5114 by Roman Musin)
@@ -1197,12 +1201,12 @@
- Fixed: Fixed search when the index contains non-existing notes (5ecac21)
- Fixed: Fixed version number on config screen (65e9268)
## [android-v2.1.2](https://github.com/laurent22/joplin/releases/tag/android-v2.1.2) (Pre-release) - 2021-06-20T18:36:23Z
## [android-v2.1.2](https://github.com/laurent22/joplin/releases/tag/android-v2.1.2) - 2021-06-20T18:36:23Z
- Fixed: Fixed error that could prevent a revision from being created, and that would prevent the revision service from processing the rest of the notes (#5051)
- Fixed: Fixed issue when trying to sync an item associated with a share that no longer exists (5bb68ba)
## [android-v2.1.1](https://github.com/laurent22/joplin/releases/tag/android-v2.1.1) (Pre-release) - 2021-06-19T16:42:57Z
## [android-v2.1.1](https://github.com/laurent22/joplin/releases/tag/android-v2.1.1) - 2021-06-19T16:42:57Z
- New: Add version number to log (525ab01)
- New: Added feature flags to disable Joplin Server sync optimisations by default, so that it still work with server 2.0 (326fef4)
@@ -1217,7 +1221,7 @@
- Improved: Prevent sync process from being stuck when the download state of a resource is invalid (5c6fd93)
## [android-v2.0.3](https://github.com/laurent22/joplin/releases/tag/android-v2.0.3) (Pre-release) - 2021-06-16T09:48:58Z
## [android-v2.0.3](https://github.com/laurent22/joplin/releases/tag/android-v2.0.3) - 2021-06-16T09:48:58Z
- Improved: Verbose mode for synchronizer (4bbb3d1)

View File

@@ -1,5 +1,27 @@
# Joplin Desktop Changelog
## [v3.6.2](https://github.com/laurent22/joplin/releases/tag/v3.6.2) (Pre-release) - 2026-01-18T20:10:43Z
- Improved: Accessibility: Include accessibility information in exported PDFs ([#14111](https://github.com/laurent22/joplin/issues/14111)) ([#14086](https://github.com/laurent22/joplin/issues/14086) by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Improved: Editor: Inline rendering: Render inline HTML (colorized text, superscript, subscript, strikethrough) ([#14133](https://github.com/laurent22/joplin/issues/14133) by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Fixed: .onepkg import: Fix Unicode issues, support Linux and MacOS ([#14094](https://github.com/laurent22/joplin/issues/14094)) ([#14084](https://github.com/laurent22/joplin/issues/14084) by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Fixed: Application crashes when profile database has been analyzed ([#14144](https://github.com/laurent22/joplin/issues/14144))
- Fixed: Built-in plugins: Upgrade Freehand Drawing to v4.3.0 ([#14123](https://github.com/laurent22/joplin/issues/14123)) ([#14092](https://github.com/laurent22/joplin/issues/14092) by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
## [v3.6.1](https://github.com/laurent22/joplin/releases/tag/v3.6.1) (Pre-release) - 2026-01-17T14:17:29Z
- New: Add more error information when the profile is corrupted ([f075b56](https://github.com/laurent22/joplin/commit/f075b56))
- New: Add support for external embeds, eg. YouTube videos ([#14012](https://github.com/laurent22/joplin/issues/14012))
- Improved: Improve Fountain notes exported as PDF ([#14120](https://github.com/laurent22/joplin/issues/14120)) ([#14106](https://github.com/laurent22/joplin/issues/14106))
- Improved: Updated packages @rollup/plugin-commonjs (v28.0.8), @rollup/plugin-node-resolve (v16.0.3), style-to-js (v1.1.18)
- Fixed: Experimental auto-updater: Fix application crash on update failure ([#14083](https://github.com/laurent22/joplin/issues/14083)) ([#13430](https://github.com/laurent22/joplin/issues/13430) by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Fixed: Rich Text Editor: Fix cut, copy, paste, and select all menu items ([#14125](https://github.com/laurent22/joplin/issues/14125) by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
## [v3.5.12](https://github.com/laurent22/joplin/releases/tag/v3.5.12) - 2026-01-17T14:20:33Z
- Fixed: Experimental auto-updater: Fix application crash on update failure ([#14083](https://github.com/laurent22/joplin/issues/14083)) ([#13430](https://github.com/laurent22/joplin/issues/13430) by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
- Fixed: Rich Text Editor: Fix cut, copy, paste, and select all menu items ([#14125](https://github.com/laurent22/joplin/issues/14125) by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))
## [v3.5.11](https://github.com/laurent22/joplin/releases/tag/v3.5.11) - 2026-01-12T15:17:25Z
- Improved: OneNote importer: Simplify error report ([#14074](https://github.com/laurent22/joplin/issues/14074) by [@personalizedrefrigerator](https://github.com/personalizedrefrigerator))

View File

@@ -8,7 +8,7 @@ Joplin Server Business is a synchronisation server that you can install on your
Your teams can collaborate on notebooks and share information. They can also publish notes to the internet or within your own intranet. All that secured by Joplin end-to-end encryption.
Interested? [Contact us for a quote](mailto:jsb-inquiry@joplin.cloud?subject=Joplin%20Server%20Business%20inquiry)
Interested? [Contact us for a quote](https://tally.so/r/D4BlOE)
</div>
@@ -88,7 +88,7 @@ Keep all your resources in one place. Save and share images, PDFs, videos, audio
To find out more about Joplin Server Business and how it can be integrated to your organisation, feel free to contact us. Our experts can prepare a demo for you. We can provide a quote to accommodate your company’s needs.
[Contact us for a quote!](mailto:jsb-inquiry@joplin.cloud?subject=Joplin%20Server%20Business%20inquiry)
[Contact us for a quote!](https://tally.so/r/D4BlOE)
## Difference with Joplin Server

View File

@@ -10,7 +10,7 @@ Your download of <span class="downloaded-filename">Joplin</span> is in progress.
Access your notes on Windows, macOS or Linux.
<!-- DESKTOP-DOWNLOAD-LINKS --><a class="download-link-windows" href='https://objects.joplinusercontent.com/v3.5.11/Joplin-Setup-3.5.11.exe?source=JoplinWebsite&type=New'><img alt='Get it on Windows' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeWindows.png'/></a> <a class="download-link-macOs" href='https://objects.joplinusercontent.com/v3.5.11/Joplin-3.5.11.dmg?source=JoplinWebsite&type=New'><img alt='Get it on macOS' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeMacOS.png'/></a> <a class="download-link-macOsM1" href='https://objects.joplinusercontent.com/v3.5.11/Joplin-3.5.11-arm64.DMG?source=JoplinWebsite&type=New'><img alt='Get it on macOS M1 (Silicon)' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeMacOSM1.png'/></a> <a class="download-link-linux" href='https://objects.joplinusercontent.com/v3.5.11/Joplin-3.5.11.AppImage?source=JoplinWebsite&type=New'><img alt='Get it on Linux' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeLinux.png'/></a><!-- DESKTOP-DOWNLOAD-LINKS -->
<!-- DESKTOP-DOWNLOAD-LINKS --><a class="download-link-windows" href='https://objects.joplinusercontent.com/v3.5.12/Joplin-Setup-3.5.12.exe?source=JoplinWebsite&type=New'><img alt='Get it on Windows' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeWindows.png'/></a> <a class="download-link-macOs" href='https://objects.joplinusercontent.com/v3.5.12/Joplin-3.5.12.dmg?source=JoplinWebsite&type=New'><img alt='Get it on macOS' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeMacOS.png'/></a> <a class="download-link-macOsM1" href='https://objects.joplinusercontent.com/v3.5.12/Joplin-3.5.12-arm64.DMG?source=JoplinWebsite&type=New'><img alt='Get it on macOS M1 (Silicon)' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeMacOSM1.png'/></a> <a class="download-link-linux" href='https://objects.joplinusercontent.com/v3.5.12/Joplin-3.5.12.AppImage?source=JoplinWebsite&type=New'><img alt='Get it on Linux' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeLinux.png'/></a><!-- DESKTOP-DOWNLOAD-LINKS -->
</div>

View File

@@ -10,12 +10,12 @@ Three types of applications are available: for **desktop** (Windows, macOS and L
Operating System | Download
---|---
Windows (32 and 64-bit) | <a href='https://objects.joplinusercontent.com/v3.5.11/Joplin-Setup-3.5.11.exe?source=JoplinWebsite&type=New'><img alt='Get it on Windows' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeWindows.png'/></a>
macOS | <a href='https://objects.joplinusercontent.com/v3.5.11/Joplin-3.5.11.dmg?source=JoplinWebsite&type=New'><img alt='Get it on macOS' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeMacOS.png'/></a>
macOS M1 (Apple Silicon) | <a href='https://objects.joplinusercontent.com/v3.5.11/Joplin-3.5.11-arm64.DMG?source=JoplinWebsite&type=New'><img alt='Get it on macOS M1 (Silicon)' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeMacOSM1.png'/></a>
Linux | <a href='https://objects.joplinusercontent.com/v3.5.11/Joplin-3.5.11.AppImage?source=JoplinWebsite&type=New'><img alt='Get it on Linux' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeLinux.png'/></a>
Windows (32 and 64-bit) | <a href='https://objects.joplinusercontent.com/v3.5.12/Joplin-Setup-3.5.12.exe?source=JoplinWebsite&type=New'><img alt='Get it on Windows' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeWindows.png'/></a>
macOS | <a href='https://objects.joplinusercontent.com/v3.5.12/Joplin-3.5.12.dmg?source=JoplinWebsite&type=New'><img alt='Get it on macOS' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeMacOS.png'/></a>
macOS M1 (Apple Silicon) | <a href='https://objects.joplinusercontent.com/v3.5.12/Joplin-3.5.12-arm64.DMG?source=JoplinWebsite&type=New'><img alt='Get it on macOS M1 (Silicon)' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeMacOSM1.png'/></a>
Linux | <a href='https://objects.joplinusercontent.com/v3.5.12/Joplin-3.5.12.AppImage?source=JoplinWebsite&type=New'><img alt='Get it on Linux' width="134px" src='https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/BadgeLinux.png'/></a>
**On Windows**, you may also use the <a href='https://objects.joplinusercontent.com/v3.5.11/JoplinPortable.exe?source=JoplinWebsite&type=New'>Portable version</a>. The [portable application](https://en.wikipedia.org/wiki/Portable_application) allows installing the software on a portable device such as a USB key. Simply copy the file JoplinPortable.exe in any directory on that USB key ; the application will then create a directory called "JoplinProfile" next to the executable file.
**On Windows**, you may also use the <a href='https://objects.joplinusercontent.com/v3.5.12/JoplinPortable.exe?source=JoplinWebsite&type=New'>Portable version</a>. The [portable application](https://en.wikipedia.org/wiki/Portable_application) allows installing the software on a portable device such as a USB key. Simply copy the file JoplinPortable.exe in any directory on that USB key ; the application will then create a directory called "JoplinProfile" next to the executable file.
**On Linux**, the recommended way is to use the following installation script as it will handle the desktop icon too:

256
yarn.lock
View File

@@ -10537,11 +10537,11 @@ __metadata:
"@react-native-community/netinfo": "npm:11.4.1"
"@react-native-community/push-notification-ios": "npm:1.11.0"
"@react-native-documents/picker": "npm:10.1.7"
"@react-native-vector-icons/fontawesome5": "npm:12.3.0"
"@react-native-vector-icons/fontawesome5": "patch:@react-native-vector-icons/fontawesome5@npm%3A12.3.0#~/.yarn/patches/@react-native-vector-icons-fontawesome5-npm-12.3.0-a1ca46610f.patch"
"@react-native-vector-icons/get-image": "npm:12.3.0"
"@react-native-vector-icons/ionicons": "npm:12.3.0"
"@react-native-vector-icons/material-design-icons": "npm:12.4.0"
"@react-native-vector-icons/material-icons": "npm:12.4.0"
"@react-native-vector-icons/ionicons": "patch:@react-native-vector-icons/ionicons@npm%3A12.3.0#~/.yarn/patches/@react-native-vector-icons-ionicons-npm-12.3.0-9bd4746f3f.patch"
"@react-native-vector-icons/material-design-icons": "patch:@react-native-vector-icons/material-design-icons@npm%3A12.4.0#~/.yarn/patches/@react-native-vector-icons-material-design-icons-npm-12.4.0-890f7f618b.patch"
"@react-native-vector-icons/material-icons": "patch:@react-native-vector-icons/material-icons@npm%3A12.4.0#~/.yarn/patches/@react-native-vector-icons-material-icons-npm-12.4.0-94138e627b.patch"
"@react-native/babel-preset": "npm:0.80.1"
"@react-native/metro-config": "npm:0.79.5"
"@react-native/typescript-config": "npm:0.80.2"
@@ -10552,7 +10552,7 @@ __metadata:
"@types/node": "npm:18.19.130"
"@types/react": "npm:19.0.14"
"@types/react-redux": "npm:7.1.33"
"@types/serviceworker": "npm:0.0.158"
"@types/serviceworker": "npm:0.0.160"
"@types/tar-stream": "npm:3.1.4"
assert-browserify: "npm:2.0.0"
babel-jest: "npm:29.7.0"
@@ -10587,7 +10587,7 @@ __metadata:
react: "npm:19.0.0"
react-dom: "npm:19.0.0"
react-native: "npm:0.79.2"
react-native-device-info: "npm:14.0.4"
react-native-device-info: "npm:14.1.1"
react-native-dropdownalert: "npm:5.2.0"
react-native-exit-app: "npm:2.0.0"
react-native-file-viewer: "npm:2.1.5"
@@ -10641,9 +10641,9 @@ __metadata:
resolution: "@joplin/default-plugins@workspace:packages/default-plugins"
dependencies:
"@joplin/utils": "npm:~3.6"
"@types/yargs": "npm:17.0.33"
"@types/yargs": "npm:17.0.34"
fs-extra: "npm:11.3.2"
joplin-plugin-freehand-drawing: "npm:4.2.0"
joplin-plugin-freehand-drawing: "npm:4.3.0"
ts-node: "npm:10.9.2"
typescript: "npm:5.8.3"
yargs: "npm:17.7.2"
@@ -11032,10 +11032,10 @@ __metadata:
"@types/mustache": "npm:4.2.6"
"@types/node": "npm:18.19.130"
"@types/node-os-utils": "npm:1.3.4"
"@types/nodemailer": "npm:6.4.20"
"@types/nodemailer": "npm:6.4.21"
"@types/qrcode": "npm:1.5.6"
"@types/uuid": "npm:10.0.0"
"@types/yargs": "npm:17.0.33"
"@types/yargs": "npm:17.0.34"
"@types/zxcvbn": "npm:4.4.5"
bcryptjs: "npm:2.4.3"
bulma: "npm:1.0.4"
@@ -11065,10 +11065,10 @@ __metadata:
prettycron: "npm:0.10.0"
qrcode: "npm:1.5.4"
query-string: "npm:7.1.3"
rate-limiter-flexible: "npm:7.3.2"
rate-limiter-flexible: "npm:7.4.0"
raw-body: "npm:3.0.1"
samlify: "npm:2.10.1"
short-uuid: "npm:4.2.0"
short-uuid: "npm:5.2.0"
source-map-support: "npm:0.5.21"
sqlite3: "npm:5.1.6"
stripe: "npm:13.9.0"
@@ -11096,7 +11096,7 @@ __metadata:
"@types/mustache": "npm:4.2.6"
"@types/node": "npm:18.19.130"
"@types/node-fetch": "npm:2.6.13"
"@types/yargs": "npm:17.0.33"
"@types/yargs": "npm:17.0.34"
compare-versions: "npm:6.1.1"
dayjs: "npm:1.11.18"
execa: "npm:4.1.0"
@@ -11142,7 +11142,7 @@ __metadata:
"@types/koa": "npm:2.15.0"
"@types/sharp": "npm:0.32.0"
"@types/uuid": "npm:10.0.0"
dotenv: "npm:16.6.1"
dotenv: "npm:17.2.3"
file-type: "npm:16.5.4"
fs-extra: "npm:11.3.2"
gulp: "npm:4.0.2"
@@ -11165,7 +11165,7 @@ __metadata:
browserify: "npm:14.5.0"
rollup: "npm:0.50.1"
standard: "npm:17.1.2"
turndown: "npm:7.2.1"
turndown: "npm:7.2.2"
turndown-attendant: "npm:0.0.3"
languageName: unknown
linkType: soft
@@ -11175,7 +11175,7 @@ __metadata:
resolution: "@joplin/turndown@workspace:packages/turndown"
dependencies:
"@adobe/css-tools": "npm:4.4.4"
"@rollup/plugin-commonjs": "npm:28.0.8"
"@rollup/plugin-commonjs": "npm:28.0.9"
"@rollup/plugin-node-resolve": "npm:16.0.3"
"@rollup/plugin-replace": "npm:6.0.2"
browserify: "npm:14.5.0"
@@ -13688,6 +13688,18 @@ __metadata:
languageName: node
linkType: hard
"@react-native-vector-icons/fontawesome5@patch:@react-native-vector-icons/fontawesome5@npm%3A12.3.0#~/.yarn/patches/@react-native-vector-icons-fontawesome5-npm-12.3.0-a1ca46610f.patch":
version: 12.3.0
resolution: "@react-native-vector-icons/fontawesome5@patch:@react-native-vector-icons/fontawesome5@npm%3A12.3.0#~/.yarn/patches/@react-native-vector-icons-fontawesome5-npm-12.3.0-a1ca46610f.patch::version=12.3.0&hash=8ed269"
dependencies:
"@react-native-vector-icons/common": "npm:^12.4.0"
peerDependencies:
react: "*"
react-native: "*"
checksum: 10/1bfe068fdf6b9edf4608d02acf2c329fe186bbee70e7949a781f56024f386d192ac00967463afc099bc43cca162e3711a884cf7dad01e40a50cea0b612f72f5e
languageName: node
linkType: hard
"@react-native-vector-icons/get-image@npm:12.3.0":
version: 12.3.0
resolution: "@react-native-vector-icons/get-image@npm:12.3.0"
@@ -13710,6 +13722,18 @@ __metadata:
languageName: node
linkType: hard
"@react-native-vector-icons/ionicons@patch:@react-native-vector-icons/ionicons@npm%3A12.3.0#~/.yarn/patches/@react-native-vector-icons-ionicons-npm-12.3.0-9bd4746f3f.patch":
version: 12.3.0
resolution: "@react-native-vector-icons/ionicons@patch:@react-native-vector-icons/ionicons@npm%3A12.3.0#~/.yarn/patches/@react-native-vector-icons-ionicons-npm-12.3.0-9bd4746f3f.patch::version=12.3.0&hash=e0b183"
dependencies:
"@react-native-vector-icons/common": "npm:^12.3.0"
peerDependencies:
react: "*"
react-native: "*"
checksum: 10/f94852e0f4973e2e695e0413d69349ae1145b46253f73963f253fc83a6fdf43e39329dee1f6535b9675170e7e4e1def679ea9c2550e70d67a669434fe9a0f08f
languageName: node
linkType: hard
"@react-native-vector-icons/material-design-icons@npm:12.4.0":
version: 12.4.0
resolution: "@react-native-vector-icons/material-design-icons@npm:12.4.0"
@@ -13722,6 +13746,18 @@ __metadata:
languageName: node
linkType: hard
"@react-native-vector-icons/material-design-icons@patch:@react-native-vector-icons/material-design-icons@npm%3A12.4.0#~/.yarn/patches/@react-native-vector-icons-material-design-icons-npm-12.4.0-890f7f618b.patch":
version: 12.4.0
resolution: "@react-native-vector-icons/material-design-icons@patch:@react-native-vector-icons/material-design-icons@npm%3A12.4.0#~/.yarn/patches/@react-native-vector-icons-material-design-icons-npm-12.4.0-890f7f618b.patch::version=12.4.0&hash=23abfd"
dependencies:
"@react-native-vector-icons/common": "npm:^12.4.0"
peerDependencies:
react: "*"
react-native: "*"
checksum: 10/75335f9e595ca314d93ef8760cca760fe64c36673841237b119e91ddd76b16bb194060471c79c1007d0920d2b703b1c19abd24bafbee09200f52cbf2357907c5
languageName: node
linkType: hard
"@react-native-vector-icons/material-icons@npm:12.4.0":
version: 12.4.0
resolution: "@react-native-vector-icons/material-icons@npm:12.4.0"
@@ -13734,6 +13770,18 @@ __metadata:
languageName: node
linkType: hard
"@react-native-vector-icons/material-icons@patch:@react-native-vector-icons/material-icons@npm%3A12.4.0#~/.yarn/patches/@react-native-vector-icons-material-icons-npm-12.4.0-94138e627b.patch":
version: 12.4.0
resolution: "@react-native-vector-icons/material-icons@patch:@react-native-vector-icons/material-icons@npm%3A12.4.0#~/.yarn/patches/@react-native-vector-icons-material-icons-npm-12.4.0-94138e627b.patch::version=12.4.0&hash=81fbb4"
dependencies:
"@react-native-vector-icons/common": "npm:^12.4.0"
peerDependencies:
react: "*"
react-native: "*"
checksum: 10/64ff8df53e5a339635278b0fc6cce6a16620d404cad1d97dfb8a5e25542654bbd30fc20ece1cfdce7cd9382a1d999b938c1554122743ee0f2193d5bbcb08c151
languageName: node
linkType: hard
"@react-native/assets-registry@npm:0.79.2":
version: 0.79.2
resolution: "@react-native/assets-registry@npm:0.79.2"
@@ -14238,9 +14286,9 @@ __metadata:
languageName: node
linkType: hard
"@rollup/plugin-commonjs@npm:28.0.8":
version: 28.0.8
resolution: "@rollup/plugin-commonjs@npm:28.0.8"
"@rollup/plugin-commonjs@npm:28.0.9":
version: 28.0.9
resolution: "@rollup/plugin-commonjs@npm:28.0.9"
dependencies:
"@rollup/pluginutils": "npm:^5.0.1"
commondir: "npm:^1.0.1"
@@ -14254,7 +14302,7 @@ __metadata:
peerDependenciesMeta:
rollup:
optional: true
checksum: 10/0533210ed86523ff2dfb952bcb13ae081a226d21c8246bf91f7e4553665c7e639f3df9bc60fadf5daae5d81de5b66101a82dc8dfc6a9f24c8ea882b40b9d8b84
checksum: 10/68b040339ac4476bc4e75444424e85f9d22726b23e54148b6e22b80c0a06d58b4cd8ccf8c1ccc8e768076b19c9c8474f53e58b11370ecaea2a4de748101bf87a
languageName: node
linkType: hard
@@ -16835,13 +16883,13 @@ __metadata:
languageName: node
linkType: hard
"@types/nodemailer@npm:6.4.20":
version: 6.4.20
resolution: "@types/nodemailer@npm:6.4.20"
"@types/nodemailer@npm:6.4.21":
version: 6.4.21
resolution: "@types/nodemailer@npm:6.4.21"
dependencies:
"@aws-sdk/client-ses": "npm:^3.731.1"
"@types/node": "npm:*"
checksum: 10/050b6aa95c97a1bf645f0735c5da20a5b4aa30df1fc8bcc8a60c039b5f45f450689e5f51821d144d038148f3dae8427bb7d91f503c6390960a1390e562147a4f
checksum: 10/3388d11defbef0b1b471720f88ff2fd206ab5879bc7089d90d3467198f869299dc374f01cf07054025c7e87b186a7297b1040d318d65e9ec19a130732aa1448d
languageName: node
linkType: hard
@@ -17156,10 +17204,10 @@ __metadata:
languageName: node
linkType: hard
"@types/serviceworker@npm:0.0.158":
version: 0.0.158
resolution: "@types/serviceworker@npm:0.0.158"
checksum: 10/ec31c5f07f24aea8ae50a54c98cef70e60b16fb2463ca4a46a8062b2d7c4113adee5b01dd6f765fec885e75818123908bde7e4b918a61cb9996b24a5287e40f3
"@types/serviceworker@npm:0.0.160":
version: 0.0.160
resolution: "@types/serviceworker@npm:0.0.160"
checksum: 10/713d7614e8810dd7ba42fa37b2a16ab4c2086f31059c2068b90b6893b8126ce50fa4b31cdd11310d6f65e97c2de588e5de0e26d3d243ccab98deba4efbdabad7
languageName: node
linkType: hard
@@ -17291,12 +17339,12 @@ __metadata:
languageName: node
linkType: hard
"@types/yargs@npm:17.0.33":
version: 17.0.33
resolution: "@types/yargs@npm:17.0.33"
"@types/yargs@npm:17.0.34":
version: 17.0.34
resolution: "@types/yargs@npm:17.0.34"
dependencies:
"@types/yargs-parser": "npm:*"
checksum: 10/16f6681bf4d99fb671bf56029141ed01db2862e3db9df7fc92d8bea494359ac96a1b4b1c35a836d1e95e665fb18ad753ab2015fc0db663454e8fd4e5d5e2ef91
checksum: 10/8e7907479e649e9115dcca94cb059dfe2322992ac5d29120f759564c078abfc13673a31f7ad86a3a5c9de7f241a4e3d70042ba38b794fd1601e44f9a1bc5cefd
languageName: node
linkType: hard
@@ -22038,13 +22086,6 @@ __metadata:
languageName: node
linkType: hard
"chalk@npm:^5.6.0":
version: 5.6.2
resolution: "chalk@npm:5.6.2"
checksum: 10/1b2f48f6fba1370670d5610f9cd54c391d6ede28f4b7062dd38244ea5768777af72e5be6b74fb6c6d54cb84c4a2dff3f3afa9b7cb5948f7f022cfd3d087989e0
languageName: node
linkType: hard
"change-case@npm:^4.1.2":
version: 4.1.2
resolution: "change-case@npm:4.1.2"
@@ -22946,7 +22987,7 @@ __metadata:
languageName: node
linkType: hard
"commander@npm:^14.0.0":
"commander@npm:^14.0.1":
version: 14.0.2
resolution: "commander@npm:14.0.2"
checksum: 10/2d202db5e5f9bb770112a3c1579b893d17ac6f6d932183077308bdd96d0f87f0bbe6a68b5b9ed2cf3b2514be6bb7de637480703c0e2db9741ee1b383237deb26
@@ -25068,7 +25109,7 @@ __metadata:
languageName: node
linkType: hard
"debug@npm:^4.4.1":
"debug@npm:^4.3.6":
version: 4.4.3
resolution: "debug@npm:4.4.3"
dependencies:
@@ -26190,10 +26231,10 @@ __metadata:
languageName: node
linkType: hard
"dotenv@npm:16.6.1":
version: 16.6.1
resolution: "dotenv@npm:16.6.1"
checksum: 10/1d1897144344447ffe62aa1a6d664f4cd2e0784e0aff787eeeec1940ded32f8e4b5b506d665134fc87157baa086fce07ec6383970a2b6d2e7985beaed6a4cc14
"dotenv@npm:17.2.3":
version: 17.2.3
resolution: "dotenv@npm:17.2.3"
checksum: 10/f8b78626ebfff6e44420f634773375c9651808b3e1a33df6d4cc19120968eea53e100f59f04ec35f2a20b2beb334b6aba4f24040b2f8ad61773f158ac042a636
languageName: node
linkType: hard
@@ -32394,7 +32435,7 @@ __metadata:
languageName: node
linkType: hard
"ignore@npm:^5.3.1":
"ignore@npm:^5.3.1, ignore@npm:^5.3.2":
version: 5.3.2
resolution: "ignore@npm:5.3.2"
checksum: 10/cceb6a457000f8f6a50e1196429750d782afce5680dd878aa4221bd79972d68b3a55b4b1458fc682be978f4d3c6a249046aa0880637367216444ab7b014cfc98
@@ -35004,13 +35045,13 @@ __metadata:
languageName: node
linkType: hard
"joplin-plugin-freehand-drawing@npm:4.2.0":
version: 4.2.0
resolution: "joplin-plugin-freehand-drawing@npm:4.2.0"
"joplin-plugin-freehand-drawing@npm:4.3.0":
version: 4.3.0
resolution: "joplin-plugin-freehand-drawing@npm:4.3.0"
dependencies:
"@js-draw/material-icons": "npm:1.33.0"
js-draw: "npm:1.33.0"
checksum: 10/457c23b7fbd6f1e3a48568395c1e456d570a3c68e25b40fba97973fd848de1b513be2f1ece6eb9babae862e19ba140aca871eee0e3a8a9cb5a033e0c0fd78934
checksum: 10/b9ee96e149637f4bd52e894f75ba72e5c64b2512002df3a95ecf23c0b6593fdb260d61c0a30cab5450da142520b1aec63c709b11bf3a2491caa3049db0174fdf
languageName: node
linkType: hard
@@ -35475,7 +35516,7 @@ __metadata:
languageName: node
linkType: hard
"jsonc-parser@npm:^3.2.1":
"jsonc-parser@npm:^3.3.1":
version: 3.3.1
resolution: "jsonc-parser@npm:3.3.1"
checksum: 10/9b0dc391f20b47378f843ef1e877e73ec652a5bdc3c5fa1f36af0f119a55091d147a86c1ee86a232296f55c929bba174538c2bf0312610e0817a22de131cc3f4
@@ -36280,13 +36321,6 @@ __metadata:
languageName: node
linkType: hard
"lilconfig@npm:^3.1.3":
version: 3.1.3
resolution: "lilconfig@npm:3.1.3"
checksum: 10/b932ce1af94985f0efbe8896e57b1f814a48c8dbd7fc0ef8469785c6303ed29d0090af3ccad7e36b626bfca3a4dc56cc262697e9a8dd867623cf09a39d54e4c3
languageName: node
linkType: hard
"lines-and-columns@npm:^1.1.6":
version: 1.2.4
resolution: "lines-and-columns@npm:1.2.4"
@@ -36303,27 +36337,24 @@ __metadata:
languageName: node
linkType: hard
"lint-staged@npm:16.1.6":
version: 16.1.6
resolution: "lint-staged@npm:16.1.6"
"lint-staged@npm:16.2.6":
version: 16.2.6
resolution: "lint-staged@npm:16.2.6"
dependencies:
chalk: "npm:^5.6.0"
commander: "npm:^14.0.0"
debug: "npm:^4.4.1"
lilconfig: "npm:^3.1.3"
listr2: "npm:^9.0.3"
commander: "npm:^14.0.1"
listr2: "npm:^9.0.5"
micromatch: "npm:^4.0.8"
nano-spawn: "npm:^1.0.2"
nano-spawn: "npm:^2.0.0"
pidtree: "npm:^0.6.0"
string-argv: "npm:^0.3.2"
yaml: "npm:^2.8.1"
bin:
lint-staged: bin/lint-staged.js
checksum: 10/922b4392ae5d3d56130e4eba706c2fa6151d5da5e21f57ab601b1d6ce9cc635ceb5e4c3dc00e7da83ba8f0cb244b82604469c7ea1470b1e6b6ea0fc12454aa08
checksum: 10/c419f1347166ddd06746d4a3e4ce6441f3d0e82dcdcbc3d3615ddf7b82b36c603df2e43dbb1edad5ed00ae29857479e1f880bcca271f8bf4b0db7d0f77861d21
languageName: node
linkType: hard
"listr2@npm:^9.0.3":
"listr2@npm:^9.0.5":
version: 9.0.5
resolution: "listr2@npm:9.0.5"
dependencies:
@@ -39088,10 +39119,10 @@ __metadata:
languageName: node
linkType: hard
"nano-spawn@npm:^1.0.2":
version: 1.0.3
resolution: "nano-spawn@npm:1.0.3"
checksum: 10/72c56e68ae733c81c459a338fd51e2aa3be06b1cca746c2abe83df7acfac7eee008b01833f5a8781f4ac9fc1eafd23036a44755257a669dfcc2ff2453850822a
"nano-spawn@npm:^2.0.0":
version: 2.0.0
resolution: "nano-spawn@npm:2.0.0"
checksum: 10/117d35d7bd85b146908de5d3d1177d2b2ee3174e5d884d6bc9555583bf6e50a265f4038b5c134b7cdd768a10d53598ccde5c00d6f55e25e7eed31b86b8d29646
languageName: node
linkType: hard
@@ -39899,30 +39930,30 @@ __metadata:
languageName: node
linkType: hard
"npm-package-json-lint@npm:8.0.0":
version: 8.0.0
resolution: "npm-package-json-lint@npm:8.0.0"
"npm-package-json-lint@npm:9.0.0":
version: 9.0.0
resolution: "npm-package-json-lint@npm:9.0.0"
dependencies:
ajv: "npm:^6.12.6"
ajv-errors: "npm:^1.0.1"
chalk: "npm:^4.1.2"
cosmiconfig: "npm:^8.3.6"
debug: "npm:^4.3.4"
debug: "npm:^4.3.6"
globby: "npm:^11.1.0"
ignore: "npm:^5.3.1"
ignore: "npm:^5.3.2"
is-plain-obj: "npm:^3.0.0"
jsonc-parser: "npm:^3.2.1"
jsonc-parser: "npm:^3.3.1"
log-symbols: "npm:^4.1.0"
meow: "npm:^9.0.0"
plur: "npm:^4.0.0"
semver: "npm:^7.6.2"
semver: "npm:^7.6.3"
slash: "npm:^3.0.0"
strip-json-comments: "npm:^3.1.1"
type-fest: "npm:^4.20.0"
validate-npm-package-name: "npm:^5.0.1"
type-fest: "npm:^4.26.1"
validate-npm-package-name: "npm:^6.0.0"
bin:
npmPkgJsonLint: dist/cli.js
checksum: 10/a60ff54288b66c7cc6a750a47ab37fd619d6747933a406daf92c95bc1efcf24300aa324c52ab34c9eeee019e05d31a9ee7406f745bf801d9000efd9929abe9d0
checksum: 10/449a0d4235e8f12f752fd181718bec7916513fe8f64b9ba3155853da7f05450021e2d7e546fcc3811d4239f16cb996b6aaabe6d7213c2e08acc6bc0c39213833
languageName: node
linkType: hard
@@ -43612,10 +43643,10 @@ __metadata:
languageName: node
linkType: hard
"rate-limiter-flexible@npm:7.3.2":
version: 7.3.2
resolution: "rate-limiter-flexible@npm:7.3.2"
checksum: 10/f95c9af17c52899ab939593a9fa60a0a2d69b8724b1b87bd67370b0414c1845d5a9e5cee5ad1dbafa7ced3f7d804fba1e42393817b1dee17eb44888987217e6d
"rate-limiter-flexible@npm:7.4.0":
version: 7.4.0
resolution: "rate-limiter-flexible@npm:7.4.0"
checksum: 10/85012124949028213494ac7a24eaf6f3b850aa6bc3d503afb1cd23d6d42721a9a05172e3b7cf1a59b818580ce3815a96eaad54b484eb573917911246f28311d9
languageName: node
linkType: hard
@@ -43933,12 +43964,12 @@ __metadata:
languageName: node
linkType: hard
"react-native-device-info@npm:14.0.4":
version: 14.0.4
resolution: "react-native-device-info@npm:14.0.4"
"react-native-device-info@npm:14.1.1":
version: 14.1.1
resolution: "react-native-device-info@npm:14.1.1"
peerDependencies:
react-native: "*"
checksum: 10/bf031048551597b1a9ab2965d498cbd073eacf50005dffa4e3496286578734a45854141d47654e7e58ef8531b8c2cd6d1670bfd75625271c91aab3b3b8d0a8d8
checksum: 10/560c5f8d990fc47e7f07ba526b0b016352854b9badac2ec1d5676912ea81df1a15eb05b78e4fb077d552e9dc4b6eff585ea0aad0f5d6a8f23a5cd4bfdf4b4652
languageName: node
linkType: hard
@@ -46162,11 +46193,11 @@ __metadata:
http-server: "npm:14.1.1"
husky: "npm:9.1.7"
lerna: "npm:3.22.1"
lint-staged: "npm:16.1.6"
lint-staged: "npm:16.2.6"
madge: "npm:8.0.0"
node-gyp: "npm:11.4.2"
nodemon: "npm:3.1.10"
npm-package-json-lint: "npm:8.0.0"
npm-package-json-lint: "npm:9.0.0"
typescript: "npm:5.8.3"
languageName: unknown
linkType: soft
@@ -46802,7 +46833,7 @@ __metadata:
languageName: node
linkType: hard
"semver@npm:^7.6.2, semver@npm:^7.6.3, semver@npm:^7.7.2":
"semver@npm:^7.6.3, semver@npm:^7.7.2":
version: 7.7.2
resolution: "semver@npm:7.7.2"
bin:
@@ -47410,13 +47441,13 @@ __metadata:
languageName: node
linkType: hard
"short-uuid@npm:4.2.0":
version: 4.2.0
resolution: "short-uuid@npm:4.2.0"
"short-uuid@npm:5.2.0":
version: 5.2.0
resolution: "short-uuid@npm:5.2.0"
dependencies:
any-base: "npm:^1.1.0"
uuid: "npm:^8.3.2"
checksum: 10/c9765dd0f21c5a6e0b2245926c73a13079b021dab34f5d152579cf76c11895c20ece6fab06c0dff786041a1c878f12032daec89adc03863fe6d68849e0a475c5
uuid: "npm:^9.0.1"
checksum: 10/9915e92c512c96684683e8fd56cdd848ce6d139db602567cc10fbd26313700db6c09f138687ee4bc13a7f41c71099948f4c9169e8b111e2bbffb1f8090b0af7c
languageName: node
linkType: hard
@@ -50918,12 +50949,12 @@ __metadata:
languageName: node
linkType: hard
"turndown@npm:7.2.1":
version: 7.2.1
resolution: "turndown@npm:7.2.1"
"turndown@npm:7.2.2":
version: 7.2.2
resolution: "turndown@npm:7.2.2"
dependencies:
"@mixmark-io/domino": "npm:^2.2.0"
checksum: 10/8c8986e7b6d2f93af25d7b1af6f50a2c53f6bb8229b5a333fb404bc54f3bd99bb96992fe78651bbb12ed830d590a8a5f5608289ed3a067de5a9d51b92ed27b49
checksum: 10/e0a6f7f0c2bc8447ca7ea145348c9e337163a00c9d95f49e5eecdbc9931002aa0253c7561fbc299280c854d5308ed5a0eacfc9b4e399d698fce64be4d2d7bb52
languageName: node
linkType: hard
@@ -51055,7 +51086,7 @@ __metadata:
languageName: node
linkType: hard
"type-fest@npm:^4.20.0, type-fest@npm:^4.41.0":
"type-fest@npm:^4.26.1, type-fest@npm:^4.41.0":
version: 4.41.0
resolution: "type-fest@npm:4.41.0"
checksum: 10/617ace794ac0893c2986912d28b3065ad1afb484cad59297835a0807dc63286c39e8675d65f7de08fafa339afcb8fe06a36e9a188b9857756ae1e92ee8bda212
@@ -52383,6 +52414,15 @@ __metadata:
languageName: node
linkType: hard
"uuid@npm:^9.0.1":
version: 9.0.1
resolution: "uuid@npm:9.0.1"
bin:
uuid: dist/bin/uuid
checksum: 10/9d0b6adb72b736e36f2b1b53da0d559125ba3e39d913b6072f6f033e0c87835b414f0836b45bcfaf2bdf698f92297fea1c3cc19b0b258bc182c9c43cc0fab9f2
languageName: node
linkType: hard
"v8-compile-cache-lib@npm:^3.0.1":
version: 3.0.1
resolution: "v8-compile-cache-lib@npm:3.0.1"
@@ -52438,10 +52478,10 @@ __metadata:
languageName: node
linkType: hard
"validate-npm-package-name@npm:^5.0.1":
version: 5.0.1
resolution: "validate-npm-package-name@npm:5.0.1"
checksum: 10/0d583a1af23aeffea7748742cf22b6802458736fb8b60323ba5949763824d46f796474b0e1b9206beb716f9d75269e19dbd7795d6b038b29d561be95dd827381
"validate-npm-package-name@npm:^6.0.0":
version: 6.0.2
resolution: "validate-npm-package-name@npm:6.0.2"
checksum: 10/f0e022b0a7f11345a92b64121b059b720204cd64406a0d65d81526181dcb70aef551c7c6bf9ca37b91607a7c6ff4d62e1f63a86c8d9b7346d722a641a4bd8789
languageName: node
linkType: hard