From 4bef79cd71a0fe46e4da716f0fc786544f985647 Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Wed, 22 Jul 2020 19:03:31 +0100 Subject: [PATCH] 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 --- .eslintignore | 3 +- .gitignore | 3 +- ElectronClient/app.js | 4 +- ElectronClient/gui/NoteEditor/NoteEditor.tsx | 15 ++- ElectronClient/gui/NoteEditor/styles/index.ts | 12 ++ .../gui/NoteEditor/utils/contextMenu.ts | 2 +- ElectronClient/gui/NoteEditor/utils/types.ts | 1 + .../gui/NoteEditor/utils/useFormNote.ts | 2 +- .../gui/NoteEditor/utils/useMessageHandler.ts | 2 +- ElectronClient/package-lock.json | 57 +++------- ElectronClient/package.json | 1 + ReactNativeClient/lib/BaseApplication.js | 8 +- ReactNativeClient/lib/reducer.js | 3 + .../index.ts} | 106 ++++++++++++------ .../services/ResourceEditWatcher/reducer.ts | 38 +++++++ joplin.code-workspace | 7 +- package-lock.json | 46 +++----- package.json | 1 + 18 files changed, 200 insertions(+), 111 deletions(-) rename ReactNativeClient/lib/services/{ResourceEditWatcher.ts => ResourceEditWatcher/index.ts} (69%) create mode 100644 ReactNativeClient/lib/services/ResourceEditWatcher/reducer.ts diff --git a/.eslintignore b/.eslintignore index 27df261cb..3b0e7bd19 100644 --- a/.eslintignore +++ b/.eslintignore @@ -151,7 +151,8 @@ ReactNativeClient/lib/services/keychain/KeychainServiceDriver.dummy.js ReactNativeClient/lib/services/keychain/KeychainServiceDriver.mobile.js ReactNativeClient/lib/services/keychain/KeychainServiceDriver.node.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/errors.js ReactNativeClient/lib/services/SettingUtils.js diff --git a/.gitignore b/.gitignore index 48a7faad3..857c3577f 100644 --- a/.gitignore +++ b/.gitignore @@ -142,7 +142,8 @@ ReactNativeClient/lib/services/keychain/KeychainServiceDriver.dummy.js ReactNativeClient/lib/services/keychain/KeychainServiceDriver.mobile.js ReactNativeClient/lib/services/keychain/KeychainServiceDriver.node.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/errors.js ReactNativeClient/lib/services/SettingUtils.js diff --git a/ElectronClient/app.js b/ElectronClient/app.js index 0da27feab..920c7ff13 100644 --- a/ElectronClient/app.js +++ b/ElectronClient/app.js @@ -21,7 +21,7 @@ const InteropServiceHelper = require('./InteropServiceHelper.js'); const ResourceService = require('lib/services/ResourceService'); const ClipperServer = require('lib/ClipperServer'); 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 { shell, webFrame, clipboard } = require('electron'); const Menu = bridge().Menu; @@ -1265,7 +1265,7 @@ class Application extends BaseApplication { ExternalEditWatcher.instance().setLogger(reg.logger()); 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(); diff --git a/ElectronClient/gui/NoteEditor/NoteEditor.tsx b/ElectronClient/gui/NoteEditor/NoteEditor.tsx index 23e3fe1d5..6262e00d7 100644 --- a/ElectronClient/gui/NoteEditor/NoteEditor.tsx +++ b/ElectronClient/gui/NoteEditor/NoteEditor.tsx @@ -17,7 +17,7 @@ import useMarkupToHtml from './utils/useMarkupToHtml'; import useFormNote, { OnLoadEvent } from './utils/useFormNote'; import styles_ from './styles'; 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'; 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 ( +
+

{_('The following attachments are being watched for changes:')} {resourceTitles.join(', ')}

+

{_('The attachments will no longer be watched when you switch to a different note.')}

+
+ ); + } + if (formNote.encryption_applied || !formNote.id || !props.noteId) { return renderNoNotes(styles.root); } @@ -487,6 +498,7 @@ function NoteEditor(props: NoteEditorProps) { return (
+ {renderResourceWatchingNotification()} {renderTitleBar()}
{renderNoteToolbar()}{renderTagBar()} @@ -528,6 +540,7 @@ const mapStateToProps = (state: any) => { selectedSearchId: state.selectedSearchId, customCss: state.customCss, noteVisiblePanes: state.noteVisiblePanes, + watchedResources: state.watchedResources, }; }; diff --git a/ElectronClient/gui/NoteEditor/styles/index.ts b/ElectronClient/gui/NoteEditor/styles/index.ts index bca143d7b..4e099e143 100644 --- a/ElectronClient/gui/NoteEditor/styles/index.ts +++ b/ElectronClient/gui/NoteEditor/styles/index.ts @@ -53,6 +53,18 @@ export default function styles(props: NoteEditorProps) { paddingLeft: 10, paddingRight: 10, }, + resourceWatchBanner: { + ...theme.textStyle, + padding: 10, + marginLeft: 5, + marginBottom: 10, + color: theme.colorWarn, + backgroundColor: theme.warningBackgroundColor, + }, + resourceWatchBannerLine: { + marginTop: 0, + marginBottom: 10, + }, }; }); } diff --git a/ElectronClient/gui/NoteEditor/utils/contextMenu.ts b/ElectronClient/gui/NoteEditor/utils/contextMenu.ts index 8eecffe08..2662c49fc 100644 --- a/ElectronClient/gui/NoteEditor/utils/contextMenu.ts +++ b/ElectronClient/gui/NoteEditor/utils/contextMenu.ts @@ -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 Menu = bridge().Menu; diff --git a/ElectronClient/gui/NoteEditor/utils/types.ts b/ElectronClient/gui/NoteEditor/utils/types.ts index 956a9b1f2..ee96eab0d 100644 --- a/ElectronClient/gui/NoteEditor/utils/types.ts +++ b/ElectronClient/gui/NoteEditor/utils/types.ts @@ -22,6 +22,7 @@ export interface NoteEditorProps { selectedSearchId: string, customCss: string, noteVisiblePanes: string[], + watchedResources: any, } export interface NoteBodyEditorProps { diff --git a/ElectronClient/gui/NoteEditor/utils/useFormNote.ts b/ElectronClient/gui/NoteEditor/utils/useFormNote.ts index eb2b652ad..0e0bbc491 100644 --- a/ElectronClient/gui/NoteEditor/utils/useFormNote.ts +++ b/ElectronClient/gui/NoteEditor/utils/useFormNote.ts @@ -11,7 +11,7 @@ const Setting = require('lib/models/Setting'); const { reg } = require('lib/registry.js'); const ResourceFetcher = require('lib/services/ResourceFetcher.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 { formNote: FormNote, diff --git a/ElectronClient/gui/NoteEditor/utils/useMessageHandler.ts b/ElectronClient/gui/NoteEditor/utils/useMessageHandler.ts index 465c23c3b..e5aeb8c94 100644 --- a/ElectronClient/gui/NoteEditor/utils/useMessageHandler.ts +++ b/ElectronClient/gui/NoteEditor/utils/useMessageHandler.ts @@ -1,7 +1,7 @@ import { useCallback } from 'react'; import { FormNote } from './types'; import contextMenu from './contextMenu'; -import ResourceEditWatcher from '../../../lib/services/ResourceEditWatcher'; +import ResourceEditWatcher from '../../../lib/services/ResourceEditWatcher/index'; const BaseItem = require('lib/models/BaseItem'); const { _ } = require('lib/locale'); const BaseModel = require('lib/BaseModel.js'); diff --git a/ElectronClient/package-lock.json b/ElectronClient/package-lock.json index 5407a6b64..fdbe6221d 100644 --- a/ElectronClient/package-lock.json +++ b/ElectronClient/package-lock.json @@ -1482,15 +1482,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", - "dev": true, - "optional": true + "dev": true }, "is-glob": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", "dev": true, - "optional": true, "requires": { "is-extglob": "^1.0.0" } @@ -2032,8 +2030,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.0.1.tgz", "integrity": "sha512-HRZPIjPcbwAVQvOTxR4YE3o8Xs98NqbbL1iEZDCz7CL8ql0Lt5iOyJFxfnAB0oFs8Oh02F/lLlg30Mexv46LjA==", - "dev": true, - "optional": true + "dev": true }, "boxen": { "version": "4.2.0", @@ -5095,15 +5092,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", - "dev": true, - "optional": true + "dev": true }, "is-glob": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", "dev": true, - "optional": true, "requires": { "is-extglob": "^1.0.0" } @@ -5271,8 +5266,7 @@ "version": "2.1.1", "resolved": false, "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -5321,8 +5315,7 @@ "version": "1.1.0", "resolved": false, "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", @@ -5335,8 +5328,7 @@ "version": "1.1.0", "resolved": false, "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -5467,8 +5459,7 @@ "version": "2.0.4", "resolved": false, "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -5482,7 +5473,6 @@ "resolved": false, "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -5508,15 +5498,13 @@ "version": "0.0.8", "resolved": false, "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.9.0", "resolved": false, "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -5537,7 +5525,6 @@ "resolved": false, "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -5636,8 +5623,7 @@ "version": "1.0.1", "resolved": false, "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -5651,7 +5637,6 @@ "resolved": false, "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -5747,8 +5732,7 @@ "version": "5.1.2", "resolved": false, "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -5790,7 +5774,6 @@ "resolved": false, "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -5812,7 +5795,6 @@ "resolved": false, "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5861,15 +5843,13 @@ "version": "1.0.2", "resolved": false, "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.1.1", "resolved": false, "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "optional": true + "dev": true } } }, @@ -6490,6 +6470,11 @@ "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": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", @@ -8833,8 +8818,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", - "dev": true, - "optional": true + "dev": true }, "is-glob": { "version": "2.0.1", @@ -10047,11 +10031,6 @@ "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": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", diff --git a/ElectronClient/package.json b/ElectronClient/package.json index f2abc68f2..3f8a9a2b4 100644 --- a/ElectronClient/package.json +++ b/ElectronClient/package.json @@ -125,6 +125,7 @@ "html-minifier": "^4.0.0", "htmlparser2": "^4.1.0", "image-type": "^3.0.0", + "immer": "^7.0.5", "joplin-turndown": "^4.0.28", "joplin-turndown-plugin-gfm": "^1.0.12", "json-stringify-safe": "^5.0.1", diff --git a/ReactNativeClient/lib/BaseApplication.js b/ReactNativeClient/lib/BaseApplication.js index 1ee8476fc..752334306 100644 --- a/ReactNativeClient/lib/BaseApplication.js +++ b/ReactNativeClient/lib/BaseApplication.js @@ -632,7 +632,13 @@ class BaseApplication { SyncTargetRegistry.addClass(SyncTargetDropbox); SyncTargetRegistry.addClass(SyncTargetAmazonS3); - await shim.fsDriver().remove(tempDir); + try { + 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(resourceDir, 0o755); diff --git a/ReactNativeClient/lib/reducer.js b/ReactNativeClient/lib/reducer.js index ecb888aa5..7122f9327 100644 --- a/ReactNativeClient/lib/reducer.js +++ b/ReactNativeClient/lib/reducer.js @@ -3,6 +3,7 @@ const Folder = require('lib/models/Folder.js'); const ArrayUtils = require('lib/ArrayUtils.js'); const { ALL_NOTES_FILTER_ID } = require('lib/reserved-ids'); const CommandService = require('lib/services/CommandService').default; +const resourceEditWatcherReducer = require('lib/services/ResourceEditWatcher/reducer').default; const defaultState = { notes: [], @@ -1057,6 +1058,8 @@ const reducer = (state = defaultState, action) => { newState = handleHistory(newState, action); } + newState = resourceEditWatcherReducer(newState, action); + CommandService.instance().scheduleMapStateToProps(newState); return newState; diff --git a/ReactNativeClient/lib/services/ResourceEditWatcher.ts b/ReactNativeClient/lib/services/ResourceEditWatcher/index.ts similarity index 69% rename from ReactNativeClient/lib/services/ResourceEditWatcher.ts rename to ReactNativeClient/lib/services/ResourceEditWatcher/index.ts index 5fabbc9d0..c8f8d4234 100644 --- a/ReactNativeClient/lib/services/ResourceEditWatcher.ts +++ b/ReactNativeClient/lib/services/ResourceEditWatcher/index.ts @@ -1,4 +1,4 @@ -import AsyncActionQueue from '../AsyncActionQueue'; +import AsyncActionQueue from '../../AsyncActionQueue'; const { Logger } = require('lib/logger.js'); const Setting = require('lib/models/Setting'); const Resource = require('lib/models/Resource'); @@ -25,7 +25,7 @@ export default class ResourceEditWatcher { private static instance_:ResourceEditWatcher; private logger_:any; - // private dispatch:Function; + private dispatch:Function; private watcher_:any; private chokidar_:any; private watchedItems_:WatchedItems = {}; @@ -34,15 +34,15 @@ export default class ResourceEditWatcher { constructor() { this.logger_ = new Logger(); - // this.dispatch = () => {}; + this.dispatch = () => {}; this.watcher_ = null; this.chokidar_ = chokidar; this.eventEmitter_ = new EventEmitter(); } - initialize(logger:any/* , dispatch:Function*/) { + initialize(logger:any, dispatch:Function) { this.logger_ = logger; - // this.dispatch = dispatch; + this.dispatch = dispatch; } 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_) { this.watcher_ = this.chokidar_.watch(fileToWatch); 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 // this.watcher_.unwatch(path); } else if (event === 'change') { - const watchedItem = this.watchedItemByPath(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; + handleChangeEvent(path); } else if (event === 'error') { this.logger().error('ResourceEditWatcher: error'); } }); + // Hack to support external watcher on some linux applications (gedit, gvim, etc) // 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 this.watcher_.on('raw', async (event:string, path:string, options:any) => { + this.logger().debug(`ResourceEditWatcher: Raw event: ${event}: ${options.watchedPath}`); if (event === 'rename') { this.watcher_.unwatch(options.watchedPath); this.watcher_.add(options.watchedPath); + handleChangeEvent(options.watchedPath); } }); } else { @@ -181,6 +202,12 @@ export default class ResourceEditWatcher { watchedItem.lastResourceUpdatedTime = resource.updated_time; this.watch(editFilePath); + + this.dispatch({ + type: 'RESOURCE_EDIT_WATCHER_SET', + id: resource.id, + title: resource.title, + }); } bridge().openItem(watchedItem.path); @@ -199,9 +226,20 @@ export default class ResourceEditWatcher { await item.asyncSaveQueue.waitForAllDone(); - if (this.watcher_) this.watcher_.unwatch(item.path); - await shim.fsDriver().remove(item.path); + try { + if (this.watcher_) this.watcher_.unwatch(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]; + + this.dispatch({ + type: 'RESOURCE_EDIT_WATCHER_REMOVE', + id: resourceId, + }); + this.logger().info(`ResourceEditWatcher: Stopped watching ${item.path}`); } @@ -211,7 +249,11 @@ export default class ResourceEditWatcher { const item = this.watchedItems_[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 { diff --git a/ReactNativeClient/lib/services/ResourceEditWatcher/reducer.ts b/ReactNativeClient/lib/services/ResourceEditWatcher/reducer.ts new file mode 100644 index 000000000..77e6bc14b --- /dev/null +++ b/ReactNativeClient/lib/services/ResourceEditWatcher/reducer.ts @@ -0,0 +1,38 @@ +import produce, { Draft } from 'immer'; + +export const defaultState = { + watchedResources: {}, +}; + +const reducer = produce((draft: Draft, 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; diff --git a/joplin.code-workspace b/joplin.code-workspace index b482b4e04..23150c3cb 100644 --- a/joplin.code-workspace +++ b/joplin.code-workspace @@ -354,7 +354,12 @@ "ElectronClient/commands/stopExternalEditing.js": true, "ElectronClient/gui/NoteEditor/commands/showRevisions.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": [ "en" diff --git a/package-lock.json b/package-lock.json index 3567cc272..0389d03a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2829,8 +2829,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -2851,14 +2850,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2873,20 +2870,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -3003,8 +2997,7 @@ "inherits": { "version": "2.0.4", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -3016,7 +3009,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -3031,7 +3023,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -3039,14 +3030,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.9.0", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -3065,7 +3054,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -3155,8 +3143,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -3168,7 +3155,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -3254,8 +3240,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -3291,7 +3276,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -3311,7 +3295,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -3355,14 +3338,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -3799,6 +3780,11 @@ "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "dev": true }, + "immer": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/immer/-/immer-7.0.5.tgz", + "integrity": "sha512-TtRAKZyuqld2eYjvWgXISLJ0ZlOl1OOTzRmrmiY8SlB0dnAhZ1OiykIDL5KDFNaPHDXiLfGQFNJGtet8z8AEmg==" + }, "import-fresh": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.1.0.tgz", diff --git a/package.json b/package.json index e5d89cd0f..1681d09f1 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ }, "dependencies": { "follow-redirects": "^1.11.0", + "immer": "^7.0.5", "joplin-turndown": "^4.0.28", "joplin-turndown-plugin-gfm": "^1.0.12", "relative": "^3.0.2"