diff --git a/ReactNativeClient/lib/components/app-nav.js b/ReactNativeClient/lib/components/app-nav.js index 26ee1f26d..581b8d5b1 100644 --- a/ReactNativeClient/lib/components/app-nav.js +++ b/ReactNativeClient/lib/components/app-nav.js @@ -5,11 +5,6 @@ import { _ } from 'lib/locale.js'; class AppNavComponent extends Component { - constructor() { - super(); - this.screenCache_ = []; - } - render() { if (!this.props.route) throw new Error('Route must not be null'); diff --git a/ReactNativeClient/lib/components/item-list.js b/ReactNativeClient/lib/components/item-list.js index e3e3edf73..b4a066755 100644 --- a/ReactNativeClient/lib/components/item-list.js +++ b/ReactNativeClient/lib/components/item-list.js @@ -4,6 +4,7 @@ import { ListView, Text, TouchableHighlight, Switch, View } from 'react-native'; import { Log } from 'lib/log.js'; import { _ } from 'lib/locale.js'; import { Checkbox } from 'lib/components/checkbox.js'; +import { reg } from 'lib/registry.js'; import { Note } from 'lib/models/note.js'; import { time } from 'lib/time-utils.js'; @@ -36,6 +37,8 @@ class ItemListComponent extends Component { async todoCheckbox_change(itemId, checked) { let note = await Note.load(itemId); await Note.save({ id: note.id, todo_completed: checked ? time.unixMs() : 0 }); + reg.scheduleSync(); + } listView_itemLongPress(itemId) {} diff --git a/ReactNativeClient/lib/components/screens/folder.js b/ReactNativeClient/lib/components/screens/folder.js index 125dc14e7..9ed97dc1f 100644 --- a/ReactNativeClient/lib/components/screens/folder.js +++ b/ReactNativeClient/lib/components/screens/folder.js @@ -6,6 +6,7 @@ import { ActionButton } from 'lib/components/action-button.js'; import { Folder } from 'lib/models/folder.js' import { BaseModel } from 'lib/base-model.js' import { ScreenHeader } from 'lib/components/screen-header.js'; +import { reg } from 'lib/registry.js'; import { NotesScreenUtils } from 'lib/components/screens/notes-utils.js' import { BaseScreenComponent } from 'lib/components/base-screen.js'; import { dialogs } from 'lib/dialogs.js'; @@ -69,6 +70,8 @@ class FolderScreenComponent extends BaseScreenComponent { duplicateCheck: true, reservedTitleCheck: true, }); + + reg.scheduleSync(); } catch (error) { dialogs.error(this, _('The folder could not be saved: %s', error.message)); return; diff --git a/ReactNativeClient/lib/components/screens/note.js b/ReactNativeClient/lib/components/screens/note.js index 52b8805fa..fd9962ff8 100644 --- a/ReactNativeClient/lib/components/screens/note.js +++ b/ReactNativeClient/lib/components/screens/note.js @@ -12,6 +12,7 @@ import { time } from 'lib/time-utils.js'; import { Checkbox } from 'lib/components/checkbox.js' import { _ } from 'lib/locale.js'; import marked from 'lib/marked.js'; +import { reg } from 'lib/registry.js'; import { BaseScreenComponent } from 'lib/components/base-screen.js'; import { dialogs } from 'lib/dialogs.js'; import { NotesScreenUtils } from 'lib/components/screens/notes-utils.js' @@ -38,7 +39,9 @@ class NoteScreenComponent extends BaseScreenComponent { showNoteMetadata: false, folder: null, lastSavedNote: null, - } + }; + + this.saveButtonHasBeenShown_ = false; this.backHandler = () => { if (!this.state.note.id) { @@ -112,11 +115,9 @@ class NoteScreenComponent extends BaseScreenComponent { } noteComponent_change(propName, propValue) { - this.setState((prevState, props) => { - let note = Object.assign({}, prevState.note); - note[propName] = propValue; - return { note: note } - }); + let note = Object.assign({}, this.state.note); + note[propName] = propValue; + this.setState({ note: note }); } async refreshNoteMetadata(force = null) { @@ -155,6 +156,8 @@ class NoteScreenComponent extends BaseScreenComponent { }); if (isNew) Note.updateGeolocation(note.id); this.refreshNoteMetadata(); + + reg.scheduleSync(); } async deleteNote_onPress() { @@ -168,6 +171,8 @@ class NoteScreenComponent extends BaseScreenComponent { await Note.delete(note.id); await NotesScreenUtils.openNoteList(folderId); + + reg.scheduleSync(); } attachFile_onPress() { @@ -200,6 +205,8 @@ class NoteScreenComponent extends BaseScreenComponent { lastSavedNote: Object.assign({}, note), note: note, }); + + reg.scheduleSync(); } else { note[name] = value; this.setState({ note: note }); @@ -207,7 +214,8 @@ class NoteScreenComponent extends BaseScreenComponent { } async todoCheckbox_change(checked) { - return this.saveOneProperty('todo_completed', checked ? time.unixMs() : 0); + await this.saveOneProperty('todo_completed', checked ? time.unixMs() : 0); + reg.scheduleSync(); } render() { @@ -307,9 +315,11 @@ class NoteScreenComponent extends BaseScreenComponent { const actionButtonComp = renderActionButton(); - let showSaveButton = this.state.mode == 'edit'; + let showSaveButton = this.state.mode == 'edit' || this.isModified() || this.saveButtonHasBeenShown_; let saveButtonDisabled = !this.isModified(); + if (showSaveButton) this.saveButtonHasBeenShown_ = true; + return ( { - let lines = sync.reportToLines(report); - this.setState({ syncReportText: lines.join("\n") }); - }, - }; - - try { - sync.start(options).then(async () => { - await FoldersScreenUtils.refreshFolders(); - }); - } catch (error) { - Log.error(error); - } + sync.start(); } else { this.props.dispatch({ type: 'SIDE_MENU_CLOSE' }); @@ -102,7 +108,7 @@ class SideMenuContentComponent extends Component { items.push( { this.folder_press(f) }}> - {title} + {title} ); diff --git a/ReactNativeClient/lib/event-dispatcher.js b/ReactNativeClient/lib/event-dispatcher.js new file mode 100644 index 000000000..738549113 --- /dev/null +++ b/ReactNativeClient/lib/event-dispatcher.js @@ -0,0 +1,35 @@ +class EventDispatcher { + + constructor() { + this.listeners_ = []; + } + + dispatch(eventName, event = null) { + if (!this.listeners_[eventName]) return; + + let ls = this.listeners_[eventName]; + for (let i = 0; i < ls.length; i++) { + ls[i](event); + } + } + + on(eventName, callback) { + if (!this.listeners_[eventName]) this.listeners_[eventName] = []; + this.listeners_[eventName].push(callback); + } + + off(eventName, callback) { + if (!this.listeners_[eventName]) return; + + let ls = this.listeners_[eventName]; + for (let i = 0; i < ls.length; i++) { + if (ls[i] === callback) { + ls.splice(i, 1); + return; + } + } + } + +} + +export { EventDispatcher }; \ No newline at end of file diff --git a/ReactNativeClient/lib/registry.js b/ReactNativeClient/lib/registry.js index 1f1d7984a..9fd2a9608 100644 --- a/ReactNativeClient/lib/registry.js +++ b/ReactNativeClient/lib/registry.js @@ -5,9 +5,16 @@ import { parameters } from 'lib/parameters.js'; import { FileApi } from 'lib/file-api.js'; import { Synchronizer } from 'lib/synchronizer.js'; import { FileApiDriverOneDrive } from 'lib/file-api-driver-onedrive.js'; +import { EventDispatcher } from 'lib/event-dispatcher.js'; const reg = {}; +reg.dispatcher = () => { + if (this.dispatcher_) return this.dispatcher_; + this.dispatcher_ = new EventDispatcher(); + return this.dispatcher_; +} + reg.logger = () => { if (!reg.logger_) { console.warn('Calling logger before it is initialized'); @@ -70,9 +77,37 @@ reg.synchronizer = async () => { let fileApi = await reg.fileApi(); reg.synchronizer_ = new Synchronizer(reg.db(), fileApi, Setting.value('appType')); reg.synchronizer_.setLogger(reg.logger()); + + reg.synchronizer_.on('progress', (report) => { + reg.dispatcher().dispatch('synchronizer_progress', report); + }); + + reg.synchronizer_.on('complete', () => { + reg.dispatcher().dispatch('synchronizer_complete'); + }); + return reg.synchronizer_; } +reg.scheduleSync = async () => { + if (reg.scheduleSyncId_) return; + + reg.logger().info('Scheduling sync operation...'); + + reg.scheduleSyncId_ = setTimeout(async () => { + reg.scheduleSyncId_ = null; + reg.logger().info('Doing scheduled sync'); + + if (!reg.oneDriveApi().auth()) { + reg.logger().info('Synchronizer is missing credentials - manual sync required to authenticate.'); + return; + } + + const sync = await reg.synchronizer(); + sync.start(); + }, 1000 * 10); +} + reg.setDb = (v) => { reg.db_ = v; } diff --git a/ReactNativeClient/lib/synchronizer.js b/ReactNativeClient/lib/synchronizer.js index e6ba3ccf0..7ec99d88e 100644 --- a/ReactNativeClient/lib/synchronizer.js +++ b/ReactNativeClient/lib/synchronizer.js @@ -7,6 +7,7 @@ import { sprintf } from 'sprintf-js'; import { time } from 'lib/time-utils.js'; import { Logger } from 'lib/logger.js' import { _ } from 'lib/locale.js'; +import { EventDispatcher } from 'lib/event-dispatcher.js'; import moment from 'moment'; class Synchronizer { @@ -23,6 +24,16 @@ class Synchronizer { this.onProgress_ = function(s) {}; this.progressReport_ = {}; + + this.dispatcher_ = new EventDispatcher(); + } + + on(eventName, callback) { + return this.dispatcher_.on(eventName, callback); + } + + off(eventName, callback) { + return this.dispatcher_.off(eventName, callback); } state() { @@ -55,6 +66,7 @@ class Synchronizer { if (report.deleteRemote) lines.push(_('Deleted remote items: %d.', report.deleteRemote)); if (report.state) lines.push(_('State: %s.', report.state.replace(/_/g, ' '))); if (report.errors && report.errors.length) lines.push(_('Last error: %s (stacktrace in log).', report.errors[report.errors.length-1].message)); + if (report.completedTime) lines.push(_('Completed: %s', time.unixMsToLocalDateTime(report.completedTime))); return lines; } @@ -88,6 +100,8 @@ class Synchronizer { this.progressReport_[action]++; this.progressReport_.state = this.state(); this.onProgress_(this.progressReport_); + + this.dispatcher_.dispatch('progress', this.progressReport_); } async logSyncSummary(report) { @@ -143,7 +157,7 @@ class Synchronizer { const syncTargetId = this.api().driver().syncTargetId(); if (this.state() != 'idle') { - this.logger().warn('Synchronization is already in progress. State: ' + this.state()); + this.logger().info('Synchronization is already in progress. State: ' + this.state()); return; } @@ -430,10 +444,14 @@ class Synchronizer { this.logSyncOperation('finished', null, null, 'Synchronization finished [' + synchronizationId + ']'); + this.progressReport_.completedTime = time.unixMs(); + await this.logSyncSummary(this.progressReport_); this.onProgress_ = function(s) {}; this.progressReport_ = {}; + + this.dispatcher_.dispatch('complete'); } }