diff --git a/CliClient/app/app-gui.js b/CliClient/app/app-gui.js index 3e51661e4..d918cc6c8 100644 --- a/CliClient/app/app-gui.js +++ b/CliClient/app/app-gui.js @@ -3,6 +3,7 @@ import { Folder } from 'lib/models/folder.js'; import { Note } from 'lib/models/note.js'; import { cliUtils } from './cli-utils.js'; import { reducer, defaultState } from 'lib/reducer.js'; +import { reg } from 'lib/registry.js'; import { _ } from 'lib/locale.js'; const chalk = require('chalk'); @@ -56,15 +57,16 @@ class AppGui { this.commandCancelCalled_ = false; this.currentShortcutKeys_ = []; - this.lastShortcutKeyTime_ = 0; + this.lastShortcutKeyTime_ = 0; cliUtils.setStdout((...object) => { return this.stdout(...object); - - // for (let i = 0; i < object.length; i++) { - // this.widget('console').bufferPush(object[i]); - // } }); + + // Recurrent sync is setup only when the GUI is started. In + // a regular command it's not necessary since the process + // exits right away. + reg.setupRecurrentSync(); } renderer() { diff --git a/CliClient/app/app.js b/CliClient/app/app.js index d9214e707..c6ca2142e 100644 --- a/CliClient/app/app.js +++ b/CliClient/app/app.js @@ -417,9 +417,16 @@ class Application { }); } + reducerActionToString(action) { + let o = [action.type]; + if (action.noteId) o.push(action.noteId); + if (action.folderI) o.push(action.folderI); + return o.join(', '); + } + generalMiddleware() { const middleware = store => next => async (action) => { - this.logger().info('Reducer action', action.type); + this.logger().info('Reducer action', this.reducerActionToString(action)); const result = next(action); const newState = store.getState(); @@ -429,6 +436,10 @@ class Application { await this.refreshNotes(); } + if (this.gui() && action.type == 'SETTINGS_UPDATE_ONE' && action.key == 'sync.interval' || action.type == 'SETTINGS_UPDATE_ALL') { + reg.setupRecurrentSync(); + } + return result; } diff --git a/CliClient/app/command-sync.js b/CliClient/app/command-sync.js index 2ba8f89c9..22afe6c38 100644 --- a/CliClient/app/command-sync.js +++ b/CliClient/app/command-sync.js @@ -86,6 +86,8 @@ class Command extends BaseCommand { if (args.options.target) this.syncTarget_ = args.options.target; if (this.syncTarget_ == Setting.SYNC_TARGET_ONEDRIVE && !reg.syncHasAuth(this.syncTarget_)) { + app().gui().showConsole(); + app().gui().maximizeConsole(); const oneDriveApiUtils = new OneDriveApiNodeUtils(reg.oneDriveApi()); const auth = await oneDriveApiUtils.oauthDance({ log: (...s) => { return this.stdout(...s); } diff --git a/CliClient/app/gui/NoteListWidget.js b/CliClient/app/gui/NoteListWidget.js index 517f5dcc9..81341d25c 100644 --- a/CliClient/app/gui/NoteListWidget.js +++ b/CliClient/app/gui/NoteListWidget.js @@ -10,7 +10,7 @@ class NoteListWidget extends ListWidget { this.updateIndexFromSelectedNoteId_ = false; this.itemRenderer = (note) => { - let label = note.title; //+ ' ' + note.id; + let label = note.title; // + ' ' + note.id; if (note.is_todo) { label = '[' + (note.todo_completed ? 'X' : ' ') + '] ' + label; } diff --git a/CliClient/app/gui/StatusBarWidget.js b/CliClient/app/gui/StatusBarWidget.js index 7bbb60d9d..1967d4397 100644 --- a/CliClient/app/gui/StatusBarWidget.js +++ b/CliClient/app/gui/StatusBarWidget.js @@ -55,8 +55,22 @@ class StatusBarWidget extends BaseWidget { return this.history_; } + resetCursor() { + if (!this.promptActive) return; + if (!this.inputEventEmitter_) return; + + this.inputEventEmitter_.redraw(); + this.inputEventEmitter_.rebase(this.absoluteInnerX + termutils.textLength(this.promptState_.promptString), this.absoluteInnerY); + this.term.moveTo(this.absoluteInnerX + termutils.textLength(this.promptState_.promptString) + this.inputEventEmitter_.getInput().length, this.absoluteInnerY); + } + render() { super.render(); + + const doSaveCursor = !this.promptActive; + + if (doSaveCursor) this.term.saveCursor(); + this.innerClear(); const textStyle = chalk.bgBlueBright.white; @@ -70,8 +84,9 @@ class StatusBarWidget extends BaseWidget { this.term.write(textStyle(this.promptState_.promptString)); if (this.inputEventEmitter_) { - this.inputEventEmitter_.redraw(); - this.inputEventEmitter_.rebase(this.absoluteInnerX + termutils.textLength(this.promptState_.promptString), this.absoluteInnerY); + // inputField is already waiting for input so in that case just make + // sure that the cursor is at the right position and exit. + this.resetCursor(); return; } @@ -122,6 +137,8 @@ class StatusBarWidget extends BaseWidget { } } + + if (doSaveCursor) this.term.restoreCursor(); } } diff --git a/CliClient/app/onedrive-api-node-utils.js b/CliClient/app/onedrive-api-node-utils.js index 2bc3b1b69..f069c51eb 100644 --- a/CliClient/app/onedrive-api-node-utils.js +++ b/CliClient/app/onedrive-api-node-utils.js @@ -66,18 +66,27 @@ class OneDriveApiNodeUtils { response.end(); } + // After the response has been received, don't destroy the server right + // away or the browser might display a connection reset error (even + // though it worked). + const waitAndDestroy = () => { + setTimeout(() => { + server.destroy(); + }, 1000); + } + if (!query.code) return writeResponse(400, '"code" query parameter is missing'); this.api().execTokenRequest(query.code, 'http://localhost:' + port.toString()).then(() => { writeResponse(200, _('The application has been authorised - you may now close this browser tab.')); targetConsole.log(''); targetConsole.log(_('The application has been successfully authorised.')); - server.destroy(); + waitAndDestroy(); }).catch((error) => { writeResponse(400, error.message); targetConsole.log(''); targetConsole.log(error.message); - server.destroy(); + waitAndDestroy(); }); }); diff --git a/ReactNativeClient/lib/models/note.js b/ReactNativeClient/lib/models/note.js index 4ae8f9248..8000cce06 100644 --- a/ReactNativeClient/lib/models/note.js +++ b/ReactNativeClient/lib/models/note.js @@ -98,10 +98,12 @@ class Note extends BaseItem { if (a[order.by] < b[order.by]) r = +1; if (a[order.by] > b[order.by]) r = -1; if (order.dir == 'ASC') r = -r; - if (r) break; + if (r !== 0) break; } - return r; + // Makes the sort deterministic, so that if, for example, a and b have the + // same updated_time, they aren't swapped every time a list is refreshed. + return a.title.toLowerCase() + a.id < b.title.toLowerCase() + b.id ? -1 : +1; }); } diff --git a/ReactNativeClient/lib/onedrive-api.js b/ReactNativeClient/lib/onedrive-api.js index b630b48ac..23ac597dc 100644 --- a/ReactNativeClient/lib/onedrive-api.js +++ b/ReactNativeClient/lib/onedrive-api.js @@ -180,6 +180,11 @@ class OneDriveApi { // or error code, so hopefully that message won't change and is not localized } else if (error.code == 'ECONNRESET') { // request to https://public-ch3302....1fab24cb1bd5f.md failed, reason: socket hang up" + } else if (error.code == 'ENOTFOUND') { + // OneDrive (or Node?) sometimes sends back a "not found" error for resources + // that definitely exist and in this case repeating the request works. + // Error is: + // request to https://graph.microsoft.com/v1.0/drive/special/approot failed, reason: getaddrinfo ENOTFOUND graph.microsoft.com graph.microsoft.com:443 } else if (error.message.indexOf('network timeout') === 0) { // network timeout at: https://public-ch3302...859f9b0e3ab.md } else { diff --git a/ReactNativeClient/lib/registry.js b/ReactNativeClient/lib/registry.js index 1c019760f..e6ea58a60 100644 --- a/ReactNativeClient/lib/registry.js +++ b/ReactNativeClient/lib/registry.js @@ -9,7 +9,6 @@ import { FileApiDriverOneDrive } from 'lib/file-api-driver-onedrive.js'; import { shim } from 'lib/shim.js'; import { time } from 'lib/time-utils.js'; import { FileApiDriverMemory } from 'lib/file-api-driver-memory.js'; -import { PoorManIntervals } from 'lib/poor-man-intervals.js'; import { _ } from 'lib/locale.js'; const reg = {}; @@ -199,7 +198,7 @@ reg.syncStarted = async () => { reg.setupRecurrentSync = () => { if (reg.recurrentSyncId_) { - PoorManIntervals.clearInterval(reg.recurrentSyncId_); + shim.clearInterval(reg.recurrentSyncId_); reg.recurrentSyncId_ = null; } @@ -208,7 +207,7 @@ reg.setupRecurrentSync = () => { } else { reg.logger().debug('Setting up recurrent sync with interval ' + Setting.value('sync.interval')); - reg.recurrentSyncId_ = PoorManIntervals.setInterval(() => { + reg.recurrentSyncId_ = shim.setInterval(() => { reg.logger().info('Running background sync on timer...'); reg.scheduleSync(0); }, 1000 * Setting.value('sync.interval')); diff --git a/ReactNativeClient/lib/shim-init-react.js b/ReactNativeClient/lib/shim-init-react.js index 3d4277103..cf6c4667f 100644 --- a/ReactNativeClient/lib/shim-init-react.js +++ b/ReactNativeClient/lib/shim-init-react.js @@ -1,10 +1,14 @@ import { shim } from 'lib/shim.js'; import { GeolocationReact } from 'lib/geolocation-react.js'; +import { PoorManIntervals } from 'lib/poor-man-intervals.js'; import RNFetchBlob from 'react-native-fetch-blob'; function shimInit() { shim.Geolocation = GeolocationReact; + shim.setInterval = PoorManIntervals.setInterval; + shim.clearInterval = PoorManIntervals.clearInterval; + shim.fetchBlob = async function(url, options) { if (!options || !options.path) throw new Error('fetchBlob: target file path is missing'); diff --git a/ReactNativeClient/lib/shim.js b/ReactNativeClient/lib/shim.js index cffc79057..83505152f 100644 --- a/ReactNativeClient/lib/shim.js +++ b/ReactNativeClient/lib/shim.js @@ -15,5 +15,7 @@ shim.fs = null; shim.FileApiDriverLocal = null; shim.readLocalFileBase64 = () => { throw new Error('Not implemented'); } shim.uploadBlob = () => { throw new Error('Not implemented'); } +shim.setInterval = setInterval; +shim.clearInterval = clearInterval; export { shim }; \ No newline at end of file