From 8dd01089070148a60eda4621e20be61740004341 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 4 Jul 2019 13:16:54 +0200 Subject: [PATCH] MOBILE-3021 calendar: Support upcoming events --- scripts/langindex.json | 6 + .../calendar/addon-calendar-calendar.html | 3 +- .../calendar/components/calendar/calendar.ts | 7 + .../calendar/components/components.module.ts | 11 +- .../addon-calendar-upcoming-events.html | 24 ++ .../upcoming-events/upcoming-events.ts | 317 ++++++++++++++++++ src/addon/calendar/lang/en.json | 8 +- src/addon/calendar/pages/index/index.html | 10 +- src/addon/calendar/pages/index/index.ts | 21 +- src/addon/calendar/providers/calendar.ts | 207 +++++++++++- src/app/app.scss | 5 + src/assets/lang/en.json | 6 + 12 files changed, 613 insertions(+), 12 deletions(-) create mode 100644 src/addon/calendar/components/upcoming-events/addon-calendar-upcoming-events.html create mode 100644 src/addon/calendar/components/upcoming-events/upcoming-events.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index 2f79cd0a0..1d2d82d13 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -83,6 +83,7 @@ "addon.blog.publishtoworld": "blog", "addon.blog.showonlyyourentries": "local_moodlemobileapp", "addon.blog.siteblogheading": "blog", + "addon.calendar.allday": "calendar", "addon.calendar.calendar": "calendar", "addon.calendar.calendarevent": "local_moodlemobileapp", "addon.calendar.calendarevents": "local_moodlemobileapp", @@ -113,6 +114,7 @@ "addon.calendar.invalidtimedurationuntil": "calendar", "addon.calendar.mon": "calendar", "addon.calendar.monday": "calendar", + "addon.calendar.monthlyview": "calendar", "addon.calendar.newevent": "calendar", "addon.calendar.noevents": "local_moodlemobileapp", "addon.calendar.nopermissiontoupdatecalendar": "error", @@ -129,6 +131,8 @@ "addon.calendar.sunday": "calendar", "addon.calendar.thu": "calendar", "addon.calendar.thursday": "calendar", + "addon.calendar.today": "calendar", + "addon.calendar.tomorrow": "calendar", "addon.calendar.tue": "calendar", "addon.calendar.tuesday": "calendar", "addon.calendar.typecategory": "calendar", @@ -140,8 +144,10 @@ "addon.calendar.typeopen": "calendar", "addon.calendar.typesite": "calendar", "addon.calendar.typeuser": "calendar", + "addon.calendar.upcomingevents": "calendar", "addon.calendar.wed": "calendar", "addon.calendar.wednesday": "calendar", + "addon.calendar.yesterday": "calendar", "addon.competency.activities": "tool_lp", "addon.competency.competencies": "competency", "addon.competency.competenciesmostoftennotproficientincourse": "tool_lp", diff --git a/src/addon/calendar/components/calendar/addon-calendar-calendar.html b/src/addon/calendar/components/calendar/addon-calendar-calendar.html index a866c4be2..007100baf 100644 --- a/src/addon/calendar/components/calendar/addon-calendar-calendar.html +++ b/src/addon/calendar/components/calendar/addon-calendar-calendar.html @@ -37,12 +37,13 @@

-
+

+ {{ event.timestart * 1000 | coreFormatDate: timeFormat }} {{event.name}}

