diff --git a/.eslintignore b/.eslintignore
index 8696f0e99..369d6fc81 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -133,6 +133,7 @@ packages/app-desktop/gui/ClipperConfigScreen.js
packages/app-desktop/gui/ConfigScreen/ButtonBar.js
packages/app-desktop/gui/ConfigScreen/ConfigScreen.js
packages/app-desktop/gui/ConfigScreen/Sidebar.js
+packages/app-desktop/gui/ConfigScreen/controls/ToggleAdvancedSettingsButton.js
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js
packages/app-desktop/gui/ConfigScreen/controls/plugins/SearchPlugins.js
diff --git a/.github/scripts/run_ci.sh b/.github/scripts/run_ci.sh
index 3c42913cb..58c3e12a7 100755
--- a/.github/scripts/run_ci.sh
+++ b/.github/scripts/run_ci.sh
@@ -171,6 +171,21 @@ if [ "$IS_PULL_REQUEST" == "1" ]; then
fi
fi
+# =============================================================================
+# Check that the website still builds
+# =============================================================================
+
+if [ "$IS_PULL_REQUEST" == "1" ] || [ "$IS_DEV_BRANCH" = "1" ]; then
+ echo "Step: Check that the website still builds..."
+
+ mkdir -p ../joplin-website/docs
+ SKIP_SPONSOR_PROCESSING=1 yarn run buildWebsite
+ testResult=$?
+ if [ $testResult -ne 0 ]; then
+ exit $testResult
+ fi
+fi
+
# =============================================================================
# Find out if we should run the build or not. Electron-builder gets stuck when
# building PRs so we disable it in this case. The Linux build should provide
diff --git a/.gitignore b/.gitignore
index e5dc69c40..477b900e8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -50,6 +50,7 @@ packages/tools/github_oauth_token.txt
lerna-debug.log
.env
docs/**/*.mustache
+.idea
# Yarn stuff
# https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored
@@ -118,6 +119,7 @@ packages/app-desktop/gui/ClipperConfigScreen.js
packages/app-desktop/gui/ConfigScreen/ButtonBar.js
packages/app-desktop/gui/ConfigScreen/ConfigScreen.js
packages/app-desktop/gui/ConfigScreen/Sidebar.js
+packages/app-desktop/gui/ConfigScreen/controls/ToggleAdvancedSettingsButton.js
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js
packages/app-desktop/gui/ConfigScreen/controls/plugins/SearchPlugins.js
diff --git a/README.md b/README.md
index 61db57793..0c56d1770 100644
--- a/README.md
+++ b/README.md
@@ -75,11 +75,10 @@ A community maintained list of these distributions can be found here: [Unofficia
| | | | |
| :---: | :---: | :---: | :---: |
| [avanderberg](https://github.com/avanderberg) | [chr15m](https://github.com/chr15m) | [CyberXZT](https://github.com/CyberXZT) | [dbrandonjohnson](https://github.com/dbrandonjohnson) |
-| [dchecks](https://github.com/dchecks) | [fats](https://github.com/fats) | [fourstepper](https://github.com/fourstepper) | [Hegghammer](https://github.com/Hegghammer) |
-| [iamwillbar](https://github.com/iamwillbar) | [jknowles](https://github.com/jknowles) | [KentBrockman](https://github.com/KentBrockman) | [kianenigma](https://github.com/kianenigma) |
-| [konishi-t](https://github.com/konishi-t) | [marcdw1289](https://github.com/marcdw1289) | [matmoly](https://github.com/matmoly) | [maxtruxa](https://github.com/maxtruxa) |
-| [mcejp](https://github.com/mcejp) | [saarantras](https://github.com/saarantras) | [sif](https://github.com/sif) | [taskcruncher](https://github.com/taskcruncher) |
-| [tateisu](https://github.com/tateisu) | | | |
+| [dchecks](https://github.com/dchecks) | [fats](https://github.com/fats) | [Galliver7](https://github.com/Galliver7) | [Hegghammer](https://github.com/Hegghammer) |
+| [jknowles](https://github.com/jknowles) | [KentBrockman](https://github.com/KentBrockman) | [konishi-t](https://github.com/konishi-t) | [marcdw1289](https://github.com/marcdw1289) |
+| [matmoly](https://github.com/matmoly) | [maxtruxa](https://github.com/maxtruxa) | [saarantras](https://github.com/saarantras) | [sif](https://github.com/sif) |
+| [taskcruncher](https://github.com/taskcruncher) | [tateisu](https://github.com/tateisu) | | |
@@ -159,6 +158,7 @@ A community maintained list of these distributions can be found here: [Unofficia
- [Guiding principles](https://github.com/laurent22/joplin/blob/dev/readme/principles.md)
- [Stats](https://github.com/laurent22/joplin/blob/dev/readme/stats.md)
- [Brand guidelines](https://joplinapp.org/brand)
+ - [Release cycle](https://github.com/laurent22/joplin/blob/dev/readme/release_cycle.md)
- [Donate](https://github.com/laurent22/joplin/blob/dev/readme/donate.md)
diff --git a/packages/app-cli/app/app.js b/packages/app-cli/app/app.js
index baef949eb..ba90091f1 100644
--- a/packages/app-cli/app/app.js
+++ b/packages/app-cli/app/app.js
@@ -452,6 +452,8 @@ class Application extends BaseApplication {
type: 'FOLDER_SELECT',
id: Setting.value('activeFolderId'),
});
+
+ this.startRotatingLogMaintenance(Setting.value('profileDir'));
}
}
}
diff --git a/packages/app-cli/package.json b/packages/app-cli/package.json
index 927866e69..fa906dba7 100644
--- a/packages/app-cli/package.json
+++ b/packages/app-cli/package.json
@@ -57,7 +57,7 @@
"proper-lockfile": "4.1.2",
"read-chunk": "2.1.0",
"server-destroy": "1.0.1",
- "sharp": "0.32.3",
+ "sharp": "0.32.4",
"sprintf-js": "1.1.2",
"sqlite3": "5.1.6",
"string-padding": "1.0.2",
@@ -66,7 +66,7 @@
"terminal-kit": "3.0.0",
"tkwidgets": "0.5.27",
"url-parse": "1.5.10",
- "word-wrap": "1.2.3",
+ "word-wrap": "1.2.4",
"yargs-parser": "21.1.1"
},
"devDependencies": {
diff --git a/packages/app-desktop/app.ts b/packages/app-desktop/app.ts
index e4f64579d..0fc995061 100644
--- a/packages/app-desktop/app.ts
+++ b/packages/app-desktop/app.ts
@@ -566,6 +566,8 @@ class Application extends BaseApplication {
await SpellCheckerService.instance().initialize(new SpellCheckerServiceDriverNative());
+ this.startRotatingLogMaintenance(Setting.value('profileDir'));
+
// await populateDatabase(reg.db(), {
// clearDatabase: true,
// folderCount: 1000,
diff --git a/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx b/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx
index 8b8edea21..8d94a9890 100644
--- a/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx
+++ b/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx
@@ -19,6 +19,7 @@ import PluginService from '@joplin/lib/services/plugins/PluginService';
import { getDefaultPluginsInstallState, updateDefaultPluginsInstallState } from '@joplin/lib/services/plugins/defaultPlugins/defaultPluginsUtils';
import getDefaultPluginsInfo from '@joplin/lib/services/plugins/defaultPlugins/desktopDefaultPluginsInfo';
import JoplinCloudConfigScreen from '../JoplinCloudConfigScreen';
+import ToggleAdvancedSettingsButton from './controls/ToggleAdvancedSettingsButton';
const { KeymapConfigScreen } = require('../KeymapConfig/KeymapConfigScreen');
const settingKeyToControl: any = {
@@ -208,17 +209,11 @@ class ConfigScreenComponent extends React.Component {
const advancedSettingsSectionStyle = { display: 'none' };
if (advancedSettingComps.length) {
- const iconName = this.state.showAdvancedSettings ? 'fa fa-angle-down' : 'fa fa-angle-right';
- // const advancedSettingsButtonStyle = { ...theme.buttonStyle, marginBottom: 10 };
advancedSettingsButton = (
-
- shared.advancedSettingsButton_click(this)}
- iconName={iconName}
- title={_('Show Advanced Settings')}
- />
-
+ shared.advancedSettingsButton_click(this)}
+ advancedSettingsVisible={this.state.showAdvancedSettings}
+ />
);
advancedSettingsSectionStyle.display = this.state.showAdvancedSettings ? 'block' : 'none';
}
diff --git a/packages/app-desktop/gui/ConfigScreen/controls/ToggleAdvancedSettingsButton.tsx b/packages/app-desktop/gui/ConfigScreen/controls/ToggleAdvancedSettingsButton.tsx
new file mode 100644
index 000000000..26a19eb05
--- /dev/null
+++ b/packages/app-desktop/gui/ConfigScreen/controls/ToggleAdvancedSettingsButton.tsx
@@ -0,0 +1,24 @@
+
+import * as React from 'react';
+import Button, { ButtonLevel } from '../../Button/Button';
+import { _ } from '@joplin/lib/locale';
+
+interface Props {
+ onClick: ()=> void;
+ advancedSettingsVisible: boolean;
+}
+
+const ToggleAdvancedSettingsButton: React.FunctionComponent = props => {
+ const iconName = props.advancedSettingsVisible ? 'fa fa-angle-down' : 'fa fa-angle-right';
+ return (
+
+
+
+ );
+};
+export default ToggleAdvancedSettingsButton;
diff --git a/packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.tsx b/packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.tsx
index a4a936e7a..3add1187e 100644
--- a/packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.tsx
+++ b/packages/app-desktop/gui/EncryptionConfigScreen/EncryptionConfigScreen.tsx
@@ -10,12 +10,13 @@ import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types';
import { getEncryptionEnabled, masterKeyEnabled, SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils';
import { getDefaultMasterKey, getMasterPasswordStatusMessage, masterPasswordIsValid, toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils';
import Button, { ButtonLevel } from '../Button/Button';
-import { useCallback, useMemo } from 'react';
+import { useCallback, useMemo, useState } from 'react';
import { connect } from 'react-redux';
import { AppState } from '../../app.reducer';
import Setting from '@joplin/lib/models/Setting';
import CommandService from '@joplin/lib/services/CommandService';
import { PublicPrivateKeyPair } from '@joplin/lib/services/e2ee/ppk';
+import ToggleAdvancedSettingsButton from '../ConfigScreen/controls/ToggleAdvancedSettingsButton';
interface Props {
themeId: any;
@@ -83,34 +84,6 @@ const EncryptionConfigScreen = (props: Props) => {
);
};
- const renderReencryptData = () => {
- if (!shim.isElectron()) return null;
- if (!props.shouldReencrypt) return null;
-
- const theme = themeStyle(props.themeId);
- const buttonLabel = _('Re-encrypt data');
-
- const intro = props.shouldReencrypt ? _('The default encryption method has been changed to a more secure one and it is recommended that you apply it to your data.') : _('You may use the tool below to re-encrypt your data, for example if you know that some of your notes are encrypted with an obsolete encryption method.');
-
- let t = `${intro}\n\n${_('In order to do so, your entire data set will have to be encrypted and synchronised, so it is best to run it overnight.\n\nTo start, please follow these instructions:\n\n1. Synchronise all your devices.\n2. Click "%s".\n3. Let it run to completion. While it runs, avoid changing any note on your other devices, to avoid conflicts.\n4. Once sync is done on this device, sync all your other devices and let it run to completion.\n\nImportant: you only need to run this ONCE on one device.', buttonLabel)}`;
-
- t = t.replace(/\n\n/g, '
');
- t = t.replace(/\n/g, ' ');
- t = `
${t}
`;
-
- return (
-
-
{_('Re-encryption')}
-
-
- void reencryptData()} style={theme.buttonStyle}>{buttonLabel}
-
-
- { !props.shouldReencrypt ? null :
dontReencryptData()} style={theme.buttonStyle}>{_('Ignore')} }
-
- );
- };
-
const renderMasterKey = (mk: MasterKeyEntity) => {
const theme = themeStyle(props.themeId);
@@ -121,6 +94,12 @@ const EncryptionConfigScreen = (props: Props) => {
borderColor: theme.dividerColor,
};
+ const missingPasswordCellStyle = {
+ ...theme.textStyle,
+ border: '3px solid',
+ borderColor: theme.colorError,
+ };
+
const password = inputPasswords[mk.id] ? inputPasswords[mk.id] : '';
const isActive = props.activeMasterKeyId === mk.id;
const activeIcon = isActive ? '✔' : '';
@@ -135,8 +114,15 @@ const EncryptionConfigScreen = (props: Props) => {
);
} else {
return (
-
- onInputPasswordChange(mk, event.target.value)} />{' '}
+
+ onInputPasswordChange(mk, event.target.value)}
+ />
+ {' '}
onSavePasswordClick(mk, { ...props.passwords, ...inputPasswords })}>
{_('Save')}
@@ -239,7 +225,6 @@ const EncryptionConfigScreen = (props: Props) => {
/>
);
const needUpgradeSection = renderNeedUpgradeSection();
- const reencryptDataSection = renderReencryptData();
return (
@@ -254,7 +239,6 @@ const EncryptionConfigScreen = (props: Props) => {
{decryptedItemsInfo}
{toggleButton}
{needUpgradeSection}
- {props.shouldReencrypt ? reencryptDataSection : null}
);
@@ -338,6 +322,56 @@ const EncryptionConfigScreen = (props: Props) => {
return nonExistingMasterKeySection;
};
+ const renderReencryptData = () => {
+ if (!shim.isElectron()) return null;
+ if (!props.encryptionEnabled) return null;
+
+ const theme = themeStyle(props.themeId);
+ const buttonLabel = _('Re-encrypt data');
+
+ const intro = props.shouldReencrypt ? _('The default encryption method has been changed to a more secure one and it is recommended that you apply it to your data.') : _('You may use the tool below to re-encrypt your data, for example if you know that some of your notes are encrypted with an obsolete encryption method.');
+
+ let t = `${intro}\n\n${_('In order to do so, your entire data set will have to be encrypted and synchronised, so it is best to run it overnight.\n\nTo start, please follow these instructions:\n\n1. Synchronise all your devices.\n2. Click "%s".\n3. Let it run to completion. While it runs, avoid changing any note on your other devices, to avoid conflicts.\n4. Once sync is done on this device, sync all your other devices and let it run to completion.\n\nImportant: you only need to run this ONCE on one device.', buttonLabel)}`;
+
+ t = t.replace(/\n\n/g, '');
+ t = t.replace(/\n/g, ' ');
+ t = `
${t}
`;
+
+ return (
+
+
{_('Re-encryption')}
+
+
+ void reencryptData()} style={theme.buttonStyle}>{buttonLabel}
+
+
+ { !props.shouldReencrypt ? null :
dontReencryptData()} style={theme.buttonStyle}>{_('Ignore')} }
+
+ );
+ };
+
+ // If the user should re-encrypt, ensure that the section is visible initially.
+ const [showAdvanced, setShowAdvanced] = useState(props.shouldReencrypt);
+ const toggleAdvanced = useCallback(() => {
+ setShowAdvanced(!showAdvanced);
+ }, [showAdvanced]);
+
+ const renderAdvancedSection = () => {
+ const reEncryptSection = renderReencryptData();
+
+ if (!reEncryptSection) return null;
+
+
+ return (
+
+
+ { showAdvanced ? reEncryptSection : null }
+
+ );
+ };
+
return (
{renderDebugSection()}
@@ -346,6 +380,7 @@ const EncryptionConfigScreen = (props: Props) => {
{renderMasterKeySection(props.masterKeys.filter(mk => masterKeyEnabled(mk)), true)}
{renderMasterKeySection(props.masterKeys.filter(mk => !masterKeyEnabled(mk)), false)}
{renderNonExistingMasterKeysSection()}
+ {renderAdvancedSection()}
);
};
diff --git a/packages/app-desktop/gui/NoteEditor/utils/resourceHandling.ts b/packages/app-desktop/gui/NoteEditor/utils/resourceHandling.ts
index 51c5a868e..2b46f231c 100644
--- a/packages/app-desktop/gui/NoteEditor/utils/resourceHandling.ts
+++ b/packages/app-desktop/gui/NoteEditor/utils/resourceHandling.ts
@@ -78,7 +78,7 @@ export async function commandAttachFileToBody(body: string, filePaths: string[]
logger.info(`Attaching ${filePath}`);
const newBody = await shim.attachFileToNoteBody(body, filePath, options.position, {
createFileURL: options.createFileURL,
- resizeLargeImages: 'ask',
+ resizeLargeImages: Setting.value('imageResizing'),
});
if (!newBody) {
diff --git a/packages/app-desktop/package.json b/packages/app-desktop/package.json
index 8209c6b12..240756565 100644
--- a/packages/app-desktop/package.json
+++ b/packages/app-desktop/package.json
@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
- "version": "2.12.10",
+ "version": "2.12.11",
"description": "Joplin for Desktop",
"main": "main.js",
"private": true,
diff --git a/packages/app-mobile/components/screens/Note.tsx b/packages/app-mobile/components/screens/Note.tsx
index 2bfc531a7..33fca0f7d 100644
--- a/packages/app-mobile/components/screens/Note.tsx
+++ b/packages/app-mobile/components/screens/Note.tsx
@@ -572,29 +572,16 @@ class NoteScreenComponent extends BaseScreenComponent {
public async resizeImage(localFilePath: string, targetPath: string, mimeType: string) {
const maxSize = Resource.IMAGE_MAX_DIMENSION;
-
const dimensions: any = await this.imageDimensions(localFilePath);
-
reg.logger().info('Original dimensions ', dimensions);
- let mustResize = dimensions.width > maxSize || dimensions.height > maxSize;
-
- if (mustResize) {
- const buttonId = await dialogs.pop(this, _('You are about to attach a large image (%dx%d pixels). Would you like to resize it down to %d pixels before attaching it?', dimensions.width, dimensions.height, maxSize), [
- { text: _('Yes'), id: 'yes' },
- { text: _('No'), id: 'no' },
- { text: _('Cancel'), id: 'cancel' },
- ]);
-
- if (buttonId === 'cancel') return false;
-
- mustResize = buttonId === 'yes';
- }
-
- if (mustResize) {
+ const saveOriginalImage = async () => {
+ await shim.fsDriver().copy(localFilePath, targetPath);
+ return true;
+ };
+ const saveResizedImage = async () => {
dimensions.width = maxSize;
dimensions.height = maxSize;
-
reg.logger().info('New dimensions ', dimensions);
const format = mimeType === 'image/png' ? 'PNG' : 'JPEG';
@@ -612,11 +599,27 @@ class NoteScreenComponent extends BaseScreenComponent {
} catch (error) {
reg.logger().warn('Error when unlinking cached file: ', error);
}
- } else {
- await shim.fsDriver().copy(localFilePath, targetPath);
+ return true;
+ };
+
+ const canResize = dimensions.width > maxSize || dimensions.height > maxSize;
+ if (canResize) {
+ const resizeLargeImages = Setting.value('imageResizing');
+ if (resizeLargeImages === 'alwaysAsk') {
+ const userAnswer = await dialogs.pop(this, `${_('You are about to attach a large image (%dx%d pixels). Would you like to resize it down to %d pixels before attaching it?', dimensions.width, dimensions.height, maxSize)}\n\n${_('(You may disable this prompt in the options)')}`, [
+ { text: _('Yes'), id: 'yes' },
+ { text: _('No'), id: 'no' },
+ { text: _('Cancel'), id: 'cancel' },
+ ]);
+ if (userAnswer === 'yes') return await saveResizedImage();
+ if (userAnswer === 'no') return await saveOriginalImage();
+ if (userAnswer === 'cancel') return false;
+ } else if (resizeLargeImages === 'alwaysResize') {
+ return await saveResizedImage();
+ }
}
- return true;
+ return await saveOriginalImage();
}
public async attachFile(pickerResponse: any, fileType: string) {
diff --git a/packages/app-mobile/ios/Podfile.lock b/packages/app-mobile/ios/Podfile.lock
index 8fba9bb67..985ef7479 100644
--- a/packages/app-mobile/ios/Podfile.lock
+++ b/packages/app-mobile/ios/Podfile.lock
@@ -343,7 +343,7 @@ PODS:
- React-Core
- react-native-camera/RN (4.2.1):
- React-Core
- - react-native-document-picker (8.2.1):
+ - react-native-document-picker (9.0.1):
- React-Core
- react-native-fingerprint-scanner (6.0.0):
- React
@@ -355,7 +355,7 @@ PODS:
- React-Core
- react-native-image-resizer (1.4.5):
- React-Core
- - react-native-netinfo (9.3.11):
+ - react-native-netinfo (9.4.1):
- React-Core
- react-native-rsa-native (2.0.5):
- React
@@ -465,9 +465,9 @@ PODS:
- React-Core
- RNCPushNotificationIOS (1.11.0):
- React-Core
- - RNDateTimePicker (7.2.0):
+ - RNDateTimePicker (7.3.0):
- React-Core
- - RNDeviceInfo (10.6.1):
+ - RNDeviceInfo (10.7.0):
- React-Core
- RNExitApp (1.1.0):
- React
@@ -819,13 +819,13 @@ SPEC CHECKSUMS:
React-logger: ef2269b3afa6ba868da90496c3e17a4ec4f4cee0
react-native-alarm-notification: 0732f97be04975a23ba60e675bdb961a0aaf6aa6
react-native-camera: 3eae183c1d111103963f3dd913b65d01aef8110f
- react-native-document-picker: 69ca2094d8780cfc1e7e613894d15290fdc54bba
+ react-native-document-picker: 2b8f18667caee73a96708a82b284a4f40b30a156
react-native-fingerprint-scanner: ac6656f18c8e45a7459302b84da41a44ad96dbbe
react-native-geolocation: 0f7fe8a4c2de477e278b0365cce27d089a8c5903
react-native-get-random-values: dee677497c6a740b71e5612e8dbd83e7539ed5bb
react-native-image-picker: db60857e03d63721f19b6f4027de20429ddd9cba
react-native-image-resizer: d9fb629a867335bdc13230ac2a58702bb8c8828f
- react-native-netinfo: 3a48f51c18dbd9253440621955e11de71bc51b32
+ react-native-netinfo: fefd4e98d75cbdd6e85fc530f7111a8afdf2b0c5
react-native-rsa-native: 12132eb627797529fdb1f0d22fd0f8f9678df64a
react-native-saf-x: 129cd2ddf120a1f6164c724b2846d172666b33de
react-native-safe-area-context: 68b07eabfb0d14547d36f6929c0e98d818064f02
@@ -849,8 +849,8 @@ SPEC CHECKSUMS:
rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba
RNCClipboard: 41d8d918092ae8e676f18adada19104fa3e68495
RNCPushNotificationIOS: 64218f3c776c03d7408284a819b2abfda1834bc8
- RNDateTimePicker: 3942382593f104af226ad9c56e16166960c7ae30
- RNDeviceInfo: ab292735ad4fccc5f2aec0c773f7a7f03c7073ae
+ RNDateTimePicker: 01e6d27ba2e0931cd05049c5bff6171c3c027ea8
+ RNDeviceInfo: 25d818c85db769cc0e7083d39efaa01a6f450df3
RNExitApp: c4e052df2568b43bec8a37c7cd61194d4cfee2c3
RNFileViewer: ce7ca3ac370e18554d35d6355cffd7c30437c592
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
diff --git a/packages/app-mobile/package.json b/packages/app-mobile/package.json
index dd40e3bc5..ba0136f70 100644
--- a/packages/app-mobile/package.json
+++ b/packages/app-mobile/package.json
@@ -26,7 +26,7 @@
"@react-native-community/clipboard": "1.5.1",
"@react-native-community/datetimepicker": "7.3.0",
"@react-native-community/geolocation": "3.0.6",
- "@react-native-community/netinfo": "9.3.11",
+ "@react-native-community/netinfo": "9.4.1",
"@react-native-community/push-notification-ios": "1.11.0",
"@react-native-community/slider": "4.4.2",
"assert-browserify": "2.0.0",
@@ -45,9 +45,9 @@
"react-native": "0.71.10",
"react-native-action-button": "2.8.5",
"react-native-camera": "4.2.1",
- "react-native-device-info": "10.6.1",
+ "react-native-device-info": "10.7.0",
"react-native-dialogbox": "0.6.10",
- "react-native-document-picker": "8.2.1",
+ "react-native-document-picker": "9.0.1",
"react-native-drawer-layout": "3.2.1",
"react-native-dropdownalert": "4.5.1",
"react-native-exit-app": "1.1.0",
@@ -60,7 +60,7 @@
"react-native-image-resizer": "1.4.5",
"react-native-localize": "3.0.2",
"react-native-modal-datetime-picker": "15.0.1",
- "react-native-paper": "5.8.0",
+ "react-native-paper": "5.9.1",
"react-native-popup-menu": "0.16.1",
"react-native-quick-actions": "0.3.13",
"react-native-reanimated": "3.3.0",
@@ -105,7 +105,7 @@
"@joplin/tools": "~2.12",
"@lezer/highlight": "1.1.4",
"@testing-library/jest-native": "5.4.2",
- "@testing-library/react-native": "12.1.2",
+ "@testing-library/react-native": "12.1.3",
"@tsconfig/react-native": "2.0.2",
"@types/fs-extra": "11.0.1",
"@types/jest": "29.5.3",
diff --git a/packages/app-mobile/utils/fs-driver-rn.ts b/packages/app-mobile/utils/fs-driver-rn.ts
index 506ac5720..a9e78db3d 100644
--- a/packages/app-mobile/utils/fs-driver-rn.ts
+++ b/packages/app-mobile/utils/fs-driver-rn.ts
@@ -351,7 +351,7 @@ export default class FsDriverRN extends FsDriverBase {
} else {
// the result is an array
if (multiple) {
- result = await DocumentPicker.pickMultiple();
+ result = await DocumentPicker.pick({ allowMultiSelection: true });
} else {
result = [await DocumentPicker.pick()];
}
diff --git a/packages/generate-plugin-doc/package.json b/packages/generate-plugin-doc/package.json
index 29f414155..89f96ba8a 100644
--- a/packages/generate-plugin-doc/package.json
+++ b/packages/generate-plugin-doc/package.json
@@ -6,6 +6,6 @@
},
"dependencies": {
"typedoc": "0.17.8",
- "typescript": "5.0.4"
+ "typescript": "4.7.4"
}
}
diff --git a/packages/lib/BaseApplication.ts b/packages/lib/BaseApplication.ts
index da944f2fc..40d95a7e7 100644
--- a/packages/lib/BaseApplication.ts
+++ b/packages/lib/BaseApplication.ts
@@ -735,6 +735,20 @@ export default class BaseApplication {
return toSystemSlashes(output, 'linux');
}
+ protected startRotatingLogMaintenance(profileDir: string) {
+ this.rotatingLogs = new RotatingLogs(profileDir);
+ const processLogs = async () => {
+ try {
+ await this.rotatingLogs.cleanActiveLogFile();
+ await this.rotatingLogs.deleteNonActiveLogFiles();
+ } catch (error) {
+ appLogger.error(error);
+ }
+ };
+ shim.setTimeout(() => { void processLogs(); }, 60000);
+ shim.setInterval(() => { void processLogs(); }, 24 * 60 * 60 * 1000);
+ }
+
public async start(argv: string[], options: StartOptions = null): Promise {
options = {
keychainEnabled: true,
@@ -932,18 +946,6 @@ export default class BaseApplication {
await MigrationService.instance().run();
- this.rotatingLogs = new RotatingLogs(profileDir);
- const processLogs = async () => {
- try {
- await this.rotatingLogs.cleanActiveLogFile();
- await this.rotatingLogs.deleteNonActiveLogFiles();
- } catch (error) {
- appLogger.error(error);
- }
- };
- shim.setTimeout(() => { void processLogs(); }, 60000);
- shim.setInterval(() => { void processLogs(); }, 24 * 60 * 60 * 1000);
-
return argv;
}
}
diff --git a/packages/lib/RotatingLogs.js b/packages/lib/RotatingLogs.js
deleted file mode 100644
index 29d70e01f..000000000
--- a/packages/lib/RotatingLogs.js
+++ /dev/null
@@ -1,56 +0,0 @@
-"use strict";
-var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
- return new (P || (P = Promise))(function (resolve, reject) {
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
- step((generator = generator.apply(thisArg, _arguments || [])).next());
- });
-};
-Object.defineProperty(exports, "__esModule", { value: true });
-const shim_1 = require("./shim");
-class RotatingLogs {
- constructor(logFilesDir, maxFileSize = null, inactiveMaxAge = null) {
- this.maxFileSize = 1024 * 1024 * 100;
- this.inactiveMaxAge = 90 * 24 * 60 * 60 * 1000;
- this.logFilesDir = logFilesDir;
- if (maxFileSize)
- this.maxFileSize = maxFileSize;
- if (inactiveMaxAge)
- this.inactiveMaxAge = inactiveMaxAge;
- }
- cleanActiveLogFile() {
- return __awaiter(this, void 0, void 0, function* () {
- const stats = yield this.fsDriver().stat(this.logFileFullpath());
- if (stats.size >= this.maxFileSize) {
- const newLogFile = this.logFileFullpath(this.getNameToNonActiveLogFile());
- yield this.fsDriver().move(this.logFileFullpath(), newLogFile);
- }
- });
- }
- getNameToNonActiveLogFile() {
- return `log-${Date.now()}.txt`;
- }
- deleteNonActiveLogFiles() {
- return __awaiter(this, void 0, void 0, function* () {
- const files = yield this.fsDriver().readDirStats(this.logFilesDir);
- for (const file of files) {
- if (!file.path.match(/^log-[0-9]+.txt$/gi))
- continue;
- const ageOfTheFile = Date.now() - file.birthtime;
- if (ageOfTheFile >= this.inactiveMaxAge) {
- yield this.fsDriver().remove(this.logFileFullpath(file.path));
- }
- }
- });
- }
- logFileFullpath(fileName = 'log.txt') {
- return `${this.logFilesDir}/${fileName}`;
- }
- fsDriver() {
- return shim_1.default.fsDriver();
- }
-}
-exports.default = RotatingLogs;
-//# sourceMappingURL=RotatingLogs.js.map
\ No newline at end of file
diff --git a/packages/lib/RotatingLogs.test.js b/packages/lib/RotatingLogs.test.js
deleted file mode 100644
index a4ecc2f03..000000000
--- a/packages/lib/RotatingLogs.test.js
+++ /dev/null
@@ -1,56 +0,0 @@
-"use strict";
-var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
- return new (P || (P = Promise))(function (resolve, reject) {
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
- step((generator = generator.apply(thisArg, _arguments || [])).next());
- });
-};
-Object.defineProperty(exports, "__esModule", { value: true });
-const fs_extra_1 = require("fs-extra");
-const test_utils_1 = require("./testing/test-utils");
-const RotatingLogs_1 = require("./RotatingLogs");
-const createTestLogFile = (dir) => __awaiter(void 0, void 0, void 0, function* () {
- yield (0, fs_extra_1.writeFile)(`${dir}/log.txt`, 'some content');
-});
-describe('RotatingLogs', () => {
- test('should rename log.txt to log-TIMESTAMP.txt', () => __awaiter(void 0, void 0, void 0, function* () {
- let dir;
- try {
- dir = yield (0, test_utils_1.createTempDir)();
- yield createTestLogFile(dir);
- let files = yield (0, fs_extra_1.readdir)(dir);
- expect(files.find(file => file.match(/^log.txt$/gi))).toBeTruthy();
- expect(files.length).toBe(1);
- const rotatingLogs = new RotatingLogs_1.default(dir, 1, 1);
- yield rotatingLogs.cleanActiveLogFile();
- files = yield (0, fs_extra_1.readdir)(dir);
- expect(files.find(file => file.match(/^log.txt$/gi))).toBeFalsy();
- expect(files.find(file => file.match(/^log-[0-9]+.txt$/gi))).toBeTruthy();
- expect(files.length).toBe(1);
- }
- finally {
- yield (0, fs_extra_1.remove)(dir);
- }
- }));
- test('should delete inative log file after 1ms', () => __awaiter(void 0, void 0, void 0, function* () {
- let dir;
- try {
- dir = yield (0, test_utils_1.createTempDir)();
- yield createTestLogFile(dir);
- const rotatingLogs = new RotatingLogs_1.default(dir, 1, 1);
- yield rotatingLogs.cleanActiveLogFile();
- yield (0, test_utils_1.msleep)(1);
- yield rotatingLogs.deleteNonActiveLogFiles();
- const files = yield (0, fs_extra_1.readdir)(dir);
- expect(files.find(file => file.match(/^log-[0-9]+.txt$/gi))).toBeFalsy();
- expect(files.length).toBe(0);
- }
- finally {
- yield (0, fs_extra_1.remove)(dir);
- }
- }));
-});
-//# sourceMappingURL=RotatingLogs.test.js.map
\ No newline at end of file
diff --git a/packages/lib/RotatingLogs.test.ts b/packages/lib/RotatingLogs.test.ts
index cbae7776e..a8990a537 100644
--- a/packages/lib/RotatingLogs.test.ts
+++ b/packages/lib/RotatingLogs.test.ts
@@ -12,7 +12,7 @@ describe('RotatingLogs', () => {
try {
dir = await createTempDir();
await createTestLogFile(dir);
- let files: string[] = await readdir(dir);
+ let files = await readdir(dir);
expect(files.find(file => file.match(/^log.txt$/gi))).toBeTruthy();
expect(files.length).toBe(1);
const rotatingLogs: RotatingLogs = new RotatingLogs(dir, 1, 1);
@@ -26,7 +26,7 @@ describe('RotatingLogs', () => {
}
});
- test('should delete inative log file after 1ms', async () => {
+ test('should delete inactive log file after 1ms', async () => {
let dir: string;
try {
dir = await createTempDir();
@@ -42,4 +42,21 @@ describe('RotatingLogs', () => {
await remove(dir);
}
});
+
+ test('should not delete the log-timestamp.txt right after its be created', async () => {
+ let dir: string;
+ try {
+ dir = await createTempDir();
+ await createTestLogFile(dir);
+ await msleep(100);
+ const rotatingLogs: RotatingLogs = new RotatingLogs(dir, 1, 100);
+ await rotatingLogs.cleanActiveLogFile();
+ await rotatingLogs.deleteNonActiveLogFiles();
+ const files = await readdir(dir);
+ expect(files.find(file => file.match(/^log-[0-9]+.txt$/gi))).toBeTruthy();
+ expect(files.length).toBe(1);
+ } finally {
+ await remove(dir);
+ }
+ });
});
diff --git a/packages/lib/RotatingLogs.ts b/packages/lib/RotatingLogs.ts
index ee6b62001..e1446f840 100644
--- a/packages/lib/RotatingLogs.ts
+++ b/packages/lib/RotatingLogs.ts
@@ -29,7 +29,8 @@ export default class RotatingLogs {
const files: Stat[] = await this.fsDriver().readDirStats(this.logFilesDir);
for (const file of files) {
if (!file.path.match(/^log-[0-9]+.txt$/gi)) continue;
- const ageOfTheFile: number = Date.now() - file.birthtime;
+ const timestamp: number = parseInt(file.path.match(/[0-9]+/g)[0], 10);
+ const ageOfTheFile: number = Date.now() - timestamp;
if (ageOfTheFile >= this.inactiveMaxAge) {
await this.fsDriver().remove(this.logFileFullpath(file.path));
}
diff --git a/packages/lib/models/Setting.ts b/packages/lib/models/Setting.ts
index ecc2009a3..5b4bfa67b 100644
--- a/packages/lib/models/Setting.ts
+++ b/packages/lib/models/Setting.ts
@@ -1121,7 +1121,25 @@ class Setting extends BaseModel {
storage: SettingStorage.File,
isGlobal: true,
},
-
+ imageResizing: {
+ value: 'alwaysAsk',
+ type: SettingItemType.String,
+ section: 'note',
+ isEnum: true,
+ public: true,
+ appTypes: [AppType.Mobile, AppType.Desktop],
+ label: () => _('Resize large images:'),
+ description: () => _('Shrink large images before adding them to notes to save storage space.'),
+ options: () => {
+ return {
+ alwaysAsk: _('Always ask'),
+ alwaysResize: _('Always resize'),
+ neverResize: _('Never resize'),
+ };
+ },
+ storage: SettingStorage.File,
+ isGlobal: true,
+ },
'plugins.states': {
value: '',
type: SettingItemType.Object,
diff --git a/packages/lib/package.json b/packages/lib/package.json
index 0d635efe1..75ba0d6af 100644
--- a/packages/lib/package.json
+++ b/packages/lib/package.json
@@ -25,7 +25,7 @@
"@types/uuid": "9.0.2",
"clean-html": "1.5.0",
"jest": "29.5.0",
- "sharp": "0.32.3",
+ "sharp": "0.32.4",
"typescript": "5.1.3"
},
"dependencies": {
@@ -91,7 +91,7 @@
"uglifycss": "0.0.29",
"url-parse": "1.5.10",
"uuid": "9.0.0",
- "word-wrap": "1.2.3",
+ "word-wrap": "1.2.4",
"xml2js": "0.4.23"
},
"gitHead": "eb4b0e64eab40a51b0895d3a40a9d8c3cb7b1b14"
diff --git a/packages/lib/shim-init-node.js b/packages/lib/shim-init-node.js
index 74d3bbb7a..0bc6b8fec 100644
--- a/packages/lib/shim-init-node.js
+++ b/packages/lib/shim-init-node.js
@@ -184,38 +184,42 @@ function shimInit(options = null) {
if (shim.isElectron()) {
// For Electron
const nativeImage = require('electron').nativeImage;
- let image = nativeImage.createFromPath(filePath);
+ const image = nativeImage.createFromPath(filePath);
if (image.isEmpty()) throw new Error(`Image is invalid or does not exist: ${filePath}`);
-
const size = image.getSize();
- let mustResize = size.width > maxDim || size.height > maxDim;
-
- if (mustResize && resizeLargeImages === 'ask') {
- const answer = shim.showMessageBox(_('You are about to attach a large image (%dx%d pixels). Would you like to resize it down to %d pixels before attaching it?', size.width, size.height, maxDim), {
- buttons: [_('Yes'), _('No'), _('Cancel')],
- });
-
- if (answer === 2) return false;
-
- mustResize = answer === 0;
- }
-
- if (!mustResize) {
+ const saveOriginalImage = async () => {
await shim.fsDriver().copy(filePath, targetPath);
return true;
+ };
+ const saveResizedImage = async () => {
+ const options = {};
+ if (size.width > size.height) {
+ options.width = maxDim;
+ } else {
+ options.height = maxDim;
+ }
+ const resizedImage = image.resize(options);
+ await shim.writeImageToFile(resizedImage, mime, targetPath);
+ return true;
+ };
+
+ const canResize = size.width > maxDim || size.height > maxDim;
+ if (canResize) {
+ if (resizeLargeImages === 'alwaysAsk') {
+ const Yes = 0, No = 1, Cancel = 2;
+ const userAnswer = shim.showMessageBox(`${_('You are about to attach a large image (%dx%d pixels). Would you like to resize it down to %d pixels before attaching it?', size.width, size.height, maxDim)}\n\n${_('(You may disable this prompt in the options)')}`, {
+ buttons: [_('Yes'), _('No'), _('Cancel')],
+ });
+ if (userAnswer === Yes) return await saveResizedImage();
+ if (userAnswer === No) return await saveOriginalImage();
+ if (userAnswer === Cancel) return false;
+ } else if (resizeLargeImages === 'alwaysResize') {
+ return await saveResizedImage();
+ }
}
- const options = {};
- if (size.width > size.height) {
- options.width = maxDim;
- } else {
- options.height = maxDim;
- }
-
- image = image.resize(options);
-
- await shim.writeImageToFile(image, mime, targetPath);
+ return await saveOriginalImage();
} else {
// For the CLI tool
const image = sharp(filePath);
@@ -241,8 +245,6 @@ function shimInit(options = null) {
});
});
}
-
- return true;
};
// This is a bit of an ugly method that's used to both create a new resource
diff --git a/packages/server/package.json b/packages/server/package.json
index 2c184f96a..694cd09ec 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -43,7 +43,7 @@
"mustache": "4.2.0",
"nanoid": "2.1.11",
"node-cron": "3.0.2",
- "nodemailer": "6.9.3",
+ "nodemailer": "6.9.4",
"nodemon": "2.0.22",
"pg": "8.11.1",
"pretty-bytes": "5.6.0",
diff --git a/packages/server/src/models/UserModel.test.ts b/packages/server/src/models/UserModel.test.ts
index adc3183e6..fd4cadb2b 100644
--- a/packages/server/src/models/UserModel.test.ts
+++ b/packages/server/src/models/UserModel.test.ts
@@ -1,6 +1,6 @@
import { createUserAndSession, beforeAllDb, afterAllTests, beforeEachDb, models, checkThrowAsync, expectThrow } from '../utils/testing/testUtils';
import { EmailSender, UserFlagType } from '../services/database/types';
-import { ErrorUnprocessableEntity } from '../utils/errors';
+import { ErrorBadRequest, ErrorUnprocessableEntity } from '../utils/errors';
import { betaUserDateRange, stripeConfig } from '../utils/stripe';
import { accountByType, AccountType } from './UserModel';
import { failedPaymentFinalAccount, failedPaymentWarningInterval } from './SubscriptionModel';
@@ -425,4 +425,13 @@ describe('UserModel', () => {
expect((await models().user().load(user1.id)).enabled).toBe(0);
});
+ test('should throw an error if the password being saved seems to be hashed', async () => {
+ const passwordSimilarToHash = '$2a$10';
+
+ const error = await checkThrowAsync(async () => await models().user().save({ password: passwordSimilarToHash }));
+
+ expect(error.message).toBe('Unable to save user because password already seems to be hashed. User id: undefined');
+ expect(error instanceof ErrorBadRequest).toBe(true);
+ });
+
});
diff --git a/packages/server/src/models/UserModel.ts b/packages/server/src/models/UserModel.ts
index 22619309d..ad41ae3a3 100644
--- a/packages/server/src/models/UserModel.ts
+++ b/packages/server/src/models/UserModel.ts
@@ -1,6 +1,6 @@
import BaseModel, { AclAction, SaveOptions, ValidateOptions } from './BaseModel';
import { EmailSender, Item, NotificationLevel, Subscription, User, UserFlagType, Uuid } from '../services/database/types';
-import * as auth from '../utils/auth';
+import { isHashedPassword, hashPassword, checkPassword } from '../utils/auth';
import { ErrorUnprocessableEntity, ErrorForbidden, ErrorPayloadTooLarge, ErrorNotFound, ErrorBadRequest } from '../utils/errors';
import { ModelType } from '@joplin/lib/BaseModel';
import { _ } from '@joplin/lib/locale';
@@ -125,7 +125,7 @@ export default class UserModel extends BaseModel {
public async login(email: string, password: string): Promise {
const user = await this.loadByEmail(email);
if (!user) return null;
- if (!auth.checkPassword(password, user.password)) return null;
+ if (!checkPassword(password, user.password)) return null;
return user;
}
@@ -636,8 +636,11 @@ export default class UserModel extends BaseModel {
const user = this.formatValues(object);
if (user.password) {
+ if (isHashedPassword(user.password)) {
+ throw new ErrorBadRequest(`Unable to save user because password already seems to be hashed. User id: ${user.id}`);
+ }
if (!options.skipValidation) this.validatePassword(user.password);
- user.password = auth.hashPassword(user.password);
+ user.password = hashPassword(user.password);
}
const isNew = await this.isNew(object, options);
diff --git a/packages/server/src/models/utils/user.test.ts b/packages/server/src/models/utils/user.test.ts
new file mode 100644
index 000000000..d219918cb
--- /dev/null
+++ b/packages/server/src/models/utils/user.test.ts
@@ -0,0 +1,19 @@
+import { isHashedPassword } from '../../utils/auth';
+
+describe('isHashedPassword', () => {
+
+ it('should be true if password starts with $2a$10', () => {
+ expect(isHashedPassword('$2a$10$LMKVPiNOWDZhtw9NizNIEuNGLsjOxQAcrwQJ0lnKuiaOtyFgZEnwO')).toBe(true);
+ });
+
+ it.each(
+ [
+ 'password',
+ '123456',
+ 'simple-password-that-takes-is-long',
+ 'nuXUhqecx!RzK3wv6^xYaVEP%9fc$T%$E2k%9Q&TKvtDhR#2PUw3kA8KX3w2baAD8m#N9@52!DvfYn*X6hP#uAvpGF57*H9avcoePbR&4Q2XzckJnSW*EVm4G@a#YvnR',
+ ]
+ )('should be false if password starts with $2a$10: %', (password) => {
+ expect(isHashedPassword(password)).toBe(false);
+ });
+});
diff --git a/packages/server/src/utils/auth.test.ts b/packages/server/src/utils/auth.test.ts
new file mode 100644
index 000000000..4ff524126
--- /dev/null
+++ b/packages/server/src/utils/auth.test.ts
@@ -0,0 +1,18 @@
+import { hashPassword } from './auth';
+
+describe('hashPassword', () => {
+
+ it.each(
+ [
+ 'password',
+ '123456',
+ 'simple-password-that-takes-is-long',
+ 'nuXUhqecx!RzK3wv6^xYaVEP%9fc$T%$E2k%9Q&TKvtDhR#2PUw3kA8KX3w2baAD8m#N9@52!DvfYn*X6hP#uAvpGF57*H9avcoePbR&4Q2XzckJnSW*EVm4G@a#YvnR',
+ '$2a$10',
+ '$2a$10$LMKVPiNOWDZhtw9NizNIEuNGLsjOxQAcrwQJ0lnKuiaOtyFgZEnwO',
+ ]
+ )('should return a string that starts with $2a$10 for the password: %', async (plainText) => {
+ expect(hashPassword(plainText).startsWith('$2a$10')).toBe(true);
+ });
+
+});
diff --git a/packages/server/src/utils/auth.ts b/packages/server/src/utils/auth.ts
index 899c803dc..e7bb03d43 100644
--- a/packages/server/src/utils/auth.ts
+++ b/packages/server/src/utils/auth.ts
@@ -8,3 +8,7 @@ export function hashPassword(password: string): string {
export function checkPassword(password: string, hash: string): boolean {
return bcrypt.compareSync(password, hash);
}
+
+export const isHashedPassword = (password: string) => {
+ return password.startsWith('$2a$10');
+};
diff --git a/packages/tools/package.json b/packages/tools/package.json
index 89d6cd8b1..63c881027 100644
--- a/packages/tools/package.json
+++ b/packages/tools/package.json
@@ -36,7 +36,7 @@
"node-fetch": "2.6.7",
"relative": "3.0.2",
"request": "2.88.2",
- "sharp": "0.32.3",
+ "sharp": "0.32.4",
"source-map-support": "0.5.21",
"uri-template": "2.0.0",
"yargs": "17.7.2"
diff --git a/packages/tools/website/build.ts b/packages/tools/website/build.ts
index f3153879f..d0c2b07cd 100644
--- a/packages/tools/website/build.ts
+++ b/packages/tools/website/build.ts
@@ -257,7 +257,7 @@ async function main() {
await remove(`${docDir}`);
await copy(websiteAssetDir, `${docDir}`);
- const sponsors = await loadSponsors();
+ const sponsors = process.env.SKIP_SPONSOR_PROCESSING ? { github: [], orgs: [] } : await loadSponsors();
const partials = await loadMustachePartials(partialDir);
const assetUrls = await getAssetUrls();
diff --git a/readme/cla_signatures.json b/readme/cla_signatures.json
index c00f6dad7..40721bc98 100644
--- a/readme/cla_signatures.json
+++ b/readme/cla_signatures.json
@@ -663,6 +663,14 @@
"created_at": "2023-08-07T11:20:16Z",
"repoId": 79162682,
"pullRequestNo": 8627
+ },
+ {
+ "name": "TuTAH1",
+ "id": 15982179,
+ "comment_id": 1668635558,
+ "created_at": "2023-08-07T22:03:21Z",
+ "repoId": 79162682,
+ "pullRequestNo": 8635
}
]
}
\ No newline at end of file
diff --git a/readme/release_cycle.md b/readme/release_cycle.md
new file mode 100644
index 000000000..06f6502f4
--- /dev/null
+++ b/readme/release_cycle.md
@@ -0,0 +1,24 @@
+# Joplin release cycle
+
+We release four major versions per year, one per quarter, following a three-phase process: "Release", "Freeze", and "Publishing". This reliable schedule empowers the community, as well as businesses and developers, to effectively plan their own roadmaps.
+
+Here's what each phase entails:
+
+**Phase 1: "Release" - Cycle start** During this phase, our team will focus on developing new features and improvements for the next release.
+
+**Phase 2: "Freeze" - Stability and bug fixing** The "Freeze" phase will begin two weeks before the intended publishing date. At this point, we will halt the addition of new features and concentrate on stabilizing the software. Our main objective will be to fix any remaining bugs and optimize performance.
+
+**Phase 3: "Publishing" - Final version** The "Publishing" phase is when we will officially release the new version of Joplin.
+
+Below is our schedule related to the phases mentioned above:
+
+| Release | Freeze | Publishing |
+| --- | --- | --- |
+| **Joplin 2.12** | Aug 17 2023 - Aug 31 2023 | Sept 1 2023 - Sept 7 2023 |
+| **Joplin 2.13** | Nov 16 2023 - Nov 30 2023 | Dec 1 2023 - Dec 7 2023 |
+| **Joplin 2.14** | Feb 15 2024 - Feb 29 2024 | Mar 1 2024 - Mar 7 2024 |
+| **Joplin 2.15** | Jun 17 2024 - Jun 31 2024 | Jul 1 2024 - Jul 7 2024 |
+| **Joplin 2.16** | Aug 17 2024 - Aug 31 2024 | Sept 1 2024 - Sept 7 2024 |
+| **Joplin 2.17** | Nov 16 2024 - Nov 30 2024 | Dec 1 2024 - Dec 7 2024 |
+
+Please note that during this release process, [prereleases are also regularly made available](https://joplinapp.org/prereleases/). This allows you to test the application before its final release and to provide feedback on the new features being added and influence development. Moreover, if you find an issue, you can report it and we will give it a high priority and attempt to fix it as soon as possible.
diff --git a/renovate.json5 b/renovate.json5
index 83ef5a020..b4471b726 100644
--- a/renovate.json5
+++ b/renovate.json5
@@ -26,8 +26,8 @@
"packages/app-cli/tests/**",
"packages/app-clipper/popup",
"packages/app-mobile/android/app/build.gradle",
+ "packages/generate-plugin-doc/**",
"packages/plugins/**",
- "packages/react-native-vosk",
],
"ignoreDeps": [
"@babel/core",
@@ -130,6 +130,7 @@
"pretty-bytes",
"strip-ansi",
"formidable",
+ "node-emoji",
// @koa/cors has undocumented breaking changes, and the package is not
// well supported so we're stuck with latest v3 for now
diff --git a/yarn.lock b/yarn.lock
index 339270806..11b710f40 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4525,11 +4525,11 @@ __metadata:
"@react-native-community/clipboard": 1.5.1
"@react-native-community/datetimepicker": 7.3.0
"@react-native-community/geolocation": 3.0.6
- "@react-native-community/netinfo": 9.3.11
+ "@react-native-community/netinfo": 9.4.1
"@react-native-community/push-notification-ios": 1.11.0
"@react-native-community/slider": 4.4.2
"@testing-library/jest-native": 5.4.2
- "@testing-library/react-native": 12.1.2
+ "@testing-library/react-native": 12.1.3
"@tsconfig/react-native": 2.0.2
"@types/fs-extra": 11.0.1
"@types/jest": 29.5.3
@@ -4565,9 +4565,9 @@ __metadata:
react-native: 0.71.10
react-native-action-button: 2.8.5
react-native-camera: 4.2.1
- react-native-device-info: 10.6.1
+ react-native-device-info: 10.7.0
react-native-dialogbox: 0.6.10
- react-native-document-picker: 8.2.1
+ react-native-document-picker: 9.0.1
react-native-drawer-layout: 3.2.1
react-native-dropdownalert: 4.5.1
react-native-exit-app: 1.1.0
@@ -4580,7 +4580,7 @@ __metadata:
react-native-image-resizer: 1.4.5
react-native-localize: 3.0.2
react-native-modal-datetime-picker: 15.0.1
- react-native-paper: 5.8.0
+ react-native-paper: 5.9.1
react-native-popup-menu: 0.16.1
react-native-quick-actions: 0.3.13
react-native-reanimated: 3.3.0
@@ -4734,7 +4734,7 @@ __metadata:
relative: 3.0.2
reselect: 4.1.8
server-destroy: 1.0.1
- sharp: 0.32.3
+ sharp: 0.32.4
sprintf-js: 1.1.2
sqlite3: 5.1.6
string-padding: 1.0.2
@@ -4745,7 +4745,7 @@ __metadata:
uglifycss: 0.0.29
url-parse: 1.5.10
uuid: 9.0.0
- word-wrap: 1.2.3
+ word-wrap: 1.2.4
xml2js: 0.4.23
languageName: unknown
linkType: soft
@@ -4916,7 +4916,7 @@ __metadata:
nanoid: 2.1.11
node-cron: 3.0.2
node-mocks-http: 1.12.2
- nodemailer: 6.9.3
+ nodemailer: 6.9.4
nodemon: 2.0.22
pg: 8.11.1
pretty-bytes: 5.6.0
@@ -4968,7 +4968,7 @@ __metadata:
request: 2.88.2
rss: 1.2.2
sass: 1.63.6
- sharp: 0.32.3
+ sharp: 0.32.4
source-map-support: 0.5.21
sqlite3: 5.1.6
typescript: 5.1.3
@@ -6875,12 +6875,12 @@ __metadata:
languageName: node
linkType: hard
-"@react-native-community/netinfo@npm:9.3.11":
- version: 9.3.11
- resolution: "@react-native-community/netinfo@npm:9.3.11"
+"@react-native-community/netinfo@npm:9.4.1":
+ version: 9.4.1
+ resolution: "@react-native-community/netinfo@npm:9.4.1"
peerDependencies:
react-native: ">=0.59"
- checksum: 1021fe0a3bda6fe98f940d067ae56e688ba00de8696bcdb02a2802af1e91a707709e83c8cd1ac01edc7c6a9af798a408448d0661cf61c7bd1795cfb61e709455
+ checksum: cf6471a50a5282f858797cda7531c61ac3d94de2e1c379b14a11f6b049f582606dae55a041dd900c56b01faf69eb5cfef9b4e84b0ea7f02de52804aa5a6e22df
languageName: node
linkType: hard
@@ -7232,9 +7232,9 @@ __metadata:
languageName: node
linkType: hard
-"@testing-library/react-native@npm:12.1.2":
- version: 12.1.2
- resolution: "@testing-library/react-native@npm:12.1.2"
+"@testing-library/react-native@npm:12.1.3":
+ version: 12.1.3
+ resolution: "@testing-library/react-native@npm:12.1.3"
dependencies:
pretty-format: ^29.0.0
peerDependencies:
@@ -7245,7 +7245,7 @@ __metadata:
peerDependenciesMeta:
jest:
optional: true
- checksum: 912fc961f213a8fa171b9b980d6f4edd8f11a012498fcf1b8e0d3ac1d20e85b61469a80914fda893aa48cb0d4b3f6075ec2723c58dae96eeac0ee1cd6e6daa3e
+ checksum: afc472bdf1f3b966d292749d9f1d4dc70112b7124935aa60f7d2e928fceef7b79dc4e7241b0461876f2ecf740440c6e400756075f684ecf9341c7528d3592603
languageName: node
linkType: hard
@@ -8773,6 +8773,13 @@ __metadata:
languageName: node
linkType: hard
+"@yarnpkg/lockfile@npm:^1.1.0":
+ version: 1.1.0
+ resolution: "@yarnpkg/lockfile@npm:1.1.0"
+ checksum: 05b881b4866a3546861fee756e6d3812776ea47fa6eb7098f983d6d0eefa02e12b66c3fff931574120f196286a7ad4879ce02743c8bb2be36c6a576c7852083a
+ languageName: node
+ linkType: hard
+
"@zkochan/cmd-shim@npm:^3.1.0":
version: 3.1.0
resolution: "@zkochan/cmd-shim@npm:3.1.0"
@@ -11553,6 +11560,13 @@ __metadata:
languageName: node
linkType: hard
+"ci-info@npm:^3.7.0":
+ version: 3.8.0
+ resolution: "ci-info@npm:3.8.0"
+ checksum: d0a4d3160497cae54294974a7246202244fff031b0a6ea20dd57b10ec510aa17399c41a1b0982142c105f3255aff2173e5c0dd7302ee1b2f28ba3debda375098
+ languageName: node
+ linkType: hard
+
"cipher-base@npm:^1.0.0, cipher-base@npm:^1.0.1, cipher-base@npm:^1.0.3":
version: 1.0.4
resolution: "cipher-base@npm:1.0.4"
@@ -14231,6 +14245,13 @@ __metadata:
languageName: node
linkType: hard
+"detect-libc@npm:^2.0.2":
+ version: 2.0.2
+ resolution: "detect-libc@npm:2.0.2"
+ checksum: 2b2cd3649b83d576f4be7cc37eb3b1815c79969c8b1a03a40a4d55d83bc74d010753485753448eacb98784abf22f7dbd3911fd3b60e29fda28fed2d1a997944d
+ languageName: node
+ linkType: hard
+
"detect-newline@npm:^3.0.0":
version: 3.1.0
resolution: "detect-newline@npm:3.1.0"
@@ -16782,6 +16803,15 @@ __metadata:
languageName: node
linkType: hard
+"find-yarn-workspace-root@npm:^2.0.0":
+ version: 2.0.0
+ resolution: "find-yarn-workspace-root@npm:2.0.0"
+ dependencies:
+ micromatch: ^4.0.2
+ checksum: fa5ca8f9d08fe7a54ce7c0a5931ff9b7e36f9ee7b9475fb13752bcea80ec6b5f180fa5102d60b376d5526ce924ea3fc6b19301262efa0a5d248dd710f3644242
+ languageName: node
+ linkType: hard
+
"findit@npm:^2.0.0":
version: 2.0.0
resolution: "findit@npm:2.0.0"
@@ -17375,7 +17405,7 @@ __metadata:
resolution: "generate-plugin-doc@workspace:packages/generate-plugin-doc"
dependencies:
typedoc: 0.17.8
- typescript: 5.0.4
+ typescript: 4.7.4
languageName: unknown
linkType: soft
@@ -20086,7 +20116,7 @@ __metadata:
languageName: node
linkType: hard
-"is-wsl@npm:^2.2.0":
+"is-wsl@npm:^2.1.1, is-wsl@npm:^2.2.0":
version: 2.2.0
resolution: "is-wsl@npm:2.2.0"
dependencies:
@@ -21020,7 +21050,7 @@ __metadata:
proper-lockfile: 4.1.2
read-chunk: 2.1.0
server-destroy: 1.0.1
- sharp: 0.32.3
+ sharp: 0.32.4
sprintf-js: 1.1.2
sqlite3: 5.1.6
string-padding: 1.0.2
@@ -21031,7 +21061,7 @@ __metadata:
tkwidgets: 0.5.27
typescript: 5.1.3
url-parse: 1.5.10
- word-wrap: 1.2.3
+ word-wrap: 1.2.4
yargs-parser: 21.1.1
bin:
joplin: ./main.js
@@ -21607,6 +21637,15 @@ __metadata:
languageName: node
linkType: hard
+"klaw-sync@npm:^6.0.0":
+ version: 6.0.0
+ resolution: "klaw-sync@npm:6.0.0"
+ dependencies:
+ graceful-fs: ^4.1.11
+ checksum: 0da397f8961313c3ef8f79fb63af9002cde5a8fb2aeb1a37351feff0dd6006129c790400c3f5c3b4e757bedcabb13d21ec0a5eaef5a593d59515d4f2c291e475
+ languageName: node
+ linkType: hard
+
"klaw@npm:^1.0.0":
version: 1.3.1
resolution: "klaw@npm:1.3.1"
@@ -23635,7 +23674,7 @@ __metadata:
languageName: node
linkType: hard
-"micromatch@npm:^4.0.0, micromatch@npm:^4.0.5":
+"micromatch@npm:^4.0.0, micromatch@npm:^4.0.2, micromatch@npm:^4.0.5":
version: 4.0.5
resolution: "micromatch@npm:4.0.5"
dependencies:
@@ -24780,10 +24819,10 @@ __metadata:
languageName: node
linkType: hard
-"nodemailer@npm:6.9.3":
- version: 6.9.3
- resolution: "nodemailer@npm:6.9.3"
- checksum: 3bea8316652c0578515d9146d2f24660e4855807520153f061d39af76b440a4f61b4e70f10fed35f8f12f115f6aea1aeb483ea7ba0337c0e3e675f117c41c611
+"nodemailer@npm:6.9.4":
+ version: 6.9.4
+ resolution: "nodemailer@npm:6.9.4"
+ checksum: 1a61039c9c6041ee9ed423c7fa3685cb300679f1ef8dcfd912fc58d9b8c429c34ac1187eebd4d86baf7158222f6e3a1ea2b4fd2a29630fb9ef0027cef46e90bb
languageName: node
linkType: hard
@@ -25528,6 +25567,16 @@ __metadata:
languageName: node
linkType: hard
+"open@npm:^7.4.2":
+ version: 7.4.2
+ resolution: "open@npm:7.4.2"
+ dependencies:
+ is-docker: ^2.0.0
+ is-wsl: ^2.1.1
+ checksum: 3333900ec0e420d64c23b831bc3467e57031461d843c801f569b2204a1acc3cd7b3ec3c7897afc9dde86491dfa289708eb92bba164093d8bd88fb2c231843c91
+ languageName: node
+ linkType: hard
+
"opencollective-postinstall@npm:^2.0.2":
version: 2.0.3
resolution: "opencollective-postinstall@npm:2.0.3"
@@ -26162,6 +26211,30 @@ __metadata:
languageName: node
linkType: hard
+"patch-package@npm:^7.0.0":
+ version: 7.0.2
+ resolution: "patch-package@npm:7.0.2"
+ dependencies:
+ "@yarnpkg/lockfile": ^1.1.0
+ chalk: ^4.1.2
+ ci-info: ^3.7.0
+ cross-spawn: ^7.0.3
+ find-yarn-workspace-root: ^2.0.0
+ fs-extra: ^9.0.0
+ klaw-sync: ^6.0.0
+ minimist: ^1.2.6
+ open: ^7.4.2
+ rimraf: ^2.6.3
+ semver: ^7.5.3
+ slash: ^2.0.0
+ tmp: ^0.0.33
+ yaml: ^2.2.2
+ bin:
+ patch-package: index.js
+ checksum: de2cf60effc8b59ee15d4930f84eea63c217525f0133c926850ee3a2437651d8aabbd0fec4ddee1b8b8da4fd380bcea90ef3c4acd69026057fa80cdc823b59a4
+ languageName: node
+ linkType: hard
+
"path-browserify@npm:1.0.1":
version: 1.0.1
resolution: "path-browserify@npm:1.0.1"
@@ -27700,12 +27773,12 @@ __metadata:
languageName: node
linkType: hard
-"react-native-device-info@npm:10.6.1":
- version: 10.6.1
- resolution: "react-native-device-info@npm:10.6.1"
+"react-native-device-info@npm:10.7.0":
+ version: 10.7.0
+ resolution: "react-native-device-info@npm:10.7.0"
peerDependencies:
react-native: "*"
- checksum: aa3e56fad9256994714aa310ef5ab2ef80118ae2af73ac830cda182fa82b72785b03fd0e522b871a59723e6914274dfa203f53e3467feebb9c913df63643a014
+ checksum: ae2f69f510f25026e128946efc108385920cd4bda5723d50f74406e960d5b0cc3bc0cf3f53a6a0f83ba274778a6ac63d1f64abcfe068728486ec642342931523
languageName: node
linkType: hard
@@ -27721,9 +27794,9 @@ __metadata:
languageName: node
linkType: hard
-"react-native-document-picker@npm:8.2.1":
- version: 8.2.1
- resolution: "react-native-document-picker@npm:8.2.1"
+"react-native-document-picker@npm:9.0.1":
+ version: 9.0.1
+ resolution: "react-native-document-picker@npm:9.0.1"
dependencies:
invariant: ^2.2.4
peerDependencies:
@@ -27733,7 +27806,7 @@ __metadata:
peerDependenciesMeta:
react-native-windows:
optional: true
- checksum: 575d3bec391044fdaf8d4d740a115a0b3e5cbafdb893ce19157b0c5fefb052c208b9db6d352e171d358c723612e47fe67cda4634a60d3a245a30ff103b1cb84c
+ checksum: a8ad0bc2ed13290e8d7ff5f77aa7e35ffda9fa380a7d01d1286111e92656415985d61469a50567b8bcb67e42c41a36b89e879d0d08b40d3fdda171eaa08721e4
languageName: node
linkType: hard
@@ -27887,19 +27960,20 @@ __metadata:
languageName: node
linkType: hard
-"react-native-paper@npm:5.8.0":
- version: 5.8.0
- resolution: "react-native-paper@npm:5.8.0"
+"react-native-paper@npm:5.9.1":
+ version: 5.9.1
+ resolution: "react-native-paper@npm:5.9.1"
dependencies:
"@callstack/react-theme-provider": ^3.0.8
color: ^3.1.2
+ patch-package: ^7.0.0
use-latest-callback: ^0.1.5
peerDependencies:
react: "*"
react-native: "*"
react-native-safe-area-context: "*"
react-native-vector-icons: "*"
- checksum: b1cdf33b8d3140991c8fb7397ddcb7e898ce592f1ad76f505bda5b2d9cf8d9a6b2da6f73430d1457a48f8b973b548451211e88a3299b6ce99a259215352d5457
+ checksum: 47b23f827d3d5d296dc888d8c10c689465ccf2b8d815b2ecea8c5a41b2244776d153a6ffeaf14df61a231cd9542cc26199e85dd94687acec6b6adeb3f0ea271d
languageName: node
linkType: hard
@@ -29954,12 +30028,12 @@ __metadata:
languageName: node
linkType: hard
-"sharp@npm:0.32.3":
- version: 0.32.3
- resolution: "sharp@npm:0.32.3"
+"sharp@npm:0.32.4":
+ version: 0.32.4
+ resolution: "sharp@npm:0.32.4"
dependencies:
color: ^4.2.3
- detect-libc: ^2.0.1
+ detect-libc: ^2.0.2
node-addon-api: ^6.1.0
node-gyp: latest
prebuild-install: ^7.1.1
@@ -29967,7 +30041,7 @@ __metadata:
simple-get: ^4.0.1
tar-fs: ^3.0.4
tunnel-agent: ^0.6.0
- checksum: 8a6ed0d00bd4d3d6ba92c392fe1f00a4a207f138257a4d903ce5afe5c6d7f684b5218c5b4e8df1097aac960ac10e0114c823f6a8a7e18255bf4a8ec364087051
+ checksum: 52e3cfe8fbba2623a9b935be8a3d00d6993a2c56c775ac5cc89b273826db95f029f68a0029a37f96dcb6790aa2e3c05a02599035535b319f50ab31f5d86a13f0
languageName: node
linkType: hard
@@ -32833,7 +32907,7 @@ __metadata:
languageName: node
linkType: hard
-"typescript@npm:4 - 5, typescript@npm:5.0.4":
+"typescript@npm:4 - 5":
version: 5.0.4
resolution: "typescript@npm:5.0.4"
bin:
@@ -32843,6 +32917,16 @@ __metadata:
languageName: node
linkType: hard
+"typescript@npm:4.7.4":
+ version: 4.7.4
+ resolution: "typescript@npm:4.7.4"
+ bin:
+ tsc: bin/tsc
+ tsserver: bin/tsserver
+ checksum: 5750181b1cd7e6482c4195825547e70f944114fb47e58e4aa7553e62f11b3f3173766aef9c281783edfd881f7b8299cf35e3ca8caebe73d8464528c907a164df
+ languageName: node
+ linkType: hard
+
"typescript@npm:5.1.3":
version: 5.1.3
resolution: "typescript@npm:5.1.3"
@@ -32873,7 +32957,7 @@ __metadata:
languageName: node
linkType: hard
-"typescript@patch:typescript@4 - 5#~builtin, typescript@patch:typescript@5.0.4#~builtin":
+"typescript@patch:typescript@4 - 5#~builtin":
version: 5.0.4
resolution: "typescript@patch:typescript@npm%3A5.0.4#~builtin::version=5.0.4&hash=ad5954"
bin:
@@ -32883,6 +32967,16 @@ __metadata:
languageName: node
linkType: hard
+"typescript@patch:typescript@4.7.4#~builtin":
+ version: 4.7.4
+ resolution: "typescript@patch:typescript@npm%3A4.7.4#~builtin::version=4.7.4&hash=65a307"
+ bin:
+ tsc: bin/tsc
+ tsserver: bin/tsserver
+ checksum: 9096d8f6c16cb80ef3bf96fcbbd055bf1c4a43bd14f3b7be45a9fbe7ada46ec977f604d5feed3263b4f2aa7d4c7477ce5f9cd87de0d6feedec69a983f3a4f93e
+ languageName: node
+ linkType: hard
+
"typescript@patch:typescript@5.1.3#~builtin":
version: 5.1.3
resolution: "typescript@patch:typescript@npm%3A5.1.3#~builtin::version=5.1.3&hash=ad5954"
@@ -34244,7 +34338,14 @@ __metadata:
languageName: node
linkType: hard
-"word-wrap@npm:1.2.3, word-wrap@npm:^1.2.3, word-wrap@npm:~1.2.3":
+"word-wrap@npm:1.2.4":
+ version: 1.2.4
+ resolution: "word-wrap@npm:1.2.4"
+ checksum: 8f1f2e0a397c0e074ca225ba9f67baa23f99293bc064e31355d426ae91b8b3f6b5f6c1fc9ae5e9141178bb362d563f55e62fd8d5c31f2a77e3ade56cb3e35bd1
+ languageName: node
+ linkType: hard
+
+"word-wrap@npm:^1.2.3, word-wrap@npm:~1.2.3":
version: 1.2.3
resolution: "word-wrap@npm:1.2.3"
checksum: 30b48f91fcf12106ed3186ae4fa86a6a1842416df425be7b60485de14bec665a54a68e4b5156647dec3a70f25e84d270ca8bc8cd23182ed095f5c7206a938c1f