From d4ddfc74a1f59ef886927c7a5803dd3740e7b11e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Thu, 28 Dec 2017 16:10:26 +0100 Subject: [PATCH] MOBILE-2308 calendar: Single event page --- src/addon/calendar/lang/en.json | 15 +- src/addon/calendar/pages/event/event.html | 57 +++++++ .../calendar/pages/event/event.module.ts | 35 +++++ src/addon/calendar/pages/event/event.scss | 3 + src/addon/calendar/pages/event/event.ts | 142 ++++++++++++++++++ src/addon/calendar/pages/list/list.html | 6 +- src/addon/calendar/pages/list/list.ts | 31 +++- src/addon/calendar/pages/settings/settings.ts | 2 +- src/addon/calendar/providers/calendar.ts | 80 +++++++++- src/addon/calendar/providers/helper.ts | 13 +- src/providers/app.ts | 12 +- 11 files changed, 373 insertions(+), 23 deletions(-) create mode 100644 src/addon/calendar/pages/event/event.html create mode 100644 src/addon/calendar/pages/event/event.module.ts create mode 100644 src/addon/calendar/pages/event/event.scss create mode 100644 src/addon/calendar/pages/event/event.ts diff --git a/src/addon/calendar/lang/en.json b/src/addon/calendar/lang/en.json index 4bb5a7893..63e4b17ec 100644 --- a/src/addon/calendar/lang/en.json +++ b/src/addon/calendar/lang/en.json @@ -2,6 +2,19 @@ "calendar": "Calendar", "calendarevents": "Calendar events", "defaultnotificationtime": "Default notification time", + "errorloadevent": "Error loading event.", "errorloadevents": "Error loading events.", - "noevents": "There are no events" + "eventendtime": "End time", + "eventstarttime": "Start time", + "noevents": "There are no events", + "notifications": "Notifications", + "typeclose": "Close event", + "typecourse": "Course event", + "typecategory": "Category event", + "typedue": "Due event", + "typegradingdue": "Grading due event", + "typegroup": "Group event", + "typeopen": "Open event", + "typesite": "Site event", + "typeuser": "User event" } \ No newline at end of file diff --git a/src/addon/calendar/pages/event/event.html b/src/addon/calendar/pages/event/event.html new file mode 100644 index 000000000..61451a1cb --- /dev/null +++ b/src/addon/calendar/pages/event/event.html @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + +

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

+

{{ event.timestart | coreToLocaleString }}

+
+ +

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

+

{{ (event.timestart + event.timeduration) |  coreToLocaleString }}

+
+ +

{{ 'core.course' | translate}}

+

+
+ + {{event.moduleName}} + + +

+ +