diff --git a/src/addon/calendar/components/calendar/calendar.ts b/src/addon/calendar/components/calendar/calendar.ts index 053863483..203db79e8 100644 --- a/src/addon/calendar/components/calendar/calendar.ts +++ b/src/addon/calendar/components/calendar/calendar.ts @@ -42,6 +42,7 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest weekDays: any[]; weeks: any[]; loaded = false; + timeFormat: string; protected year: number; protected month: number; @@ -141,6 +142,11 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest this.deletedEvents = ids; })); + // Get time format to use. + promises.push(this.calendarProvider.getCalendarTimeFormat().then((value) => { + this.timeFormat = value; + })); + return Promise.all(promises).then(() => { return this.fetchEvents(); }).catch((error) => { @@ -232,6 +238,7 @@ export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDest promises.push(this.calendarProvider.invalidateMonthlyEvents(this.year, this.month)); promises.push(this.coursesProvider.invalidateCategories(0, true)); + promises.push(this.calendarProvider.invalidateTimeFormat()); this.categoriesRetrieved = false; // Get categories again. diff --git a/src/addon/calendar/components/components.module.ts b/src/addon/calendar/components/components.module.ts index a6d5afe22..3928f6dd9 100644 --- a/src/addon/calendar/components/components.module.ts +++ b/src/addon/calendar/components/components.module.ts @@ -18,23 +18,28 @@ import { IonicModule } 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 { AddonCalendarCalendarComponent } from '../components/calendar/calendar'; +import { AddonCalendarUpcomingEventsComponent } from '../components/upcoming-events/upcoming-events'; @NgModule({ declarations: [ - AddonCalendarCalendarComponent + AddonCalendarCalendarComponent, + AddonCalendarUpcomingEventsComponent ], imports: [ CommonModule, IonicModule, TranslateModule.forChild(), CoreComponentsModule, - CoreDirectivesModule + CoreDirectivesModule, + CorePipesModule ], providers: [ ], exports: [ - AddonCalendarCalendarComponent + AddonCalendarCalendarComponent, + AddonCalendarUpcomingEventsComponent ] }) export class AddonCalendarComponentsModule {} diff --git a/src/addon/calendar/components/upcoming-events/addon-calendar-upcoming-events.html b/src/addon/calendar/components/upcoming-events/addon-calendar-upcoming-events.html new file mode 100644 index 000000000..b338f0eca --- /dev/null +++ b/src/addon/calendar/components/upcoming-events/addon-calendar-upcoming-events.html @@ -0,0 +1,24 @@ + + + + + + + + + +

+

