MOBILE-1332 notes: Delete notes in offline mode
parent
58b2e33156
commit
f597abd33f
|
@ -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>
|
||||
|
|
|
@ -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,7 +103,9 @@ 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) => {
|
||||
|
||||
this.hasOffline = notes.some((note) => note.offline || note.deleted);
|
||||
|
||||
if (this.userId) {
|
||||
this.notes = notes;
|
||||
|
@ -116,6 +120,7 @@ export class AddonNotesListComponent implements OnInit, OnDestroy {
|
|||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}).catch((message) => {
|
||||
this.domUtils.showErrorModal(message);
|
||||
}).finally(() => {
|
||||
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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 });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,10 +177,54 @@ 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);
|
||||
});
|
||||
}));
|
||||
|
||||
// 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.
|
||||
|
@ -193,7 +240,6 @@ export class AddonNotesSyncProvider extends CoreSyncBaseProvider {
|
|||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}).then(() => {
|
||||
// All done, return the warnings.
|
||||
return warnings;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
const data = {
|
||||
notes: [note.id]
|
||||
// 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;
|
||||
});
|
||||
};
|
||||
|
||||
return site.write('core_notes_delete_notes', data);
|
||||
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: noteIds
|
||||
};
|
||||
|
||||
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.
|
||||
*
|
||||
|
|
Loading…
Reference in New Issue