+
+
+
+ + + + {{ 'addon.calendar.notifications' | translate }} + + {{ 'core.defaultvalue' | translate :{$a: defaultTimeReadable} }} + {{ 'core.settings.disabled' | translate }} + {{ 600 | coreDuration }} + {{ 1800 | coreDuration }} + {{ 3600 | coreDuration }} + {{ 7200 | coreDuration }} + {{ 21600 | coreDuration }} + {{ 43200 | coreDuration }} + {{ 86400 | coreDuration }} + + + +
+
diff --git a/src/addon/calendar/pages/event/event.module.ts b/src/addon/calendar/pages/event/event.module.ts new file mode 100644 index 000000000..7731c3b2d --- /dev/null +++ b/src/addon/calendar/pages/event/event.module.ts @@ -0,0 +1,35 @@ +// (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 { NgModule } from '@angular/core'; +import { IonicPageModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '../../../../components/components.module'; +import { CoreDirectivesModule } from '../../../../directives/directives.module'; +import { CorePipesModule } from '../../../../pipes/pipes.module'; +import { AddonCalendarEventPage } from './event'; + +@NgModule({ + declarations: [ + AddonCalendarEventPage, + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + CorePipesModule, + IonicPageModule.forChild(AddonCalendarEventPage), + TranslateModule.forChild() + ], +}) +export class AddonCalendarEventPageModule {} diff --git a/src/addon/calendar/pages/event/event.scss b/src/addon/calendar/pages/event/event.scss new file mode 100644 index 000000000..345d0d21c --- /dev/null +++ b/src/addon/calendar/pages/event/event.scss @@ -0,0 +1,3 @@ +page-addon-calendar-list { + +} \ No newline at end of file diff --git a/src/addon/calendar/pages/event/event.ts b/src/addon/calendar/pages/event/event.ts new file mode 100644 index 000000000..e0c102ca7 --- /dev/null +++ b/src/addon/calendar/pages/event/event.ts @@ -0,0 +1,142 @@ +// (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 } from '@angular/core'; +import { IonicPage, Content, NavParams } 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 { CoreSitesProvider } from '../../../../providers/sites'; +import { CoreLocalNotificationsProvider } from '../../../../providers/local-notifications'; +//import { CoreCourseProvider } from '../../../core/course/providers/course'; +import * as moment from 'moment'; + +/** + * Page that displays a single calendar event. + */ +@IonicPage() +@Component({ + selector: 'page-addon-calendar-event', + templateUrl: 'event.html', +}) +export class AddonCalendarEventPage { + @ViewChild(Content) content: Content; + + protected eventId; + protected siteHomeId: number; + eventLoaded: boolean; + notificationTime: number; + defaultTimeReadable: string; + event = {}; + title: string; + courseName: string; + notificationsEnabled = false; + + constructor(private translate: TranslateService, private calendarProvider: AddonCalendarProvider, private navParams: NavParams, + private domUtils: CoreDomUtilsProvider, private coursesProvider: CoreCoursesProvider, + private calendarHelper: AddonCalendarHelperProvider, private sitesProvider: CoreSitesProvider, + private localNotificationsProvider: CoreLocalNotificationsProvider/*, private courseProvider: CoreCourseProvider*/) { + + this.eventId = navParams.get('id'); + this.notificationsEnabled = localNotificationsProvider.isAvailable(); + this.siteHomeId = sitesProvider.getCurrentSite().getSiteHomeId(); + if (this.notificationsEnabled) { + this.calendarProvider.getEventNotificationTimeOption(this.eventId).then((notificationTime) => { + this.notificationTime = notificationTime; + }); + + this.calendarProvider.getDefaultNotificationTime().then((defaultTime) => { + if (defaultTime === 0) { + // Disabled by default. + this.defaultTimeReadable = this.translate.instant('core.settings.disabled'); + } else { + this.defaultTimeReadable = moment.duration(defaultTime * 60 * 1000).humanize(); + } + }); + } + } + + /** + * View loaded. + */ + ionViewDidLoad() { + this.fetchEvent().finally(() => { + this.eventLoaded = true; + }); + } + + updateNotificationTime() { + if (!isNaN(this.notificationTime) && this.event && this.event.id) { + this.calendarProvider.updateNotificationTime(this.event, this.notificationTime); + } + } + + /** + * Fetches the event and updates the view. + * + * @param {boolean} refresh Empty events array first. + */ + fetchEvent() { + return this.calendarProvider.getEvent(this.eventId).then((event) => { + this.calendarHelper.formatEventData(event); + this.event = event; + + // Guess event title. + let title = this.translate.instant('addon.calendar.type' + event.eventtype); + if (event.moduleIcon) { + // @todo: It's a module event, translate the module name to the current language. + let name = "" //this.courseProvider.translateModuleName(event.modulename); + if (name.indexOf('core.mod_') === -1) { + event.moduleName = name; + } + if (title == 'addon.calendar.type' + event.eventtype) { + title = this.translate.instant('core.mod_'+ event.modulename + '.' + event.eventtype); + + if (title == 'core.mod_'+ event.modulename + '.' + event.eventtype) { + title = name; + } + } + } else { + if (title == 'addon.calendar.type' + event.eventtype) { + title = event.name; + } + } + this.title = title; + + if (event.courseid != this.siteHomeId) { + // It's a course event, retrieve the course name. + return this.coursesProvider.getUserCourse(event.courseid, true).then((course) => { + this.courseName = course.fullname; + }); + } + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevent', true); + }); + } + + /** + * Refresh the event. + * + * @param {any} refresher Refresher. + */ + refreshEvent(refresher: any) { + this.calendarProvider.invalidateEvent(this.eventId).finally(() => { + this.fetchEvent().finally(() => { + refresher.complete(); + }); + }); + } +} \ No newline at end of file diff --git a/src/addon/calendar/pages/list/list.html b/src/addon/calendar/pages/list/list.html index 3ad253f5a..9052d8bb1 100644 --- a/src/addon/calendar/pages/list/list.html +++ b/src/addon/calendar/pages/list/list.html @@ -21,10 +21,10 @@ - + - - + +

{{ event.timestart | coreToLocaleString }}