+ + + {{ 'core.notsent' | translate }} + + + + {{ 'core.deletedoffline' | translate }} + +
+
+
+ +
diff --git a/src/addon/calendar/components/upcoming-events/upcoming-events.ts b/src/addon/calendar/components/upcoming-events/upcoming-events.ts new file mode 100644 index 000000000..41751c0d5 --- /dev/null +++ b/src/addon/calendar/components/upcoming-events/upcoming-events.ts @@ -0,0 +1,317 @@ +// (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, OnDestroy, OnInit, Input, OnChanges, SimpleChange, Output, EventEmitter } from '@angular/core'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { AddonCalendarProvider } from '../../providers/calendar'; +import { AddonCalendarHelperProvider } from '../../providers/helper'; +import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline'; +import { CoreCoursesProvider } from '@core/courses/providers/courses'; +import { CoreConstants } from '@core/constants'; + +/** + * Component that displays upcoming events. + */ +@Component({ + selector: 'addon-calendar-upcoming-events', + templateUrl: 'addon-calendar-upcoming-events.html', +}) +export class AddonCalendarUpcomingEventsComponent implements OnInit, OnChanges, OnDestroy { + @Input() courseId: number | string; + @Input() categoryId: number | string; // Category ID the course belongs to. + @Output() onEventClicked = new EventEmitter(); + + events = []; // Events (both online and offline). + filteredEvents = []; + loaded = false; + + protected year: number; + protected month: number; + protected categoriesRetrieved = false; + protected categories = {}; + protected currentSiteId: string; + protected onlineEvents = []; + protected offlineEvents = []; // Offline events. + protected deletedEvents = []; // Events deleted in offline. + protected lookAhead: number; + protected timeFormat: string; + + // Observers. + protected undeleteEventObserver: any; + + constructor(eventsProvider: CoreEventsProvider, + sitesProvider: CoreSitesProvider, + private calendarProvider: AddonCalendarProvider, + private calendarHelper: AddonCalendarHelperProvider, + private calendarOffline: AddonCalendarOfflineProvider, + private domUtils: CoreDomUtilsProvider, + private coursesProvider: CoreCoursesProvider) { + + this.currentSiteId = sitesProvider.getCurrentSiteId(); + + // 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.undeleteEvent(data.eventId); + + // 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.currentSiteId); + } + + /** + * Component loaded. + */ + ngOnInit(): void { + this.fetchData(); + } + + /** + * Detect changes on input properties. + */ + ngOnChanges(changes: {[name: string]: SimpleChange}): void { + if (changes.courseId || changes.categoryId) { + this.filterEvents(); + } + } + + /** + * Fetch data. + * + * @param {boolean} [refresh=false] True if we are refreshing events. + * @return {Promise} Promise resolved when done. + */ + fetchData(refresh: boolean = false): Promise { + const promises = []; + + promises.push(this.loadCategories()); + + // Get offline events. + promises.push(this.calendarOffline.getAllEditedEvents().then((events) => { + // Format data. + events.forEach((event) => { + event.offline = true; + this.calendarHelper.formatEventData(event); + }); + + this.offlineEvents = this.sortEvents(events); + })); + + // Get events deleted in offline. + promises.push(this.calendarOffline.getAllDeletedEventsIds().then((ids) => { + this.deletedEvents = ids; + })); + + // Get user preferences. + promises.push(this.calendarProvider.getCalendarLookAhead().then((value) => { + this.lookAhead = value; + })); + + promises.push(this.calendarProvider.getCalendarTimeFormat().then((value) => { + this.timeFormat = value; + })); + + return Promise.all(promises).then(() => { + return this.fetchEvents(); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true); + }).finally(() => { + this.loaded = true; + }); + } + + /** + * Fetch upcoming events. + * + * @return {Promise} Promise resolved when done. + */ + fetchEvents(): Promise { + // Don't pass courseId and categoryId, we'll filter them locally. + return this.calendarProvider.getUpcomingEvents().then((result) => { + this.onlineEvents = result.events; + + this.onlineEvents.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper)); + + // Merge the online events with offline data. + this.events = this.mergeEvents(); + + // Filter events by course. + this.filterEvents(); + + // Re-calculate the formatted time so it uses the device date. + this.events.forEach((event) => { + event.formattedtime = this.calendarProvider.formatEventTime(event, this.timeFormat); + }); + }); + } + + /** + * Load categories to be able to filter events. + * + * @return {Promise} Promise resolved when done. + */ + protected loadCategories(): Promise { + if (this.categoriesRetrieved) { + // Already retrieved, stop. + return Promise.resolve(); + } + + 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. + }); + } + + /** + * Filter events to only display events belonging to a certain course. + */ + filterEvents(): void { + const courseId = this.courseId ? Number(this.courseId) : undefined, + categoryId = this.categoryId ? Number(this.categoryId) : undefined; + + if (!courseId || courseId < 0) { + this.filteredEvents = this.events; + } else { + this.filteredEvents = this.events.filter((event) => { + return this.calendarHelper.shouldDisplayEvent(event, courseId, categoryId, this.categories); + }); + } + } + + /** + * Refresh events. + * + * @param {boolean} [sync] Whether it should try to synchronize offline events. + * @param {boolean} [showErrors] Whether to show sync errors to the user. + * @return {Promise} Promise resolved when done. + */ + refreshData(sync?: boolean, showErrors?: boolean): Promise { + const promises = []; + + promises.push(this.calendarProvider.invalidateAllUpcomingEvents()); + promises.push(this.coursesProvider.invalidateCategories(0, true)); + promises.push(this.calendarProvider.invalidateLookAhead()); + promises.push(this.calendarProvider.invalidateTimeFormat()); + + this.categoriesRetrieved = false; // Get categories again. + + return Promise.all(promises).then(() => { + return this.fetchData(true); + }); + } + + /** + * An event was clicked. + * + * @param {any} event Event. + */ + eventClicked(event: any): void { + this.onEventClicked.emit(event.id); + } + + /** + * Merge online events with the offline events of that period. + * + * @return {any[]} Merged events. + */ + protected mergeEvents(): any[] { + if (!this.offlineEvents.length && !this.deletedEvents.length) { + // No offline events, nothing to merge. + return this.onlineEvents; + } + + const start = Date.now(), + end = start + (CoreConstants.SECONDS_DAY * this.lookAhead); + let result = this.onlineEvents; + + if (this.deletedEvents.length) { + // Mark as deleted the events that were deleted in offline. + result.forEach((event) => { + event.deleted = this.deletedEvents.indexOf(event.id) != -1; + }); + } + + if (this.offlineEvents.length) { + // Remove the online events that were modified in offline. + result = result.filter((event) => { + const offlineEvent = this.offlineEvents.find((ev) => { + return ev.id == event.id; + }); + + return !offlineEvent; + }); + } + + // Now get the offline events that belong to this period. + const periodOfflineEvents = this.offlineEvents.filter((event) => { + return (event.timestart >= start || event.timestart + event.timeduration >= start) && event.timestart <= end; + }); + + // Merge both arrays and sort them. + result = result.concat(periodOfflineEvents); + + return this.sortEvents(result); + } + + /** + * Sort events by timestart. + * + * @param {any[]} events List to sort. + */ + protected sortEvents(events: any[]): any[] { + return events.sort((a, b) => { + if (a.timestart == b.timestart) { + return a.timeduration - b.timeduration; + } + + return a.timestart - b.timestart; + }); + } + + /** + * Undelete a certain event. + * + * @param {number} eventId Event ID. + */ + protected undeleteEvent(eventId: number): void { + const event = this.onlineEvents.find((event) => { + return event.id == eventId; + }); + + if (event) { + event.deleted = false; + } + } + + /** + * Component destroyed. + */ + ngOnDestroy(): void { + this.undeleteEventObserver && this.undeleteEventObserver.off(); + } +} diff --git a/src/addon/calendar/lang/en.json b/src/addon/calendar/lang/en.json index 412c81742..ba81a9a89 100644 --- a/src/addon/calendar/lang/en.json +++ b/src/addon/calendar/lang/en.json @@ -1,4 +1,5 @@ { + "allday": "All day", "calendar": "Calendar", "calendarevent": "Calendar event", "calendarevents": "Calendar events", @@ -29,6 +30,7 @@ "invalidtimedurationuntil": "The date and time you selected for duration until is before the start time of the event. Please correct this before proceeding.", "mon": "Mon", "monday": "Monday", + "monthlyview": "Monthly view", "newevent": "New event", "noevents": "There are no events", "nopermissiontoupdatecalendar": "Sorry, but you do not have permission to update the calendar event", @@ -45,6 +47,8 @@ "sunday": "Sunday", "thu": "Thu", "thursday": "Thursday", + "today": "Today", + "tomorrow": "Tomorrow", "tue": "Tue", "tuesday": "Tuesday", "typeclose": "Close event", @@ -56,6 +60,8 @@ "typeopen": "Open event", "typesite": "Site event", "typeuser": "User event", + "upcomingevents": "Upcoming events", "wed": "Wed", - "wednesday": "Wednesday" + "wednesday": "Wednesday", + "yesterday": "Yesterday" } \ No newline at end of file diff --git a/src/addon/calendar/pages/index/index.html b/src/addon/calendar/pages/index/index.html index fa0877b46..120c9dc2c 100644 --- a/src/addon/calendar/pages/index/index.html +++ b/src/addon/calendar/pages/index/index.html @@ -2,6 +2,12 @@ {{ 'addon.calendar.calendarevents' | translate }} + + @@ -22,7 +28,9 @@ {{ 'core.hasdatatosync' | translate:{$a: 'addon.calendar.calendar' | translate} }} - + + + diff --git a/src/addon/calendar/pages/index/index.ts b/src/addon/calendar/pages/index/index.ts index 8f134ed90..396690427 100644 --- a/src/addon/calendar/pages/index/index.ts +++ b/src/addon/calendar/pages/index/index.ts @@ -23,6 +23,7 @@ import { AddonCalendarProvider } from '../../providers/calendar'; import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline'; import { AddonCalendarHelperProvider } from '../../providers/helper'; import { AddonCalendarCalendarComponent } from '../../components/calendar/calendar'; +import { AddonCalendarUpcomingEventsComponent } from '../../components/upcoming-events/upcoming-events'; import { AddonCalendarSyncProvider } from '../../providers/calendar-sync'; import { CoreCoursesProvider } from '@core/courses/providers/courses'; import { CoreCoursePickerMenuPopoverComponent } from '@components/course-picker-menu/course-picker-menu-popover'; @@ -39,6 +40,7 @@ import { Network } from '@ionic-native/network'; }) export class AddonCalendarIndexPage implements OnInit, OnDestroy { @ViewChild(AddonCalendarCalendarComponent) calendarComponent: AddonCalendarCalendarComponent; + @ViewChild(AddonCalendarUpcomingEventsComponent) upcomingEventsComponent: AddonCalendarUpcomingEventsComponent; protected allCourses = { id: -1, @@ -67,6 +69,8 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { hasOffline = false; isOnline = false; syncIcon: string; + showCalendar = true; + loadUpcoming = false; constructor(localNotificationsProvider: CoreLocalNotificationsProvider, navParams: NavParams, @@ -274,7 +278,11 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { })); // Refresh the sub-component. - promises.push(this.calendarComponent.refreshData()); + if (this.showCalendar && this.calendarComponent) { + promises.push(this.calendarComponent.refreshData()); + } else if (!this.showCalendar && this.upcomingEventsComponent) { + promises.push(this.upcomingEventsComponent.refreshData()); + } return Promise.all(promises).finally(() => { return this.fetchData(sync, showErrors); @@ -349,6 +357,17 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { this.navCtrl.push('AddonCalendarSettingsPage'); } + /** + * Toogle display: monthly view or upcoming events. + */ + toggleDisplay(): void { + this.showCalendar = !this.showCalendar; + + if (!this.showCalendar) { + this.loadUpcoming = true; + } + } + /** * Page destroyed. */ diff --git a/src/addon/calendar/providers/calendar.ts b/src/addon/calendar/providers/calendar.ts index 4705a8ad8..c759210db 100644 --- a/src/addon/calendar/providers/calendar.ts +++ b/src/addon/calendar/providers/calendar.ts @@ -27,7 +27,9 @@ import { CoreConfigProvider } from '@providers/config'; import { ILocalNotification } from '@ionic-native/local-notifications'; import { SQLiteDB } from '@classes/sqlitedb'; import { AddonCalendarOfflineProvider } from './calendar-offline'; +import { CoreUserProvider } from '@core/user/providers/user'; import { TranslateService } from '@ngx-translate/core'; +import * as moment from 'moment'; /** * Service to handle calendar events. @@ -49,6 +51,10 @@ export class AddonCalendarProvider { static TYPE_GROUP = 'group'; static TYPE_SITE = 'site'; static TYPE_USER = 'user'; + + static CALENDAR_TF_24 = '%H:%M'; // Calendar time in 24 hours format. + static CALENDAR_TF_12 = '%I:%M %p'; // Calendar time in 12 hours format. + protected ROOT_CACHE_KEY = 'mmaCalendar:'; protected weekDays = [ @@ -249,11 +255,19 @@ export class AddonCalendarProvider { protected logger; - constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private groupsProvider: CoreGroupsProvider, - private coursesProvider: CoreCoursesProvider, private timeUtils: CoreTimeUtilsProvider, - private localNotificationsProvider: CoreLocalNotificationsProvider, private configProvider: CoreConfigProvider, - private utils: CoreUtilsProvider, private calendarOffline: AddonCalendarOfflineProvider, - private appProvider: CoreAppProvider, private translate: TranslateService) { + constructor(logger: CoreLoggerProvider, + private sitesProvider: CoreSitesProvider, + private groupsProvider: CoreGroupsProvider, + private coursesProvider: CoreCoursesProvider, + private timeUtils: CoreTimeUtilsProvider, + private localNotificationsProvider: CoreLocalNotificationsProvider, + private configProvider: CoreConfigProvider, + private utils: CoreUtilsProvider, + private calendarOffline: AddonCalendarOfflineProvider, + private appProvider: CoreAppProvider, + private translate: TranslateService, + private userProvider: CoreUserProvider) { + this.logger = logger.getInstance('AddonCalendarProvider'); this.sitesProvider.registerSiteSchema(this.siteSchema); } @@ -456,6 +470,90 @@ export class AddonCalendarProvider { }); } + /** + * Check if event ends the same day or not. + * + * @param {any} event Event info. + * @return {boolean} If the . + */ + endsSameDay(event: any): boolean { + if (!event.timeduration) { + // No duration. + return true; + } + + // Check if day has changed. + return moment(event.timestart * 1000).isSame((event.timestart + event.timeduration) * 1000, 'day'); + } + + /** + * Format event time. Equivalent to calendar_format_event_time. + * + * @param {any} event Event to format. + * @param {string} format Calendar time format (from getCalendarTimeFormat). + * @param {boolean} [useCommonWords=true] Whether to use common words like "Today", "Yesterday", etc. + * @param {number} [showTime=0] Determine the show time GMT timestamp. + * @return {string} Formatted event time. + */ + formatEventTime(event: any, format: string, useCommonWords: boolean = true, showTime: number = 0): string { + const start = event.timestart * 1000, + end = (event.timestart + event.timeduration) * 1000; + let time; + + if (event.timeduration) { + + if (moment(start).isSame(end, 'day')) { + // Event starts and ends the same day. + if (event.timeduration == CoreConstants.SECONDS_DAY) { + time = this.translate.instant('addon.calendar.allday'); + } else { + time = this.timeUtils.userDate(start, format) + ' » ' + + this.timeUtils.userDate(end, format); + } + + if (!showTime) { + return this.getDayRepresentation(start, useCommonWords) + ', ' + time; + } else { + return time; + } + + } else { + // Event lasts more than one day. + const midnightStart = moment(start).startOf('day').unix() * 1000, + midnightEnd = moment(end).startOf('day').unix() * 1000, + timeStart = this.timeUtils.userDate(start, format), + timeEnd = this.timeUtils.userDate(end, format); + let dayStart = this.getDayRepresentation(start, useCommonWords) + ', ', + dayEnd = this.getDayRepresentation(end, useCommonWords) + ', '; + + if (showTime == midnightStart) { + dayStart = ''; + } + + if (showTime == midnightEnd) { + dayEnd = ''; + } + + // Set printable representation. + if (moment().isSame(start, 'day')) { + // Event starts today, don't display the day. + return timeStart + ' » ' + dayEnd + timeEnd; + } else { + // The event starts in the future, print both days. + return dayStart + timeStart + ' » ' + dayEnd + timeEnd; + } + } + } else { // There is no time duration. + const time = this.timeUtils.userDate(start, format); + + if (!showTime) { + return this.getDayRepresentation(start, useCommonWords) + ', ' + time.trim(); + } else { + return time; + } + } + } + /** * Get access information for a calendar (either course calendar or site calendar). * @@ -545,6 +643,84 @@ export class AddonCalendarProvider { return this.ROOT_CACHE_KEY + 'allowedEventTypes:' + (courseId || 0); } + /** + * Get the "look ahead" for a certain user. + * + * @param {string} [siteId] ID of the site. If not defined, use current site. + * @return {Promise} Promise resolved with the look ahead (number of days). + */ + getCalendarLookAhead(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return this.userProvider.getUserPreference('calendar_lookahead').catch((error) => { + // Ignore errors. + }).then((value): any => { + if (typeof value != 'undefined') { + return value; + } + + return site.getStoredConfig('calendar_lookahead'); + }); + }); + } + + /** + * Get the time format to use in calendar. + * + * @param {string} [siteId] ID of the site. If not defined, use current site. + * @return {Promise} Promise resolved with the format. + */ + getCalendarTimeFormat(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return this.userProvider.getUserPreference('calendar_timeformat').catch((error) => { + // Ignore errors. + }).then((format) => { + + if (!format || format === '0') { + format = site.getStoredConfig('calendar_site_timeformat'); + } + + if (format === AddonCalendarProvider.CALENDAR_TF_12) { + format = this.translate.instant('core.strftimetime12'); + } else if (format === AddonCalendarProvider.CALENDAR_TF_24) { + format = this.translate.instant('core.strftimetime24'); + } + + return format && format !== '0' ? format : this.translate.instant('core.strftimetime'); + }); + }); + } + + /** + * Return the representation day. Equivalent to Moodle's calendar_day_representation. + * + * @param {number} time Timestamp to get the day from. + * @param {boolean} [useCommonWords=true] Whether to use common words like "Today", "Yesterday", etc. + * @return {string} The formatted date/time. + */ + getDayRepresentation(time: number, useCommonWords: boolean = true): string { + + if (!useCommonWords) { + // We don't want words, just a date. + return this.timeUtils.userDate(time, 'core.strftimedayshort'); + } + + const date = moment(time), + today = moment(); + + if (date.isSame(today, 'day')) { + return this.translate.instant('addon.calendar.today'); + + } else if (date.isSame(today.clone().subtract(1, 'days'), 'day')) { + return this.translate.instant('addon.calendar.yesterday'); + + } else if (date.isSame(today.clone().add(1, 'days'), 'day')) { + return this.translate.instant('addon.calendar.tomorrow'); + + } else { + return this.timeUtils.userDate(time, 'core.strftimedayshort'); + } + } + /** * Get the configured default notification time. * @@ -1019,6 +1195,7 @@ export class AddonCalendarProvider { * * @param {number} [courseId] Course ID. * @param {number} [categoryId] Category ID. + * @param {string} [siteId] Site Id. If not defined, use current site. * @return {Promise} Promise resolved when the data is invalidated. */ invalidateUpcomingEvents(courseId?: number, categoryId?: number, siteId?: string): Promise { @@ -1027,6 +1204,26 @@ export class AddonCalendarProvider { }); } + /** + * Invalidates look ahead setting. + * + * @param {string} [siteId] Site Id. If not defined, use current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateLookAhead(siteId?: string): Promise { + return this.userProvider.invalidateUserPreference('calendar_lookahead', siteId); + } + + /** + * Invalidates time format setting. + * + * @param {string} [siteId] Site Id. If not defined, use current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateTimeFormat(siteId?: string): Promise { + return this.userProvider.invalidateUserPreference('calendar_timeformat', siteId); + } + /** * Check if Calendar is disabled in a certain site. * diff --git a/src/app/app.scss b/src/app/app.scss index 41759e8ef..7123b9909 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -1152,3 +1152,8 @@ ion-app.platform-desktop { min-height: $button-ios-small-height; } } + +// Make funnel icon have iOS look. +.ion-md-funnel::before { + content: "\f182"; +} diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 3e4a0a984..7dae77a70 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -83,6 +83,7 @@ "addon.blog.publishtoworld": "Anyone in the world", "addon.blog.showonlyyourentries": "Show only your entries", "addon.blog.siteblogheading": "Site blog", + "addon.calendar.allday": "All day", "addon.calendar.calendar": "Calendar", "addon.calendar.calendarevent": "Calendar event", "addon.calendar.calendarevents": "Calendar events", @@ -113,6 +114,7 @@ "addon.calendar.invalidtimedurationuntil": "The date and time you selected for duration until is before the start time of the event. Please correct this before proceeding.", "addon.calendar.mon": "Mon", "addon.calendar.monday": "Monday", + "addon.calendar.monthlyview": "Monthly view", "addon.calendar.newevent": "New event", "addon.calendar.noevents": "There are no events", "addon.calendar.nopermissiontoupdatecalendar": "Sorry, but you do not have permission to update the calendar event", @@ -129,6 +131,8 @@ "addon.calendar.sunday": "Sunday", "addon.calendar.thu": "Thu", "addon.calendar.thursday": "Thursday", + "addon.calendar.today": "Today", + "addon.calendar.tomorrow": "Tomorrow", "addon.calendar.tue": "Tue", "addon.calendar.tuesday": "Tuesday", "addon.calendar.typecategory": "Category event", @@ -140,8 +144,10 @@ "addon.calendar.typeopen": "Open event", "addon.calendar.typesite": "Site event", "addon.calendar.typeuser": "User event", + "addon.calendar.upcomingevents": "Upcoming events", "addon.calendar.wed": "Wed", "addon.calendar.wednesday": "Wednesday", + "addon.calendar.yesterday": "Yesterday", "addon.competency.activities": "Activities", "addon.competency.competencies": "Competencies", "addon.competency.competenciesmostoftennotproficientincourse": "Competencies most often not proficient in this course",