diff --git a/scripts/langindex.json b/scripts/langindex.json index 35e016443..db6dabeb6 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -87,13 +87,19 @@ "addon.calendar.calendarevent": "local_moodlemobileapp", "addon.calendar.calendarevents": "local_moodlemobileapp", "addon.calendar.calendarreminders": "local_moodlemobileapp", + "addon.calendar.confirmeventdelete": "calendar", + "addon.calendar.confirmeventseriesdelete": "calendar", "addon.calendar.defaultnotificationtime": "local_moodlemobileapp", + "addon.calendar.deleteallevents": "calendar", + "addon.calendar.deleteevent": "calendar", + "addon.calendar.deleteoneevent": "calendar", "addon.calendar.durationminutes": "calendar", "addon.calendar.durationnone": "calendar", "addon.calendar.durationuntil": "calendar", "addon.calendar.editevent": "calendar", "addon.calendar.errorloadevent": "local_moodlemobileapp", "addon.calendar.errorloadevents": "local_moodlemobileapp", + "addon.calendar.eventcalendareventdeleted": "calendar", "addon.calendar.eventduration": "calendar", "addon.calendar.eventendtime": "calendar", "addon.calendar.eventkind": "calendar", diff --git a/src/addon/calendar/lang/en.json b/src/addon/calendar/lang/en.json index d5cf329a2..8792b48c5 100644 --- a/src/addon/calendar/lang/en.json +++ b/src/addon/calendar/lang/en.json @@ -3,13 +3,19 @@ "calendarevent": "Calendar event", "calendarevents": "Calendar events", "calendarreminders": "Calendar reminders", + "confirmeventdelete": "Are you sure you want to delete the \"{{$a}}\" event?", + "confirmeventseriesdelete": "The \"{{$a.name}}\" event is part of a series. Do you want to delete just this event, or all {{$a.count}} events in the series?", "defaultnotificationtime": "Default notification time", + "deleteallevents": "Delete all events", + "deleteevent": "Delete event", + "deleteoneevent": "Delete this event", "durationminutes": "Duration in minutes", "durationnone": "Without duration", "durationuntil": "Until", "editevent": "Editing event", "errorloadevent": "Error loading event.", "errorloadevents": "Error loading events.", + "eventcalendareventdeleted": "Calendar event deleted", "eventduration": "Duration", "eventendtime": "End time", "eventkind": "Type of event", diff --git a/src/addon/calendar/pages/event/event.html b/src/addon/calendar/pages/event/event.html index 2608581ae..23e55aac7 100644 --- a/src/addon/calendar/pages/event/event.html +++ b/src/addon/calendar/pages/event/event.html @@ -8,8 +8,10 @@ - - + + + + @@ -18,7 +20,7 @@ - + {{ 'core.hasdatatosync' | translate:{$a: 'addon.calendar.calendarevent' | translate} }} @@ -27,6 +29,9 @@

+ + {{ 'core.deletedoffline' | translate }} +

{{ 'addon.calendar.eventstarttime' | translate}}

