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

Desktop: Fixes #3407: In some cases, changes made to an attachment would not be saved. Also added banner to show that an attachment is being edited

This commit is contained in:
Laurent Cozic 2020-07-22 19:03:31 +01:00
parent f3dc3602c8
commit 4bef79cd71
18 changed files with 200 additions and 111 deletions

View File

@ -151,7 +151,8 @@ ReactNativeClient/lib/services/keychain/KeychainServiceDriver.dummy.js
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.mobile.js ReactNativeClient/lib/services/keychain/KeychainServiceDriver.mobile.js
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.node.js ReactNativeClient/lib/services/keychain/KeychainServiceDriver.node.js
ReactNativeClient/lib/services/keychain/KeychainServiceDriverBase.js ReactNativeClient/lib/services/keychain/KeychainServiceDriverBase.js
ReactNativeClient/lib/services/ResourceEditWatcher.js ReactNativeClient/lib/services/ResourceEditWatcher/index.js
ReactNativeClient/lib/services/ResourceEditWatcher/reducer.js
ReactNativeClient/lib/services/rest/actionApi.desktop.js ReactNativeClient/lib/services/rest/actionApi.desktop.js
ReactNativeClient/lib/services/rest/errors.js ReactNativeClient/lib/services/rest/errors.js
ReactNativeClient/lib/services/SettingUtils.js ReactNativeClient/lib/services/SettingUtils.js

3
.gitignore vendored
View File

@ -142,7 +142,8 @@ ReactNativeClient/lib/services/keychain/KeychainServiceDriver.dummy.js
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.mobile.js ReactNativeClient/lib/services/keychain/KeychainServiceDriver.mobile.js
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.node.js ReactNativeClient/lib/services/keychain/KeychainServiceDriver.node.js
ReactNativeClient/lib/services/keychain/KeychainServiceDriverBase.js ReactNativeClient/lib/services/keychain/KeychainServiceDriverBase.js
ReactNativeClient/lib/services/ResourceEditWatcher.js ReactNativeClient/lib/services/ResourceEditWatcher/index.js
ReactNativeClient/lib/services/ResourceEditWatcher/reducer.js
ReactNativeClient/lib/services/rest/actionApi.desktop.js ReactNativeClient/lib/services/rest/actionApi.desktop.js
ReactNativeClient/lib/services/rest/errors.js ReactNativeClient/lib/services/rest/errors.js
ReactNativeClient/lib/services/SettingUtils.js ReactNativeClient/lib/services/SettingUtils.js

View File

