MOBILE-1332 notes: Delete notes in offline mode

main
Pau Ferrer Ocaña 2019-06-26 16:41:56 +02:00
parent 58b2e33156
commit f597abd33f
5 changed files with 277 additions and 59 deletions

View File

@ -38,9 +38,18 @@
<ion-item text-wrap>
<ion-avatar core-user-avatar [user]="note" [courseId]="courseId" item-start *ngIf="!userId"></ion-avatar>
<h2 *ngIf="!userId">{{note.userfullname}}</h2>
<p *ngIf="!note.offline" item-end>{{note.lastmodified | coreDateDayOrTime}}</p>
<p *ngIf="note.offline" item-end><ion-icon name="time"></ion-icon> {{ 'core.notsent' | translate }}</p>
<button *ngIf="showDelete && (type != 'personal' || note.usermodified == currentUserId)" item-end ion-button icon-only clear [@coreSlideInOut]="'fromRight'" color="danger" (click)="deleteNote($event, note)" [attr.aria-label]="'core.delete' | translate">
<p *ngIf="!note.deleted && !note.offline" item-end>
<span text-wrap>{{note.lastmodified | coreDateDayOrTime}}</span>
</p>
<p *ngIf="note.offline" item-end>
<ion-icon name="time"></ion-icon> <span text-wrap>{{ 'core.notsent' | translate }}</span></p>
<p *ngIf="note.deleted" item-end>
<ion-icon name="trash"></ion-icon> <span text-wrap>{{ 'core.deletedoffline' | translate }}</span>
</p>
<button *ngIf="note.deleted" item-end ion-button icon-only clear color="danger" (click)="undoDeleteNote($event, note)" [attr.aria-label]="'core.restore' | translate">
<ion-icon name="undo"></ion-icon>
</button>
<button *ngIf="showDelete && !note.deleted && (type != 'personal' || note.usermodified == currentUserId)" item-end ion-button icon-only clear [@coreSlideInOut]="'fromRight'" color="danger" (click)="deleteNote($event, note)" [attr.aria-label]="'core.delete' | translate">
<ion-icon name="trash"></ion-icon>
</button>
</ion-item>

View File

@ -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.
*/

View File

@ -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<any>} Promise resolved if deleted, rejected if failure.
*/
deleteNote(userId: number, content: string, timecreated: number, siteId?: string): Promise<any> {
deleteOfflineNote(userId: number, content: string, timecreated: number, siteId?: string): Promise<any> {
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<any>} Promise resolved with notes.
*/
getAllDeletedNotes(siteId?: string): Promise<any> {
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<any>} Promise resolved with notes.
*/
getCourseDeletedNotes(courseId: number, siteId?: string): Promise<any> {
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<any>} Promise resolved if stored, rejected if failure.
*/
deleteNote(noteId: number, courseId: number, siteId?: string): Promise<any> {
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<any>} Promise resolved if deleted, rejected if failure.
*/
undoDeleteNote(noteId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.getDb().deleteRecords(AddonNotesOfflineProvider.NOTES_DELETED_TABLE, { noteid: noteId });
});
}
}

View File

@ -63,18 +63,24 @@ export class AddonNotesSyncProvider extends CoreSyncBaseProvider {
* @return {Promise<any>} Promise resolved if sync is successful, rejected if sync fails.
*/
private syncAllNotesFunc(siteId: string, force: boolean): Promise<any> {
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;

View File

@ -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<void>} Promise resolved when done.
* @return {Promise<void>} 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<void> {
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<void> {
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<any> => {
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<void>} 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<void> {
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<any>} [description]
*/
setOfflineDeletedNotes(notes: any[], courseId: number, siteId?: string): Promise<any> {
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.
*