diff --git a/src/addon/calendar/pages/event/event.scss b/src/addon/calendar/pages/event/event.scss new file mode 100644 index 000000000..6a9913737 --- /dev/null +++ b/src/addon/calendar/pages/event/event.scss @@ -0,0 +1,5 @@ +ion-app.app-root page-addon-calendar-event { + .card ion-note { + font-size: 1.6rem; + } +} diff --git a/src/addon/calendar/pages/event/event.ts b/src/addon/calendar/pages/event/event.ts index 6fb0a16d1..9f312a3e8 100644 --- a/src/addon/calendar/pages/event/event.ts +++ b/src/addon/calendar/pages/event/event.ts @@ -68,6 +68,7 @@ export class AddonCalendarEventPage implements OnDestroy { defaultTime: number; reminders: any[]; canEdit = false; + canDelete = false; hasOffline = false; isOnline = false; syncIcon: string; // Sync icon. @@ -89,8 +90,9 @@ export class AddonCalendarEventPage implements OnDestroy { this.currentSiteId = sitesProvider.getCurrentSiteId(); this.isSplitViewOn = this.svComponent && this.svComponent.isOn(); - // Check if site supports editing. No need to check allowed types, event.canedit already does it. + // Check if site supports editing and deleting. No need to check allowed types, event.canedit already does it. this.canEdit = this.calendarProvider.canEditEventsInSite(); + this.canDelete = this.calendarProvider.canDeleteEventsInSite(); if (this.notificationsEnabled) { this.calendarProvider.getEventReminders(this.eventId).then((reminders) => { @@ -123,10 +125,10 @@ export class AddonCalendarEventPage implements OnDestroy { this.currentSiteId); // Refresh online status when changes. - this.onlineObserver = network.onchange().subscribe((online) => { + this.onlineObserver = network.onchange().subscribe(() => { // Execute the callback in the Angular zone, so change detection doesn't stop working. zone.run(() => { - this.isOnline = online; + this.isOnline = this.appProvider.isOnline(); }); }); } @@ -150,7 +152,8 @@ export class AddonCalendarEventPage implements OnDestroy { fetchEvent(sync?: boolean, showErrors?: boolean): Promise { const currentSite = this.sitesProvider.getCurrentSite(), canGetById = this.calendarProvider.isGetEventByIdAvailable(); - let promise; + let promise, + deleted = false; this.isOnline = this.appProvider.isOnline(); @@ -161,6 +164,11 @@ export class AddonCalendarEventPage implements OnDestroy { this.domUtils.showErrorModal(result.warnings[0]); } + if (result.deleted && result.deleted.indexOf(this.eventId) != -1) { + // This event was deleted during the sync. + deleted = true; + } + if (result.updated) { // Trigger a manual sync event. result.source = 'event'; @@ -177,6 +185,10 @@ export class AddonCalendarEventPage implements OnDestroy { } return promise.then(() => { + if (deleted) { + return; + } + const promises = []; // Get the event data. @@ -204,6 +216,10 @@ export class AddonCalendarEventPage implements OnDestroy { }); }).then((event) => { + if (deleted) { + return; + } + const promises = []; this.calendarHelper.formatEventData(event); @@ -251,7 +267,7 @@ export class AddonCalendarEventPage implements OnDestroy { this.title = title; // If the event belongs to a course, get the course name and the URL to view it. - if (canGetById && event.course) { + if (canGetById && event.course && event.course.id != this.siteHomeId) { this.courseName = event.course.fullname; this.courseUrl = event.course.viewurl; } else if (event.courseid && event.courseid != this.siteHomeId) { @@ -290,6 +306,11 @@ export class AddonCalendarEventPage implements OnDestroy { event.encodedLocation = this.textUtils.buildAddressURL(event.location); } + // Check if event was deleted in offine. + promises.push(this.calendarOffline.isEventDeleted(this.eventId).then((deleted) => { + event.deleted = deleted; + })); + return Promise.all(promises); }).catch((error) => { this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevent', true); @@ -391,6 +412,97 @@ export class AddonCalendarEventPage implements OnDestroy { navCtrl.push('AddonCalendarEditEventPage', {eventId: this.eventId}); } + /** + * Delete the event. + */ + deleteEvent(): void { + const title = this.translate.instant('addon.calendar.deleteevent'), + options: any = {}; + let message: string; + + if (this.event.eventcount > 1) { + // It's a repeated event. + message = this.translate.instant('addon.calendar.confirmeventseriesdelete', + {$a: {name: this.event.name, count: this.event.eventcount}}); + + options.inputs = [ + { + type: 'radio', + name: 'deleteall', + checked: true, + value: false, + label: this.translate.instant('addon.calendar.deleteoneevent') + }, + { + type: 'radio', + name: 'deleteall', + checked: false, + value: true, + label: this.translate.instant('addon.calendar.deleteallevents') + } + ]; + } else { + // Not repeated, display a simple confirm. + message = this.translate.instant('addon.calendar.confirmeventdelete', {$a: this.event.name}); + } + + this.domUtils.showConfirm(message, title, undefined, undefined, options).then((deleteAll) => { + + const modal = this.domUtils.showModalLoading('core.sending', true); + + this.calendarProvider.deleteEvent(this.event.id, this.event.name, deleteAll).then((sent) => { + + // Trigger an event. + this.eventsProvider.trigger(AddonCalendarProvider.DELETED_EVENT_EVENT, { + eventId: this.eventId, + sent: sent + }, this.sitesProvider.getCurrentSiteId()); + + if (sent) { + this.domUtils.showToast('addon.calendar.eventcalendareventdeleted', true, 3000, undefined, false); + + // Event deleted, close the view. + if (!this.svComponent || !this.svComponent.isOn()) { + this.navCtrl.pop(); + } + } else { + // Event deleted in offline, just mark it as deleted. + this.event.deleted = true; + } + + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'Error deleting event.'); + }).finally(() => { + modal.dismiss(); + }); + }, () => { + // User canceled. + }); + } + + /** + * Undo delete the event. + */ + undoDelete(): void { + const modal = this.domUtils.showModalLoading('core.sending', true); + + this.calendarOffline.unmarkDeleted(this.event.id).then(() => { + + // Trigger an event. + this.eventsProvider.trigger(AddonCalendarProvider.UNDELETED_EVENT_EVENT, { + eventId: this.eventId + }, this.sitesProvider.getCurrentSiteId()); + + this.domUtils.showToast('addon.calendar.eventcalendareventdeleted', true, 3000, undefined, false); + this.event.deleted = false; + + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'Error undeleting event.'); + }).finally(() => { + modal.dismiss(); + }); + } + /** * Check the result of an automatic sync or a manual sync not done by this page. * @@ -398,7 +510,18 @@ export class AddonCalendarEventPage implements OnDestroy { * @param {any} data Sync result. */ protected checkSyncResult(isManual: boolean, data: any): void { - if (data && data.events && (!isManual || data.source != 'event')) { + if (!data) { + return; + } + + if (data.deleted && data.deleted.indexOf(this.eventId) != -1) { + this.domUtils.showToast('addon.calendar.eventcalendareventdeleted', true, 3000, undefined, false); + + // Event was deleted, close the view. + if (!this.svComponent || !this.svComponent.isOn()) { + this.navCtrl.pop(); + } + } else if (data.events && (!isManual || data.source != 'event')) { const event = data.events.find((ev) => { return ev.id == this.eventId; }); diff --git a/src/addon/calendar/pages/list/list.html b/src/addon/calendar/pages/list/list.html index a224599f7..bd5c848ad 100644 --- a/src/addon/calendar/pages/list/list.html +++ b/src/addon/calendar/pages/list/list.html @@ -40,9 +40,13 @@ - {{ (event.timestart + event.timeduration) * 1000 | coreFormatDate: "strftimetime" }} - {{ (event.timestart + event.timeduration) * 1000 | coreFormatDate: "strftimedatetimeshort" }}

- + - {{ 'core.notsent' | translate }} + {{ 'core.notsent' | translate }} + + + + {{ 'core.deletedoffline' | translate }} diff --git a/src/addon/calendar/pages/list/list.scss b/src/addon/calendar/pages/list/list.scss new file mode 100644 index 000000000..9f40d9746 --- /dev/null +++ b/src/addon/calendar/pages/list/list.scss @@ -0,0 +1,5 @@ +ion-app.app-root page-addon-calendar-list { + ion-note { + max-width: 30%; + } +} diff --git a/src/addon/calendar/pages/list/list.ts b/src/addon/calendar/pages/list/list.ts index e3ce88a44..0dac7f74b 100644 --- a/src/addon/calendar/pages/list/list.ts +++ b/src/addon/calendar/pages/list/list.ts @@ -63,16 +63,19 @@ export class AddonCalendarListPage implements OnDestroy { protected newEventObserver: any; protected discardedObserver: any; protected editEventObserver: any; + protected deleteEventObserver: any; + protected undeleteEventObserver: any; protected syncObserver: any; protected manualSyncObserver: any; protected onlineObserver: any; protected currentSiteId: string; + protected onlineEvents = []; + protected offlineEvents = []; + protected deletedEvents = []; courses: any[]; eventsLoaded = false; events = []; // Events (both online and offline). - onlineEvents = []; - offlineEvents = []; notificationsEnabled = false; filteredEvents = []; canLoadMore = false; @@ -149,6 +152,11 @@ export class AddonCalendarListPage implements OnDestroy { this.syncObserver = eventsProvider.on(AddonCalendarSyncProvider.AUTO_SYNCED, (data) => { this.eventsLoaded = false; this.refreshEvents(); + + if (this.splitviewCtrl.isOn() && this.eventId && data && data.deleted && data.deleted.indexOf(this.eventId) != -1) { + // Current selected event was deleted. Clear details. + this.splitviewCtrl.emptyDetails(); + } }, this.currentSiteId); // Refresh data if calendar events are synchronized manually but not by this page. @@ -157,13 +165,51 @@ export class AddonCalendarListPage implements OnDestroy { this.eventsLoaded = false; this.refreshEvents(); } + + if (this.splitviewCtrl.isOn() && this.eventId && data && data.deleted && data.deleted.indexOf(this.eventId) != -1) { + // Current selected event was deleted. Clear details. + this.splitviewCtrl.emptyDetails(); + } + }, this.currentSiteId); + + // Update the list when an event is deleted. + this.deleteEventObserver = eventsProvider.on(AddonCalendarProvider.DELETED_EVENT_EVENT, (data) => { + if (data && !data.sent) { + // Event was deleted in offline. Just mark it as deleted, no need to refresh. + this.markAsDeleted(data.eventId, true); + this.hasOffline = true; + } else { + // Event deleted, clear the details if needed and refresh the view. + if (this.splitviewCtrl.isOn()) { + this.splitviewCtrl.emptyDetails(); + } + + this.eventsLoaded = false; + this.refreshEvents(); + } + }, this.currentSiteId); + + // Listen for events "undeleted" (offline). + this.undeleteEventObserver = eventsProvider.on(AddonCalendarProvider.UNDELETED_EVENT_EVENT, (data) => { + if (data && data.eventId) { + // Mark it as undeleted, no need to refresh. + this.markAsDeleted(data.eventId, false); + + // Remove it from the list of deleted events if it's there. + const index = this.deletedEvents.indexOf(data.eventId); + if (index != -1) { + this.deletedEvents.splice(index, 1); + } + + this.hasOffline = !!this.offlineEvents.length || !!this.deletedEvents.length; + } }, this.currentSiteId); // Refresh online status when changes. - this.onlineObserver = network.onchange().subscribe((online) => { + this.onlineObserver = network.onchange().subscribe(() => { // Execute the callback in the Angular zone, so change detection doesn't stop working. zone.run(() => { - this.isOnline = online; + this.isOnline = this.appProvider.isOnline(); }); }); } @@ -234,6 +280,8 @@ export class AddonCalendarListPage implements OnDestroy { const promises = []; const courseId = this.filter.course.id != this.allCourses.id ? this.filter.course.id : undefined; + this.hasOffline = false; + promises.push(this.calendarHelper.canEditEvents(courseId).then((canEdit) => { this.canCreate = canEdit; })); @@ -254,8 +302,8 @@ export class AddonCalendarListPage implements OnDestroy { })); // Get offline events. - promises.push(this.calendarOffline.getAllEvents().then((events) => { - this.hasOffline = !!events.length; + promises.push(this.calendarOffline.getAllEditedEvents().then((events) => { + this.hasOffline = this.hasOffline || !!events.length; // Format data and sort by timestart. events.forEach((event) => { @@ -265,6 +313,12 @@ export class AddonCalendarListPage implements OnDestroy { this.offlineEvents = this.sortEvents(events); })); + // Get events deleted in offline. + promises.push(this.calendarOffline.getAllDeletedEventsIds().then((ids) => { + this.hasOffline = this.hasOffline || !!ids.length; + this.deletedEvents = ids; + })); + return Promise.all(promises); }).finally(() => { this.eventsLoaded = true; @@ -457,22 +511,32 @@ export class AddonCalendarListPage implements OnDestroy { * @return {any[]} Merged events. */ protected mergeEvents(onlineEvents: any[]): any[] { - if (!this.offlineEvents || !this.offlineEvents.length) { + if (!this.offlineEvents.length && !this.deletedEvents.length) { // No offline events, nothing to merge. return onlineEvents; } const start = this.initialTime + (CoreConstants.SECONDS_DAY * this.daysLoaded), end = start + (CoreConstants.SECONDS_DAY * AddonCalendarProvider.DAYS_INTERVAL) - 1; + let result = onlineEvents; - // First of all, remove the online events that were modified in offline. - let result = onlineEvents.filter((event) => { - const offlineEvent = this.offlineEvents.find((ev) => { - return ev.id == event.id; + if (this.deletedEvents.length) { + // Mark as deleted the events that were deleted in offline. + result.forEach((event) => { + event.deleted = this.deletedEvents.indexOf(event.id) != -1; }); + } - return !offlineEvent; - }); + if (this.offlineEvents.length) { + // Remove the online events that were modified in offline. + result = result.filter((event) => { + const offlineEvent = this.offlineEvents.find((ev) => { + return ev.id == event.id; + }); + + return !offlineEvent; + }); + } // Now get the offline events that belong to this period. const periodOfflineEvents = this.offlineEvents.filter((event) => { @@ -658,6 +722,22 @@ export class AddonCalendarListPage implements OnDestroy { } } + /** + * Find an event and mark it as deleted. + * + * @param {number} eventId Event ID. + * @param {boolean} deleted Whether to mark it as deleted or not. + */ + protected markAsDeleted(eventId: number, deleted: boolean): void { + const event = this.onlineEvents.find((event) => { + return event.id == eventId; + }); + + if (event) { + event.deleted = deleted; + } + } + /** * Page destroyed. */ @@ -666,6 +746,8 @@ export class AddonCalendarListPage implements OnDestroy { this.newEventObserver && this.newEventObserver.off(); this.discardedObserver && this.discardedObserver.off(); this.editEventObserver && this.editEventObserver.off(); + this.deleteEventObserver && this.deleteEventObserver.off(); + this.undeleteEventObserver && this.undeleteEventObserver.off(); this.syncObserver && this.syncObserver.off(); this.manualSyncObserver && this.manualSyncObserver.off(); this.onlineObserver && this.onlineObserver.unsubscribe(); diff --git a/src/addon/calendar/providers/calendar-offline.ts b/src/addon/calendar/providers/calendar-offline.ts index a929113bc..3d48baa38 100644 --- a/src/addon/calendar/providers/calendar-offline.ts +++ b/src/addon/calendar/providers/calendar-offline.ts @@ -14,6 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; +import { CoreUtilsProvider } from '@providers/utils/utils'; /** * Service to handle offline calendar events. @@ -23,6 +24,7 @@ export class AddonCalendarOfflineProvider { // Variables for database. static EVENTS_TABLE = 'addon_calendar_offline_events'; + static DELETED_EVENTS_TABLE = 'addon_calendar_deleted_events'; protected siteSchema: CoreSiteSchema = { name: 'AddonCalendarOfflineProvider', @@ -34,6 +36,7 @@ export class AddonCalendarOfflineProvider { { name: 'id', // Negative for offline entries. type: 'INTEGER', + primaryKey: true }, { name: 'name', @@ -110,13 +113,35 @@ export class AddonCalendarOfflineProvider { name: 'timecreated', type: 'INTEGER', } - ], - primaryKeys: ['id'] + ] + }, + { + name: AddonCalendarOfflineProvider.DELETED_EVENTS_TABLE, + columns: [ + { + name: 'id', + type: 'INTEGER', + primaryKey: true + }, + { + name: 'name', // Save the name to be able to notify the user. + type: 'TEXT', + notNull: true + }, + { + name: 'repeat', + type: 'INTEGER' + }, + { + name: 'timemodified', + type: 'INTEGER', + } + ] } ] }; - constructor(private sitesProvider: CoreSitesProvider) { + constructor(private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider) { this.sitesProvider.registerSiteSchema(this.siteSchema); } @@ -138,17 +163,91 @@ export class AddonCalendarOfflineProvider { } /** - * Get all offline events. + * Get the IDs of all the events created/edited/deleted in offline. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the IDs. + */ + getAllEventsIds(siteId?: string): Promise { + const promises = []; + + promises.push(this.getAllDeletedEventsIds(siteId)); + promises.push(this.getAllEditedEventsIds(siteId)); + + return Promise.all(promises).then((result) => { + return this.utils.mergeArraysWithoutDuplicates(result[0], result[1]); + }); + } + + /** + * Get all the events deleted in offline. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with all the events deleted in offline. + */ + getAllDeletedEvents(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecords(AddonCalendarOfflineProvider.DELETED_EVENTS_TABLE); + }); + } + + /** + * Get the IDs of all the events deleted in offline. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the IDs of all the events deleted in offline. + */ + getAllDeletedEventsIds(siteId?: string): Promise { + return this.getAllDeletedEvents(siteId).then((events) => { + return events.map((event) => { + return event.id; + }); + }); + } + + /** + * Get all the events created/edited in offline. * * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved with events. */ - getAllEvents(siteId?: string): Promise { + getAllEditedEvents(siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { return site.getDb().getRecords(AddonCalendarOfflineProvider.EVENTS_TABLE); }); } + /** + * Get the IDs of all the events created/edited in offline. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with events IDs. + */ + getAllEditedEventsIds(siteId?: string): Promise { + return this.getAllEditedEvents(siteId).then((events) => { + return events.map((event) => { + return event.id; + }); + }); + } + + /** + * Get an event deleted in offline. + * + * @param {number} eventId Event ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the deleted event. + */ + getDeletedEvent(eventId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const conditions: any = { + id: eventId + }; + + return site.getDb().getRecord(AddonCalendarOfflineProvider.DELETED_EVENTS_TABLE, conditions); + }); + } + /** * Get an offline event. * @@ -172,8 +271,8 @@ export class AddonCalendarOfflineProvider { * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved with boolean: true if has offline events, false otherwise. */ - hasEvents(siteId?: string): Promise { - return this.getAllEvents(siteId).then((events) => { + hasEditedEvents(siteId?: string): Promise { + return this.getAllEditedEvents(siteId).then((events) => { return !!events.length; }).catch(() => { // No offline data found, return false. @@ -181,6 +280,43 @@ export class AddonCalendarOfflineProvider { }); } + /** + * Check if an event is deleted. + * + * @param {number} eventId Event ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with boolean: whether the event is deleted. + */ + isEventDeleted(eventId: number, siteId?: string): Promise { + return this.getDeletedEvent(eventId, siteId).then((event) => { + return !!event; + }).catch(() => { + return false; + }); + } + + /** + * Mark an event as deleted. + * + * @param {number} eventId Event ID to delete. + * @param {number} name Name of the event to delete. + * @param {boolean} [deleteAll] If it's a repeated event. whether to delete all events of the series. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when done. + */ + markDeleted(eventId: number, name: string, deleteAll?: boolean, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const event = { + id: eventId, + name: name || '', + repeat: deleteAll ? 1 : 0, + timemodified: Date.now() + }; + + return site.getDb().insertRecord(AddonCalendarOfflineProvider.DELETED_EVENTS_TABLE, event); + }); + } + /** * Offline version for adding a new discussion to a forum. * @@ -221,4 +357,21 @@ export class AddonCalendarOfflineProvider { }); }); } + + /** + * Unmark an event as deleted. + * + * @param {number} eventId Event ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if deleted, rejected if failure. + */ + unmarkDeleted(eventId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const conditions: any = { + id: eventId + }; + + return site.getDb().deleteRecords(AddonCalendarOfflineProvider.DELETED_EVENTS_TABLE, conditions); + }); + } } diff --git a/src/addon/calendar/providers/calendar-sync.ts b/src/addon/calendar/providers/calendar-sync.ts index 834d1ac34..6b81d39ae 100644 --- a/src/addon/calendar/providers/calendar-sync.ts +++ b/src/addon/calendar/providers/calendar-sync.ts @@ -81,7 +81,8 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider { // Sync successful, send event. this.eventsProvider.trigger(AddonCalendarSyncProvider.AUTO_SYNCED, { warnings: result.warnings, - events: result.events + events: result.events, + deleted: result.deleted }, siteId); } }); @@ -122,18 +123,19 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider { const result = { warnings: [], events: [], + deleted: [], updated: false }; - let offlineEvents; + let offlineEventIds: number[]; // Get offline events. - const syncPromise = this.calendarOffline.getAllEvents(siteId).catch(() => { + const syncPromise = this.calendarOffline.getAllEventsIds(siteId).catch(() => { // No offline data found, return empty list. return []; - }).then((events) => { - offlineEvents = events; + }).then((eventIds) => { + offlineEventIds = eventIds; - if (!events.length) { + if (!eventIds.length) { // Nothing to sync. return; } else if (!this.appProvider.isOnline()) { @@ -143,8 +145,8 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider { const promises = []; - events.forEach((event) => { - promises.push(this.syncOfflineEvent(event, result, siteId)); + offlineEventIds.forEach((eventId) => { + promises.push(this.syncOfflineEvent(eventId, result, siteId)); }); return this.utils.allPromises(promises); @@ -155,10 +157,10 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider { this.calendarProvider.invalidateEventsList(siteId), ]; - offlineEvents.forEach((event) => { - if (event.id > 0) { + offlineEventIds.forEach((eventId) => { + if (eventId > 0) { // An event was edited, invalidate its data too. - promises.push(this.calendarProvider.invalidateEvent(event.id, siteId)); + promises.push(this.calendarProvider.invalidateEvent(eventId, siteId)); } }); @@ -182,47 +184,95 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider { /** * Synchronize an offline event. * - * @param {any} event The event to sync. + * @param {number} eventId The event ID to sync. * @param {any} result Object where to store the result of the sync. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved if sync is successful, rejected otherwise. */ - protected syncOfflineEvent(event: any, result: any, siteId?: string): Promise { + protected syncOfflineEvent(eventId: number, result: any, siteId?: string): Promise { // Verify that event isn't blocked. - if (this.syncProvider.isBlocked(AddonCalendarProvider.COMPONENT, event.id, siteId)) { - this.logger.debug('Cannot sync event ' + event.name + ' because it is blocked.'); + if (this.syncProvider.isBlocked(AddonCalendarProvider.COMPONENT, eventId, siteId)) { + this.logger.debug('Cannot sync event ' + eventId + ' because it is blocked.'); return Promise.reject(this.translate.instant('core.errorsyncblocked', {$a: this.translate.instant('addon.calendar.calendarevent')})); } - // Try to send the data. - const data = this.utils.clone(event); // Clone the object because it will be modified in the submit function. - - return this.calendarProvider.submitEventOnline(event.id > 0 ? event.id : undefined, data, siteId).then((newEvent) => { - result.updated = true; - result.events.push(newEvent); - - // Event sent, delete the offline data. - return this.calendarOffline.deleteEvent(event.id, siteId); - }).catch((error) => { - if (this.utils.isWebServiceError(error)) { - // The WebService has thrown an error, this means that the event cannot be created. Delete it. + // First of all, check if the event has been deleted. + return this.calendarOffline.getDeletedEvent(eventId, siteId).then((data) => { + // Delete the event. + return this.calendarProvider.deleteEventOnline(data.id, data.repeat, siteId).then(() => { result.updated = true; + result.deleted.push(eventId); - return this.calendarOffline.deleteEvent(event.id, siteId).then(() => { - // Event deleted, add a warning. - result.warnings.push(this.translate.instant('core.warningofflinedatadeleted', { - component: this.translate.instant('addon.calendar.calendarevent'), - name: event.name, - error: this.textUtils.getErrorMessageFromError(error) + // Event sent, delete the offline data. + const promises = []; + + promises.push(this.calendarOffline.unmarkDeleted(eventId, siteId)); + promises.push(this.calendarOffline.deleteEvent(eventId, siteId).catch(() => { + // Ignore errors, maybe there was no edit data. + })); + + return Promise.all(promises); + }).catch((error) => { + + if (this.utils.isWebServiceError(error)) { + // The WebService has thrown an error, this means that the event cannot be created. Delete it. + result.updated = true; + + const promises = []; + + promises.push(this.calendarOffline.unmarkDeleted(eventId, siteId)); + promises.push(this.calendarOffline.deleteEvent(eventId, siteId).catch(() => { + // Ignore errors, maybe there was no edit data. })); - }); - } - // Local error, reject. - return Promise.reject(error); + return Promise.all(promises).then(() => { + // Event deleted, add a warning. + result.warnings.push(this.translate.instant('core.warningofflinedatadeleted', { + component: this.translate.instant('addon.calendar.calendarevent'), + name: data.name, + error: this.textUtils.getErrorMessageFromError(error) + })); + }); + } + + // Local error, reject. + return Promise.reject(error); + }); + }, () => { + + // Not deleted. Now get the event data. + return this.calendarOffline.getEvent(eventId, siteId).then((event) => { + // Try to send the data. + const data = this.utils.clone(event); // Clone the object because it will be modified in the submit function. + + return this.calendarProvider.submitEventOnline(eventId > 0 ? eventId : undefined, data, siteId).then((newEvent) => { + result.updated = true; + result.events.push(newEvent); + + // Event sent, delete the offline data. + return this.calendarOffline.deleteEvent(event.id, siteId); + }).catch((error) => { + if (this.utils.isWebServiceError(error)) { + // The WebService has thrown an error, this means that the event cannot be created. Delete it. + result.updated = true; + + return this.calendarOffline.deleteEvent(event.id, siteId).then(() => { + // Event deleted, add a warning. + result.warnings.push(this.translate.instant('core.warningofflinedatadeleted', { + component: this.translate.instant('addon.calendar.calendarevent'), + name: event.name, + error: this.textUtils.getErrorMessageFromError(error) + })); + }); + } + + // Local error, reject. + return Promise.reject(error); + }); + }); }); } } diff --git a/src/addon/calendar/providers/calendar.ts b/src/addon/calendar/providers/calendar.ts index bfd3a03df..9e34520a1 100644 --- a/src/addon/calendar/providers/calendar.ts +++ b/src/addon/calendar/providers/calendar.ts @@ -42,6 +42,8 @@ export class AddonCalendarProvider { static NEW_EVENT_EVENT = 'addon_calendar_new_event'; static NEW_EVENT_DISCARDED_EVENT = 'addon_calendar_new_event_discarded'; static EDIT_EVENT_EVENT = 'addon_calendar_edit_event'; + static DELETED_EVENT_EVENT = 'addon_calendar_deleted_event'; + static UNDELETED_EVENT_EVENT = 'addon_calendar_undeleted_event'; static TYPE_CATEGORY = 'category'; static TYPE_COURSE = 'course'; static TYPE_GROUP = 'group'; @@ -225,11 +227,38 @@ export class AddonCalendarProvider { this.sitesProvider.registerSiteSchema(this.siteSchema); } + /** + * Check if a certain site allows deleting events. + * + * @param {string} [siteId] Site Id. If not defined, use current site. + * @return {Promise} Promise resolved with true if can delete. + * @since 3.3 + */ + canDeleteEvents(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return this.canDeleteEventsInSite(site); + }); + } + + /** + * Check if a certain site allows deleting events. + * + * @param {CoreSite} [site] Site. If not defined, use current site. + * @return {boolean} Whether events can be deleted. + * @since 3.3 + */ + canDeleteEventsInSite(site?: CoreSite): boolean { + site = site || this.sitesProvider.getCurrentSite(); + + return site.wsAvailable('core_calendar_delete_calendar_events'); + } + /** * Check if a certain site allows creating and editing events. * * @param {string} [siteId] Site Id. If not defined, use current site. * @return {Promise} Promise resolved with true if can create/edit. + * @since 3.7.1 */ canEditEvents(siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { @@ -242,6 +271,7 @@ export class AddonCalendarProvider { * * @param {CoreSite} [site] Site. If not defined, use current site. * @return {boolean} Whether events can be created and edited. + * @since 3.7.1 */ canEditEventsInSite(site?: CoreSite): boolean { site = site || this.sitesProvider.getCurrentSite(); @@ -261,20 +291,89 @@ export class AddonCalendarProvider { return site.getDb().getRecordsSelect(AddonCalendarProvider.EVENTS_TABLE, 'timestart + timeduration < ?', [this.timeUtils.timestamp()]).then((events) => { return Promise.all(events.map((event) => { - return this.deleteEvent(event.id, siteId); + return this.deleteLocalEvent(event.id, siteId); })); }); }); } /** - * Delete event cancelling all the reminders and notifications. + * Delete an event. + * + * @param {number} eventId Event ID to delete. + * @param {string} name Name of the event to delete. + * @param {boolean} [deleteAll] If it's a repeated event. whether to delete all events of the series. + * @param {boolean} [forceOffline] True to always save it in offline. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when done. + */ + deleteEvent(eventId: number, name: string, deleteAll?: boolean, forceOffline?: boolean, siteId?: string): Promise { + + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + // Function to store the submission to be synchronized later. + const storeOffline = (): Promise => { + return this.calendarOffline.markDeleted(eventId, name, deleteAll, siteId).then(() => { + return false; + }); + }; + + if (forceOffline || !this.appProvider.isOnline()) { + // App is offline, store the action. + return storeOffline(); + } + + // If the event is already stored, discard it first. + return this.calendarOffline.unmarkDeleted(eventId, siteId).then(() => { + return this.deleteEventOnline(eventId, deleteAll, siteId).then(() => { + return true; + }).catch((error) => { + if (error && !this.utils.isWebServiceError(error)) { + // Couldn't connect to server, store in offline. + return storeOffline(); + } else { + // The WebService has thrown an error, reject. + return Promise.reject(error); + } + }); + }); + } + + /** + * Delete an event. It will fail if offline or cannot connect. + * + * @param {number} eventId Event ID to delete. + * @param {boolean} [deleteAll] If it's a repeated event. whether to delete all events of the series. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when done. + */ + deleteEventOnline(eventId: number, deleteAll?: boolean, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + + const params = { + events: [ + { + eventid: eventId, + repeat: deleteAll ? 1 : 0 + } + ] + }, + preSets = { + responseExpected: false + }; + + return site.write('core_calendar_delete_calendar_events', params, preSets); + }); + } + + /** + * Delete a locally stored event cancelling all the reminders and notifications. * * @param {number} eventId Event ID. * @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site. * @return {Promise} Resolved when done. */ - protected deleteEvent(eventId: number, siteId?: string): Promise { + protected deleteLocalEvent(eventId: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { siteId = site.getId(); @@ -833,7 +932,7 @@ export class AddonCalendarProvider { if (timeEnd <= new Date().getTime()) { // The event has finished already, don't schedule it. - return this.deleteEvent(event.id, siteId); + return this.deleteLocalEvent(event.id, siteId); } return this.getEventReminders(event.id, siteId).then((reminders) => { diff --git a/src/addon/mod/chat/pages/chat/chat.ts b/src/addon/mod/chat/pages/chat/chat.ts index 99bcbec94..41bd0e1ca 100644 --- a/src/addon/mod/chat/pages/chat/chat.ts +++ b/src/addon/mod/chat/pages/chat/chat.ts @@ -65,7 +65,7 @@ export class AddonModChatChatPage { this.logger = logger.getInstance('AddonModChoiceChoicePage'); this.currentUserBeep = 'beep ' + sitesProvider.getCurrentSiteUserId(); this.isOnline = this.appProvider.isOnline(); - this.onlineObserver = network.onchange().subscribe((online) => { + this.onlineObserver = network.onchange().subscribe(() => { // Execute the callback in the Angular zone, so change detection doesn't stop working. zone.run(() => { this.isOnline = this.appProvider.isOnline(); diff --git a/src/addon/mod/chat/pages/users/users.ts b/src/addon/mod/chat/pages/users/users.ts index a9f4f175a..90e59df6d 100644 --- a/src/addon/mod/chat/pages/users/users.ts +++ b/src/addon/mod/chat/pages/users/users.ts @@ -44,7 +44,7 @@ export class AddonModChatUsersPage { this.sessionId = navParams.get('sessionId'); this.isOnline = this.appProvider.isOnline(); this.currentUserId = this.sitesProvider.getCurrentSiteUserId(); - this.onlineObserver = network.onchange().subscribe((online) => { + this.onlineObserver = network.onchange().subscribe(() => { // Execute the callback in the Angular zone, so change detection doesn't stop working. zone.run(() => { this.isOnline = this.appProvider.isOnline(); diff --git a/src/addon/mod/feedback/pages/form/form.ts b/src/addon/mod/feedback/pages/form/form.ts index 65a21bec8..f1da26ca8 100644 --- a/src/addon/mod/feedback/pages/form/form.ts +++ b/src/addon/mod/feedback/pages/form/form.ts @@ -82,10 +82,10 @@ export class AddonModFeedbackFormPage implements OnDestroy { this.currentSite = sitesProvider.getCurrentSite(); // Refresh online status when changes. - this.onlineObserver = network.onchange().subscribe((online) => { + this.onlineObserver = network.onchange().subscribe(() => { // Execute the callback in the Angular zone, so change detection doesn't stop working. zone.run(() => { - this.offline = !online; + this.offline = !this.appProvider.isOnline(); }); }); } diff --git a/src/addon/mod/forum/pages/discussion/discussion.ts b/src/addon/mod/forum/pages/discussion/discussion.ts index 68598a18e..7c395fc57 100644 --- a/src/addon/mod/forum/pages/discussion/discussion.ts +++ b/src/addon/mod/forum/pages/discussion/discussion.ts @@ -115,7 +115,7 @@ export class AddonModForumDiscussionPage implements OnDestroy { this.postId = navParams.get('postId'); this.isOnline = this.appProvider.isOnline(); - this.onlineObserver = network.onchange().subscribe((online) => { + this.onlineObserver = network.onchange().subscribe(() => { // Execute the callback in the Angular zone, so change detection doesn't stop working. zone.run(() => { this.isOnline = this.appProvider.isOnline(); diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 12b6ecacf..3052c5473 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -87,13 +87,19 @@ "addon.calendar.calendarevent": "Calendar event", "addon.calendar.calendarevents": "Calendar events", "addon.calendar.calendarreminders": "Calendar reminders", + "addon.calendar.confirmeventdelete": "Are you sure you want to delete the \"{{$a}}\" event?", + "addon.calendar.confirmeventseriesdelete": "The \"{{$a.name}}\" event is part of a series. Do you want to delete just this event, or all {{$a.count}} events in the series?", "addon.calendar.defaultnotificationtime": "Default notification time", + "addon.calendar.deleteallevents": "Delete all events", + "addon.calendar.deleteevent": "Delete event", + "addon.calendar.deleteoneevent": "Delete this event", "addon.calendar.durationminutes": "Duration in minutes", "addon.calendar.durationnone": "Without duration", "addon.calendar.durationuntil": "Until", "addon.calendar.editevent": "Editing event", "addon.calendar.errorloadevent": "Error loading event.", "addon.calendar.errorloadevents": "Error loading events.", + "addon.calendar.eventcalendareventdeleted": "Calendar event deleted", "addon.calendar.eventduration": "Duration", "addon.calendar.eventendtime": "End time", "addon.calendar.eventkind": "Type of event", diff --git a/src/core/course/classes/main-activity-component.ts b/src/core/course/classes/main-activity-component.ts index 925f9109b..752a140e2 100644 --- a/src/core/course/classes/main-activity-component.ts +++ b/src/core/course/classes/main-activity-component.ts @@ -63,10 +63,10 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR const zone = injector.get(NgZone); // Refresh online status when changes. - this.onlineObserver = network.onchange().subscribe((online) => { + this.onlineObserver = network.onchange().subscribe(() => { // Execute the callback in the Angular zone, so change detection doesn't stop working. zone.run(() => { - this.isOnline = online; + this.isOnline = this.appProvider.isOnline(); }); }); } diff --git a/src/providers/utils/dom.ts b/src/providers/utils/dom.ts index 4a9ad7ed6..09babf387 100644 --- a/src/providers/utils/dom.ts +++ b/src/providers/utils/dom.ts @@ -1389,9 +1389,12 @@ export class CoreDomUtilsProvider { * @param {boolean} [needsTranslate] Whether the 'text' needs to be translated. * @param {number} [duration=2000] Duration in ms of the dimissable toast. * @param {string} [cssClass=""] Class to add to the toast. + * @param {boolean} [dismissOnPageChange=true] Dismiss the Toast on page change. * @return {Toast} Toast instance. */ - showToast(text: string, needsTranslate?: boolean, duration: number = 2000, cssClass: string = ''): Toast { + showToast(text: string, needsTranslate?: boolean, duration: number = 2000, cssClass: string = '', + dismissOnPageChange: boolean = true): Toast { + if (needsTranslate) { text = this.translate.instant(text); } @@ -1401,7 +1404,7 @@ export class CoreDomUtilsProvider { duration: duration, position: 'bottom', cssClass: cssClass, - dismissOnPageChange: true + dismissOnPageChange: dismissOnPageChange }); loader.present();