@ -21,7 +21,7 @@ const InteropServiceHelper = require('./InteropServiceHelper.js');
const ResourceService = require('lib/services/ResourceService'); const ResourceService = require('lib/services/ResourceService');
const ClipperServer = require('lib/ClipperServer'); const ClipperServer = require('lib/ClipperServer');
const ExternalEditWatcher = require('lib/services/ExternalEditWatcher'); const ExternalEditWatcher = require('lib/services/ExternalEditWatcher');
const ResourceEditWatcher = require('lib/services/ResourceEditWatcher').default; const ResourceEditWatcher = require('lib/services/ResourceEditWatcher/index').default;
const { bridge } = require('electron').remote.require('./bridge'); const { bridge } = require('electron').remote.require('./bridge');
const { shell, webFrame, clipboard } = require('electron'); const { shell, webFrame, clipboard } = require('electron');
const Menu = bridge().Menu; const Menu = bridge().Menu;
@ -1265,7 +1265,7 @@ class Application extends BaseApplication {
ExternalEditWatcher.instance().setLogger(reg.logger()); ExternalEditWatcher.instance().setLogger(reg.logger());
ExternalEditWatcher.instance().dispatch = this.store().dispatch; ExternalEditWatcher.instance().dispatch = this.store().dispatch;
ResourceEditWatcher.instance().initialize(reg.logger(), this.store().dispatch); ResourceEditWatcher.instance().initialize(reg.logger(), (action) => { console.info('ACTION', action); this.store().dispatch(action); });
RevisionService.instance().runInBackground(); RevisionService.instance().runInBackground();

View File

@ -17,7 +17,7 @@ import useMarkupToHtml from './utils/useMarkupToHtml';
import useFormNote, { OnLoadEvent } from './utils/useFormNote'; import useFormNote, { OnLoadEvent } from './utils/useFormNote';
import styles_ from './styles'; import styles_ from './styles';
import { NoteEditorProps, FormNote, ScrollOptions, ScrollOptionTypes, OnChangeEvent, NoteBodyEditorProps } from './utils/types'; import { NoteEditorProps, FormNote, ScrollOptions, ScrollOptionTypes, OnChangeEvent, NoteBodyEditorProps } from './utils/types';
import ResourceEditWatcher from '../../lib/services/ResourceEditWatcher'; import ResourceEditWatcher from '../../lib/services/ResourceEditWatcher/index';
import CommandService from '../../lib/services/CommandService'; import CommandService from '../../lib/services/CommandService';
const { themeStyle } = require('lib/theme'); const { themeStyle } = require('lib/theme');
@ -480,6 +480,17 @@ function NoteEditor(props: NoteEditorProps) {
); );
} }
function renderResourceWatchingNotification() {
if (!Object.keys(props.watchedResources).length) return null;
const resourceTitles = Object.keys(props.watchedResources).map(id => props.watchedResources[id].title);
return (
<div style={styles.resourceWatchBanner}>
<p style={styles.resourceWatchBannerLine}>{_('The following attachments are being watched for changes:')} <strong>{resourceTitles.join(', ')}</strong></p>
<p style={{ ...styles.resourceWatchBannerLine, marginBottom: 0 }}>{_('The attachments will no longer be watched when you switch to a different note.')}</p>
</div>
);
}
if (formNote.encryption_applied || !formNote.id || !props.noteId) { if (formNote.encryption_applied || !formNote.id || !props.noteId) {
return renderNoNotes(styles.root); return renderNoNotes(styles.root);
} }
@ -487,6 +498,7 @@ function NoteEditor(props: NoteEditorProps) {
return ( return (
<div style={styles.root} onDrop={onDrop}> <div style={styles.root} onDrop={onDrop}>
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}> <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
{renderResourceWatchingNotification()}
{renderTitleBar()} {renderTitleBar()}
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}> <div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
{renderNoteToolbar()}{renderTagBar()} {renderNoteToolbar()}{renderTagBar()}
@ -528,6 +540,7 @@ const mapStateToProps = (state: any) => {
selectedSearchId: state.selectedSearchId, selectedSearchId: state.selectedSearchId,
customCss: state.customCss, customCss: state.customCss,
noteVisiblePanes: state.noteVisiblePanes, noteVisiblePanes: state.noteVisiblePanes,
watchedResources: state.watchedResources,
}; };
}; };

View File

@ -53,6 +53,18 @@ export default function styles(props: NoteEditorProps) {
paddingLeft: 10, paddingLeft: 10,
paddingRight: 10, paddingRight: 10,
}, },
resourceWatchBanner: {
...theme.textStyle,
padding: 10,
marginLeft: 5,
marginBottom: 10,
color: theme.colorWarn,
backgroundColor: theme.warningBackgroundColor,
},
resourceWatchBannerLine: {
marginTop: 0,
marginBottom: 10,
},
}; };
}); });
} }

View File

@ -1,4 +1,4 @@
import ResourceEditWatcher from '../../../lib/services/ResourceEditWatcher'; import ResourceEditWatcher from '../../../lib/services/ResourceEditWatcher/index';
const { bridge } = require('electron').remote.require('./bridge'); const { bridge } = require('electron').remote.require('./bridge');
const Menu = bridge().Menu; const Menu = bridge().Menu;