diff --git a/src/addon/calendar/pages/list/list.ts b/src/addon/calendar/pages/list/list.ts index e49e4d2f9..8e473800e 100644 --- a/src/addon/calendar/pages/list/list.ts +++ b/src/addon/calendar/pages/list/list.ts @@ -24,6 +24,7 @@ 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'; /** * Page that displays the list of calendar events. @@ -48,6 +49,7 @@ export class AddonCalendarListPage implements OnDestroy { protected categories = {}; protected siteHomeId: number; protected obsDefaultTimeChange: any; + protected eventId: number; courses: any[]; eventsLoaded = false; @@ -64,10 +66,10 @@ export class AddonCalendarListPage implements OnDestroy { private domUtils: CoreDomUtilsProvider, private coursesProvider: CoreCoursesProvider, private utils: CoreUtilsProvider, private calendarHelper: AddonCalendarHelperProvider, private sitesProvider: CoreSitesProvider, private localNotificationsProvider: CoreLocalNotificationsProvider, private popoverCtrl: PopoverController, - private eventsProvider: CoreEventsProvider, private navCtrl: NavController) { + private eventsProvider: CoreEventsProvider, private navCtrl: NavController, private appProvider: CoreAppProvider) { this.siteHomeId = sitesProvider.getCurrentSite().getSiteHomeId(); - this.notificationsEnabled = true;//localNotificationsProvider.isAvailable(); + this.notificationsEnabled = localNotificationsProvider.isAvailable(); if (this.notificationsEnabled) { // Re-schedule events if default time changes. this.obsDefaultTimeChange = eventsProvider.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, () => { @@ -75,16 +77,24 @@ export class AddonCalendarListPage implements OnDestroy { }, sitesProvider.getCurrentSiteId()); } - // @TODO: Split view once single event is done. - // let eventId = navParams.get('eventid') || false; + this.eventId = navParams.get('eventid') || false; } /** * View loaded. */ ionViewDidLoad() { - this.fetchData().then(() => { + if (this.eventId && !this.appProvider.isWide()) { + // There is an event to load and it's a phone device, open the event in a new state. + this.gotoEvent(this.eventId); + } + this.fetchData().then(() => { + // @TODO: Split view once single event is done. + if (this.eventId && this.appProvider.isWide()) { + // There is an event to load and it's a phone device, open the event in a new state. + this.gotoEvent(this.eventId); + } }).finally(() => { this.eventsLoaded = true; }); @@ -151,7 +161,7 @@ export class AddonCalendarListPage implements OnDestroy { } // 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. + // @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); @@ -298,7 +308,14 @@ export class AddonCalendarListPage implements OnDestroy { */ openSettings() { this.navCtrl.push('AddonCalendarSettingsPage'); - }; + } + + /** + * Navigate to a particular event. + */ + gotoEvent(eventId) { + this.navCtrl.push('AddonCalendarEventPage', {id: eventId}); + } /** * Page destroyed. diff --git a/src/addon/calendar/pages/settings/settings.ts b/src/addon/calendar/pages/settings/settings.ts index 5e1be133c..10d0deccc 100644 --- a/src/addon/calendar/pages/settings/settings.ts +++ b/src/addon/calendar/pages/settings/settings.ts @@ -19,7 +19,7 @@ import { CoreEventsProvider } from '../../../../providers/events'; import { CoreSitesProvider } from '../../../../providers/sites'; /** - * Page that displays the list of calendar events. + * Page that displays the calendar settings. */ @IonicPage() @Component({ diff --git a/src/addon/calendar/providers/calendar.ts b/src/addon/calendar/providers/calendar.ts index bb5b0a4ec..1e9d85ffb 100644 --- a/src/addon/calendar/providers/calendar.ts +++ b/src/addon/calendar/providers/calendar.ts @@ -124,6 +124,44 @@ export class AddonCalendarProvider { return this.configProvider.get(key, AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME); } + /** + * Get a calendar event. If the server request fails and data is not cached, try to get it from local DB. + * + * @param {number} id Event ID. + * @param {boolean} [refresh] True when we should update the event data. + * @param {string} [siteId] ID of the site. If not defined, use current site. + * @return {Promise} Promise resolved when the event data is retrieved. + */ + getEvent(id: number, siteId?: string) : Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + let presets = { + cacheKey: this.getEventCacheKey(id) + }, + data = { + "options[userevents]": 0, + "options[siteevents]": 0, + "events[eventids][0]": id + }; + return site.read('core_calendar_get_calendar_events', data, presets).then((response) => { + // The WebService returns all category events. Check the response to search for the event we want. + let event = response.events.find((e) => {return e.id == id}); + return event || this.getEventFromLocalDb(id); + }).catch(() => { + return this.getEventFromLocalDb(id); + }); + }); + } + + /** + * Get cache key for a single event WS call. + * + * @param {number} id Event ID. + * @return {string} Cache key. + */ + protected getEventCacheKey(id: number): string { + return 'mmaCalendar:events:' + id; + } + /** * Get a calendar event from local Db. * @@ -245,14 +283,14 @@ export class AddonCalendarProvider { return 'mmaCalendar:'; } - /** + /** * Invalidates events list and all the single events and related info. * * @param {any[]} courses List of courses or course ids. * @param {string} [siteId] Site Id. If not defined, use current site. - * @return {Promise} Promise resolved when the list is invalidated. + * @return {Promise} Promise resolved when the list is invalidated. */ - invalidateEventsList(courses: any[], siteId?: string) { + invalidateEventsList(courses: any[], siteId?: string) : Promise { return this.sitesProvider.getSite(siteId).then((site) => { siteId = site.getId(); @@ -265,6 +303,19 @@ export class AddonCalendarProvider { }); } + /** + * Invalidates a single event. + * + * @param {number} eventId List of courses or course ids. + * @param {string} [siteId] Site Id. If not defined, use current site. + * @return {Promise} Promise resolved when the list is invalidated. + */ + invalidateEvent(eventId: number, siteId?: string) : Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getEventCacheKey(eventId)); + }); + } + /** * Check if Calendar is disabled in a certain site. * @@ -408,4 +459,27 @@ export class AddonCalendarProvider { return Promise.all(promises); }); } + + /** + * Updates an event notification time and schedule a new notification. + * + * @param {any} event Event to update its notification time. + * @param {number} time New notification setting time (in minutes). E.g. 10 means "notificate 10 minutes before start". + * @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site. + * @return {Promise} Promise resolved when the notification is updated. + */ + updateNotificationTime(event: any, time: number, siteId?: string) : Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + if (!this.sitesProvider.isLoggedIn()) { + // Not logged in, we can't get the site DB. User logged out or session expired while an operation was ongoing. + return Promise.reject(null); + } + + event.notificationtime = time; + + return site.getDb().insertOrUpdateRecord(AddonCalendarProvider.EVENTS_TABLE, event, {id: event.id}).then(() => { + return this.scheduleEventNotification(event, time); + }); + }); + } } diff --git a/src/addon/calendar/providers/helper.ts b/src/addon/calendar/providers/helper.ts index f8ec7450b..36547ecb1 100644 --- a/src/addon/calendar/providers/helper.ts +++ b/src/addon/calendar/providers/helper.ts @@ -32,7 +32,7 @@ export class AddonCalendarHelperProvider { 'category': 'albums' }; - constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider) { + constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider/*, private courseProvider: CoreCourseProvider*/) { this.logger = logger.getInstance('AddonCalendarHelperProvider'); } @@ -42,12 +42,11 @@ export class AddonCalendarHelperProvider { * @param {any} e Event to format. */ formatEventData(e: any) { - let icon = AddonCalendarHelperProvider.eventicons[e.eventtype] || false; - if (!icon) { - // @TODO: It's a module event. - //icon = this.courseProvider.getModuleIconSrc(e.modulename); - e.moduleicon = icon; + e.icon = AddonCalendarHelperProvider.eventicons[e.eventtype] || false; + if (!e.icon) { + // @todo: It's a module event. + //e.icon = this.courseProvider.getModuleIconSrc(e.modulename); + e.moduleIcon = e.icon; } - e.icon = icon; }; } diff --git a/src/providers/app.ts b/src/providers/app.ts index 2f343a95e..d0cc6e579 100644 --- a/src/providers/app.ts +++ b/src/providers/app.ts @@ -184,7 +184,7 @@ export class CoreAppProvider { return online; } - /* + /** * Check if device uses a limited connection. * * @return {boolean} Whether the device uses a limited connection. @@ -200,6 +200,16 @@ export class CoreAppProvider { return limited.indexOf(type) > -1; } + /** + * Check if device is wide enough. It's used i.e. to show split view. + * + * @return {boolean} Whether the device uses a limited connection. + */ + isWide() : boolean { + //@todo Should use media querys like splitpane + return this.platform.is('tablet'); + } + /** * Check if the app is running in a Windows environment. *