MOBILE-3090 calendar: Support deleting events

main
Dani Palou 2019-06-28 11:42:35 +02:00
parent a9d62274c9
commit 1d8f617338
18 changed files with 628 additions and 81 deletions

View File

@ -87,13 +87,19 @@
"addon.calendar.calendarevent": "local_moodlemobileapp", "addon.calendar.calendarevent": "local_moodlemobileapp",
"addon.calendar.calendarevents": "local_moodlemobileapp", "addon.calendar.calendarevents": "local_moodlemobileapp",
"addon.calendar.calendarreminders": "local_moodlemobileapp", "addon.calendar.calendarreminders": "local_moodlemobileapp",
"addon.calendar.confirmeventdelete": "calendar",
"addon.calendar.confirmeventseriesdelete": "calendar",
"addon.calendar.defaultnotificationtime": "local_moodlemobileapp", "addon.calendar.defaultnotificationtime": "local_moodlemobileapp",
"addon.calendar.deleteallevents": "calendar",
"addon.calendar.deleteevent": "calendar",
"addon.calendar.deleteoneevent": "calendar",
"addon.calendar.durationminutes": "calendar", "addon.calendar.durationminutes": "calendar",
"addon.calendar.durationnone": "calendar", "addon.calendar.durationnone": "calendar",
"addon.calendar.durationuntil": "calendar", "addon.calendar.durationuntil": "calendar",
"addon.calendar.editevent": "calendar", "addon.calendar.editevent": "calendar",
"addon.calendar.errorloadevent": "local_moodlemobileapp", "addon.calendar.errorloadevent": "local_moodlemobileapp",
"addon.calendar.errorloadevents": "local_moodlemobileapp", "addon.calendar.errorloadevents": "local_moodlemobileapp",
"addon.calendar.eventcalendareventdeleted": "calendar",
"addon.calendar.eventduration": "calendar", "addon.calendar.eventduration": "calendar",
"addon.calendar.eventendtime": "calendar", "addon.calendar.eventendtime": "calendar",
"addon.calendar.eventkind": "calendar", "addon.calendar.eventkind": "calendar",

View File

@ -3,13 +3,19 @@
"calendarevent": "Calendar event", "calendarevent": "Calendar event",
"calendarevents": "Calendar events", "calendarevents": "Calendar events",
"calendarreminders": "Calendar reminders", "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", "defaultnotificationtime": "Default notification time",
"deleteallevents": "Delete all events",
"deleteevent": "Delete event",
"deleteoneevent": "Delete this event",
"durationminutes": "Duration in minutes", "durationminutes": "Duration in minutes",
"durationnone": "Without duration", "durationnone": "Without duration",
"durationuntil": "Until", "durationuntil": "Until",
"editevent": "Editing event", "editevent": "Editing event",
"errorloadevent": "Error loading event.", "errorloadevent": "Error loading event.",
"errorloadevents": "Error loading events.", "errorloadevents": "Error loading events.",
"eventcalendareventdeleted": "Calendar event deleted",
"eventduration": "Duration", "eventduration": "Duration",
"eventendtime": "End time", "eventendtime": "End time",
"eventkind": "Type of event", "eventkind": "Type of event",

View File