View File

@ -22,6 +22,7 @@ export interface NoteEditorProps {
selectedSearchId: string, selectedSearchId: string,
customCss: string, customCss: string,
noteVisiblePanes: string[], noteVisiblePanes: string[],
watchedResources: any,
} }
export interface NoteBodyEditorProps { export interface NoteBodyEditorProps {

View File

@ -11,7 +11,7 @@ const Setting = require('lib/models/Setting');
const { reg } = require('lib/registry.js'); const { reg } = require('lib/registry.js');
const ResourceFetcher = require('lib/services/ResourceFetcher.js'); const ResourceFetcher = require('lib/services/ResourceFetcher.js');
const DecryptionWorker = require('lib/services/DecryptionWorker.js'); const DecryptionWorker = require('lib/services/DecryptionWorker.js');
const ResourceEditWatcher = require('lib/services/ResourceEditWatcher.js').default; const ResourceEditWatcher = require('lib/services/ResourceEditWatcher/index').default;
export interface OnLoadEvent { export interface OnLoadEvent {
formNote: FormNote, formNote: FormNote,

View File

@ -1,7 +1,7 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { FormNote } from './types'; import { FormNote } from './types';
import contextMenu from './contextMenu'; import contextMenu from './contextMenu';
import ResourceEditWatcher from '../../../lib/services/ResourceEditWatcher'; import ResourceEditWatcher from '../../../lib/services/ResourceEditWatcher/index';
const BaseItem = require('lib/models/BaseItem'); const BaseItem = require('lib/models/BaseItem');
const { _ } = require('lib/locale'); const { _ } = require('lib/locale');
const BaseModel = require('lib/BaseModel.js'); const BaseModel = require('lib/BaseModel.js');

View File

@ -1482,15 +1482,13 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
"integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
"dev": true, "dev": true
"optional": true
}, },
"is-glob": { "is-glob": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
"integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"is-extglob": "^1.0.0" "is-extglob": "^1.0.0"
} }
@ -2032,8 +2030,7 @@
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/boolean/-/boolean-3.0.1.tgz", "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.0.1.tgz",
"integrity": "sha512-HRZPIjPcbwAVQvOTxR4YE3o8Xs98NqbbL1iEZDCz7CL8ql0Lt5iOyJFxfnAB0oFs8Oh02F/lLlg30Mexv46LjA==", "integrity": "sha512-HRZPIjPcbwAVQvOTxR4YE3o8Xs98NqbbL1iEZDCz7CL8ql0Lt5iOyJFxfnAB0oFs8Oh02F/lLlg30Mexv46LjA==",
"dev": true, "dev": true
"optional": true
}, },
"boxen": { "boxen": {
"version": "4.2.0", "version": "4.2.0",
@ -5095,15 +5092,13 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
"integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
"dev": true, "dev": true
"optional": true
}, },
"is-glob": { "is-glob": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
"integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"is-extglob": "^1.0.0" "is-extglob": "^1.0.0"
} }
@ -5271,8 +5266,7 @@
"version": "2.1.1", "version": "2.1.1",
"resolved": false, "resolved": false,
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
"dev": true, "dev": true
"optional": true
}, },
"aproba": { "aproba": {
"version": "1.2.0", "version": "1.2.0",
@ -5321,8 +5315,7 @@
"version": "1.1.0", "version": "1.1.0",
"resolved": false, "resolved": false,
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
"dev": true, "dev": true
"optional": true
}, },
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
@ -5335,8 +5328,7 @@
"version": "1.1.0", "version": "1.1.0",
"resolved": false, "resolved": false,
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
"dev": true, "dev": true
"optional": true
}, },
"core-util-is": { "core-util-is": {
"version": "1.0.2", "version": "1.0.2",
@ -5467,8 +5459,7 @@
"version": "2.0.4", "version": "2.0.4",
"resolved": false, "resolved": false,
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true, "dev": true
"optional": true
}, },
"ini": { "ini": {
"version": "1.3.5", "version": "1.3.5",
@ -5482,7 +5473,6 @@
"resolved": false, "resolved": false,
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"number-is-nan": "^1.0.0" "number-is-nan": "^1.0.0"
} }
@ -5508,15 +5498,13 @@
"version": "0.0.8", "version": "0.0.8",
"resolved": false, "resolved": false,
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
"dev": true, "dev": true
"optional": true
}, },
"minipass": { "minipass": {
"version": "2.9.0", "version": "2.9.0",
"resolved": false, "resolved": false,
"integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"safe-buffer": "^5.1.2", "safe-buffer": "^5.1.2",
"yallist": "^3.0.0" "yallist": "^3.0.0"
@ -5537,7 +5525,6 @@
"resolved": false, "resolved": false,
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"minimist": "0.0.8" "minimist": "0.0.8"
} }
@ -5636,8 +5623,7 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": false, "resolved": false,
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
"dev": true, "dev": true
"optional": true
}, },
"object-assign": { "object-assign": {
"version": "4.1.1", "version": "4.1.1",
@ -5651,7 +5637,6 @@
"resolved": false, "resolved": false,
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"wrappy": "1" "wrappy": "1"
} }
@ -5747,8 +5732,7 @@
"version": "5.1.2", "version": "5.1.2",
"resolved": false, "resolved": false,
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true, "dev": true
"optional": true
}, },
"safer-buffer": { "safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
@ -5790,7 +5774,6 @@
"resolved": false, "resolved": false,
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"code-point-at": "^1.0.0", "code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0", "is-fullwidth-code-point": "^1.0.0",
@ -5812,7 +5795,6 @@
"resolved": false, "resolved": false,
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"ansi-regex": "^2.0.0" "ansi-regex": "^2.0.0"
} }
@ -5861,15 +5843,13 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": false, "resolved": false,
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true, "dev": true
"optional": true
}, },
"yallist": { "yallist": {
"version": "3.1.1", "version": "3.1.1",
"resolved": false, "resolved": false,
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"dev": true, "dev": true
"optional": true
} }
} }
}, },
@ -6490,6 +6470,11 @@
"file-type": "^4.1.0" "file-type": "^4.1.0"
} }
}, },
"immer": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/immer/-/immer-7.0.5.tgz",
"integrity": "sha512-TtRAKZyuqld2eYjvWgXISLJ0ZlOl1OOTzRmrmiY8SlB0dnAhZ1OiykIDL5KDFNaPHDXiLfGQFNJGtet8z8AEmg=="
},
"import-fresh": { "import-fresh": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz",
@ -8833,8 +8818,7 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
"integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
"dev": true, "dev": true
"optional": true
}, },
"is-glob": { "is-glob": {
"version": "2.0.1", "version": "2.0.1",
@ -10047,11 +10031,6 @@
"through2": "^2.0.3" "through2": "^2.0.3"
} }
}, },
"remove-markdown": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/remove-markdown/-/remove-markdown-0.3.0.tgz",
"integrity": "sha1-XktmdJOpNXlyjz1S7MHbnKUF3Jg="
},
"remove-trailing-separator": { "remove-trailing-separator": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",

View File

@ -125,6 +125,7 @@
"html-minifier": "^4.0.0", "html-minifier": "^4.0.0",
"htmlparser2": "^4.1.0", "htmlparser2": "^4.1.0",
"image-type": "^3.0.0", "image-type": "^3.0.0",
"immer": "^7.0.5",
"joplin-turndown": "^4.0.28", "joplin-turndown": "^4.0.28",
"joplin-turndown-plugin-gfm": "^1.0.12", "joplin-turndown-plugin-gfm": "^1.0.12",
"json-stringify-safe": "^5.0.1", "json-stringify-safe": "^5.0.1",

View File

@ -632,7 +632,13 @@ class BaseApplication {
SyncTargetRegistry.addClass(SyncTargetDropbox); SyncTargetRegistry.addClass(SyncTargetDropbox);
SyncTargetRegistry.addClass(SyncTargetAmazonS3); SyncTargetRegistry.addClass(SyncTargetAmazonS3);
try {
await shim.fsDriver().remove(tempDir); await shim.fsDriver().remove(tempDir);
} catch (error) {
// Can't do anything in this case, not even log, since the logger
// is not yet ready. But normally it's not an issue if the temp
// dir cannot be deleted.
}
await fs.mkdirp(profileDir, 0o755); await fs.mkdirp(profileDir, 0o755);
await fs.mkdirp(resourceDir, 0o755); await fs.mkdirp(resourceDir, 0o755);

View File

@ -3,6 +3,7 @@ const Folder = require('lib/models/Folder.js');
const ArrayUtils = require('lib/ArrayUtils.js'); const ArrayUtils = require('lib/ArrayUtils.js');
const { ALL_NOTES_FILTER_ID } = require('lib/reserved-ids'); const { ALL_NOTES_FILTER_ID } = require('lib/reserved-ids');
const CommandService = require('lib/services/CommandService').default; const CommandService = require('lib/services/CommandService').default;
const resourceEditWatcherReducer = require('lib/services/ResourceEditWatcher/reducer').default;
const defaultState = { const defaultState = {
notes: [], notes: [],
@ -1057,6 +1058,8 @@ const reducer = (state = defaultState, action) => {
newState = handleHistory(newState, action); newState = handleHistory(newState, action);
} }
newState = resourceEditWatcherReducer(newState, action);
CommandService.instance().scheduleMapStateToProps(newState); CommandService.instance().scheduleMapStateToProps(newState);
return newState; return newState;

View File

@ -1,4 +1,4 @@
import AsyncActionQueue from '../AsyncActionQueue'; import AsyncActionQueue from '../../AsyncActionQueue';
const { Logger } = require('lib/logger.js'); const { Logger } = require('lib/logger.js');
const Setting = require('lib/models/Setting'); const Setting = require('lib/models/Setting');
const Resource = require('lib/models/Resource'); const Resource = require('lib/models/Resource');
@ -25,7 +25,7 @@ export default class ResourceEditWatcher {
private static instance_:ResourceEditWatcher; private static instance_:ResourceEditWatcher;
private logger_:any; private logger_:any;
// private dispatch:Function; private dispatch:Function;
private watcher_:any; private watcher_:any;
private chokidar_:any; private chokidar_:any;
private watchedItems_:WatchedItems = {}; private watchedItems_:WatchedItems = {};
@ -34,15 +34,15 @@ export default class ResourceEditWatcher {
constructor() { constructor() {
this.logger_ = new Logger(); this.logger_ = new Logger();
// this.dispatch = () => {}; this.dispatch = () => {};
this.watcher_ = null; this.watcher_ = null;
this.chokidar_ = chokidar; this.chokidar_ = chokidar;
this.eventEmitter_ = new EventEmitter(); this.eventEmitter_ = new EventEmitter();
} }
initialize(logger:any/* , dispatch:Function*/) { initialize(logger:any, dispatch:Function) {
this.logger_ = logger; this.logger_ = logger;
// this.dispatch = dispatch; this.dispatch = dispatch;
} }
static instance() { static instance() {
@ -95,6 +95,41 @@ export default class ResourceEditWatcher {
}; };
}; };
const handleChangeEvent = async (path:string) => {
this.logger().debug('ResourceEditWatcher: handleChangeEvent: ' + path);
const watchedItem = this.watchedItemByPath(path);
if (!watchedItem) {
// The parent directory of the edited resource often gets a change event too
// and ends up here. Print a warning, but most likely it's nothing important.
this.logger().debug(`ResourceEditWatcher: could not find resource ID from path: ${path}`);
return;
}
const resourceId = watchedItem.resourceId;
const stat = await shim.fsDriver().stat(path);
const editedFileUpdatedTime = stat.mtime.getTime();
if (watchedItem.lastFileUpdatedTime === editedFileUpdatedTime) {
// chokidar is buggy and emits "change" events even when nothing has changed
// so double-check the modified time and skip processing if there's no change.
// In particular it emits two such events just after the file has been copied
// in openAndWatch().
//
// We also need this because some events are handled twice - once in the "all" event
// handle and once in the "raw" event handler, due to a bug in chokidar. So having
// this check means we don't unecessarily save the resource twice when the file is
// modified by the user.
this.logger().debug(`ResourceEditWatcher: No timestamp change - skip: ${resourceId}`);
return;
}
this.logger().debug(`ResourceEditWatcher: Queuing save action: ${resourceId}`);
watchedItem.asyncSaveQueue.push(makeSaveAction(resourceId, path));
watchedItem.lastFileUpdatedTime = editedFileUpdatedTime;
}
if (!this.watcher_) { if (!this.watcher_) {
this.watcher_ = this.chokidar_.watch(fileToWatch); this.watcher_ = this.chokidar_.watch(fileToWatch);
this.watcher_.on('all', async (event:any, path:string) => { this.watcher_.on('all', async (event:any, path:string) => {
@ -108,41 +143,27 @@ export default class ResourceEditWatcher {
// See: https://github.com/laurent22/joplin/issues/710#issuecomment-420997167 // See: https://github.com/laurent22/joplin/issues/710#issuecomment-420997167
// this.watcher_.unwatch(path); // this.watcher_.unwatch(path);
} else if (event === 'change') { } else if (event === 'change') {
const watchedItem = this.watchedItemByPath(path); handleChangeEvent(path);
const resourceId = watchedItem.resourceId;
if (!watchedItem) {
this.logger().error(`ResourceEditWatcher: could not find resource ID from path: ${path}`);
return;
}
const stat = await shim.fsDriver().stat(path);
const editedFileUpdatedTime = stat.mtime.getTime();
if (watchedItem.lastFileUpdatedTime === editedFileUpdatedTime) {
// chokidar is buggy and emits "change" events even when nothing has changed
// so double-check the modified time and skip processing if there's no change.
// In particular it emits two such events just after the file has been copied
// in openAndWatch().
this.logger().debug(`ResourceEditWatcher: No timestamp change - skip: ${resourceId}`);
return;
}
this.logger().debug(`ResourceEditWatcher: Queuing save action: ${resourceId}`);
watchedItem.asyncSaveQueue.push(makeSaveAction(resourceId, path));
watchedItem.lastFileUpdatedTime = editedFileUpdatedTime;
} else if (event === 'error') { } else if (event === 'error') {
this.logger().error('ResourceEditWatcher: error'); this.logger().error('ResourceEditWatcher: error');
} }
}); });
// Hack to support external watcher on some linux applications (gedit, gvim, etc) // Hack to support external watcher on some linux applications (gedit, gvim, etc)
// taken from https://github.com/paulmillr/chokidar/issues/591 // taken from https://github.com/paulmillr/chokidar/issues/591
//
// 2020-07-22: It also applies when editing Excel files, which copy the new file
// then rename, so handling the "change" event alone is not enough as sometimes
// that event is not event triggered.
// https://github.com/laurent22/joplin/issues/3407
//
// @ts-ignore Leave unused path variable // @ts-ignore Leave unused path variable
this.watcher_.on('raw', async (event:string, path:string, options:any) => { this.watcher_.on('raw', async (event:string, path:string, options:any) => {
this.logger().debug(`ResourceEditWatcher: Raw event: ${event}: ${options.watchedPath}`);
if (event === 'rename') { if (event === 'rename') {
this.watcher_.unwatch(options.watchedPath); this.watcher_.unwatch(options.watchedPath);
this.watcher_.add(options.watchedPath); this.watcher_.add(options.watchedPath);
handleChangeEvent(options.watchedPath);
} }
}); });
} else { } else {
@ -181,6 +202,12 @@ export default class ResourceEditWatcher {
watchedItem.lastResourceUpdatedTime = resource.updated_time; watchedItem.lastResourceUpdatedTime = resource.updated_time;
this.watch(editFilePath); this.watch(editFilePath);
this.dispatch({
type: 'RESOURCE_EDIT_WATCHER_SET',
id: resource.id,
title: resource.title,
});
} }
bridge().openItem(watchedItem.path); bridge().openItem(watchedItem.path);
@ -199,9 +226,20 @@ export default class ResourceEditWatcher {
await item.asyncSaveQueue.waitForAllDone(); await item.asyncSaveQueue.waitForAllDone();
try {
if (this.watcher_) this.watcher_.unwatch(item.path); if (this.watcher_) this.watcher_.unwatch(item.path);
await shim.fsDriver().remove(item.path); await shim.fsDriver().remove(item.path);
} catch (error) {
this.logger().warn(`ResourceEditWatcher: There was an error unwatching resource ${resourceId}. Joplin will ignore the file regardless.`, error);
}
delete this.watchedItems_[resourceId]; delete this.watchedItems_[resourceId];
this.dispatch({
type: 'RESOURCE_EDIT_WATCHER_REMOVE',
id: resourceId,
});
this.logger().info(`ResourceEditWatcher: Stopped watching ${item.path}`); this.logger().info(`ResourceEditWatcher: Stopped watching ${item.path}`);
} }
@ -211,7 +249,11 @@ export default class ResourceEditWatcher {
const item = this.watchedItems_[resourceId]; const item = this.watchedItems_[resourceId];
promises.push(this.stopWatching(item.resourceId)); promises.push(this.stopWatching(item.resourceId));
} }
return Promise.all(promises); await Promise.all(promises);
this.dispatch({
type: 'RESOURCE_EDIT_WATCHER_CLEAR',
});
} }
private watchedItemByResourceId(resourceId:string):WatchedItem { private watchedItemByResourceId(resourceId:string):WatchedItem {

View File

@ -0,0 +1,38 @@
import produce, { Draft } from 'immer';
export const defaultState = {
watchedResources: {},
};
const reducer = produce((draft: Draft<any>, action:any) => {
if (action.type.indexOf('RESOURCE_EDIT_WATCHER_') !== 0) return;
try {
switch (action.type) {
case 'RESOURCE_EDIT_WATCHER_SET':
draft.watchedResources[action.id] = {
id: action.id,
title: action.title,
};
break;
case 'RESOURCE_EDIT_WATCHER_REMOVE':
delete draft.watchedResources[action.id];
break;
case 'RESOURCE_EDIT_WATCHER_CLEAR':
draft.watchedResources = {};
break;
}
} catch (error) {
error.message = `In plugin reducer: ${error.message} Action: ${JSON.stringify(action)}`;
throw error;
}
});
export default reducer;

View File

@ -354,7 +354,12 @@
"ElectronClient/commands/stopExternalEditing.js": true, "ElectronClient/commands/stopExternalEditing.js": true,
"ElectronClient/gui/NoteEditor/commands/showRevisions.js": true, "ElectronClient/gui/NoteEditor/commands/showRevisions.js": true,
"ReactNativeClient/lib/commands/historyBackward.js": true, "ReactNativeClient/lib/commands/historyBackward.js": true,
"ReactNativeClient/lib/commands/historyForward.js": true "ReactNativeClient/lib/commands/historyForward.js": true,
"CliClient/tests/support/amazon-s3-auth.json": true,
"ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useJoplinMode.js": true,
"ReactNativeClient/lib/hooks/useEffectDebugger.js": true,
"ReactNativeClient/lib/services/ResourceEditWatcher/index.js": true,
"ReactNativeClient/lib/services/ResourceEditWatcher/reducer.js": true
}, },
"spellright.language": [ "spellright.language": [
"en" "en"

46
package-lock.json generated
View File

@ -2829,8 +2829,7 @@
"ansi-regex": { "ansi-regex": {
"version": "2.1.1", "version": "2.1.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"aproba": { "aproba": {
"version": "1.2.0", "version": "1.2.0",
@ -2851,14 +2850,12 @@
"balanced-match": { "balanced-match": {
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"brace-expansion": { "brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@ -2873,20 +2870,17 @@
"code-point-at": { "code-point-at": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"console-control-strings": { "console-control-strings": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"core-util-is": { "core-util-is": {
"version": "1.0.2", "version": "1.0.2",
@ -3003,8 +2997,7 @@
"inherits": { "inherits": {
"version": "2.0.4", "version": "2.0.4",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"ini": { "ini": {
"version": "1.3.5", "version": "1.3.5",
@ -3016,7 +3009,6 @@
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"number-is-nan": "^1.0.0" "number-is-nan": "^1.0.0"
} }
@ -3031,7 +3023,6 @@
"version": "3.0.4", "version": "3.0.4",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
} }
@ -3039,14 +3030,12 @@
"minimist": { "minimist": {
"version": "0.0.8", "version": "0.0.8",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"minipass": { "minipass": {
"version": "2.9.0", "version": "2.9.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"safe-buffer": "^5.1.2", "safe-buffer": "^5.1.2",
"yallist": "^3.0.0" "yallist": "^3.0.0"
@ -3065,7 +3054,6 @@
"version": "0.5.1", "version": "0.5.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"minimist": "0.0.8" "minimist": "0.0.8"
} }
@ -3155,8 +3143,7 @@
"number-is-nan": { "number-is-nan": {
"version": "1.0.1", "version": "1.0.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"object-assign": { "object-assign": {
"version": "4.1.1", "version": "4.1.1",
@ -3168,7 +3155,6 @@
"version": "1.4.0", "version": "1.4.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"wrappy": "1" "wrappy": "1"
} }
@ -3254,8 +3240,7 @@
"safe-buffer": { "safe-buffer": {
"version": "5.1.2", "version": "5.1.2",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"safer-buffer": { "safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
@ -3291,7 +3276,6 @@
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"code-point-at": "^1.0.0", "code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0", "is-fullwidth-code-point": "^1.0.0",
@ -3311,7 +3295,6 @@
"version": "3.0.1", "version": "3.0.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"ansi-regex": "^2.0.0" "ansi-regex": "^2.0.0"
} }
@ -3355,14 +3338,12 @@
"wrappy": { "wrappy": {
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"yallist": { "yallist": {
"version": "3.1.1", "version": "3.1.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
} }
} }
}, },
@ -3799,6 +3780,11 @@
"integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
"dev": true "dev": true
}, },
"immer": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/immer/-/immer-7.0.5.tgz",
"integrity": "sha512-TtRAKZyuqld2eYjvWgXISLJ0ZlOl1OOTzRmrmiY8SlB0dnAhZ1OiykIDL5KDFNaPHDXiLfGQFNJGtet8z8AEmg=="
},
"import-fresh": { "import-fresh": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.1.0.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.1.0.tgz",

View File

@ -38,6 +38,7 @@
}, },
"dependencies": { "dependencies": {
"follow-redirects": "^1.11.0", "follow-redirects": "^1.11.0",
"immer": "^7.0.5",
"joplin-turndown": "^4.0.28", "joplin-turndown": "^4.0.28",
"joplin-turndown-plugin-gfm": "^1.0.12", "joplin-turndown-plugin-gfm": "^1.0.12",
"relative": "^3.0.2" "relative": "^3.0.2"