551 lines
21 KiB
TypeScript

// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, ViewChild, Optional, OnDestroy, NgZone } from '@angular/core';
import { IonicPage, Content, NavParams, NavController } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { AddonCalendarProvider } from '../../providers/calendar';
import { AddonCalendarHelperProvider } from '../../providers/helper';
import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline';
import { AddonCalendarSyncProvider } from '../../providers/calendar-sync';
import { CoreCoursesProvider } from '@core/courses/providers/courses';
import { CoreAppProvider } from '@providers/app';
import { CoreEventsProvider } from '@providers/events';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreSitesProvider } from '@providers/sites';
import { CoreLocalNotificationsProvider } from '@providers/local-notifications';
import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreTimeUtilsProvider } from '@providers/utils/time';
import { CoreGroupsProvider } from '@providers/groups';
import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { Network } from '@ionic-native/network';
/**
* Page that displays a single calendar event.
*/
@IonicPage({ segment: 'addon-calendar-event' })
@Component({
selector: 'page-addon-calendar-event',
templateUrl: 'event.html',
})
export class AddonCalendarEventPage implements OnDestroy {
@ViewChild(Content) content: Content;
protected eventId;
protected siteHomeId: number;
protected editEventObserver: any;
protected syncObserver: any;
protected manualSyncObserver: any;
protected onlineObserver: any;
protected currentSiteId: string;
eventLoaded: boolean;
notificationFormat: string;
notificationMin: string;
notificationMax: string;
notificationTimeText: string;
event: any = {};
courseName: string;
groupName: string;
courseUrl = '';
notificationsEnabled = false;
moduleUrl = '';
categoryPath = '';
currentTime: number;
defaultTime: number;
reminders: any[];
canEdit = false;
canDelete = false;
hasOffline = false;
isOnline = false;
syncIcon: string; // Sync icon.
isSplitViewOn = false;
constructor(private translate: TranslateService, private calendarProvider: AddonCalendarProvider, navParams: NavParams,
private domUtils: CoreDomUtilsProvider, private coursesProvider: CoreCoursesProvider,
private calendarHelper: AddonCalendarHelperProvider, private sitesProvider: CoreSitesProvider,
localNotificationsProvider: CoreLocalNotificationsProvider, private courseProvider: CoreCourseProvider,
private textUtils: CoreTextUtilsProvider, private timeUtils: CoreTimeUtilsProvider,
private groupsProvider: CoreGroupsProvider, @Optional() private svComponent: CoreSplitViewComponent,
private navCtrl: NavController, private eventsProvider: CoreEventsProvider, network: Network, zone: NgZone,
private calendarSync: AddonCalendarSyncProvider, private appProvider: CoreAppProvider,
private calendarOffline: AddonCalendarOfflineProvider) {
this.eventId = navParams.get('id');
this.notificationsEnabled = localNotificationsProvider.isAvailable();
this.siteHomeId = sitesProvider.getCurrentSite().getSiteHomeId();
this.currentSiteId = sitesProvider.getCurrentSiteId();
this.isSplitViewOn = this.svComponent && this.svComponent.isOn();
// 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) => {
this.reminders = reminders;
});
this.calendarProvider.getDefaultNotificationTime().then((defaultTime) => {
this.defaultTime = defaultTime * 60;
});
// Calculate format to use.
this.notificationFormat = this.timeUtils.fixFormatForDatetime(this.timeUtils.convertPHPToMoment(
this.translate.instant('core.strftimedatetime')));
}
// Listen for event edited. If current event is edited, reload the data.
this.editEventObserver = eventsProvider.on(AddonCalendarProvider.EDIT_EVENT_EVENT, (data) => {
if (data && data.event && data.event.id == this.eventId) {
this.eventLoaded = false;
this.refreshEvent(true, false);
}
}, this.currentSiteId);
// Refresh data if this calendar event is synchronized automatically.
this.syncObserver = eventsProvider.on(AddonCalendarSyncProvider.AUTO_SYNCED, this.checkSyncResult.bind(this, false),
this.currentSiteId);
// Refresh data if calendar events are synchronized manually but not by this page.
this.manualSyncObserver = eventsProvider.on(AddonCalendarSyncProvider.MANUAL_SYNCED, this.checkSyncResult.bind(this, true),
this.currentSiteId);
// Refresh online status when changes.
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();
});
});
}
/**
* View loaded.
*/
ionViewDidLoad(): void {
this.syncIcon = 'spinner';
this.fetchEvent();
}
/**
* Fetches the event and updates the view.
*
* @param {boolean} [sync] Whether it should try to synchronize offline events.
* @param {boolean} [showErrors] Whether to show sync errors to the user.
* @return {Promise<any>} Promise resolved when done.
*/
fetchEvent(sync?: boolean, showErrors?: boolean): Promise<any> {
const currentSite = this.sitesProvider.getCurrentSite(),
canGetById = this.calendarProvider.isGetEventByIdAvailableInSite();
let promise,
deleted = false;
this.isOnline = this.appProvider.isOnline();
if (sync) {
// Try to synchronize offline events.
promise = this.calendarSync.syncEvents().then((result) => {
if (result.warnings && result.warnings.length) {
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';
this.eventsProvider.trigger(AddonCalendarSyncProvider.MANUAL_SYNCED, result, this.currentSiteId);
}
}).catch((error) => {
if (showErrors) {
this.domUtils.showErrorModalDefault(error, 'core.errorsync', true);
}
});
} else {
promise = Promise.resolve();
}
return promise.then(() => {
if (deleted) {
return;
}
const promises = [];
// Get the event data.
if (canGetById) {
promises.push(this.calendarProvider.getEventById(this.eventId));
} else {
promises.push(this.calendarProvider.getEvent(this.eventId));
}
// Get offline data.
promises.push(this.calendarOffline.getEvent(this.eventId).catch(() => {
// No offline data.
}));
return Promise.all(promises).then((results) => {
if (results[1]) {
// There is offline data, apply it.
this.hasOffline = true;
Object.assign(results[0], results[1]);
} else {
this.hasOffline = false;
}
return results[0];
});
}).then((event) => {
if (deleted) {
return;
}
const promises = [];
this.calendarHelper.formatEventData(event);
this.event = event;
this.currentTime = this.timeUtils.timestamp();
this.notificationMin = this.timeUtils.userDate(this.currentTime * 1000, 'YYYY-MM-DDTHH:mm', false);
this.notificationMax = this.timeUtils.userDate((event.timestart + event.timeduration) * 1000,
'YYYY-MM-DDTHH:mm', false);
// Reset some of the calculated data.
this.categoryPath = '';
this.courseName = '';
this.courseUrl = '';
this.moduleUrl = '';
if (event.moduleIcon) {
// It's a module event, translate the module name to the current language.
const name = this.courseProvider.translateModuleName(event.modulename);
if (name.indexOf('core.mod_') === -1) {
event.moduleName = name;
}
// Get the module URL.
if (canGetById) {
this.moduleUrl = event.url;
}
}
// If the event belongs to a course, get the course name and the URL to view it.
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) {
// Retrieve the course.
promises.push(this.coursesProvider.getUserCourse(event.courseid, true).then((course) => {
this.courseName = course.fullname;
this.courseUrl = currentSite ? this.textUtils.concatenatePaths(currentSite.siteUrl,
'/course/view.php?id=' + event.courseid) : '';
}).catch(() => {
// Error getting course, just don't show the course name.
}));
}
// If it's a group event, get the name of the group.
const courseId = canGetById && event.course ? event.course.id : event.courseid;
if (courseId && event.groupid) {
promises.push(this.groupsProvider.getUserGroupsInCourse(event.courseid).then((groups) => {
const group = groups.find((group) => {
return group.id == event.groupid;
});
this.groupName = group ? group.name : '';
}).catch(() => {
// Error getting groups, just don't show the group name.
this.groupName = '';
}));
}
if (canGetById && event.iscategoryevent && event.category) {
this.categoryPath = event.category.nestedname;
}
if (event.location) {
// Build a link to open the address in maps.
event.location = this.textUtils.decodeHTML(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;
}));
// Re-calculate the formatted time so it uses the device date.
promises.push(this.calendarProvider.getCalendarTimeFormat().then((timeFormat) => {
this.calendarProvider.formatEventTime(event, timeFormat).then((time) => {
event.formattedtime = time;
});
}));
return Promise.all(promises);
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevent', true);
}).finally(() => {
this.eventLoaded = true;
this.syncIcon = 'sync';
});
}
/**
* Add a reminder for this event.
*
* @param {Event} e Click event.
*/
addNotificationTime(e: Event): void {
e.preventDefault();
e.stopPropagation();
if (this.notificationTimeText && this.event && this.event.id) {
let notificationTime = this.timeUtils.convertToTimestamp(this.notificationTimeText);
const currentTime = this.timeUtils.timestamp(),
minute = Math.floor(currentTime / 60) * 60;
// Check if the notification time is in the same minute as we are, so the notification is triggered.
if (notificationTime >= minute && notificationTime < minute + 60) {
notificationTime = currentTime + 1;
}
this.calendarProvider.addEventReminder(this.event, notificationTime).then(() => {
this.calendarProvider.getEventReminders(this.eventId).then((reminders) => {
this.reminders = reminders;
});
this.notificationTimeText = null;
});
}
}
/**
* Cancel the selected notification.
*
* @param {number} id Reminder ID.
* @param {Event} e Click event.
*/
cancelNotification(id: number, e: Event): void {
e.preventDefault();
e.stopPropagation();
this.calendarProvider.deleteEventReminder(id).then(() => {
this.calendarProvider.getEventReminders(this.eventId).then((reminders) => {
this.reminders = reminders;
});
});
}
/**
* Refresh the data.
*
* @param {any} [refresher] Refresher.
* @param {Function} [done] Function to call when done.
* @param {boolean} [showErrors] Whether to show sync errors to the user.
* @return {Promise<any>} Promise resolved when done.
*/
doRefresh(refresher?: any, done?: () => void, showErrors?: boolean): Promise<any> {
if (this.eventLoaded) {
return this.refreshEvent(true, showErrors).finally(() => {
refresher && refresher.complete();
done && done();
});
}
return Promise.resolve();
}
/**
* Refresh the event.
*
* @param {boolean} [sync] Whether it should try to synchronize offline events.
* @param {boolean} [showErrors] Whether to show sync errors to the user.
* @return {Promise<any>} Promise resolved when done.
*/
refreshEvent(sync?: boolean, showErrors?: boolean): Promise<any> {
this.syncIcon = 'spinner';
const promises = [];
promises.push(this.calendarProvider.invalidateEvent(this.eventId));
promises.push(this.calendarProvider.invalidateTimeFormat());
return Promise.all(promises).catch(() => {
// Ignore errors.
}).then(() => {
return this.fetchEvent(sync, showErrors);
});
}
/**
* Open the page to edit the event.
*/
openEdit(): void {
// Decide which navCtrl to use. If this page is inside a split view, use the split view's master nav.
const navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl;
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) => {
let promise;
if (sent) {
// Event deleted, invalidate right days & months.
promise = this.calendarHelper.refreshAfterChangeEvent(this.event, deleteAll ? this.event.eventcount : 1)
.catch(() => {
// Ignore errors.
});
} else {
promise = Promise.resolve();
}
return promise.then(() => {
// 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.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.
*
* @param {boolean} isManual Whether it's a manual sync.
* @param {any} data Sync result.
*/
protected checkSyncResult(isManual: boolean, data: any): void {
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;
});
if (event) {
this.eventLoaded = false;
this.refreshEvent();
}
}
}
/**
* Page destroyed.
*/
ngOnDestroy(): void {
this.editEventObserver && this.editEventObserver.off();
this.syncObserver && this.syncObserver.off();
this.manualSyncObserver && this.manualSyncObserver.off();
this.onlineObserver && this.onlineObserver.unsubscribe();
}
}