From f597abd33f5420618b45b0bc4b7f5bc36e142864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 26 Jun 2019 16:41:56 +0200 Subject: [PATCH] MOBILE-1332 notes: Delete notes in offline mode --- .../components/list/addon-notes-list.html | 15 ++- src/addon/notes/components/list/list.ts | 46 +++++-- src/addon/notes/providers/notes-offline.ts | 86 +++++++++++++- src/addon/notes/providers/notes-sync.ts | 112 ++++++++++++------ src/addon/notes/providers/notes.ts | 77 ++++++++++-- 5 files changed, 277 insertions(+), 59 deletions(-) diff --git a/src/addon/notes/components/list/addon-notes-list.html b/src/addon/notes/components/list/addon-notes-list.html index fbbb27c78..7908279f8 100644 --- a/src/addon/notes/components/list/addon-notes-list.html +++ b/src/addon/notes/components/list/addon-notes-list.html @@ -38,9 +38,18 @@

{{note.userfullname}}

-

{{note.lastmodified | coreDateDayOrTime}}

-

{{ 'core.notsent' | translate }}

- +
diff --git a/src/addon/notes/components/list/list.ts b/src/addon/notes/components/list/list.ts index 78f439efd..e9b6ffd7a 100644 --- a/src/addon/notes/components/list/list.ts +++ b/src/addon/notes/components/list/list.ts @@ -22,6 +22,7 @@ import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreUserProvider } from '@core/user/providers/user'; import { coreSlideInOut } from '@classes/animations'; import { AddonNotesProvider } from '../../providers/notes'; +import { AddonNotesOfflineProvider } from '../../providers/notes-offline'; import { AddonNotesSyncProvider } from '../../providers/notes-sync'; /** @@ -54,7 +55,8 @@ export class AddonNotesListComponent implements OnInit, OnDestroy { constructor(private domUtils: CoreDomUtilsProvider, private textUtils: CoreTextUtilsProvider, sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider, private modalCtrl: ModalController, private notesProvider: AddonNotesProvider, private notesSync: AddonNotesSyncProvider, - private userProvider: CoreUserProvider, private translate: TranslateService) { + private userProvider: CoreUserProvider, private translate: TranslateService, + private notesOffline: AddonNotesOfflineProvider) { // Refresh data if notes are synchronized automatically. this.syncObserver = eventsProvider.on(AddonNotesSyncProvider.AUTO_SYNCED, (data) => { if (data.courseId == this.courseId) { @@ -101,20 +103,23 @@ export class AddonNotesListComponent implements OnInit, OnDestroy { return this.notesProvider.getNotes(this.courseId, this.userId).then((notes) => { notes = notes[this.type + 'notes'] || []; - this.hasOffline = notes.some((note) => note.offline); + return this.notesProvider.setOfflineDeletedNotes(notes, this.courseId).then((notes) => { - if (this.userId) { - this.notes = notes; + this.hasOffline = notes.some((note) => note.offline || note.deleted); - // Get the user profile to retrieve the user image. - return this.userProvider.getProfile(this.userId, this.courseId, true).then((user) => { - this.user = user; - }); - } else { - return this.notesProvider.getNotesUserData(notes, this.courseId).then((notes) => { + if (this.userId) { this.notes = notes; - }); - } + + // Get the user profile to retrieve the user image. + return this.userProvider.getProfile(this.userId, this.courseId, true).then((user) => { + this.user = user; + }); + } else { + return this.notesProvider.getNotesUserData(notes, this.courseId).then((notes) => { + this.notes = notes; + }); + } + }); }); }).catch((message) => { this.domUtils.showErrorModal(message); @@ -201,7 +206,7 @@ export class AddonNotesListComponent implements OnInit, OnDestroy { e.stopPropagation(); this.domUtils.showConfirm(this.translate.instant('addon.notes.deleteconfirm')).then(() => { - this.notesProvider.deleteNote(note).then(() => { + this.notesProvider.deleteNote(note, this.courseId).then(() => { this.showDelete = false; this.refreshNotes(true); @@ -215,6 +220,21 @@ export class AddonNotesListComponent implements OnInit, OnDestroy { }); } + /** + * Restore a note. + * + * @param {Event} e Click event. + * @param {any} note Note to delete. + */ + undoDeleteNote(e: Event, note: any): void { + e.preventDefault(); + e.stopPropagation(); + + this.notesOffline.undoDeleteNote(note.id).then(() => { + this.refreshNotes(true); + }); + } + /** * Toggle delete. */ diff --git a/src/addon/notes/providers/notes-offline.ts b/src/addon/notes/providers/notes-offline.ts index 486fa0111..28bae97bf 100644 --- a/src/addon/notes/providers/notes-offline.ts +++ b/src/addon/notes/providers/notes-offline.ts @@ -26,9 +26,10 @@ export class AddonNotesOfflineProvider { // Variables for database. static NOTES_TABLE = 'addon_notes_offline_notes'; + static NOTES_DELETED_TABLE = 'addon_notes_deleted_offline_notes'; protected siteSchema: CoreSiteSchema = { name: 'AddonNotesOfflineProvider', - version: 1, + version: 2, tables: [ { name: AddonNotesOfflineProvider.NOTES_TABLE, @@ -63,6 +64,24 @@ export class AddonNotesOfflineProvider { } ], primaryKeys: ['userid', 'content', 'created'] + }, + { + name: AddonNotesOfflineProvider.NOTES_DELETED_TABLE, + columns: [ + { + name: 'noteid', + type: 'INTEGER', + primaryKey: true + }, + { + name: 'deleted', + type: 'INTEGER' + }, + { + name: 'courseid', + type: 'INTEGER' + } + ] } ] }; @@ -73,7 +92,7 @@ export class AddonNotesOfflineProvider { } /** - * Delete a note. + * Delete an offline note. * * @param {number} userId User ID the note is about. * @param {string} content The note content. @@ -81,7 +100,7 @@ export class AddonNotesOfflineProvider { * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved if deleted, rejected if failure. */ - deleteNote(userId: number, content: string, timecreated: number, siteId?: string): Promise { + deleteOfflineNote(userId: number, content: string, timecreated: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { return site.getDb().deleteRecords(AddonNotesOfflineProvider.NOTES_TABLE, { userid: userId, @@ -91,6 +110,31 @@ export class AddonNotesOfflineProvider { }); } + /** + * Get all offline deleted notes. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with notes. + */ + getAllDeletedNotes(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecords(AddonNotesOfflineProvider.NOTES_DELETED_TABLE); + }); + } + + /** + * Get course offline deleted notes. + * + * @param {number} courseId Course ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with notes. + */ + getCourseDeletedNotes(courseId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecords(AddonNotesOfflineProvider.NOTES_DELETED_TABLE, {courseid: courseId}); + }); + } + /** * Get all offline notes. * @@ -246,4 +290,40 @@ export class AddonNotesOfflineProvider { }); }); } + + /** + * Delete a note offline to be sent later. + * + * @param {number} noteId Note ID. + * @param {number} courseId Course ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if stored, rejected if failure. + */ + deleteNote(noteId: number, courseId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const now = this.timeUtils.timestamp(); + const data = { + noteid: noteId, + courseid: courseId, + deleted: now + }; + + return site.getDb().insertRecord(AddonNotesOfflineProvider.NOTES_DELETED_TABLE, data).then(() => { + return data; + }); + }); + } + + /** + * Undo delete a note. + * + * @param {number} noteId Note ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if deleted, rejected if failure. + */ + undoDeleteNote(noteId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().deleteRecords(AddonNotesOfflineProvider.NOTES_DELETED_TABLE, { noteid: noteId }); + }); + } } diff --git a/src/addon/notes/providers/notes-sync.ts b/src/addon/notes/providers/notes-sync.ts index 77b7039c3..cebf92d73 100644 --- a/src/addon/notes/providers/notes-sync.ts +++ b/src/addon/notes/providers/notes-sync.ts @@ -63,18 +63,24 @@ export class AddonNotesSyncProvider extends CoreSyncBaseProvider { * @return {Promise} Promise resolved if sync is successful, rejected if sync fails. */ private syncAllNotesFunc(siteId: string, force: boolean): Promise { - return this.notesOffline.getAllNotes(siteId).then((notes) => { - // Get all the courses to be synced. - const courseIds = []; - notes.forEach((note) => { - if (courseIds.indexOf(note.courseid) == -1) { - courseIds.push(note.courseid); - } - }); + const proms = []; + proms.push(this.notesOffline.getAllNotes(siteId)); + proms.push(this.notesOffline.getAllDeletedNotes(siteId)); + + return Promise.all(proms).then((notesArray) => { + // Get all the courses to be synced. + const courseIds = {}; + notesArray.forEach((notes) => { + notes.forEach((note) => { + courseIds[note.courseid] = note.courseid; + }); + }); // Sync all courses. - const promises = courseIds.map((courseId) => { - const promise = force ? this.syncNotes(courseId, siteId) : this.syncNotesIfNeeded(courseId, siteId); + const promises = Object.keys(courseIds).map((courseId) => { + const cId = parseInt(courseIds[courseId], 10); + + const promise = force ? this.syncNotes(cId, siteId) : this.syncNotesIfNeeded(cId, siteId); return promise.then((warnings) => { if (typeof warnings != 'undefined') { @@ -124,9 +130,12 @@ export class AddonNotesSyncProvider extends CoreSyncBaseProvider { this.logger.debug('Try to sync notes for course ' + courseId); const warnings = []; + const errors = []; + + const proms = []; // Get offline notes to be sent. - const syncPromise = this.notesOffline.getNotesForCourse(courseId, siteId).then((notes) => { + proms.push(this.notesOffline.getNotesForCourse(courseId, siteId).then((notes) => { if (!notes.length) { // Nothing to sync. return; @@ -157,12 +166,6 @@ export class AddonNotesSyncProvider extends CoreSyncBaseProvider { } }); - // Fetch the notes from server to be sure they're up to date. - return this.notesProvider.invalidateNotes(courseId, undefined, siteId).then(() => { - return this.notesProvider.getNotes(courseId, undefined, false, true, siteId); - }).catch(() => { - // Ignore errors. - }); }).catch((error) => { if (this.utils.isWebServiceError(error)) { // It's a WebService error, this means the user cannot send notes. @@ -174,26 +177,69 @@ export class AddonNotesSyncProvider extends CoreSyncBaseProvider { }).then(() => { // Notes were sent, delete them from local DB. const promises = notes.map((note) => { - return this.notesOffline.deleteNote(note.userid, note.content, note.created, siteId); + return this.notesOffline.deleteOfflineNote(note.userid, note.content, note.created, siteId); }); return Promise.all(promises); - }).then(() => { - if (errors && errors.length) { - // At least an error occurred, get course name and add errors to warnings array. - return this.coursesProvider.getUserCourse(courseId, true, siteId).catch(() => { - // Ignore errors. - return {}; - }).then((course) => { - errors.forEach((error) => { - warnings.push(this.translate.instant('addon.notes.warningnotenotsent', { - course: course.fullname ? course.fullname : courseId, - error: error - })); - }); - }); - } }); + })); + + // Get offline notes to be sent. + proms.push(this.notesOffline.getCourseDeletedNotes(courseId, siteId).then((notes) => { + if (!notes.length) { + // Nothing to sync. + return; + } else if (!this.appProvider.isOnline()) { + // Cannot sync in offline. + return Promise.reject(this.translate.instant('core.networkerrormsg')); + } + + // Format the notes to be sent. + const notesToDelete = notes.map((note) => { + return note.noteid; + }); + + // Delete the notes. + return this.notesProvider.deleteNotesOnline(notesToDelete, courseId, siteId).catch((error) => { + if (this.utils.isWebServiceError(error)) { + // It's a WebService error, this means the user cannot send notes. + errors.push(error); + } else { + // Not a WebService error, reject the synchronization to try again. + return Promise.reject(error); + } + }).then(() => { + // Notes were sent, delete them from local DB. + const promises = notes.map((noteId) => { + return this.notesOffline.undoDeleteNote(noteId, siteId); + }); + + return Promise.all(promises); + }); + })); + + const syncPromise = Promise.all(proms).then(() => { + // Fetch the notes from server to be sure they're up to date. + return this.notesProvider.invalidateNotes(courseId, undefined, siteId).then(() => { + return this.notesProvider.getNotes(courseId, undefined, false, true, siteId); + }).catch(() => { + // Ignore errors. + }); + }).then(() => { + if (errors && errors.length) { + // At least an error occurred, get course name and add errors to warnings array. + return this.coursesProvider.getUserCourse(courseId, true, siteId).catch(() => { + // Ignore errors. + return {}; + }).then((course) => { + errors.forEach((error) => { + warnings.push(this.translate.instant('addon.notes.warningnotenotsent', { + course: course.fullname ? course.fullname : courseId, + error: error + })); + }); + }); + } }).then(() => { // All done, return the warnings. return warnings; diff --git a/src/addon/notes/providers/notes.ts b/src/addon/notes/providers/notes.ts index 52caa2c37..82f095e41 100644 --- a/src/addon/notes/providers/notes.ts +++ b/src/addon/notes/providers/notes.ts @@ -137,20 +137,65 @@ export class AddonNotesProvider { * Delete a note. * * @param {any} note Note object to delete. + * @param {number} courseId Course ID where the note belongs. * @param {string} [siteId] Site ID. If not defined, current site. - * @return {Promise} Promise resolved when done. + * @return {Promise} Promise resolved when deleted, rejected otherwise. Promise resolved doesn't mean that notes + * have been deleted, the resolve param can contain errors for notes not deleted. */ - deleteNote(note: any, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - if (typeof note.offline != 'undefined' && note.offline) { - return this.notesOffline.deleteNote(note.userid, note.content, note.created, site.id); + deleteNote(note: any, courseId: number, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + if (note.offline) { + return this.notesOffline.deleteOfflineNote(note.userid, note.content, note.created, siteId); + } + + // Convenience function to store the action to be synchronized later. + const storeOffline = (): Promise => { + return this.notesOffline.deleteNote(note.id, courseId, siteId).then(() => { + return false; + }); + }; + + if (!this.appProvider.isOnline()) { + // App is offline, store the note. + return storeOffline(); + } + + // Send note to server. + return this.deleteNotesOnline([note.id], courseId, siteId).then(() => { + return true; + }).catch((error) => { + if (this.utils.isWebServiceError(error)) { + // It's a WebService error, the user cannot send the note so don't store it. + return Promise.reject(error); } + // Error sending note, store it to retry later. + return storeOffline(); + }); + } + + /** + * Delete a note. It will fail if offline or cannot connect. + * + * @param {number[]} noteIds Note IDs to delete. + * @param {number} courseId Course ID where the note belongs. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when deleted, rejected otherwise. Promise resolved doesn't mean that notes + * have been deleted, the resolve param can contain errors for notes not deleted. + */ + deleteNotesOnline(noteIds: number[], courseId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { const data = { - notes: [note.id] + notes: noteIds }; - return site.write('core_notes_delete_notes', data); + return site.write('core_notes_delete_notes', data).then((response) => { + // A note was deleted, invalidate the course notes. + return this.invalidateNotes(courseId, undefined, siteId).catch(() => { + // Ignore errors. + }); + }); }); } @@ -288,6 +333,24 @@ export class AddonNotesProvider { }); } + /** + * Get offline deleted notes and set the state. + * + * @param {any[]} notes Array of notes. + * @param {number} courseId ID of the course the notes belong to. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} [description] + */ + setOfflineDeletedNotes(notes: any[], courseId: number, siteId?: string): Promise { + return this.notesOffline.getCourseDeletedNotes(courseId, siteId).then((deletedNotes) => { + notes.forEach((note) => { + note.deleted = deletedNotes.some((n) => n.noteid == note.id); + }); + + return notes; + }); + } + /** * Get user data for notes since they only have userid. *