2018-09-19 12:27:33 +02:00

355 lines
13 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, OnDestroy } from '@angular/core';
import { IonicPage, Content, PopoverController, NavParams, NavController } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { AddonCalendarProvider } from '../../providers/calendar';
import { AddonCalendarHelperProvider } from '../../providers/helper';
import { CoreCoursesProvider } from '@core/courses/providers/courses';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreSitesProvider } from '@providers/sites';
import { CoreLocalNotificationsProvider } from '@providers/local-notifications';
import { CoreCoursePickerMenuPopoverComponent } from '@components/course-picker-menu/course-picker-menu-popover';
import { CoreEventsProvider } from '@providers/events';
import { CoreAppProvider } from '@providers/app';
import { CoreSplitViewComponent } from '@components/split-view/split-view';
/**
* Page that displays the list of calendar events.
*/
@IonicPage({ segment: 'addon-calendar-list' })
@Component({
selector: 'page-addon-calendar-list',
templateUrl: 'list.html',
})
export class AddonCalendarListPage implements OnDestroy {
@ViewChild(Content) content: Content;
@ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent;
protected daysLoaded = 0;
protected emptyEventsTimes = 0; // Variable to identify consecutive calls returning 0 events.
protected categoriesRetrieved = false;
protected getCategories = false;
protected allCourses = {
id: -1,
fullname: this.translate.instant('core.fulllistofcourses'),
category: -1
};
protected categories = {};
protected siteHomeId: number;
protected obsDefaultTimeChange: any;
protected eventId: number;
courses: any[];
eventsLoaded = false;
events = [];
notificationsEnabled = false;
filteredEvents = [];
canLoadMore = false;
filter = {
course: this.allCourses
};
constructor(private translate: TranslateService, private calendarProvider: AddonCalendarProvider, navParams: NavParams,
private domUtils: CoreDomUtilsProvider, private coursesProvider: CoreCoursesProvider, private utils: CoreUtilsProvider,
private calendarHelper: AddonCalendarHelperProvider, sitesProvider: CoreSitesProvider,
localNotificationsProvider: CoreLocalNotificationsProvider, private popoverCtrl: PopoverController,
eventsProvider: CoreEventsProvider, private navCtrl: NavController, appProvider: CoreAppProvider) {
this.siteHomeId = sitesProvider.getCurrentSite().getSiteHomeId();
this.notificationsEnabled = localNotificationsProvider.isAvailable();
if (this.notificationsEnabled) {
// Re-schedule events if default time changes.
this.obsDefaultTimeChange = eventsProvider.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, () => {
calendarProvider.scheduleEventsNotifications(this.events);
}, sitesProvider.getCurrentSiteId());
}
this.eventId = navParams.get('eventId') || false;
}
/**
* View loaded.
*/
ionViewDidLoad(): void {
if (this.eventId) {
// There is an event to load, open the event in a new state.
this.gotoEvent(this.eventId);
}
this.fetchData().then(() => {
if (!this.eventId && this.splitviewCtrl.isOn() && this.events.length > 0) {
// Take first and load it.
this.gotoEvent(this.events[0].id);
}
}).finally(() => {
this.eventsLoaded = true;
});
}
/**
* Fetch all the data required for the view.
*
* @param {boolean} [refresh] Empty events array first.
* @return {Promise<any>} Promise resolved when done.
*/
fetchData(refresh: boolean = false): Promise<any> {
this.daysLoaded = 0;
this.emptyEventsTimes = 0;
// Load courses for the popover.
return this.coursesProvider.getUserCourses(false).then((courses) => {
// Add "All courses".
courses.unshift(this.allCourses);
this.courses = courses;
return this.fetchEvents(refresh);
});
}
/**
* Fetches the events and updates the view.
*
* @param {boolean} [refresh] Empty events array first.
* @return {Promise<any>} Promise resolved when done.
*/
fetchEvents(refresh: boolean = false): Promise<any> {
return this.calendarProvider.getEventsList(this.daysLoaded, AddonCalendarProvider.DAYS_INTERVAL).then((events) => {
this.daysLoaded += AddonCalendarProvider.DAYS_INTERVAL;
if (events.length === 0) {
this.emptyEventsTimes++;
if (this.emptyEventsTimes > 5) { // Stop execution if we retrieve empty list 6 consecutive times.
this.canLoadMore = false;
if (refresh) {
this.events = [];
this.filteredEvents = [];
}
} else {
// No events returned, load next events.
return this.fetchEvents();
}
} else {
// Sort the events by timestart, they're ordered by id.
events.sort((a, b) => {
return a.timestart - b.timestart;
});
events.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper));
this.getCategories = this.shouldLoadCategories(events);
if (refresh) {
this.events = events;
} else {
// Filter events with same ID. Repeated events are returned once per WS call, show them only once.
this.events = this.utils.mergeArraysWithoutDuplicates(this.events, events, 'id');
}
this.filteredEvents = this.getFilteredEvents();
this.canLoadMore = true;
// Schedule notifications for the events retrieved (might have new events).
this.calendarProvider.scheduleEventsNotifications(this.events);
}
// Resize the content so infinite loading is able to calculate if it should load more items or not.
// @todo: Infinite loading is not working if content is not high enough.
this.content.resize();
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
this.canLoadMore = false; // Set to false to prevent infinite calls with infinite-loading.
}).then(() => {
// Success retrieving events. Get categories if needed.
if (this.getCategories) {
this.getCategories = false;
return this.loadCategories();
}
});
}
/**
* Function to load more events.
*
* @param {any} [infiniteComplete] Infinite scroll complete function. Only used from core-infinite-loading.
* @return {Promise<any>} Resolved when done.
*/
loadMoreEvents(infiniteComplete?: any): Promise<any> {
return this.fetchEvents().finally(() => {
infiniteComplete && infiniteComplete();
});
}
/**
* Get filtered events.
*
* @return {any[]} Filtered events.
*/
protected getFilteredEvents(): any[] {
if (this.filter.course.id == -1) {
// No filter, display everything.
return this.events;
}
return this.events.filter(this.shouldDisplayEvent.bind(this));
}
/**
* Check if an event should be displayed based on the filter.
*
* @param {any} event Event object.
* @return {boolean} Whether it should be displayed.
*/
protected shouldDisplayEvent(event: any): boolean {
if (event.eventtype == 'user' || event.eventtype == 'site') {
// User or site event, display it.
return true;
}
if (event.eventtype == 'category') {
if (!event.categoryid || !Object.keys(this.categories).length) {
// We can't tell if the course belongs to the category, display them all.
return true;
}
if (event.categoryid == this.filter.course.category) {
// The event is in the same category as the course, display it.
return true;
}
// Check parent categories.
let category = this.categories[this.filter.course.category];
while (category) {
if (!category.parent) {
// Category doesn't have parent, stop.
break;
}
if (event.categoryid == category.parent) {
return true;
}
category = this.categories[category.parent];
}
return false;
}
// Show the event if it is from site home or if it matches the selected course.
return event.courseid === this.siteHomeId || event.courseid == this.filter.course.id;
}
/**
* Returns if the current state should load categories or not.
* @param {any[]} events Events to parse.
* @return {boolean} True if categories should be loaded.
*/
protected shouldLoadCategories(events: any[]): boolean {
if (this.categoriesRetrieved || this.getCategories) {
// Use previous value
return this.getCategories;
}
// Categories not loaded yet. We should get them if there's any category event.
const found = events.some((event) => event.categoryid != 'undefined' && event.categoryid > 0);
return found || this.getCategories;
}
/**
* Load categories to be able to filter events.
*
* @return {Promise<any>} Promise resolved when done.
*/
protected loadCategories(): Promise<any> {
return this.coursesProvider.getCategories(0, true).then((cats) => {
this.categoriesRetrieved = true;
this.categories = {};
// Index categories by ID.
cats.forEach((category) => {
this.categories[category.id] = category;
});
}).catch(() => {
// Ignore errors.
});
}
/**
* Refresh the events.
*
* @param {any} refresher Refresher.
*/
refreshEvents(refresher: any): void {
const promises = [];
promises.push(this.calendarProvider.invalidateEventsList(this.courses));
if (this.categoriesRetrieved) {
promises.push(this.coursesProvider.invalidateCategories(0, true));
this.categoriesRetrieved = false;
}
Promise.all(promises).finally(() => {
this.fetchData(true).finally(() => {
refresher.complete();
});
});
}
/**
* Show the context menu.
*
* @param {MouseEvent} event Event.
*/
openCourseFilter(event: MouseEvent): void {
const popover = this.popoverCtrl.create(CoreCoursePickerMenuPopoverComponent, {
courses: this.courses,
courseId: this.filter.course.id
});
popover.onDidDismiss((course) => {
if (course) {
this.filter.course = course;
this.domUtils.scrollToTop(this.content);
this.filteredEvents = this.getFilteredEvents();
}
});
popover.present({
ev: event
});
}
/**
* Open calendar events settings.
*/
openSettings(): void {
this.navCtrl.push('AddonCalendarSettingsPage');
}
/**
* Navigate to a particular event.
*
* @param {number} eventId Event to load.
*/
gotoEvent(eventId: number): void {
this.eventId = eventId;
this.splitviewCtrl.push('AddonCalendarEventPage', { id: eventId });
}
/**
* Page destroyed.
*/
ngOnDestroy(): void {
this.obsDefaultTimeChange && this.obsDefaultTimeChange.off();
}
}