@ -8,8 +8,10 @@
</ion-header> </ion-header>
<core-navbar-buttons end> <core-navbar-buttons end>
<core-context-menu> <core-context-menu>
<core-context-menu-item [hidden]="isSplitViewOn || !eventLoaded || !hasOffline || !isOnline" [priority]="400" [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(null, $event, true)" [iconAction]="syncIcon" [closeOnClick]="false"></core-context-menu-item> <core-context-menu-item [hidden]="isSplitViewOn || !eventLoaded || (!hasOffline && !event.deleted) || !isOnline" [priority]="400" [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(null, $event, true)" [iconAction]="syncIcon" [closeOnClick]="false"></core-context-menu-item>
<core-context-menu-item [hidden]="!canEdit || !event || !event.canedit" [priority]="300" [content]="'core.edit' | translate" (action)="openEdit()" [iconAction]="'create'"></core-context-menu-item> <core-context-menu-item [hidden]="!canEdit || !event || !event.canedit || event.deleted" [priority]="300" [content]="'core.edit' | translate" (action)="openEdit()" [iconAction]="'create'"></core-context-menu-item>
<core-context-menu-item [hidden]="!canDelete || !event || !event.candelete || event.deleted" [priority]="200" [content]="'core.delete' | translate" (action)="deleteEvent()" [iconAction]="'trash'"></core-context-menu-item>
<core-context-menu-item [hidden]="!event || !event.deleted" [priority]="200" [content]="'core.restore' | translate" (action)="undoDelete()" [iconAction]="'undo'"></core-context-menu-item>
</core-context-menu> </core-context-menu>
</core-navbar-buttons> </core-navbar-buttons>
<ion-content> <ion-content>
@ -18,7 +20,7 @@
</ion-refresher> </ion-refresher>
<core-loading [hideUntil]="eventLoaded"> <core-loading [hideUntil]="eventLoaded">
<!-- There is data to be synchronized --> <!-- There is data to be synchronized -->
<ion-card class="core-warning-card" icon-start *ngIf="hasOffline"> <ion-card class="core-warning-card" icon-start *ngIf="hasOffline || event.deleted">
<ion-icon name="warning"></ion-icon> {{ 'core.hasdatatosync' | translate:{$a: 'addon.calendar.calendarevent' | translate} }} <ion-icon name="warning"></ion-icon> {{ 'core.hasdatatosync' | translate:{$a: 'addon.calendar.calendarevent' | translate} }}
</ion-card> </ion-card>
@ -27,6 +29,9 @@
<ion-item text-wrap> <ion-item text-wrap>
<core-icon *ngIf="event.icon && !event.moduleIcon" [name]="event.icon" item-start></core-icon> <core-icon *ngIf="event.icon && !event.moduleIcon" [name]="event.icon" item-start></core-icon>
<h2><core-format-text [text]="event.name"></core-format-text></h2> <h2><core-format-text [text]="event.name"></core-format-text></h2>
<ion-note item-end *ngIf="event.deleted">
<ion-icon name="trash"></ion-icon> {{ 'core.deletedoffline' | translate }}
</ion-note>
</ion-item> </ion-item>
<ion-item text-wrap> <ion-item text-wrap>
<h2>{{ 'addon.calendar.eventstarttime' | translate}}</h2> <h2>{{ 'addon.calendar.eventstarttime' | translate}}</h2>

View File

@ -0,0 +1,5 @@
ion-app.app-root page-addon-calendar-event {
.card ion-note {
font-size: 1.6rem;
}
}

View File

@ -68,6 +68,7 @@ export class AddonCalendarEventPage implements OnDestroy {
defaultTime: number; defaultTime: number;
reminders: any[]; reminders: any[];
canEdit = false; canEdit = false;
canDelete = false;
hasOffline = false; hasOffline = false;
isOnline = false; isOnline = false;
syncIcon: string; // Sync icon. syncIcon: string; // Sync icon.
@ -89,8 +90,9 @@ export class AddonCalendarEventPage implements OnDestroy {
this.currentSiteId = sitesProvider.getCurrentSiteId(); this.currentSiteId = sitesProvider.getCurrentSiteId();
this.isSplitViewOn = this.svComponent && this.svComponent.isOn(); 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.canEdit = this.calendarProvider.canEditEventsInSite();
this.canDelete = this.calendarProvider.canDeleteEventsInSite();
if (this.notificationsEnabled) { if (this.notificationsEnabled) {
this.calendarProvider.getEventReminders(this.eventId).then((reminders) => { this.calendarProvider.getEventReminders(this.eventId).then((reminders) => {
@ -123,10 +125,10 @@ export class AddonCalendarEventPage implements OnDestroy {
this.currentSiteId); this.currentSiteId);
// Refresh online status when changes. // 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. // Execute the callback in the Angular zone, so change detection doesn't stop working.
zone.run(() => { 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<any> { fetchEvent(sync?: boolean, showErrors?: boolean): Promise<any> {
const currentSite = this.sitesProvider.getCurrentSite(), const currentSite = this.sitesProvider.getCurrentSite(),
canGetById = this.calendarProvider.isGetEventByIdAvailable(); canGetById = this.calendarProvider.isGetEventByIdAvailable();
let promise; let promise,
deleted = false;
this.isOnline = this.appProvider.isOnline(); this.isOnline = this.appProvider.isOnline();
@ -161,6 +164,11 @@ export class AddonCalendarEventPage implements OnDestroy {
this.domUtils.showErrorModal(result.warnings[0]); 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) { if (result.updated) {
// Trigger a manual sync event. // Trigger a manual sync event.
result.source = 'event'; result.source = 'event';
@ -177,6 +185,10 @@ export class AddonCalendarEventPage implements OnDestroy {
} }
return promise.then(() => { return promise.then(() => {
if (deleted) {
return;
}
const promises = []; const promises = [];
// Get the event data. // Get the event data.
@ -204,6 +216,10 @@ export class AddonCalendarEventPage implements OnDestroy {
}); });
}).then((event) => { }).then((event) => {
if (deleted) {
return;
}
const promises = []; const promises = [];
this.calendarHelper.formatEventData(event); this.calendarHelper.formatEventData(event);
@ -251,7 +267,7 @@ export class AddonCalendarEventPage implements OnDestroy {
this.title = title; this.title = title;
// If the event belongs to a course, get the course name and the URL to view it. // 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.courseName = event.course.fullname;
this.courseUrl = event.course.viewurl; this.courseUrl = event.course.viewurl;
} else if (event.courseid && event.courseid != this.siteHomeId) { } else if (event.courseid && event.courseid != this.siteHomeId) {
@ -290,6 +306,11 @@ export class AddonCalendarEventPage implements OnDestroy {
event.encodedLocation = this.textUtils.buildAddressURL(event.location); 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); return Promise.all(promises);
}).catch((error) => { }).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevent', true); this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevent', true);
@ -391,6 +412,97 @@ export class AddonCalendarEventPage implements OnDestroy {
navCtrl.push('AddonCalendarEditEventPage', {eventId: this.eventId}); 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. * 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. * @param {any} data Sync result.
*/ */
protected checkSyncResult(isManual: boolean, data: any): void { 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) => { const event = data.events.find((ev) => {
return ev.id == this.eventId; return ev.id == this.eventId;
}); });

View File

@ -40,9 +40,13 @@
<span *ngIf="event.timeduration && event.endsSameDay"> - {{ (event.timestart + event.timeduration) * 1000 | coreFormatDate: "strftimetime" }}</span> <span *ngIf="event.timeduration && event.endsSameDay"> - {{ (event.timestart + event.timeduration) * 1000 | coreFormatDate: "strftimetime" }}</span>
<span *ngIf="event.timeduration && !event.endsSameDay"> - {{ (event.timestart + event.timeduration) * 1000 | coreFormatDate: "strftimedatetimeshort" }}</span> <span *ngIf="event.timeduration && !event.endsSameDay"> - {{ (event.timestart + event.timeduration) * 1000 | coreFormatDate: "strftimedatetimeshort" }}</span>
</p> </p>
<ion-note *ngIf="event.offline" item-end> <ion-note *ngIf="event.offline && !event.deleted" item-end>
<ion-icon name="time"></ion-icon> <ion-icon name="time"></ion-icon>
{{ 'core.notsent' | translate }} <span text-wrap>{{ 'core.notsent' | translate }}</span>
</ion-note>
<ion-note *ngIf="event.deleted" item-end>
<ion-icon name="trash"></ion-icon>
<span text-wrap>{{ 'core.deletedoffline' | translate }}</span>
</ion-note> </ion-note>
</a> </a>
</ng-container> </ng-container>

View File

@ -0,0 +1,5 @@
ion-app.app-root page-addon-calendar-list {
ion-note {
max-width: 30%;
}
}

View File

@ -63,16 +63,19 @@ export class AddonCalendarListPage implements OnDestroy {
protected newEventObserver: any; protected newEventObserver: any;
protected discardedObserver: any; protected discardedObserver: any;
protected editEventObserver: any; protected editEventObserver: any;
protected deleteEventObserver: any;
protected undeleteEventObserver: any;
protected syncObserver: any; protected syncObserver: any;
protected manualSyncObserver: any; protected manualSyncObserver: any;
protected onlineObserver: any; protected onlineObserver: any;
protected currentSiteId: string; protected currentSiteId: string;
protected onlineEvents = [];
protected offlineEvents = [];
protected deletedEvents = [];
courses: any[]; courses: any[];
eventsLoaded = false; eventsLoaded = false;
events = []; // Events (both online and offline). events = []; // Events (both online and offline).
onlineEvents = [];
offlineEvents = [];
notificationsEnabled = false; notificationsEnabled = false;
filteredEvents = []; filteredEvents = [];
canLoadMore = false; canLoadMore = false;
@ -149,6 +152,11 @@ export class AddonCalendarListPage implements OnDestroy {
this.syncObserver = eventsProvider.on(AddonCalendarSyncProvider.AUTO_SYNCED, (data) => { this.syncObserver = eventsProvider.on(AddonCalendarSyncProvider.AUTO_SYNCED, (data) => {
this.eventsLoaded = false; this.eventsLoaded = false;
this.refreshEvents(); 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); }, this.currentSiteId);
// Refresh data if calendar events are synchronized manually but not by this page. // 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.eventsLoaded = false;
this.refreshEvents(); 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); }, this.currentSiteId);
// Refresh online status when changes. // 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. // Execute the callback in the Angular zone, so change detection doesn't stop working.
zone.run(() => { zone.run(() => {
this.isOnline = online; this.isOnline = this.appProvider.isOnline();
}); });
}); });
} }
@ -234,6 +280,8 @@ export class AddonCalendarListPage implements OnDestroy {
const promises = []; const promises = [];
const courseId = this.filter.course.id != this.allCourses.id ? this.filter.course.id : undefined; 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) => { promises.push(this.calendarHelper.canEditEvents(courseId).then((canEdit) => {
this.canCreate = canEdit; this.canCreate = canEdit;
})); }));
@ -254,8 +302,8 @@ export class AddonCalendarListPage implements OnDestroy {
})); }));
// Get offline events. // Get offline events.
promises.push(this.calendarOffline.getAllEvents().then((events) => { promises.push(this.calendarOffline.getAllEditedEvents().then((events) => {
this.hasOffline = !!events.length; this.hasOffline = this.hasOffline || !!events.length;
// Format data and sort by timestart. // Format data and sort by timestart.
events.forEach((event) => { events.forEach((event) => {
@ -265,6 +313,12 @@ export class AddonCalendarListPage implements OnDestroy {
this.offlineEvents = this.sortEvents(events); 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); return Promise.all(promises);
}).finally(() => { }).finally(() => {
this.eventsLoaded = true; this.eventsLoaded = true;
@ -457,22 +511,32 @@ export class AddonCalendarListPage implements OnDestroy {
* @return {any[]} Merged events. * @return {any[]} Merged events.
*/ */
protected mergeEvents(onlineEvents: any[]): any[] { protected mergeEvents(onlineEvents: any[]): any[] {
if (!this.offlineEvents || !this.offlineEvents.length) { if (!this.offlineEvents.length && !this.deletedEvents.length) {
// No offline events, nothing to merge. // No offline events, nothing to merge.
return onlineEvents; return onlineEvents;
} }
const start = this.initialTime + (CoreConstants.SECONDS_DAY * this.daysLoaded), const start = this.initialTime + (CoreConstants.SECONDS_DAY * this.daysLoaded),
end = start + (CoreConstants.SECONDS_DAY * AddonCalendarProvider.DAYS_INTERVAL) - 1; end = start + (CoreConstants.SECONDS_DAY * AddonCalendarProvider.DAYS_INTERVAL) - 1;
let result = onlineEvents;
// First of all, remove the online events that were modified in offline. if (this.deletedEvents.length) {
let result = onlineEvents.filter((event) => { // Mark as deleted the events that were deleted in offline.
result.forEach((event) => {
event.deleted = this.deletedEvents.indexOf(event.id) != -1;
});
}
if (this.offlineEvents.length) {
// Remove the online events that were modified in offline.
result = result.filter((event) => {
const offlineEvent = this.offlineEvents.find((ev) => { const offlineEvent = this.offlineEvents.find((ev) => {
return ev.id == event.id; return ev.id == event.id;
}); });
return !offlineEvent; return !offlineEvent;
}); });
}
// Now get the offline events that belong to this period. // Now get the offline events that belong to this period.
const periodOfflineEvents = this.offlineEvents.filter((event) => { 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. * Page destroyed.
*/ */
@ -666,6 +746,8 @@ export class AddonCalendarListPage implements OnDestroy {
this.newEventObserver && this.newEventObserver.off(); this.newEventObserver && this.newEventObserver.off();
this.discardedObserver && this.discardedObserver.off(); this.discardedObserver && this.discardedObserver.off();
this.editEventObserver && this.editEventObserver.off(); this.editEventObserver && this.editEventObserver.off();
this.deleteEventObserver && this.deleteEventObserver.off();
this.undeleteEventObserver && this.undeleteEventObserver.off();
this.syncObserver && this.syncObserver.off(); this.syncObserver && this.syncObserver.off();
this.manualSyncObserver && this.manualSyncObserver.off(); this.manualSyncObserver && this.manualSyncObserver.off();
this.onlineObserver && this.onlineObserver.unsubscribe(); this.onlineObserver && this.onlineObserver.unsubscribe();

View File

@ -14,6 +14,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites';
import { CoreUtilsProvider } from '@providers/utils/utils';
/** /**
* Service to handle offline calendar events. * Service to handle offline calendar events.
@ -23,6 +24,7 @@ export class AddonCalendarOfflineProvider {
// Variables for database. // Variables for database.
static EVENTS_TABLE = 'addon_calendar_offline_events'; static EVENTS_TABLE = 'addon_calendar_offline_events';
static DELETED_EVENTS_TABLE = 'addon_calendar_deleted_events';
protected siteSchema: CoreSiteSchema = { protected siteSchema: CoreSiteSchema = {
name: 'AddonCalendarOfflineProvider', name: 'AddonCalendarOfflineProvider',
@ -34,6 +36,7 @@ export class AddonCalendarOfflineProvider {
{ {
name: 'id', // Negative for offline entries. name: 'id', // Negative for offline entries.
type: 'INTEGER', type: 'INTEGER',
primaryKey: true
}, },
{ {
name: 'name', name: 'name',
@ -110,13 +113,35 @@ export class AddonCalendarOfflineProvider {
name: 'timecreated', name: 'timecreated',
type: 'INTEGER', 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); 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<number[]>} Promise resolved with the IDs.
*/
getAllEventsIds(siteId?: string): Promise<number[]> {
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<any[]>} Promise resolved with all the events deleted in offline.
*/
getAllDeletedEvents(siteId?: string): Promise<any[]> {
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<number[]>} Promise resolved with the IDs of all the events deleted in offline.
*/
getAllDeletedEventsIds(siteId?: string): Promise<number[]> {
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. * @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any[]>} Promise resolved with events. * @return {Promise<any[]>} Promise resolved with events.
*/ */
getAllEvents(siteId?: string): Promise<any[]> { getAllEditedEvents(siteId?: string): Promise<any[]> {
return this.sitesProvider.getSite(siteId).then((site) => { return this.sitesProvider.getSite(siteId).then((site) => {
return site.getDb().getRecords(AddonCalendarOfflineProvider.EVENTS_TABLE); 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<number[]>} Promise resolved with events IDs.
*/
getAllEditedEventsIds(siteId?: string): Promise<number[]> {
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<any>} Promise resolved with the deleted event.
*/
getDeletedEvent(eventId: number, siteId?: string): Promise<any> {
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. * Get an offline event.
* *
@ -172,8 +271,8 @@ export class AddonCalendarOfflineProvider {
* @param {string} [siteId] Site ID. If not defined, current site. * @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<boolean>} Promise resolved with boolean: true if has offline events, false otherwise. * @return {Promise<boolean>} Promise resolved with boolean: true if has offline events, false otherwise.
*/ */
hasEvents(siteId?: string): Promise<boolean> { hasEditedEvents(siteId?: string): Promise<boolean> {
return this.getAllEvents(siteId).then((events) => { return this.getAllEditedEvents(siteId).then((events) => {
return !!events.length; return !!events.length;
}).catch(() => { }).catch(() => {
// No offline data found, return false. // 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<boolean>} Promise resolved with boolean: whether the event is deleted.
*/
isEventDeleted(eventId: number, siteId?: string): Promise<boolean> {
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<any>} Promise resolved when done.
*/
markDeleted(eventId: number, name: string, deleteAll?: boolean, siteId?: string): Promise<any> {
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. * 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<any>} Promise resolved if deleted, rejected if failure.
*/
unmarkDeleted(eventId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const conditions: any = {
id: eventId
};
return site.getDb().deleteRecords(AddonCalendarOfflineProvider.DELETED_EVENTS_TABLE, conditions);
});
}
} }

View File

@ -81,7 +81,8 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider {
// Sync successful, send event. // Sync successful, send event.
this.eventsProvider.trigger(AddonCalendarSyncProvider.AUTO_SYNCED, { this.eventsProvider.trigger(AddonCalendarSyncProvider.AUTO_SYNCED, {
warnings: result.warnings, warnings: result.warnings,
events: result.events events: result.events,
deleted: result.deleted
}, siteId); }, siteId);
} }
}); });
@ -122,18 +123,19 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider {
const result = { const result = {
warnings: [], warnings: [],
events: [], events: [],
deleted: [],
updated: false updated: false
}; };
let offlineEvents; let offlineEventIds: number[];
// Get offline events. // Get offline events.
const syncPromise = this.calendarOffline.getAllEvents(siteId).catch(() => { const syncPromise = this.calendarOffline.getAllEventsIds(siteId).catch(() => {
// No offline data found, return empty list. // No offline data found, return empty list.
return []; return [];
}).then((events) => { }).then((eventIds) => {
offlineEvents = events; offlineEventIds = eventIds;
if (!events.length) { if (!eventIds.length) {
// Nothing to sync. // Nothing to sync.
return; return;
} else if (!this.appProvider.isOnline()) { } else if (!this.appProvider.isOnline()) {
@ -143,8 +145,8 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider {
const promises = []; const promises = [];
events.forEach((event) => { offlineEventIds.forEach((eventId) => {
promises.push(this.syncOfflineEvent(event, result, siteId)); promises.push(this.syncOfflineEvent(eventId, result, siteId));
}); });
return this.utils.allPromises(promises); return this.utils.allPromises(promises);
@ -155,10 +157,10 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider {
this.calendarProvider.invalidateEventsList(siteId), this.calendarProvider.invalidateEventsList(siteId),
]; ];
offlineEvents.forEach((event) => { offlineEventIds.forEach((eventId) => {
if (event.id > 0) { if (eventId > 0) {
// An event was edited, invalidate its data too. // An event was edited, invalidate its data too.
promises.push(this.calendarProvider.invalidateEvent(event.id, siteId)); promises.push(this.calendarProvider.invalidateEvent(eventId, siteId));
} }
}); });
@ -182,25 +184,71 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider {
/** /**
* Synchronize an offline event. * 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 {any} result Object where to store the result of the sync.
* @param {string} [siteId] Site ID. If not defined, current site. * @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved if sync is successful, rejected otherwise. * @return {Promise<any>} Promise resolved if sync is successful, rejected otherwise.
*/ */
protected syncOfflineEvent(event: any, result: any, siteId?: string): Promise<any> { protected syncOfflineEvent(eventId: number, result: any, siteId?: string): Promise<any> {
// Verify that event isn't blocked. // Verify that event isn't blocked.
if (this.syncProvider.isBlocked(AddonCalendarProvider.COMPONENT, event.id, siteId)) { if (this.syncProvider.isBlocked(AddonCalendarProvider.COMPONENT, eventId, siteId)) {
this.logger.debug('Cannot sync event ' + event.name + ' because it is blocked.'); this.logger.debug('Cannot sync event ' + eventId + ' because it is blocked.');
return Promise.reject(this.translate.instant('core.errorsyncblocked', return Promise.reject(this.translate.instant('core.errorsyncblocked',
{$a: this.translate.instant('addon.calendar.calendarevent')})); {$a: this.translate.instant('addon.calendar.calendarevent')}));
} }
// 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);
// 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.
}));
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. // Try to send the data.
const data = this.utils.clone(event); // Clone the object because it will be modified in the submit function. 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) => { return this.calendarProvider.submitEventOnline(eventId > 0 ? eventId : undefined, data, siteId).then((newEvent) => {
result.updated = true; result.updated = true;
result.events.push(newEvent); result.events.push(newEvent);
@ -224,5 +272,7 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider {
// Local error, reject. // Local error, reject.
return Promise.reject(error); return Promise.reject(error);
}); });
});
});
} }
} }

View File

@ -42,6 +42,8 @@ export class AddonCalendarProvider {
static NEW_EVENT_EVENT = 'addon_calendar_new_event'; static NEW_EVENT_EVENT = 'addon_calendar_new_event';
static NEW_EVENT_DISCARDED_EVENT = 'addon_calendar_new_event_discarded'; static NEW_EVENT_DISCARDED_EVENT = 'addon_calendar_new_event_discarded';
static EDIT_EVENT_EVENT = 'addon_calendar_edit_event'; 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_CATEGORY = 'category';
static TYPE_COURSE = 'course'; static TYPE_COURSE = 'course';
static TYPE_GROUP = 'group'; static TYPE_GROUP = 'group';
@ -225,11 +227,38 @@ export class AddonCalendarProvider {
this.sitesProvider.registerSiteSchema(this.siteSchema); 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<boolean>} Promise resolved with true if can delete.
* @since 3.3
*/
canDeleteEvents(siteId?: string): Promise<boolean> {
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. * Check if a certain site allows creating and editing events.
* *
* @param {string} [siteId] Site Id. If not defined, use current site. * @param {string} [siteId] Site Id. If not defined, use current site.
* @return {Promise<boolean>} Promise resolved with true if can create/edit. * @return {Promise<boolean>} Promise resolved with true if can create/edit.
* @since 3.7.1
*/ */
canEditEvents(siteId?: string): Promise<boolean> { canEditEvents(siteId?: string): Promise<boolean> {
return this.sitesProvider.getSite(siteId).then((site) => { return this.sitesProvider.getSite(siteId).then((site) => {
@ -242,6 +271,7 @@ export class AddonCalendarProvider {
* *
* @param {CoreSite} [site] Site. If not defined, use current site. * @param {CoreSite} [site] Site. If not defined, use current site.
* @return {boolean} Whether events can be created and edited. * @return {boolean} Whether events can be created and edited.
* @since 3.7.1
*/ */
canEditEventsInSite(site?: CoreSite): boolean { canEditEventsInSite(site?: CoreSite): boolean {
site = site || this.sitesProvider.getCurrentSite(); site = site || this.sitesProvider.getCurrentSite();
@ -261,20 +291,89 @@ export class AddonCalendarProvider {
return site.getDb().getRecordsSelect(AddonCalendarProvider.EVENTS_TABLE, 'timestart + timeduration < ?', return site.getDb().getRecordsSelect(AddonCalendarProvider.EVENTS_TABLE, 'timestart + timeduration < ?',
[this.timeUtils.timestamp()]).then((events) => { [this.timeUtils.timestamp()]).then((events) => {
return Promise.all(events.map((event) => { 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<any>} Promise resolved when done.
*/
deleteEvent(eventId: number, name: string, deleteAll?: boolean, forceOffline?: boolean, siteId?: string): Promise<boolean> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
// Function to store the submission to be synchronized later.
const storeOffline = (): Promise<boolean> => {
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<any>} Promise resolved when done.
*/
deleteEventOnline(eventId: number, deleteAll?: boolean, siteId?: string): Promise<any> {
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 {number} eventId Event ID.
* @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site. * @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site.
* @return {Promise<any>} Resolved when done. * @return {Promise<any>} Resolved when done.
*/ */
protected deleteEvent(eventId: number, siteId?: string): Promise<any> { protected deleteLocalEvent(eventId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => { return this.sitesProvider.getSite(siteId).then((site) => {
siteId = site.getId(); siteId = site.getId();
@ -833,7 +932,7 @@ export class AddonCalendarProvider {
if (timeEnd <= new Date().getTime()) { if (timeEnd <= new Date().getTime()) {
// The event has finished already, don't schedule it. // 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) => { return this.getEventReminders(event.id, siteId).then((reminders) => {

View File

@ -65,7 +65,7 @@ export class AddonModChatChatPage {
this.logger = logger.getInstance('AddonModChoiceChoicePage'); this.logger = logger.getInstance('AddonModChoiceChoicePage');
this.currentUserBeep = 'beep ' + sitesProvider.getCurrentSiteUserId(); this.currentUserBeep = 'beep ' + sitesProvider.getCurrentSiteUserId();
this.isOnline = this.appProvider.isOnline(); 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. // Execute the callback in the Angular zone, so change detection doesn't stop working.
zone.run(() => { zone.run(() => {
this.isOnline = this.appProvider.isOnline(); this.isOnline = this.appProvider.isOnline();

View File

@ -44,7 +44,7 @@ export class AddonModChatUsersPage {
this.sessionId = navParams.get('sessionId'); this.sessionId = navParams.get('sessionId');
this.isOnline = this.appProvider.isOnline(); this.isOnline = this.appProvider.isOnline();
this.currentUserId = this.sitesProvider.getCurrentSiteUserId(); 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. // Execute the callback in the Angular zone, so change detection doesn't stop working.
zone.run(() => { zone.run(() => {
this.isOnline = this.appProvider.isOnline(); this.isOnline = this.appProvider.isOnline();

View File

@ -82,10 +82,10 @@ export class AddonModFeedbackFormPage implements OnDestroy {
this.currentSite = sitesProvider.getCurrentSite(); this.currentSite = sitesProvider.getCurrentSite();
// Refresh online status when changes. // 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. // Execute the callback in the Angular zone, so change detection doesn't stop working.
zone.run(() => { zone.run(() => {
this.offline = !online; this.offline = !this.appProvider.isOnline();
}); });
}); });
} }

View File

@ -115,7 +115,7 @@ export class AddonModForumDiscussionPage implements OnDestroy {
this.postId = navParams.get('postId'); this.postId = navParams.get('postId');
this.isOnline = this.appProvider.isOnline(); 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. // Execute the callback in the Angular zone, so change detection doesn't stop working.
zone.run(() => { zone.run(() => {
this.isOnline = this.appProvider.isOnline(); this.isOnline = this.appProvider.isOnline();

View File

@ -87,13 +87,19 @@
"addon.calendar.calendarevent": "Calendar event", "addon.calendar.calendarevent": "Calendar event",
"addon.calendar.calendarevents": "Calendar events", "addon.calendar.calendarevents": "Calendar events",
"addon.calendar.calendarreminders": "Calendar reminders", "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.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.durationminutes": "Duration in minutes",
"addon.calendar.durationnone": "Without duration", "addon.calendar.durationnone": "Without duration",
"addon.calendar.durationuntil": "Until", "addon.calendar.durationuntil": "Until",
"addon.calendar.editevent": "Editing event", "addon.calendar.editevent": "Editing event",
"addon.calendar.errorloadevent": "Error loading event.", "addon.calendar.errorloadevent": "Error loading event.",
"addon.calendar.errorloadevents": "Error loading events.", "addon.calendar.errorloadevents": "Error loading events.",
"addon.calendar.eventcalendareventdeleted": "Calendar event deleted",
"addon.calendar.eventduration": "Duration", "addon.calendar.eventduration": "Duration",
"addon.calendar.eventendtime": "End time", "addon.calendar.eventendtime": "End time",
"addon.calendar.eventkind": "Type of event", "addon.calendar.eventkind": "Type of event",

View File

@ -63,10 +63,10 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
const zone = injector.get(NgZone); const zone = injector.get(NgZone);
// Refresh online status when changes. // 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. // Execute the callback in the Angular zone, so change detection doesn't stop working.
zone.run(() => { zone.run(() => {
this.isOnline = online; this.isOnline = this.appProvider.isOnline();
}); });
}); });
} }

View File

@ -1344,9 +1344,12 @@ export class CoreDomUtilsProvider {
* @param {boolean} [needsTranslate] Whether the 'text' needs to be translated. * @param {boolean} [needsTranslate] Whether the 'text' needs to be translated.
* @param {number} [duration=2000] Duration in ms of the dimissable toast. * @param {number} [duration=2000] Duration in ms of the dimissable toast.
* @param {string} [cssClass=""] Class to add to the toast. * @param {string} [cssClass=""] Class to add to the toast.
* @param {boolean} [dismissOnPageChange=true] Dismiss the Toast on page change.
* @return {Toast} Toast instance. * @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) { if (needsTranslate) {
text = this.translate.instant(text); text = this.translate.instant(text);
} }
@ -1356,7 +1359,7 @@ export class CoreDomUtilsProvider {
duration: duration, duration: duration,
position: 'bottom', position: 'bottom',
cssClass: cssClass, cssClass: cssClass,
dismissOnPageChange: true dismissOnPageChange: dismissOnPageChange
}); });
loader.present(); loader.present();